docs(C226): frame preview para vídeos da Central

This commit is contained in:
Francisco Solla 2026-09-25 11:23:14 -03:00
parent 1cfeff0250
commit 9404c03df3
2 changed files with 662 additions and 0 deletions

View file

@ -0,0 +1,549 @@
<!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 — C226 · Frame estático para vídeos com arquivo</title>
<style type="text/tailwindcss">
@theme {
--font-sans: Arimo, Arial, sans-serif;
--color-kit-red: #e4102f;
--color-kit-blue: #184e92;
--color-kit-yellow: #ffeb00;
}
:root {
--pt-red: #a21c1c;
--pt-red-dark: #47130e;
--pt-yellow: #ffe607;
--pt-yellow-ink: #000000;
--campaign-cream: #fff8f2;
--campaign-muted: #6c615e;
--campaign-surface: #f9faff;
--campaign-band: #ebe9e9;
--campaign-line: rgb(0 0 0 / 12%);
--site-header: #ae1603;
--display: 'Exo 2', 'Arial Black', Arial, sans-serif;
}
* {
box-sizing: border-box;
}
body {
font-family: Arimo, Arial, sans-serif;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
.display {
font-family: var(--display);
}
.board {
background: #e7e3e0;
background-image: radial-gradient(rgb(24 78 146 / 9%) 0.7px, transparent 0.7px);
background-size: 18px 18px;
}
.artboard {
box-shadow: 0 22px 60px rgb(39 25 22 / 13%);
}
.desktop {
width: 1280px;
}
.phone {
width: 390px;
border-radius: 28px;
}
.scene-heading::after {
content: '';
height: 1px;
flex: 1;
background: #cfc9c5;
}
.card {
overflow: hidden;
border: 1px solid var(--campaign-line);
border-radius: 14px;
background: #fff;
box-shadow: 0 8px 24px rgb(71 19 14 / 7%);
}
.tag {
display: inline-flex;
border-radius: 999px;
background: #eef4fb;
padding: 4px 8px;
color: #184e92;
font-size: 11px;
font-weight: 800;
}
.btn {
display: inline-flex;
min-height: 44px;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 10px;
padding: 9px 14px;
font-family: var(--display);
font-size: 13px;
font-weight: 800;
}
.btn-outline {
border: 1.5px solid rgb(162 28 28 / 28%);
background: #fff;
color: var(--pt-red);
}
.btn-primary {
background: var(--pt-yellow);
color: var(--pt-yellow-ink);
box-shadow: 0 4px 0 #cfb900;
}
.play-button {
display: grid;
width: 64px;
height: 64px;
place-items: center;
border-radius: 999px;
background: var(--pt-yellow);
color: var(--pt-yellow-ink);
box-shadow: 0 6px 0 #cfb900, 0 12px 26px rgb(0 0 0 / 28%);
}
.play-triangle {
width: 0;
height: 0;
margin-left: 4px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 16px solid currentColor;
}
.asset-label {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px dashed rgb(255 255 255 / 60%);
border-radius: 999px;
background: rgb(0 0 0 / 58%);
padding: 5px 9px;
color: #fff;
font-size: 10px;
font-weight: 900;
letter-spacing: 0.08em;
}
.generic-frame {
position: relative;
overflow: hidden;
background:
linear-gradient(90deg, transparent 49.75%, rgb(255 255 255 / 20%) 50%, transparent 50.25%),
linear-gradient(transparent 49.75%, rgb(255 255 255 / 20%) 50%, transparent 50.25%),
radial-gradient(circle at 50% 44%, #8e949a 0 12%, transparent 12.5%),
linear-gradient(145deg, #4b555f 0%, #77828c 46%, #3d474f 100%);
}
.generic-frame::before {
content: '';
position: absolute;
right: 7%;
bottom: -24%;
left: 7%;
height: 58%;
border-radius: 50% 50% 0 0;
background: rgb(24 78 146 / 24%);
}
.neutral-slot {
background:
radial-gradient(circle at 20% 15%, rgb(24 78 146 / 7%) 0 18%, transparent 18.5%),
radial-gradient(circle at 88% 88%, rgb(228 16 47 / 7%) 0 23%, transparent 23.5%),
linear-gradient(135deg, #e5e2df, #f7f5f3);
}
.player-surface {
background:
radial-gradient(circle at 50% 43%, #495057 0 12%, transparent 12.5%),
linear-gradient(145deg, #171717, #343434 50%, #111);
}
.logo-window {
width: 146px;
height: 54px;
overflow: hidden;
}
.logo-window img {
width: 188px;
max-width: none;
transform: translate(-21px, -29px);
}
@media (prefers-reduced-motion: reduce) {
* {
scroll-behavior: auto !important;
transition: none !important;
}
}
</style>
</head>
<body class="board min-h-screen text-black antialiased">
<main class="mx-auto max-w-[1480px] space-y-16 px-4 py-12 md:px-8">
<header class="max-w-3xl">
<span class="rounded-full bg-kit-red px-3 py-1 text-xs font-bold text-white"
>C226 · design para o gate</span
>
<h1 class="display mt-4 text-4xl font-black leading-tight">
Reconheça o vídeo antes de apertar play.
</h1>
<p class="mt-3 max-w-2xl text-sm leading-6 text-[#6c615e]">
O frame é uma pista visual leve. O arquivo de vídeo só entra depois do gesto; quando o
frame não existe, o card continua íntegro e honesto.
</p>
<div class="mt-5 flex flex-wrap gap-2 text-xs font-bold text-[#514945]">
<span class="rounded-full border border-black/10 bg-white px-3 py-2"
>Antes do play · só imagem</span
>
<span class="rounded-full border border-black/10 bg-white px-3 py-2"
>Depois do play · player no mesmo slot</span
>
<span class="rounded-full border border-black/10 bg-white px-3 py-2"
>Falha · sem imagem quebrada</span
>
</div>
</header>
<section aria-labelledby="desktop-title">
<div class="scene-heading mb-3 flex items-center gap-3 text-xs font-bold text-[#6c615e]">
<span>CENA 01</span>
<h2 id="desktop-title">Desktop 1280 · frame e fallback lado a lado no catálogo</h2>
</div>
<div class="overflow-x-auto pb-5">
<div class="desktop artboard mx-auto overflow-hidden rounded-xl bg-white">
<header class="flex h-16 items-center justify-between bg-[#ae1603] px-10">
<div class="logo-window" aria-label="Jorge Solla">
<img src="../../public/campaign-kit/jorge-solla-negativo.png" alt="Jorge Solla" />
</div>
<span class="text-sm font-bold text-white">Central de Conteúdos</span>
</header>
<div class="bg-[linear-gradient(120deg,#e4102f,#b60825_60%,#184e92)] text-white">
<div class="mx-auto flex max-w-6xl items-center justify-between px-10 py-8">
<div>
<p class="text-xs font-black tracking-[0.12em] text-[#ffeb00]">
PEÇA VOTO PRA SOLLA 1313
</p>
<h3 class="display mt-2 text-3xl font-black">Escolha uma peça para compartilhar.</h3>
</div>
<img
src="../../public/campaign-kit/numero-negativo.png"
class="h-20"
alt="1313 Deputado Federal"
/>
</div>
</div>
<div class="mx-auto max-w-6xl px-10 py-9">
<div class="flex items-end justify-between gap-6">
<div>
<h3 class="display text-2xl font-black">Vídeos para ver e enviar</h3>
<p class="mt-1 text-sm text-[#6c615e]">
Escolha pelo frame. O vídeo só carrega quando você apertar play.
</p>
</div>
<span class="rounded-full bg-[#fff3c4] px-3 py-2 text-xs font-bold text-[#6b5100]"
>Conexão preservada antes do play</span
>
</div>
<div class="mt-5 grid grid-cols-3 gap-6">
<article class="card">
<div class="generic-frame relative aspect-video">
<span class="asset-label absolute top-3 left-3 z-10">NEEDS ASSET · FRAME REAL</span>
<button
class="play-button absolute inset-0 z-10 m-auto"
aria-label="Reproduzir Título da peça em vídeo"
>
<span class="play-triangle" aria-hidden="true"></span>
</button>
<span
class="absolute right-3 bottom-3 z-10 rounded bg-black/70 px-2 py-1 text-xs font-bold text-white"
>Duração</span
>
</div>
<div class="p-4">
<span class="tag">Vídeo</span>
<h4 class="mt-2 text-base font-extrabold">Título da peça em vídeo</h4>
<p class="mt-1 text-xs text-[#6c615e]">Tema · Local</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<button class="btn btn-outline">↓ Baixar</button>
<button class="btn btn-outline">↗ Compartilhar</button>
</div>
</div>
</article>
<article class="card">
<div class="neutral-slot relative grid aspect-video place-items-center">
<div class="absolute top-3 left-3 flex items-center gap-2 text-[10px] font-black tracking-[0.1em] text-[#6c615e]">
<span class="grid size-6 place-items-center rounded-full border border-black/10 bg-white" aria-hidden="true"
>×</span
>
FRAME INDISPONÍVEL
</div>
<button
class="play-button relative z-10"
aria-label="Reproduzir outro vídeo"
>
<span class="play-triangle" aria-hidden="true"></span>
</button>
<span
class="absolute right-3 bottom-3 rounded bg-black/70 px-2 py-1 text-xs font-bold text-white"
>Duração</span
>
</div>
<div class="p-4">
<span class="tag">Vídeo</span>
<h4 class="mt-2 text-base font-extrabold">Outro título de vídeo</h4>
<p class="mt-1 text-xs text-[#6c615e]">Tema · Local</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<button class="btn btn-outline">↓ Baixar</button>
<button class="btn btn-outline">↗ Compartilhar</button>
</div>
</div>
</article>
<article class="card flex items-center gap-4 self-start p-3">
<div class="neutral-slot grid h-24 w-32 shrink-0 place-items-center rounded-lg px-3 text-center">
<span class="text-[10px] font-black tracking-[0.08em] text-[#6c615e]"
>CONTEÚDO SEM VÍDEO</span
>
</div>
<div class="min-w-0 flex-1">
<span class="tag">Foto</span>
<h4 class="mt-2 text-sm font-extrabold">Título de outra peça</h4>
<p class="mt-1 text-xs text-[#6c615e]">Contexto da grade preservado</p>
<button class="btn btn-outline mt-3 min-h-9 px-3">Compartilhar</button>
</div>
</article>
</div>
<div class="mt-6 grid grid-cols-2 gap-6 text-xs leading-5">
<p class="rounded-lg border border-[#184e92]/15 bg-[#f9faff] p-3 text-[#514945]">
<b class="text-[#184e92]">Com frame:</b> a imagem ocupa todo o slot de mídia e o
play continua sendo o ponto de maior contraste.
</p>
<p class="rounded-lg border border-black/10 bg-[#faf9f8] p-3 text-[#514945]">
<b>Sem frame:</b> nenhuma tentativa de simular conteúdo; play, duração e informações
da peça permanecem disponíveis.
</p>
</div>
</div>
</div>
</div>
</section>
<section aria-labelledby="mobile-title">
<div class="scene-heading mb-3 flex items-center gap-3 text-xs font-bold text-[#6c615e]">
<span>CENA 02</span>
<h2 id="mobile-title">Mobile 390 · frame antes do play, sem crowding</h2>
</div>
<div class="phone artboard mx-auto min-h-[800px] overflow-hidden bg-white">
<header class="flex h-14 items-center justify-between bg-[#ae1603] px-4">
<div class="logo-window scale-90 origin-left" aria-label="Jorge Solla">
<img src="../../public/campaign-kit/jorge-solla-negativo.png" alt="Jorge Solla" />
</div>
<span class="grid size-11 place-items-center rounded-lg text-xl text-white" aria-label="Menu"
>☰</span
>
</header>
<div class="bg-[linear-gradient(145deg,#e4102f,#184e92)] px-5 py-6 text-white">
<p class="text-xs font-black tracking-[0.1em] text-[#ffeb00]">CENTRAL DE CONTEÚDOS</p>
<h3 class="display mt-1 text-2xl font-black leading-tight">
Escolha pelo frame.<br />Dê play quando quiser.
</h3>
</div>
<div class="p-4">
<p class="mb-3 text-xs leading-5 text-[#6c615e]">
Antes do gesto · imagem leve, sem arquivo de vídeo carregado.
</p>
<article class="card">
<div class="generic-frame relative aspect-video">
<span class="asset-label absolute top-3 left-3 z-10">NEEDS ASSET · FRAME REAL</span>
<button
class="play-button absolute inset-0 z-10 m-auto size-[68px]"
aria-label="Reproduzir Título da peça em vídeo"
>
<span class="play-triangle" aria-hidden="true"></span>
</button>
<span
class="absolute right-3 bottom-3 z-10 rounded bg-black/70 px-2 py-1 text-xs font-bold text-white"
>Duração</span
>
</div>
<div class="p-4">
<div class="flex items-center justify-between gap-3">
<span class="tag">Vídeo</span>
<span class="text-[11px] font-bold text-[#6c615e]">Ainda não carregado</span>
</div>
<h4 class="mt-2 text-lg font-extrabold leading-6">Título da peça em vídeo</h4>
<p class="mt-1 text-xs text-[#6c615e]">Tema · Local</p>
<p class="mt-3 rounded-lg bg-[#f5f5f4] px-3 py-2 text-xs leading-5 text-[#514945]">
Toque no play para carregar só este vídeo.
</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<button class="btn btn-outline">↓ Baixar</button>
<button class="btn btn-outline">↗ Compartilhar</button>
</div>
<a class="mt-3 block min-h-11 py-3 text-center text-xs font-bold text-[#a21c1c]" href="#"
>Ver detalhes da peça →</a
>
</div>
</article>
</div>
</div>
</section>
<section aria-labelledby="after-play-title">
<div class="scene-heading mb-3 flex items-center gap-3 text-xs font-bold text-[#6c615e]">
<span>CENA 03</span>
<h2 id="after-play-title">Depois do play · o player ocupa exatamente o mesmo encaixe</h2>
</div>
<div class="grid gap-8 lg:grid-cols-[390px_1fr] lg:items-start lg:justify-center">
<div class="phone artboard mx-auto overflow-hidden bg-white lg:mx-0">
<div class="border-b border-black/10 px-4 py-4">
<p class="text-[10px] font-black tracking-[0.12em] text-[#a21c1c]">ESTADO APÓS O GESTO</p>
<p class="mt-1 text-sm font-bold">O arquivo de vídeo pode carregar agora.</p>
</div>
<article class="card m-4">
<div class="player-surface relative aspect-video text-white">
<div class="absolute inset-0 grid place-items-center">
<button
class="grid size-14 place-items-center rounded-full bg-white/95 text-lg font-black text-black shadow-lg"
aria-label="Pausar vídeo"
>
Ⅱ
</button>
</div>
<div class="absolute inset-x-3 bottom-3">
<div class="h-1.5 overflow-hidden rounded-full bg-white/35">
<div class="h-full w-[28%] bg-[#ffeb00]"></div>
</div>
<div class="mt-1 flex items-center justify-between text-[10px] font-bold">
<span>0:00</span>
<div class="flex items-center gap-3">
<span aria-hidden="true">◖))</span><span aria-hidden="true">⛶</span>
</div>
</div>
</div>
</div>
<div class="p-4">
<span class="tag">Vídeo · em reprodução</span>
<h4 class="mt-2 text-lg font-extrabold">Título da peça em vídeo</h4>
<p class="mt-1 text-xs text-[#6c615e]">Tema · Local</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<button class="btn btn-outline">↓ Baixar</button>
<button class="btn btn-outline">↗ Compartilhar</button>
</div>
</div>
</article>
</div>
<aside class="artboard max-w-xl rounded-xl bg-white p-6 lg:mt-20">
<p class="text-xs font-black tracking-[0.12em] text-[#184e92]">CONTINUIDADE VISUAL</p>
<h3 class="display mt-2 text-2xl font-black">O card não salta nem muda de hierarquia.</h3>
<ul class="mt-4 space-y-3 text-sm leading-6 text-[#514945]">
<li class="flex gap-3">
<span class="mt-1 grid size-5 shrink-0 place-items-center rounded-full bg-[#eef4fb] text-[11px] font-black text-[#184e92]">1</span>
O player substitui apenas o frame dentro do mesmo aspecto 16:9.
</li>
<li class="flex gap-3">
<span class="mt-1 grid size-5 shrink-0 place-items-center rounded-full bg-[#eef4fb] text-[11px] font-black text-[#184e92]">2</span>
Título, metadados e ações permanecem no mesmo lugar.
</li>
<li class="flex gap-3">
<span class="mt-1 grid size-5 shrink-0 place-items-center rounded-full bg-[#eef4fb] text-[11px] font-black text-[#184e92]">3</span>
Os controles deixam evidente que este estado só existe depois do play.
</li>
</ul>
</aside>
</div>
</section>
<section aria-labelledby="fallback-title">
<div class="scene-heading mb-3 flex items-center gap-3 text-xs font-bold text-[#6c615e]">
<span>CENA 04</span>
<h2 id="fallback-title">Frame ausente · fallback neutro, sem imagem quebrada</h2>
</div>
<div class="artboard mx-auto max-w-5xl rounded-xl bg-white p-6 md:p-8">
<div class="grid gap-8 md:grid-cols-[360px_1fr] md:items-center">
<article class="card">
<div class="neutral-slot relative grid aspect-video place-items-center">
<div class="absolute top-3 left-3 flex items-center gap-2">
<span
class="grid size-7 place-items-center rounded-full border border-black/10 bg-white text-sm font-bold text-[#6c615e]"
aria-hidden="true"
>×</span
>
<span class="text-[10px] font-black tracking-[0.1em] text-[#6c615e]"
>FRAME INDISPONÍVEL</span
>
</div>
<button class="play-button" aria-label="Reproduzir Título da peça em vídeo">
<span class="play-triangle" aria-hidden="true"></span>
</button>
<span
class="absolute right-3 bottom-3 rounded bg-black/70 px-2 py-1 text-xs font-bold text-white"
>Duração</span
>
</div>
<div class="p-4">
<span class="tag">Vídeo</span>
<h4 class="mt-2 text-base font-extrabold">Título da peça em vídeo</h4>
<p class="mt-1 text-xs text-[#6c615e]">Tema · Local</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<button class="btn btn-outline">↓ Baixar</button>
<button class="btn btn-outline">↗ Compartilhar</button>
</div>
</div>
</article>
<div>
<span class="rounded-full bg-[#f5f5f4] px-3 py-1.5 text-xs font-extrabold text-[#514945]"
>Falha segura</span
>
<h3 class="display mt-4 text-3xl font-black leading-tight">
A falta do frame não parece erro da página.
</h3>
<p class="mt-3 max-w-xl text-sm leading-6 text-[#6c615e]">
O slot usa uma superfície neutra, sem ícone de imagem quebrada e sem fingir que há uma
cena disponível. A pessoa ainda pode reproduzir o vídeo e reconhecer a peça pelo título
e pelos metadados.
</p>
<div class="mt-5 rounded-xl border border-[#a21c1c]/15 bg-[#fff8f2] p-4">
<p class="text-xs font-black tracking-[0.1em] text-[#a21c1c]">NÃO FAZER</p>
<p class="mt-1 text-sm leading-6 text-[#514945]">
Não mostrar miniatura de plataforma externa, não prometer um timestamp e não ocultar o
card quando a imagem faltar.
</p>
</div>
</div>
</div>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,113 @@
# C226 — Central de Conteúdos: frame estático para vídeos com arquivo
Status: rascunho
Atualizado em: 2026-09-25
Issue: #1352
Priority: P2
Impeccable: B — encaixe no slot de mídia já existente do card público, sem rota ou tela nova
Design UI: docs/plans/central-conteudos-preview-frame-video-ui-design.html
Appetite: ~1–2 dias eng; um outcome verificável — reconhecer um vídeo com arquivo antes de reproduzi-lo
Responsável: —
## Intenção
Na Central pública, o carregamento do vídeo só acontece depois do play. Isso é bom para quem está numa conexão lenta, mas deixa o card sem uma pista visual quando a pessoa ainda está escolhendo o que vale assistir.
Este item quer uma imagem leve e estática para cada peça de vídeo que já tem arquivo no acervo da Central. A pessoa deve reconhecer a peça antes de apertar play, sem buscar o arquivo de vídeo antecipadamente.
## Persona e fluxo
- **Persona / contexto:** visitante da campanha no celular ou desktop, folheando peças para escolher material para assistir e compartilhar.
- **Job principal:** distinguir vídeos pelo que aparece na tela e decidir qual deles quer reproduzir, sem abrir vários players.
- **Fluxo desejado:** abre `/conteudos` → vê o frame estático, o botão de play e os metadados → toca em play quando escolher → o vídeo carrega naquele momento.
- **Anti-goals de produto:** não transformar a Central em galeria pesada, não carregar todos os vídeos, não criar player ou tela de edição, não baixar mídia de terceiro e não misturar esta fatia com o acervo de falas.
### Esboço de fluxo (B)
```text
[/conteudos]
→ card de vídeo com arquivo
→ frame estático + play + título/metadados
→ a pessoa reconhece a peça e decide
→ toca no play
→ somente esse vídeo é buscado e reproduzido
→ sem frame disponível: slot neutro + play honesto
```
### Design UI (B)
- Design UI (gate): `docs/plans/central-conteudos-preview-frame-video-ui-design.html` — cenas desktop e mobile do card antes do play, fallback neutro e contraste entre frame e player durante a reprodução.
## Objetivo e aceite
- Toda peça de vídeo publicada e com arquivo no acervo exibe pelo menos um frame estático no catálogo público antes do play.
- O frame é uma pista visual leve: não promete representar um momento específico, não substitui o arquivo e não altera a reprodução.
- A peça não busca o arquivo de vídeo antes do play. O frame pode ser carregado como imagem, mas não deve puxar o vídeo nem o áudio.
- Se o frame não puder ser produzido, o card mantém um estado neutro com play e não mostra imagem quebrada nem tenta repetir a busca sem limite.
- Peça-link de YouTube ou Instagram, áudio, foto, texto e card mantêm o comportamento atual; esta entrega não promete frame local para uma peça que não tem arquivo.
- O kill switch continua valendo: uma peça despublicada não expõe sua mídia privada pelo novo preview.
- A lista interna `/campanha/comunicacao/conteudos` não ganha player nem uma segunda visualização de frame; a lista pública é o alvo obrigatório.
## Dados (intenção)
- **Vou apresentar dados?** Não — não há número, série, ranking ou métrica. O frame serve para reconhecimento visual.
- **Decisões desbloqueadas:** a pessoa escolhe qual vídeo reproduzir pela imagem; a equipe de conteúdo não precisa abrir cada peça para descobrir a cena.
- **Forma:** _adiada ao plano de implementação_ — nenhuma tabela, gráfico ou contador; a restrição é uma imagem leve e estática.
## Dados da decisão (literais)
- Identificador: `C226`.
- Rota pública: `/conteudos`.
- Regra visual: **pelo menos um frame estático por peça de vídeo com arquivo**.
- Regra de carregamento: **nenhum request do arquivo de vídeo antes do play**.
- Regra de falha: **fallback neutro sem imagem quebrada**.
- Peças com `sourceUrl` e sem arquivo local continuam peça-link; thumbnail remoto de plataforma não conta como frame do arquivo nesta fatia.
- O frame é uma pista de reconhecimento, não uma escolha editorial obrigatória nem um novo arquivo para baixar.
## Direção no codebase (hipótese)
- **Áreas prováveis:** `src/components/conteudos/ContentPieceMedia.tsx` e `ContentPieceCard.tsx` para o slot de mídia; `src/lib/contentPieceCatalog.ts` e `src/utilities/content/contentPieceReads.ts` para a projeção pública; `src/collections/ContentMedia.ts`, `src/collections/ContentPiece.ts` e o processamento de `src/utilities/content/contentPieceJob.ts` para a origem do arquivo. A forma de gerar, guardar e invalidar o frame fica para o plano de implementação.
- **Precedente a olhar:** o contrato lazy do S27 em `docs/plans/central-conteudos-publica.md:44-66`; a separação entre arquivo e peça-link em `docs/plans/central-conteudos-ingestao.md:42-51` e `central-conteudos-link-instagram.md:47-60`; a política de mídia privada já documentada em `ContentMedia.ts`.
- **Risco de acoplamento:** `ContentPieceMedia` também é usado em outras superfícies públicas. O executor deve preservar o play sob demanda, o kill switch e a separação entre arquivo privado e peça-link; não deve reusar nem alterar o preview do acervo de falas C182.
## Dependências
- C211 — pipeline e acervo de peças com arquivo.
- S27 — catálogo público, filtro e contrato de reprodução sob demanda.
- C193/C199 e C220 — soft: preservam a política de mídia privada e a distinção entre arquivo oficial e link.
## Fora de escopo
- Preview do acervo de falas em `/campanha/comunicacao/acervo` e o frame do C182.
- Player, autoplay, hover-play, preview em movimento, editor de timestamp, seleção editorial ou recorte de vídeo.
- Download, persistência ou thumbnail de mídia de terceiros; peça-link continua circulando pelo link.
- Backfill/reprocessamento em massa, nova tela de ingestão, nova analytics, Consent ou alteração do kill switch.
- Mudança na lista interna de comunicação, no estúdio de cards ou no shell da Central.
## Rabbit holes de produto
- **Gerar frame para tudo.** Se alguém “só completar”, o item vira fila, backfill, armazenamento e reconciliação de todos os vídeos. **Corte neste item:** peças de vídeo com arquivo no acervo; a forma de geração fica no impl-plan.
- **Usar thumbnail de plataforma como se fosse frame local.** Isso mistura uma origem externa com um arquivo oficial e promete uma imagem que não pertence à peça. **Corte:** peça-link não recebe frame local nesta fatia.
- **Fazer o vídeo carregar junto da imagem para ficar mais rápido.** Isso desfaz o ganho do carregamento sob demanda. **Corte:** imagem antes, arquivo apenas depois do play.
- **Abrir um editor de frame.** Escolher timestamp, cortar imagem e revisar tudo vira trabalho de produção. **Corte:** um frame automático ou o fallback neutro.
## Questões em aberto (produto)
- **Qual frame usar quando o vídeo é elegível?** **Opções:** A) primeiro frame decodificável | B) frame representativo escolhido por uma regra determinística, como uma fração da duração | C) seleção manual pela assessoria. **Recomendação:** A — atende ao pedido com uma decisão simples e evita fingir que existe um momento editorial escolhido. _(assumido — validar no gate)_
- **O que fazer quando a extração falhar?** **Opções:** A) fallback neutro com play | B) thumbnail de uma plataforma externa | C) esconder o card. **Recomendação:** A — o card continua honesto e não inventa uma origem. _(assumido — validar no gate)_
- **O preview precisa aparecer também na página individual?** **Opções:** A) tornar obrigatório também na página da peça | B) validar apenas o catálogo nesta fatia. **Recomendação:** B — o relato é sobre o catálogo; qualquer efeito em uma superfície compartilhada será avaliado sem criar um segundo outcome. _(assumido — validar no gate)_
## Referências
- `docs/plans/central-conteudos-publica.md:40-80` — S27, rota, slot de mídia e play sob demanda.
- `docs/plans/central-conteudos-ingestao.md:42-76` — C211, arquivo versus peça-link e processamento.
- `docs/plans/central-conteudos-link-instagram.md:40-67` — C220, mídia oficial e limites de terceiro.
- `src/components/conteudos/ContentPieceMedia.tsx:147-196` — estado atual de vídeo antes do play.
- `src/components/conteudos/ContentPieceCard.tsx:132-144` — slot de mídia do card público.
- `src/lib/contentPieceCatalog.ts:655-739` — projeção pública do arquivo e distinção `isLink`.
- `src/collections/ContentMedia.ts:1-45` — dono atual do arquivo privado.
- `AGENTS.md` e `AGENTS-public.md` — convenções de mídia, UI pública e limites de produto.
## Self-score (shaping)
**5/5** — (1) a fatia tem um outcome verificável: frame antes do play para vídeo com arquivo; (2) o apetite declarado comporta a geração, a projeção e o fallback sem abrir uma fila editorial; (3) persona, job e aceite estão em linguagem de produto; (4) a direção no codebase é hipótese; (5) não há assinatura, migration, schema ou estratégia de armazenamento decidida no plano.