docs(S23,S24): planos + designs hi-fi — drawer mobile do card e rádio embed direto (Related #1234 #1235)

This commit is contained in:
Francisco Solla 2026-09-20 20:31:33 -03:00
parent d86b3aa0a1
commit 0bbba099d1
4 changed files with 650 additions and 0 deletions

View file

@ -0,0 +1,315 @@
<!doctype html>
<html lang="pt-BR" data-theme="campaign-site">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>Design S23 · Drawer mobile do card Time de você</title>
<style type="text/tailwindcss">
:root {
--background: #ffffff;
--foreground: #000000;
--pt-red: #a21c1c;
--pt-red-dark: #47130e;
--pt-yellow: #ffe607;
--campaign-cream: #fff8f2;
--campaign-ink: #000000;
--campaign-muted: #6c615e;
--campaign-surface: #f9faff;
--campaign-band: #ebe9e9;
--campaign-line: rgb(0 0 0 / 12%);
--field-border: rgb(0 0 0 / 18%);
--font-exo2: 'Exo 2', 'Arial Black', sans-serif;
--font-arimo: Arimo, Arial, sans-serif;
}
* { box-sizing: border-box; }
html { color-scheme: light; }
body { margin: 0; font-family: var(--font-arimo); }
button, input { font: inherit; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
outline: 2px solid var(--pt-red);
outline-offset: 2px;
}
.font-display { font-family: var(--font-exo2); }
.stage-shadow { box-shadow: 0 26px 80px rgb(0 0 0 / 32%); }
.mobile-screen { width: 390px; height: 844px; overflow: hidden; background: #fff; }
.drawer-full {
position: absolute;
inset: 0;
display: flex;
min-height: 0;
flex-direction: column;
overflow: hidden;
border-top: 1px solid var(--campaign-line);
border-radius: 12px 12px 0 0;
background: var(--background);
color: var(--foreground);
}
.swipe-handle { width: 96px; height: 4px; border-radius: 999px; background: #ebe9e9; }
.control-button {
display: inline-flex;
width: 44px;
height: 44px;
flex: none;
align-items: center;
justify-content: center;
border: 1px solid var(--campaign-line);
border-radius: 8px;
background: white;
color: var(--campaign-ink);
}
.range {
appearance: none;
height: 4px;
min-width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--pt-red) 0 47%, #d6d3d1 47% 100%);
}
.range::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border: 3px solid white;
border-radius: 999px;
background: var(--pt-red);
box-shadow: 0 0 0 1px rgb(0 0 0 / 18%);
}
.preview-placeholder {
position: relative;
aspect-ratio: 1080 / 1440;
overflow: hidden;
border: 1px solid var(--campaign-line);
border-radius: 8px;
background:
linear-gradient(135deg, transparent 46%, rgb(0 0 0 / 5%) 46% 54%, transparent 54%),
#dedbd8;
box-shadow: 0 1px 2px rgb(0 0 0 / 8%);
}
.preview-placeholder::before {
content: '';
position: absolute;
inset: 12% 14%;
border: 1px dashed rgb(0 0 0 / 25%);
border-radius: 6px;
}
.switch {
position: relative;
width: 44px;
height: 26px;
flex: none;
border-radius: 999px;
background: var(--pt-red);
}
.switch::after {
content: '';
position: absolute;
top: 3px;
left: 21px;
width: 20px;
height: 20px;
border-radius: 999px;
background: white;
box-shadow: 0 1px 2px rgb(0 0 0 / 25%);
}
.primary-button, .secondary-button {
display: inline-flex;
min-height: 44px;
flex: 1;
align-items: center;
justify-content: center;
border-radius: 8px;
padding: 0 20px;
font-size: 14px;
font-weight: 800;
}
.primary-button { border: 0; background: var(--pt-red); color: white; }
.secondary-button {
border: 2px solid var(--campaign-line);
background: white;
color: var(--pt-red);
}
.gate-dash { background-image: linear-gradient(90deg, #a21c1c 50%, transparent 50%); background-size: 8px 1px; }
</style>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<div class="border-b border-emerald-400/40 bg-emerald-400/10 px-4 py-2 text-center text-[11px] font-black tracking-[0.14em] text-emerald-300 uppercase">
Design tier: openai/gpt-5.6-sol · frontier · gate humano obrigatório
</div>
<main class="mx-auto max-w-[1440px] px-4 py-10 sm:px-6 lg:px-10">
<header class="max-w-4xl">
<p class="m-0 text-xs font-black tracking-[0.12em] text-red-400 uppercase">Gate visual · S23</p>
<h1 class="mt-2 text-2xl font-black tracking-[-0.02em] sm:text-3xl">Ajuste fino sem vaivém no celular</h1>
<p class="mt-3 max-w-3xl text-sm leading-6 text-zinc-400">
Alvo do port: no mobile de 390px, o drawer do estúdio ocupa toda a viewport, respeitando a safe-area. O aviso abre a área rolável e sai de cena ao descer; nada fica sobre a prévia. Zoom e setas continuam no mesmo fluxo. O shell do dialog desktop mantém sua geometria.
</p>
<div class="mt-5 flex flex-wrap gap-2 text-[11px] font-bold text-zinc-300">
<span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">390px · alvo do port</span>
<span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">1280px · conferência de escopo</span>
<span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">Conteúdo da prévia · NEEDS ASSET</span>
</div>
</header>
<section class="mt-12" aria-labelledby="scene-01-title">
<div class="mb-5 flex flex-wrap items-end justify-between gap-3">
<div>
<p class="text-[11px] font-black tracking-[0.12em] text-emerald-300 uppercase">Cena 01 · mobile 390 × 844</p>
<h2 id="scene-01-title" class="mt-1 text-xl font-black">Altura total · aviso no fluxo, prévia e controles</h2>
</div>
<span class="rounded-full bg-emerald-400/15 px-3 py-1.5 text-xs font-bold text-emerald-300">Alvo do port</span>
</div>
<div class="overflow-x-auto pb-4">
<div class="mobile-screen stage-shadow relative mx-auto text-black" data-theme="campaign-site">
<div class="absolute inset-0 bg-black/10" aria-hidden="true"></div>
<article class="drawer-full" aria-label="Drawer do estúdio de cards">
<div class="flex h-3 shrink-0 items-end justify-center"><div class="swipe-handle"></div></div>
<header class="flex shrink-0 items-start gap-3 px-5 pt-5">
<div class="min-w-0 flex-1 text-left">
<p class="m-0 text-[10px] font-black tracking-[0.1em] text-(--pt-red) uppercase">Time de você</p>
<h3 class="m-0 mt-0.5 text-lg leading-tight font-black tracking-[-0.01em]">Confira seu card</h3>
</div>
<button class="inline-flex size-11 shrink-0 items-center justify-center rounded-lg text-xl text-(--campaign-muted)" aria-label="Fechar">×</button>
</header>
<div class="min-h-0 flex-1 overflow-hidden px-5 pt-3 pb-4">
<p class="mx-auto mb-3 max-w-[350px] text-sm leading-5 text-(--campaign-muted)">
O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles.
</p>
<div class="relative mx-auto w-[180px]" tabindex="0" aria-label="Ajuste da foto">
<div class="preview-placeholder" aria-hidden="true"></div>
</div>
<p class="m-0 mt-1 text-center text-[9px] font-black tracking-[0.08em] text-zinc-500 uppercase">Prévia real do visitante · NEEDS ASSET</p>
<div class="mt-4">
<div class="flex items-center gap-3">
<label for="zoom-1" class="text-sm font-bold">Zoom</label>
<button class="control-button" aria-label="Diminuir zoom">−</button>
<input id="zoom-1" class="range h-11 w-full flex-1" type="range" value="47" aria-label="Zoom da foto" />
<button class="control-button" aria-label="Aumentar zoom">+</button>
</div>
<div class="mt-3">
<p class="m-0 text-sm font-bold">Ajuste fino</p>
<div class="mt-2 grid grid-cols-4 gap-2">
<button class="control-button w-full" aria-label="Mover foto para a esquerda">←</button>
<button class="control-button w-full" aria-label="Mover foto para cima">↑</button>
<button class="control-button w-full" aria-label="Mover foto para baixo">↓</button>
<button class="control-button w-full" aria-label="Mover foto para a direita">→</button>
</div>
</div>
<div class="mt-3 flex min-h-11 items-start gap-3 rounded-lg border border-(--campaign-line) bg-white p-3 opacity-60">
<span class="switch mt-1.5" aria-hidden="true"></span>
<span class="min-w-0">
<span class="block text-sm font-bold">Harmonizar cores</span>
<span class="mt-0.5 block text-xs leading-5 text-(--campaign-muted)">Ajusta o brilho e as cores da sua foto para combinar com as fotos do card.</span>
</span>
</div>
</div>
</div>
<footer class="flex shrink-0 gap-3 border-t border-(--campaign-line) px-5 py-4">
<button class="secondary-button">Cancelar</button>
<button class="primary-button">Criar meu card</button>
</footer>
</article>
</div>
</div>
<p class="mx-auto mt-3 max-w-[620px] text-center text-xs leading-5 text-zinc-400">O topo branco coincide com a viewport e reserva a safe-area. A área rolável começa no aviso; imagem, zoom e setas vêm em seguida, sem qualquer camada sobre a prévia.</p>
</section>
<section class="mt-16" aria-labelledby="scene-02-title">
<div class="mb-5">
<p class="text-[11px] font-black tracking-[0.12em] text-amber-300 uppercase">Cena 02 · mobile 390 × 844 · conteúdo rolado</p>
<h2 id="scene-02-title" class="mt-1 text-xl font-black">Aviso fora de cena · nada sobre a prévia</h2>
</div>
<div class="grid items-start gap-6 lg:grid-cols-[390px_1fr] lg:justify-center">
<div class="mobile-screen stage-shadow relative mx-auto text-black lg:mx-0" data-theme="campaign-site">
<article class="drawer-full">
<div class="flex h-3 shrink-0 items-end justify-center"><div class="swipe-handle"></div></div>
<header class="flex shrink-0 items-start gap-3 px-5 pt-5">
<div class="min-w-0 flex-1">
<p class="m-0 text-[10px] font-black tracking-[0.1em] text-(--pt-red) uppercase">Time de você</p>
<h3 class="m-0 mt-0.5 text-lg leading-tight font-black">Confira seu card</h3>
</div>
<button class="inline-flex size-11 items-center justify-center rounded-lg text-xl text-(--campaign-muted)" aria-label="Fechar">×</button>
</header>
<div class="min-h-0 flex-1 overflow-hidden px-5 pt-3 pb-4">
<div class="relative mx-auto w-[180px]">
<div class="preview-placeholder" aria-hidden="true"></div>
</div>
<p class="m-0 mt-1 text-center text-[9px] font-black tracking-[0.08em] text-zinc-500 uppercase">Prévia real do visitante · NEEDS ASSET</p>
<div class="mt-4 flex items-center gap-3">
<span class="text-sm font-bold">Zoom</span><button class="control-button">−</button><div class="range h-1 flex-1"></div><button class="control-button">+</button>
</div>
<p class="m-0 mt-3 text-sm font-bold">Ajuste fino</p>
<div class="mt-2 grid grid-cols-4 gap-2"><button class="control-button w-full">←</button><button class="control-button w-full">↑</button><button class="control-button w-full">↓</button><button class="control-button w-full">→</button></div>
</div>
<footer class="flex shrink-0 gap-3 border-t border-(--campaign-line) px-5 py-4"><button class="secondary-button">Cancelar</button><button class="primary-button">Criar meu card</button></footer>
</article>
</div>
<aside class="mx-auto max-w-xl rounded-2xl border border-amber-400/35 bg-amber-400/10 p-5 lg:mx-0">
<p class="m-0 text-[11px] font-black tracking-[0.12em] text-amber-300 uppercase">Estado após rolar</p>
<p class="mt-3 text-sm leading-6 text-zinc-200"><strong>O aviso continua existindo</strong>, mas ficou acima do recorte visível da área rolável. Ele se move com o conteúdo — não acompanha a imagem nem fica preso ao header.</p>
<p class="mt-4 text-sm leading-6 text-zinc-200"><strong>O badge “Arraste para ajustar” sai.</strong> A prévia permanece totalmente limpa; gesto, zoom e setas comunicam o ajuste sem cobrir a foto.</p>
<div class="mt-5 rounded-xl border border-zinc-700 bg-zinc-900 p-4 text-xs leading-5 text-zinc-400">Esta cena é o mesmo drawer da Cena 01 depois de rolar até os controles. Não há anotação inserida dentro da interface-alvo.</div>
</aside>
</div>
</section>
<section class="mt-16" aria-labelledby="scene-03-title">
<div class="mb-5 flex flex-wrap items-end justify-between gap-3">
<div>
<p class="text-[11px] font-black tracking-[0.12em] text-sky-300 uppercase">Cena 03 · desktop 1280 × 800</p>
<h2 id="scene-03-title" class="mt-1 text-xl font-black">Dialog desktop · aviso no fluxo, imagem limpa</h2>
</div>
<span class="rounded-full border border-sky-400/30 bg-sky-400/10 px-3 py-1.5 text-xs font-bold text-sky-200">Sem mudança no shell desktop</span>
</div>
<div class="overflow-x-auto pb-4">
<div class="stage-shadow relative mx-auto h-[800px] w-[1280px] overflow-hidden bg-[#fff8f2] text-black" data-theme="campaign-site">
<div class="absolute inset-0 bg-black/60"></div>
<article class="absolute top-1/2 left-1/2 flex max-h-[736px] w-[512px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-(--campaign-line) bg-white shadow-2xl">
<header class="flex shrink-0 items-start gap-3 px-5 pt-5">
<div class="min-w-0 flex-1">
<p class="m-0 text-xs font-black tracking-[0.1em] text-(--pt-red) uppercase">Time de você</p>
<h3 class="m-0 mt-0.5 text-xl font-black tracking-[-0.01em]">Confira seu card</h3>
</div>
<button class="inline-flex size-11 items-center justify-center rounded-lg text-xl text-(--campaign-muted)" aria-label="Fechar">×</button>
</header>
<div class="min-h-0 flex-1 overflow-y-auto px-5 pt-4 pb-5">
<p class="mx-auto mb-4 max-w-[420px] text-sm leading-5 text-(--campaign-muted)">O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles.</p>
<div class="relative mx-auto w-[240px]">
<div class="preview-placeholder" aria-hidden="true"></div>
</div>
<p class="m-0 mt-1 text-center text-[9px] font-black tracking-[0.08em] text-zinc-500 uppercase">Prévia real do visitante · NEEDS ASSET</p>
<div class="mx-auto mt-5 max-w-[420px]">
<div class="flex items-center gap-3"><span class="text-sm font-bold">Zoom</span><button class="control-button">−</button><div class="range h-1 flex-1"></div><button class="control-button">+</button></div>
<p class="m-0 mt-4 text-sm font-bold">Ajuste fino</p>
<div class="mt-2 grid grid-cols-4 gap-2"><button class="control-button w-full">←</button><button class="control-button w-full">↑</button><button class="control-button w-full">↓</button><button class="control-button w-full">→</button></div>
</div>
</div>
<footer class="flex shrink-0 gap-3 border-t border-(--campaign-line) px-5 py-4"><button class="secondary-button">Cancelar</button><button class="primary-button">Criar meu card</button></footer>
</article>
<div class="absolute top-6 right-6 rounded-full border border-white/25 bg-black/50 px-4 py-2 text-xs font-black tracking-[0.08em] text-white uppercase">Dialog 512px · 92dvh</div>
</div>
</div>
<p class="mt-3 text-xs leading-5 text-zinc-400">O dialog permanece centralizado, com largura e teto atuais. A nova regra vale no conteúdo compartilhado: aviso dentro do fluxo rolável e nenhuma camada sobre a imagem.</p>
</section>
<section class="mt-16 rounded-2xl border border-zinc-800 bg-zinc-900/70 p-6">
<h2 class="text-lg font-black">Contrato visual para o port</h2>
<ul class="mt-4 grid gap-3 text-sm leading-6 text-zinc-300 md:grid-cols-2">
<li>• Mobile: drawer encosta no topo, cobre os 844px e respeita a safe-area.</li>
<li>• O aviso é o primeiro item da área rolável, acima da prévia.</li>
<li>• Ao descer, o aviso rola para fora; prévia, zoom e quatro setas ficam juntos.</li>
<li>• Nada se sobrepõe à imagem em drawer ou dialog.</li>
<li>• O badge “Arraste para ajustar” sai; gesto, zoom e setas permanecem.</li>
<li>• Desktop mantém dialog centralizado e dimensões atuais.</li>
</ul>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,93 @@
# Ajuste fino da foto no celular: drawer "Time de você" até o topo, sem nada sobre a imagem
Status: rascunho
Atualizado em: 2026-09-20
Issue: #1234
Priority: P2
Impeccable: B — encaixe no drawer mobile do estúdio de cards (funil público)
Design UI: docs/plans/cards-time-de-voce-drawer-mobile-ui-design.html
Appetite: ~0,5–1 dia eng; um outcome verificável — no celular, o ajuste fino da foto cabe na tela sem vaivém de rolagem
Responsável: —
## Intenção
No celular, a etapa de ajuste fino da foto do card "Time de você" é desastrada: o visitante ou vê a imagem, ou vê os controles — precisa rolar para cima e para baixo no meio do gesto. Duas correções pedidas no gate: (a) nada de sobreposto à imagem — o badge "Arraste para ajustar" sai de cima da prévia — e o aviso "O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles." pode continuar existindo, desde que role junto com o conteúdo, sem ficar em cima da imagem quando o visitante desce para ver o conteúdo completo; (b) fazer o drawer inferior subir até o topo da página, removendo a faixa livre que hoje sobra acima dele e devolvendo espaço ao conteúdo. O enquadramento automático (S18), os controles existentes (arrastar, zoom, setas) e o resto do funil não mudam; o shell desktop (dialog) mantém a geometria.
## Persona e fluxo
- **Persona / contexto:** visitante do site público no celular, no meio do funil de cards, já com a foto preparada pelo recorte automático; quer ajustar e compartilhar sem fricção.
- **Job principal:** conferir e ajustar o enquadramento da foto com a prévia e os controles visíveis ao mesmo tempo, sem rolar a tela para lá e para cá.
- **Fluxo desejado:** escolhe o modelo "Time de você" → envia a foto → o drawer sobe até o topo da tela → vê a prévia; ao descer, o aviso rola junto e sai de cena, sem nada fixo sobre a imagem, e os controles de zoom/ajuste fino aparecem juntos → ajusta → segue para o card pronto e o compartilhamento.
- **Anti-goals de produto:** não redesenhar o estúdio nem o composer; não mudar a primitiva `Drawer` global; não mexer na geometria do dialog desktop; não remover a frase própria dos modelos de foto; não reabrir S18/S20.
### Esboço de fluxo (B)
```text
[galeria de modelos] → [foto enviada / recorte automático] → [drawer sobe até o topo]
→ [prévia; aviso rola junto e sai de cena ao descer] → [controles na mesma tela] → [card pronto] → [compartilhar]
```
### Design UI (B)
- Design UI (gate): `docs/plans/cards-time-de-voce-drawer-mobile-ui-design.html`
## Objetivo e aceite
- Nada fica sobreposto à imagem: o badge "Arraste para ajustar" sai de cima da prévia, no drawer e no dialog.
- O aviso "O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles." permanece, mas dentro da área rolável: rola junto com o conteúdo e sai de cena quando o visitante desce para ver os controles — nunca fica fixo por cima da imagem.
- No celular, o drawer ocupa até o topo da tela, sem faixa livre; o passo de ajuste fino mostra a prévia e os controles de zoom/ajuste fino sem o vaivém de rolagem (o restante do conteúdo pode rolar).
- A geometria do shell desktop (dialog) e os demais modelos de card permanecem intocados; a regra "nada sobreposto à imagem / aviso rolável" vale nos dois shells.
- Swipe-down/fechar e acessibilidade continuam funcionando; o drawer segue anunciando título e conteúdo.
- **Guardrail:** a orientação `aria-describedby` do drawer precisa ser tratada ao remover a descrição — sem prescrever como (a decisão fica para o plano de implementação).
- A intenção diz "até o topo": nenhum token ou valor de altura é fixado aqui; a implementação decide o valor.
## Dados (intenção)
- **Vou apresentar dados?** Não — nenhuma PII é coletada ou exibida; a foto e a posição do recorte existem só no aparelho do visitante e nunca saem dele. Sem métrica nova neste item.
- **Decisões desbloqueadas:** N/A — ajuste de layout/UX, sem decisão de dado.
- **Forma:** N/A.
## Dados da decisão (literais)
- ID: `S23`; slug: `cards-time-de-voce-drawer-mobile`; tipo: `feature`; Priority: `P2`; Impeccable: `B`.
- Texto exato do aviso do time (verbatim): "O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles." — permanece, porém dentro da área rolável (rola com o conteúdo); a frase própria dos modelos de foto ("Arraste para posicionar e use os controles para aproximar ou ajustar.") fica intocada no header.
- Badge sobre a prévia: "Arraste para ajustar" — sai da posição sobreposta à imagem; nada fica por cima da prévia.
- Alvo do drawer no celular: subir até o topo da viewport, respeitando safe-area — o `92dvh` de hoje vira teto efetivo da tela (validar o teclado no aparelho real).
- Rotas afetadas: `/` (home, seção de cards) e `/cards`.
- Drawer desktop (dialog) fica como está.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/cards/CardComposer.tsx` (composição da descrição/header), `src/components/cards/CardsStudio.tsx` (override do drawer mobile no ponto de uso), `src/components/ui/Drawer.tsx` (primitiva compartilhada — leitura apenas).
- **Precedente a olhar:** `docs/plans/atualizacoes-mobile-sheet-criacao-usavel.md` e o impl (C107: mesmo padrão de sheet que expande só o necessário, teto no topo; decisão de manter o override no ponto de uso), `docs/plans/bottom-drawer-busca-fullscreen-polimento.md` (fullscreen `100dvh`), `docs/plans/cards-time-de-voce-ajuste-lateral.md`.
- **Risco de acoplamento:** a primitiva `Drawer` serve outros drawers de campanha; manter o override no ponto de uso para não ampliar o blast radius. Respeitar `data-theme="campaign-site"` e o swipe handle/foco do drawer.
## Dependências
- Nenhuma. Sucessor de S20 (Issue #1216, entregue) — não reabre S18/S20.
## Fora de escopo
- Tutorial ou e2e novo de rolagem.
- Mudanças de copy do funil (o aviso do time permanece — só muda de lugar).
- Outros drawers de campanha.
## Rabbit holes de produto
- **"Já que vamos mexer no drawer, ajustamos todos os drawers de campanha."** Se alguém "só completar": vira refactor da primitiva com blast radius em campanha. **Corte neste item:** só o drawer do estúdio de cards; primitiva intacta.
- **"Já que o badge sai, tira o aviso também."** Se alguém "só completar": reabre a decisão do gate. **Corte neste item:** o aviso fica, rolando com o conteúdo.
- **"Aproveita e redesenha os controles do passo de ajuste."** Se alguém "só completar": redesign do composer estoura o appetite. **Corte neste item:** só espaço vertical e a remoção do aviso.
## Questões em aberto (produto)
- **Nada sobreposto à imagem e aviso rolável (resolvido no gate):** o badge "Arraste para ajustar" sai de cima da prévia; o aviso do time rola com o conteúdo; nada fica fixo sobre a imagem. _(decidido com o humano no gate)_
- **Topo da tela com safe-area (resolvido no gate):** subir até o topo respeitando safe-area; validar o teclado no celular real durante o work-issue. _(decidido com o humano no gate)_
## Referências
- GitHub Issue: a registrar (`S23`); S20 = Issue #1216 (entregue, sucessor).
- Design UI (gate): `docs/plans/cards-time-de-voce-drawer-mobile-ui-design.html`
- `docs/plans/atualizacoes-mobile-sheet-criacao-usavel.md` (precedente C107) e `docs/plans/atualizacoes-mobile-sheet-criacao-usavel-impl.md`
- `docs/plans/bottom-drawer-busca-fullscreen-polimento.md`
- `docs/plans/cards-time-de-voce-ajuste-lateral.md`
- `src/components/cards/CardComposer.tsx`, `src/components/cards/CardsStudio.tsx`, `src/components/ui/Drawer.tsx`

View file

@ -0,0 +1,143 @@
<!doctype html>
<html lang="pt-BR" data-theme="campaign-site">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>Design S24 · Embed direto da Rádio 1313</title>
<style type="text/tailwindcss">
:root {
--background: #ffffff;
--foreground: #000000;
--pt-red: #a21c1c;
--pt-red-dark: #47130e;
--pt-yellow: #ffe607;
--pt-yellow-ink: #000000;
--campaign-cream: #fff8f2;
--campaign-ink: #000000;
--campaign-muted: #6c615e;
--campaign-surface: #f9faff;
--campaign-band: #ebe9e9;
--campaign-line: rgb(0 0 0 / 12%);
--font-exo2: 'Exo 2', 'Arial Black', sans-serif;
--font-arimo: Arimo, Arial, sans-serif;
}
* { box-sizing: border-box; }
html { color-scheme: light; }
body { margin: 0; font-family: var(--font-arimo); }
.font-display, .campaign-section-eyebrow, .campaign-section-title { font-family: var(--font-exo2); }
.campaign-section-eyebrow { font-size: clamp(10px, calc(0.32vw + 8.75px), 12px); line-height: 1.25; }
.campaign-section-title { font-size: clamp(34px, calc(1.7vw + 18px), 40px); line-height: 1.02; }
.campaign-section-copy { font-size: clamp(14px, calc(0.32vw + 12.75px), 16px); line-height: 1.55; }
.stage-shadow { box-shadow: 0 26px 80px rgb(0 0 0 / 30%); }
.screen { overflow: hidden; background: white; color: black; }
.player-frame {
overflow: hidden;
border: 1px solid rgb(0 0 0 / 12%);
border-radius: 14px;
background: white;
box-shadow: 0 10px 28px rgb(71 19 14 / 7%);
}
.widget-placeholder {
position: relative;
display: flex;
width: 100%;
height: 168px;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 12px;
background:
linear-gradient(135deg, transparent 46%, rgb(0 0 0 / 5%) 46% 54%, transparent 54%),
#dedbd8;
}
.widget-placeholder::after {
content: '';
position: absolute;
inset: 14px;
border: 1px dashed rgb(0 0 0 / 24%);
border-radius: 9px;
}
.jingle-card { overflow: hidden; border: 1px solid var(--campaign-line); border-radius: 14px; background: white; box-shadow: 0 12px 34px rgb(71 19 14 / 8%); }
.cover-placeholder {
position: relative;
display: flex;
aspect-ratio: 1;
align-items: center;
justify-content: center;
background: #dedbd8;
color: #57534e;
}
.cover-placeholder::after { content: ''; position: absolute; inset: 58% 0 0; background: linear-gradient(to bottom, transparent, rgb(24 10 9 / 20%)); }
.play-control { display: inline-flex; width: 56px; height: 56px; flex: none; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: var(--pt-yellow); color: #000; box-shadow: 0 6px 0 #cfb900, 0 10px 22px rgb(0 0 0 / 15%); }
.download-control { display: inline-flex; min-height: 44px; width: 100%; align-items: center; justify-content: center; gap: 8px; border: 1.5px solid rgb(162 28 28 / 32%); border-radius: 10px; color: var(--pt-red); font-family: var(--font-exo2); font-weight: 800; }
.mobile-screen { width: 390px; overflow: hidden; }
@media (max-width: 639px) { .widget-placeholder { height: 150px; } }
</style>
</head>
<body class="bg-zinc-950 text-zinc-100 antialiased">
<div class="border-b border-emerald-400/40 bg-emerald-400/10 px-4 py-2 text-center text-[11px] font-black tracking-[0.14em] text-emerald-300 uppercase">Design tier: openai/gpt-5.6-sol · frontier · gate humano obrigatório</div>
<main class="mx-auto max-w-[1440px] px-4 py-10 sm:px-6 lg:px-10">
<header class="max-w-4xl">
<p class="m-0 text-xs font-black tracking-[0.12em] text-red-400 uppercase">Gate visual · S24</p>
<h1 class="mt-2 text-2xl font-black tracking-[-0.02em] sm:text-3xl">Rádio 1313: o player já está na página</h1>
<p class="mt-3 max-w-3xl text-sm leading-6 text-zinc-400">A seção mantém seu cabeçalho e sua copy. No lugar do card-facade entra somente o embed oficial, em moldura mínima e com footprint reservado. O widget fica nu: sem arte, eyebrow interno, escudo, botão intermediário, link “Abrir no Zeno” ou aviso de terceiro.</p>
<div class="mt-5 flex flex-wrap gap-2 text-[11px] font-bold text-zinc-300"><span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">1280px · desktop</span><span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">390px · alvo mobile do port</span><span class="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2">Widget zeno.fm · NEEDS ASSET</span></div>
</header>
<section class="mt-12" aria-labelledby="radio-scene-01">
<div class="mb-5 flex flex-wrap items-end justify-between gap-3"><div><p class="text-[11px] font-black tracking-[0.12em] text-emerald-300 uppercase">Cena 01 · desktop 1280 · 0 jingles publicados</p><h2 id="radio-scene-01" class="mt-1 text-xl font-black">Rádio sozinha · estado real de produção</h2></div><span class="rounded-full bg-emerald-400/15 px-3 py-1.5 text-xs font-bold text-emerald-300">Variante compact preservada</span></div>
<div class="overflow-x-auto pb-4">
<div class="screen stage-shadow relative mx-auto min-h-[650px] w-[1280px] border-b border-(--campaign-line) bg-(--campaign-cream)" data-theme="campaign-site">
<div class="pointer-events-none absolute -top-28 -right-20 h-64 w-64 rounded-full bg-(--pt-yellow)/45"></div>
<div class="relative mx-auto w-full max-w-[1160px] px-10 py-16">
<header class="mx-auto max-w-2xl text-center"><p class="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">A trilha da nossa caminhada</p><h3 class="campaign-section-title m-0 mt-2 font-black tracking-[-0.03em] text-balance">Sintonize com a Rádio 1313</h3><p class="campaign-section-copy mx-auto mt-3 max-w-xl text-(--campaign-muted)">Ainda não há jingles publicados por aqui. Enquanto isso, ouça a Rádio Jorge Solla 1313.</p></header>
<article class="player-frame mx-auto mt-8 max-w-[880px] p-4" aria-label="Player da Rádio Jorge Solla 1313">
<div class="widget-placeholder"><span class="relative z-10 rounded-lg border border-black/10 bg-white/90 px-4 py-3 text-center text-xs font-black tracking-[0.08em] text-zinc-600 uppercase">Embed oficial zeno.fm<br /><span class="mt-1 block text-[10px] font-bold tracking-normal normal-case">conteúdo de terceiro · NEEDS ASSET</span></span></div>
</article>
<div class="mx-auto mt-7 flex max-w-[880px] items-center gap-3 rounded-xl border border-dashed border-(--pt-red)/35 bg-white/45 px-4 py-3 text-xs text-(--campaign-muted)"><span class="font-black text-(--pt-red)">168px reservados</span><span>O espaço do widget existe desde o primeiro paint: sem salto quando o iframe responder.</span></div>
</div>
</div>
</div>
</section>
<section class="mt-16" aria-labelledby="radio-scene-02">
<div class="mb-5"><p class="text-[11px] font-black tracking-[0.12em] text-amber-300 uppercase">Cena 02 · mobile 390 · 0 jingles publicados</p><h2 id="radio-scene-02" class="mt-1 text-xl font-black">Player direto sem overflow horizontal</h2></div>
<div class="mobile-screen screen stage-shadow relative mx-auto bg-(--campaign-cream) text-black" data-theme="campaign-site">
<div class="pointer-events-none absolute -top-16 -right-16 size-44 rounded-full bg-(--pt-yellow)/45"></div>
<div class="relative px-5 py-12">
<header class="mx-auto text-center"><p class="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">A trilha da nossa caminhada</p><h3 class="campaign-section-title m-0 mt-2 text-[34px] font-black tracking-[-0.03em] text-balance">Sintonize com a Rádio 1313</h3><p class="campaign-section-copy mx-auto mt-3 text-(--campaign-muted)">Ainda não há jingles publicados por aqui. Enquanto isso, ouça a Rádio Jorge Solla 1313.</p></header>
<article class="player-frame mt-8 p-3"><div class="widget-placeholder"><span class="relative z-10 rounded-lg border border-black/10 bg-white/90 px-3 py-2 text-center text-[11px] font-black tracking-[0.07em] text-zinc-600 uppercase">Embed oficial zeno.fm<br /><span class="block text-[9px] font-bold tracking-normal normal-case">NEEDS ASSET · 150px reservados</span></span></div></article>
</div>
<div class="border-t border-dashed border-(--pt-red)/30 bg-white/50 px-5 py-3 text-center text-[10px] font-black tracking-[0.08em] text-(--pt-red) uppercase">390px · alvo mobile do port · sem botão intermediário</div>
</div>
</section>
<section class="mt-16" aria-labelledby="radio-scene-03">
<div class="mb-5"><p class="text-[11px] font-black tracking-[0.12em] text-sky-300 uppercase">Cena 03 · desktop 1280 · com jingles</p><h2 id="radio-scene-03" class="mt-1 text-xl font-black">Embed direto + grade existente com 3 cards</h2></div>
<div class="overflow-x-auto pb-4">
<div class="screen stage-shadow relative mx-auto w-[1280px] bg-(--campaign-cream)" data-theme="campaign-site">
<div class="pointer-events-none absolute -top-28 -right-20 h-64 w-64 rounded-full bg-(--pt-yellow)/45"></div>
<div class="relative mx-auto w-full max-w-[1160px] px-10 py-16">
<header class="mx-auto max-w-2xl text-center"><p class="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">A trilha da nossa caminhada</p><h3 class="campaign-section-title m-0 mt-2 font-black tracking-[-0.03em] text-balance">Cante, baixe e espalhe</h3><p class="campaign-section-copy mx-auto mt-3 max-w-xl text-(--campaign-muted)">Ouça a Rádio Jorge Solla 1313 e os jingles oficiais da campanha. Dê o play e baixe o seu preferido para levar com você.</p></header>
<article class="player-frame mt-9 p-4"><div class="widget-placeholder"><span class="relative z-10 rounded-lg border border-black/10 bg-white/90 px-4 py-3 text-center text-xs font-black tracking-[0.08em] text-zinc-600 uppercase">Embed oficial zeno.fm<br /><span class="block text-[10px] font-bold tracking-normal normal-case">conteúdo de terceiro · NEEDS ASSET</span></span></div></article>
<div class="mt-12 flex items-end justify-between gap-6"><div><p class="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">Jingles oficiais</p><h3 class="font-display m-0 mt-1 text-2xl font-black tracking-[-0.02em]">Dê o play. O próximo ritmo é seu.</h3></div><span class="text-sm text-(--campaign-muted)">Um jingle toca por vez.</span></div>
<div class="mt-6 grid grid-cols-3 gap-6">
<article class="jingle-card"><div class="cover-placeholder"><span class="relative z-10 text-center text-[10px] font-black tracking-[0.08em] uppercase">Capa do jingle<br />NEEDS ASSET</span></div><div class="p-5"><div class="flex items-center gap-4"><button class="play-control" aria-label="Tocar jingle"><svg viewBox="0 0 24 24" class="size-6 fill-current"><path d="m8 5 11 7-11 7z" /></svg></button><div class="min-w-0"><h4 class="font-display m-0 truncate text-[26px] leading-none font-black">Nome do jingle</h4><p class="mt-2 text-sm text-(--campaign-muted)">Pronto para tocar</p></div></div><div class="mt-5 h-1.5 rounded-full bg-[#dedbd8]"></div><div class="mt-2 flex justify-between text-[10px] font-bold text-black/50 uppercase"><span>Tempo —:—</span><span>Duração —:—</span></div><a class="download-control mt-5">↓ Baixar MP3</a></div></article>
<article class="jingle-card"><div class="cover-placeholder"><span class="relative z-10 text-center text-[10px] font-black tracking-[0.08em] uppercase">Capa do jingle<br />NEEDS ASSET</span></div><div class="p-5"><div class="flex items-center gap-4"><button class="play-control" aria-label="Tocar jingle"><svg viewBox="0 0 24 24" class="size-6 fill-current"><path d="m8 5 11 7-11 7z" /></svg></button><div class="min-w-0"><h4 class="font-display m-0 truncate text-[26px] leading-none font-black">Nome do jingle</h4><p class="mt-2 text-sm text-(--campaign-muted)">Pronto para tocar</p></div></div><div class="mt-5 h-1.5 rounded-full bg-[#dedbd8]"></div><div class="mt-2 flex justify-between text-[10px] font-bold text-black/50 uppercase"><span>Tempo —:—</span><span>Duração —:—</span></div><a class="download-control mt-5">↓ Baixar MP3</a></div></article>
<article class="jingle-card"><div class="cover-placeholder"><span class="relative z-10 text-center text-[10px] font-black tracking-[0.08em] uppercase">Capa do jingle<br />NEEDS ASSET</span></div><div class="p-5"><div class="flex items-center gap-4"><button class="play-control" aria-label="Tocar jingle"><svg viewBox="0 0 24 24" class="size-6 fill-current"><path d="m8 5 11 7-11 7z" /></svg></button><div class="min-w-0"><h4 class="font-display m-0 truncate text-[26px] leading-none font-black">Nome do jingle</h4><p class="mt-2 text-sm text-(--campaign-muted)">Pronto para tocar</p></div></div><div class="mt-5 h-1.5 rounded-full bg-[#dedbd8]"></div><div class="mt-2 flex justify-between text-[10px] font-bold text-black/50 uppercase"><span>Tempo —:—</span><span>Duração —:—</span></div><a class="download-control mt-5">↓ Baixar MP3</a></div></article>
</div>
<div class="mt-8 text-center"><a class="inline-flex min-h-11 items-center gap-1.5 rounded-md px-2 font-bold text-(--pt-red) underline-offset-4">Ver todos os jingles <span aria-hidden="true">›</span></a></div>
</div>
</div>
</div>
</section>
<section class="mt-16 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl border border-zinc-800 bg-zinc-900/70 p-6"><p class="text-[11px] font-black tracking-[0.12em] text-red-300 uppercase">Removido da superfície</p><h2 class="mt-2 text-lg font-black">Nada entre a seção e o player</h2><ul class="mt-4 space-y-2 text-sm leading-6 text-zinc-300"><li>• Sem arte “Rádio online” e ondas.</li><li>• Sem eyebrow “Sintonize com a gente” dentro do card.</li><li>• Sem título/textos próprios da facade.</li><li>• Sem escudo, “Ouvir a rádio” ou “Abrir no Zeno”.</li><li>• Sem estados da facade nem aviso de serviço externo.</li></ul></div>
<div class="rounded-2xl border border-zinc-800 bg-zinc-900/70 p-6"><p class="text-[11px] font-black tracking-[0.12em] text-emerald-300 uppercase">Mantido no port</p><h2 class="mt-2 text-lg font-black">Seção e footprint reservado</h2><ul class="mt-4 space-y-2 text-sm leading-6 text-zinc-300"><li>• Header/copy variam com 0 ou N jingles como hoje.</li><li>• Embed direto ocupa 150px mobile / 168px desktop na cena.</li><li>• Moldura mínima: borda, raio e sombra baixa.</li><li>• Widget nu, sem linha, chip, badge ou escudo abaixo.</li><li>• Grade e “Ver todos os jingles” seguem abaixo quando existirem.</li></ul></div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,99 @@
# Rádio 1313 na home — embed direto (S24)
Status: rascunho
Atualizado em: 2026-09-20
Issue: #1235
Priority: P2
Impeccable: B — encaixe/remoção na seção de som da home (substitui o card-facade pela moldura mínima do embed)
Design UI: docs/plans/radio-embed-direto-ui-design.html
Appetite: ~0,5–1 dia eng; um outcome verificável — a rádio toca a partir do embed direto, sem clique prévio
Responsável: —
## Intenção
A Rádio Jorge Solla 1313 na home hoje mora dentro de um card-wrapper com facade click-to-load: arte "Rádio online" com ondas, eyebrow "Sintonize com a gente", título, textos explicativos, aviso com escudo, botão "Ouvir a rádio" e estados de carregamento — o iframe do zeno.fm só monta depois do clique. O humano achou esse layout desnecessário: basta o embed direto do zeno.fm (`https://zeno.fm/player/jorge-solla-1313/`), carregado automaticamente desde o início do carregamento da página. O embed oficial já traz os detalhes da rádio (capa, nome, programação). A seção de jingles publicados e a página `/jingles` não mudam.
**Trade-off assumido (decisão do humano):** o auto-carregamento reverte de propósito a razão da facade do S22 (#1228) — o widget do zeno carrega Google Analytics `G-2T527NZWVM` e redes de anúncio em todo pageview. Isso passa a ser esperado, não um defeito: decisão explícita do humano, registrada com honestidade; o plano antigo é imutável e não é reaberto. Não coletamos PII nova; o visitante apenas acessa um serviço externo. **Sem linha de transparência (decisão do humano no gate):** o embed fica nu — sem aviso de terceiro, chip, badge ou escudo sob o player.
## Persona e fluxo
- **Persona / contexto:** visitante e militante que chega pela home no celular (link no WhatsApp, busca, indicação), com pressa e em conexão qualquer.
- **Job principal:** ouvir a Rádio Jorge Solla 1313 na home no primeiro play, sem um passo de "carregar player".
- **Fluxo desejado:** abre a home → rola até a seção de som → o embed oficial já está ali, tocável → dá play direto. Com jingles publicados, a grade e o "Ver todos" seguem abaixo; sem jingles, a rádio fica sozinha.
- **Anti-goals de produto:** não redesenha a seção de som; não vira player custom nem proxy/stream; não mexe nos jingles nem em `/jingles`; não autoplay com som; não promete analytics/contador de ouvintes; não cria framework de consent/CSP.
### Esboço de fluxo (B)
```text
[visitante na home] → rola até a seção de som
→ embed oficial da Rádio 1313 já montado e tocável (sem clique intermediário)
→ dá play direto · segue ouvindo enquanto rola
[zero jingles publicados] → rádio sozinha (comportamento da variante compact)
[com jingles] → grade + "Ver todos os jingles" → /jingles
```
### Design UI (B)
- Design UI (gate): `docs/plans/radio-embed-direto-ui-design.html` (cenas da seção com o embed direto, com 0 e N jingles, e o footprint reservado antes do iframe responder).
## Objetivo e aceite
- Ao carregar a home, o embed oficial do zeno já está montado e tocável, sem botão nem clique prévio.
- Os elementos do wrapper/facade somem: arte "Rádio online", eyebrow "Sintonize com a gente", textos explicativos, aviso com escudo, botão "Ouvir a rádio" e estados loading/loaded.
- A regra da seção de som permanece: rádio sempre visível; grade de jingles fail-closed por publicação (funciona com 0 publicados; kill switch do S21 valendo).
- Sem salto vertical perceptível ao carregar o iframe; celular primeiro, sem overflow horizontal a 390px; o embed nunca toca sozinho com som e mantém rótulo acessível.
- `/jingles` e o player dos jingles publicados ficam intocados.
- O site segue sem coletar PII e a home segue estática (cache tag `jingles`).
## Dados (intenção)
- **Vou apresentar dados?** Não — nenhum dado nosso é apresentado ou coletado; a superfície é um player de terceiro (sem PII).
- **Decisões desbloqueadas:** N/A — a decisão é do visitante (ouvir agora), não leitura de números.
- **Forma:** N/A — sem dado agregado a definir.
## Dados da decisão (literais)
- ID `S24`; slug `radio-embed-direto`; tipo `feature`; Priority `P2`; Impeccable `B`; rota: a home `/` (sem rota nova).
- Embed a auto-carregar: `https://zeno.fm/player/jorge-solla-1313/`; página da rádio: `https://zeno.fm/radio/jorge-solla-1313/`.
- Estado de hoje em produção (verificado 2026-09-20): **0 jingles publicados** — a seção mostra a rádio sozinha na variante `compact`.
- Rastro de terceiros que passa a rodar em todo pageview da home: Google Analytics `G-2T527NZWVM` + redes de anúncio do widget zeno — assumido; sem PII nossa.
- Sem linha nem aviso de transparência sob o embed (decidido com o humano no gate): o player fica nu.
- Design UI (gate): `docs/plans/radio-embed-direto-ui-design.html`.
## Direção no codebase (hipótese)
- **Áreas prováveis:** seção de som da home — `src/components/jingles/` (hoje `JingleHomeSection.tsx` renderiza `RadioFacade.tsx`, que sai) e, se preciso, `src/app/(frontend)/(home)/page.tsx`; CSS da facade em `src/app/(frontend)/styles.css` (`campaign-radio-art/wave/spinner`) tende a ficar órfão e deve sair junto.
- **Precedente a olhar:** S22 (#1228) em `docs/plans/jingles-radio-homepage.md` + `-impl.md`; a leitura cacheada da publicação (tag `jingles`) que alimenta a seção.
- **Risco de acoplamento:** manter a home estática/ISR; não tocar no player nem em `/jingles`; o spec e2e `frontend` roda em paralelo e não stuba zeno — o hermetismo dos testes que pinam a facade hoje terá de ser resolvido na execução (fixture só roteia youtube/facebook e falha em erro de console externo); e2e mede overflow a 390px.
## Dependências
- Nenhuma dura. Estação ativa no zeno.fm; S22 (#1228) entregue (decisão revertida de propósito). Design hi-fi aprovado no gate.
## Fora de escopo
- Consent/CSP/click-to-play global; analytics de reprodução; trocar o embed por outro provedor.
- Alterar a decisão de facade dos jingles publicados; qualquer mudança em `/jingles`.
- Player custom, proxy ou stream próprio.
## Rabbit holes de produto
- **Player custom ou proxy do stream.** Se alguém "só completar": vira projeto de player e infra de stream. **Corte neste item:** embed oficial do zeno.
- **Framework de consent/CSP global.** Se alguém "só completar": banner, categorias, política nova. **Corte:** embed oficial do zeno, nu, sem moldura de consent/aviso.
- **Analytics/contador de ouvintes.** Se alguém "só completar": API do zeno, polling, dado frágil. **Corte:** a rádio é um play, não um dashboard.
- **Redesenhar a seção de som.** Se alguém "só completar": craft da seção inteira, copy e headings. **Corte:** a mudança é remoção do wrapper + moldura mínima do embed; header/copy atuais ficam.
## Questões em aberto (produto)
- **Linha de transparência sob o embed (resolvido no gate):** não existe — o embed fica nu, sem aviso de terceiro. _(decidido com o humano no gate)_
- **Manter a variante compact (zero jingles)?** **Opções:** A) manter o comportamento da seção, com o embed direto no lugar do card | B) unificar os dois casos. **Recomendação:** A — validar no design.
- **O link externo "Abrir no Zeno" some?** **Opções:** A) some com o wrapper | B) manter ao lado do embed. **Recomendação:** A — o embed tem os próprios links.
## Referências
- GitHub Issue: a registrar (`S24`); S22 = Issue #1228 (entregue; decisão revertida de propósito, não reabrir).
- Design UI (gate): `docs/plans/radio-embed-direto-ui-design.html`
- `docs/plans/jingles-radio-homepage.md` e `docs/plans/jingles-radio-homepage-impl.md` — decisão original de facade (histórico; não editar).
- `src/components/jingles/RadioFacade.tsx`, `src/components/jingles/JingleHomeSection.tsx` — o que sai/encaixa.
- `tests/unit/radioFacade.unit.spec.tsx`, `tests/unit/jingleHomeSection.unit.spec.tsx`, `tests/unit/campaignHome.unit.spec.tsx`, `tests/e2e/frontend.e2e.spec.ts`, `tests/e2e/frontendJingles.e2e.spec.ts` — contratos que pinam a facade hoje.
- `AGENTS.md` — home pública estática/cache tag `jingles` e postura LGPD.