# Catálogo de animações

Cada tipo tem: quando usar, os campos no `projeto.json`, o que o `build.py` escreve (HTML, CSS e GSAP) e o som
que pede. Os trechos saíram de uma composição real gerada pelo modelo (estilo `papelaria`, 1080x1920); os
números mudam com o rosto e com a fala de cada vídeo. Se nenhuma peça servir, escreva uma cena sob medida à
mão (`cenas_proprias` no `projeto.json`, contrato em `modelo-projeto/compositions/cenas/LEIA.md`) seguindo os
mesmos padrões e as pegadinhas do fim.

## O esqueleto que todas usam

O `index.html` tem o vídeo, a câmera, a legenda e um lugar (host) para cada peça. Cada peça do catálogo vira
uma sub-composição própria em `compositions/<id>.html`, e por isso ganha uma faixa com nome na linha do
tempo do estúdio ("Rótulo · Produto digital", "Texto atrás · O FIM"). As cenas sob medida entram do mesmo
jeito, de `compositions/cenas/`.

```html
<script src="assets/gsap.min.js"></script>   <!-- ou o CDN do GSAP 3.14.2 -->
<div id="root" data-composition-id="main" data-start="0" data-duration="10.0" data-width="1080" data-height="1920" data-fps="30">
  <div id="palco">                          <!-- tudo que treme junto no carimbo -->
    <div id="bg"></div>                     <!-- papel quadriculado (moldura e tela verde) -->
    <div id="borda"></div>                  <!-- aro da moldura -->
    <div id="frame">                        <!-- enquadramento: clip-path no frame, x/y/scale na .camera -->
      <div id="inner" class="camera">
        <video id="cam" src="assets/cut.mp4" muted playsinline data-start="0" data-duration="10.0" data-media-start="17.9"
               data-track-index="0" data-timeline-label="Vídeo do corte"></video>
      </div>
      <div id="atras"> ...hosts do texto atrás da pessoa... </div>
      <div id="innerp" class="camera"> ...pessoa recortada (webm com transparência)... </div>
    </div>
    <div id="pecas">
      <div id="faixa-tag" class="faixa" data-composition-id="peca-tag" data-composition-src="compositions/tag.html"
           data-start="2.39" data-duration="1.62" data-track-index="10" data-timeline-label="Rótulo · Produto digital"
           data-width="1080" data-height="1920"></div>
      <div class="cap caption-cue clip" id="cap0" data-start="0" data-duration="1.34" data-track-index="1"
           data-track-kind="captions" data-timeline-label="Legenda · EU GRAVEI">EU GRAVEI</div>
    </div>
  </div>
  <audio id="voz" src="assets/cut.mp4" data-start="0" data-duration="10.0" data-media-start="17.9" data-track-index="80"></audio>
</div>
<script>
const tl = gsap.timeline({ paused: true });   // câmera, moldura, legenda e a tremida do carimbo
window.__timelines["main"] = tl;
</script>
```

E cada peça, em `compositions/tag.html` (o build gera e apaga a cada vez; não edite à mão):

```html
<template id="peca-tag-template">
<style>
@font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
#root{position:absolute;inset:0;font-family:Montserrat}
.rotulo{...}
</style>
<div id="root" data-composition-id="peca-tag" data-width="1080" data-height="1920" data-duration="1.72">
  <div class="rotulo" id="tag" style="left:264px;top:444px;width:551px;height:100px;font-size:46px"><span>Produto <b>digital</b></span></div>
</div>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
tl.fromTo("#tag",{opacity:0,y:-44},{opacity:1,y:0,duration:0.35,ease:"back.out(2)",immediateRender:false},0);
tl.to("#tag",{opacity:0,y:-28,duration:0.25,ease:"power3.in",immediateRender:false},1.161);
window.__timelines["peca-tag"] = tl;
})();
</script>
</template>
```

- Os trechos de JS das seções abaixo mostram o tempo como se tudo estivesse numa timeline só (tempo do
  corte menos o início da amostra). No arquivo gerado, cada peça conta o tempo a partir do começo dela
  (0 = primeiro movimento da peça); o host no `index.html` põe esse 0 no lugar certo (`data-start`). Se a
  amostra começa no meio da peça, o host leva `data-playback-start` e a peça continua do ponto certo.
- Na linha do tempo do estúdio: vídeo, legenda (faixa 1, um clipe por legenda), peças (10 em diante), cenas
  sob medida (30 em diante), pessoa recortada (40), estrutura (câmera, moldura, papel: 60 em diante), voz (80)
  e sons (81 em diante). Faixa com mais de um clipe aparece como "Track N".
- Tempo na timeline = tempo do corte menos o início da amostra (no `completo`, o próprio tempo do corte).
  Nunca negativo.
- Toda peça nasce com `opacity: 0` no CSS e entra por `fromTo`. Depois do primeiro tween de um elemento,
  `immediateRender: false`.
- Posição fixa no `style` (left/top) é calculada uma vez pelo `build.py`; o movimento é só `x`, `y`,
  `rotation`, `opacity` e `clip-path`, sem escala (pegadinha 25): dois renders do mesmo projeto saem iguais.
- Imagem de peça (polaroide, objeto na mão, frente, logo do cartão final) vai no tamanho em que aparece: o
  build reduz uma cópia para `assets/gerado/<nome>_<largura>x<altura>` (a original não muda; imagem que já cabe
  fica como está).
- Visual `papelaria`: cartão `#fffdfd` com base lilás (`box-shadow: 0 12px 0 -2px #c7b7ef, 0 22px 44px
  rgba(53,44,64,.35), inset 0 -8px 0 rgba(199,183,239,.35)`), texto `#352c40`, destaque `#665080`,
  verde `#2e9e5b`, vermelho `#e5484d`, Montserrat 900 e Caveat 700 (fontes em `assets/fontes/`).
- Visual `laranja` (`"estilo": "laranja"`): cartão escuro `#15161d` com fio laranja (`box-shadow: 0 0 0 2px
  rgba(255,122,26,.6), ...`), texto claro `#f5f5f7`, destaque `#ff7a1a` (o texto e o ícone sobre o laranja saem
  `#140a02`), verde `#35d07f`, vermelho `#ff3b30`, fundo `#0c0d13` com grade e brilho laranja. A mesma peça, as
  cores vêm dos tokens do estilo (`ESTILOS` no `build.py`).

Campos comuns a todos os tipos: `id` (sem espaço, único), `tipo`, `em` (segundos ou a palavra),
`depois_de`, `sai` (segundos ou a palavra), `sai_depois_de`, `antecipa` (padrão 0,1 s), `onde` (`auto`,
`acima`, `topo`, `baixo_esquerda` ou `[x, y]`), `esconde_legenda` (`true` quando a peça já escreve a fala).
Peça com o `sai` no fim do corte (até 0,25 s antes) fica até o último quadro, sem saída (com `segura_fim`, até o fim
do quadro parado).

**Na moldura, o quadro vence.** A moldura leva 0,7 s para fechar (a pessoa encolhe da tela cheia para o
quadro). Peça cuja palavra cai nesse tempo (ou até 0,4 s antes da troca) entra quando o quadro termina de
fechar, e o build avisa (`aviso: lista: entra em 7.79 s, quando o quadro da moldura terminou de fechar`). Os
itens de lista, passo, notificação e foto que ficariam antes disso entram logo depois, em ordem. A peça chega
uns 0,3 a 0,6 s depois da palavra: é melhor que cobrir o rosto ainda grande. Numa cena própria, faça igual com
`window.__cenas[...].moldura.fechada_em` (o build avisa quando a cena começa antes).

**Legenda não é peça.** A legenda do modelo tem dois estilos (`"legenda": {"estilo": "padrao"}` ou
`"discreta"`), no máximo 3 palavras por vez (`max_palavras`), e nenhuma palavra dita fica de fora (a frase se divide
por sentido; a palavra que piscaria junta com a vizinha). Legenda "papelaria" (a frase se escrevendo à
mão), "Impacto" ou "Manchete" é cena própria: `"esconde_legenda": true` na cena e a fala escrita por ela, com as
`palavras` do trecho (`modelo-projeto/compositions/cenas/LEIA.md`).

---

## 1. Rótulo em cima da cabeça (`rotulo`)

**Quando:** a pessoa diz o nome de uma coisa que a gente quer fixar ("produto digital", "Sistema de IA",
"um celular", "30 min por dia"). Peça pequena: cabe em tela cheia. Na prova, entra sozinho pelo campo
`rotulo` do bloco, 0,62 s depois do corte (o whoosh da troca vem primeiro e o pop do rótulo não briga com ele).

```json
{"id": "tag", "tipo": "rotulo", "texto": "Produto *digital*", "em": "produtos", "depois_de": 20,
 "sai": "Produtos", "sai_depois_de": 21.5}
```

O `build.py` mede o topo da cabeça na tomada (rosto do `face_layout.json` mais 35% da altura do rosto, para
o cabelo e o boné) e põe o rótulo 40 px acima. Sem espaço acima, vai para baixo do queixo.

```html
<div class="rotulo" id="tag" style="left:264px;top:444px;width:551px;height:100px;font-size:46px"><span>Produto <b>digital</b></span></div>
```
```css
.rotulo{position:absolute;border-radius:999px;display:flex;align-items:center;justify-content:center;
  font-weight:900;color:#352c40;white-space:nowrap;opacity:0;background:#fffdfd;box-shadow:(cartão)}
.rotulo b{color:#665080}
```
```js
tl.fromTo("#tag",{opacity:0,y:-44},{opacity:1,y:0,duration:0.35,ease:"back.out(2)",immediateRender:false},2.494);
tl.to("#tag",{opacity:0,y:-28,duration:0.25,ease:"power3.in",immediateRender:false},3.655);
```
Som: `pop` 0,05 s depois da entrada. Cuidado: o `<span>` em volta do texto é obrigatório (texto solto em
caixa flex perde o espaço antes do `<b>`: sai "Produtodigital").

## 2. Carimbo (`carimbo`)

**Quando:** afirmação curta que fecha uma ideia ("MIGALHAS", "PRONTO!", "EM SEGUNDOS"). 1 ou 2 palavras.
Vermelho para o que é ruim ou negado, `"cor": "verde"` para o que deu certo. Use `esconde_legenda` se o
carimbo escreve a mesma palavra da fala.

```json
{"id": "mig", "tipo": "carimbo", "texto": "MIGALHAS", "em": "migalhas", "sai": 9.9, "esconde_legenda": true}
```
```html
<div class="carimbo" id="mig" style="left:311px;top:427px;width:458px;height:114px;font-size:64px">MIGALHAS</div>
```
```css
.carimbo{position:absolute;display:flex;align-items:center;justify-content:center;border:7px solid #e5484d;
  border-radius:16px;color:#e5484d;font-weight:900;letter-spacing:2px;white-space:nowrap;opacity:0;background:rgba(255,253,253,.94)}
.carimbo.verde{border-color:#2e9e5b;color:#2e9e5b}
```
```js
// cai e bate: vem de cima (1,6 vezes a altura dele) acelerando até o impacto (power4.in) e girando, sem escala
tl.fromTo("#mig",{opacity:0,y:-182,rotation:-18},{opacity:1,y:0,rotation:-8,duration:0.28,ease:"power4.in",immediateRender:false},8.277);
// no quadro do impacto a tela treme (o #palco inteiro, nunca o #root)
tl.fromTo("#palco",{x:0},{keyframes:[{x:-8,duration:0.04},{x:7,duration:0.04},{x:0,duration:0.05}],immediateRender:false},8.557);
tl.to("#mig",{opacity:0,y:-16,duration:0.2,ease:"power2.in",immediateRender:false},9.7);
```
Som: `impacto` no pouso (entrada + 0,27 s), nível chave. `"tremer": false` desliga a tremida.

## 3. Lista com check ou riscada (`lista_check`)

Cada item da `lista_check` cabe em até uns 22 caracteres no 9:16. Texto maior estoura o cartão e a bolinha do X ou do check sai da borda, e o `hf check` não avisa. Divida em dois itens ou encurte.

**Quando:** a fala enumera ("espaço, estoque, frete"; "não preciso aparecer, ter seguidores, criar do
zero"). Cada item entra na SUA palavra. `"marca": "check"` (verde, o que tem) ou `"marca": "x"` (risca o
texto e bate um X vermelho, o que não precisa). Peça grande: moldura.

```json
{"id": "sem", "tipo": "lista_check", "titulo": "Sem *custo* de", "marca": "check", "depois_de": 22, "sai": 27.9,
 "itens": [{"texto": "Espaço", "em": "espaço"}, {"texto": "Estoque", "em": "estoque"}, {"texto": "Frete", "em": "frete"}]}
```
```html
<div class="lista" id="sem" style="left:150px;top:326px;width:780px">
  <div class="tit" style="font-size:44px;margin-bottom:12px">Sem <b>custo</b> de</div>
  <div class="linha" id="sem-l0" style="height:110px"><span class="txt" style="font-size:44px">Espaço</span>
    <span class="marca ok" id="sem-m0"><svg viewBox="0 0 40 40"><path pathLength="1" d="M8 21 L17 30 L33 11"/></svg></span></div>
  <!-- ...uma .linha por item... -->
</div>
```
```css
.linha{position:relative;display:flex;align-items:center;gap:20px;border-top:3px dashed #e6ddf8;opacity:0}
.marca{margin-left:auto;width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;opacity:0;flex:none}
.marca.ok{background:#2e9e5b} .marca.x{background:#e5484d}
.marca path{fill:none;stroke:#fff;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1;stroke-dashoffset:1}
.risco{position:absolute;left:-8px;right:-8px;top:50%;height:8px;margin-top:-4px;border-radius:4px;background:#e5484d;opacity:0}
```
```js
tl.fromTo("#sem",{opacity:0,y:64},{opacity:1,y:0,duration:0.35,ease:"back.out(2)",immediateRender:false},23.625);
// por item: linha desliza na palavra, a marca bate 0,25 s depois e o traço se desenha
tl.fromTo("#sem-l0",{opacity:0,x:-40},{opacity:1,x:0,duration:0.3,ease:"back.out(2)",immediateRender:false},23.975);
// a bolinha bate: cai acelerando e se abre do meio para fora (clip-path circle), sem escala
tl.fromTo("#sem-m0",{opacity:0,y:-26,clipPath:"circle(20% at 50% 50%)"},{opacity:1,y:0,clipPath:"circle(71% at 50% 50%)",duration:0.18,ease:"power4.in",immediateRender:false},24.225);
tl.fromTo("#sem-m0 path",{strokeDashoffset:1},{strokeDashoffset:0,duration:0.25,ease:"power2.out",immediateRender:false},24.275);
// com "marca": "x", também o risco crescendo da esquerda:
// tl.fromTo("#sem-r0",{opacity:1,clipPath:"inset(0% 100% 0% 0%)"},{clipPath:"inset(0% 0% 0% 0%)",duration:0.22,ease:"power2.out",immediateRender:false},24.275);
tl.to("#sem",{opacity:0,y:-28,duration:0.25,ease:"power3.in",immediateRender:false},27.65);
```
Som: `pop` no cartão; `tique` (ou `risco`) em cada marca. `pathLength="1"` no path deixa o traço de 0 a 1
sem medir o comprimento. Item cuja palavra cai depois da saída sai da lista, com aviso.

## 4. Passo a passo (`passo_a_passo`)

**Quando:** a pessoa descreve etapas em ordem ("copiei, colei, ativei"). Caixas numeradas uma embaixo da
outra, seta se desenhando entre elas, e um carimbo final opcional ("PRONTO!").

```json
{"id": "passos", "tipo": "passo_a_passo", "depois_de": 28,
 "passos": [{"texto": "Você posta", "em": "preocupar"}, {"texto": "Cliente compra", "em": "entregar"}, {"texto": "Recebe na hora", "em": "recebe"}],
 "final": {"texto": "PRONTO!", "em": "automática"}}
```
```html
<div id="passos">
  <div class="passo" id="passos-p0" style="left:240px;top:321px;width:600px;height:118px"><span class="n">1</span><b style="font-size:48px">Você posta</b></div>
  <svg class="seta" id="passos-s1" style="left:520px;top:439px;width:40px;height:52px" viewBox="0 0 40 52"><path pathLength="1" d="M20 4 L20 44 M9 33 L20 46 L31 33"/></svg>
  <div class="passo" id="passos-p1" ...>...</div>
</div>
```
```js
tl.fromTo("#passos-p0",{opacity:0,x:-60},{opacity:1,x:0,duration:0.35,ease:"back.out(2)",immediateRender:false},29.375);
tl.fromTo("#passos-s1",{opacity:0},{opacity:1,duration:0.05,immediateRender:false},30.005);
tl.fromTo("#passos-s1 path",{strokeDashoffset:1},{strokeDashoffset:0,duration:0.3,immediateRender:false},30.005);   // seta antes do passo
tl.fromTo("#passos-p1",{opacity:0,x:-60},{opacity:1,x:0,duration:0.35,ease:"back.out(2)",immediateRender:false},30.355);
tl.to("#passos > *",{opacity:0,y:-20,duration:0.2,stagger:0.03,ease:"power3.in",immediateRender:false},31.9);
```
Som: `clique` em cada passo, `impacto` no carimbo final.

## 5. Número contando (`contador`)

**Quando:** a fala dá um número que importa: quantidade ("3 formatos", "40 produtos"), tempo ("30 dias"),
preço.

```json
{"id": "cont", "tipo": "contador", "valor_inicial": 0, "valor_final": 3, "sufixo": " formatos",
 "rotulo": "feitos pela IA", "em": "e-books", "sai": 45.8, "duracao": 0.9, "casas": 0}
```
```html
<div class="contador" id="cont" style="left:286px;top:394px;width:507px;height:250px">
  <div class="num" style="font-size:120px"><i></i><span id="cont-v">&#8203;0</span><i> formatos</i></div>
  <div class="rot">feitos pela IA</div></div>
```
```js
tl.fromTo("#cont",{opacity:0,y:60},{opacity:1,y:0,duration:0.35,ease:"back.out(2)",immediateRender:false},43.395);
// um valor por quadro (dados + laço): o seek em qualquer quadro mostra o número certo
for (const [t, p] of [[43.6,{textContent:"​0"}],[43.633,{textContent:"​0"}],[43.666,{textContent:"​1"}] /* ... */]) tl.set("#cont-v", p, t);
tl.fromTo("#cont",{y:0},{keyframes:[{y:-14,duration:0.12,ease:"power2.out"},{y:0,duration:0.25,ease:"power2.in"}],immediateRender:false},44.495);   // pousa: um pulinho
```
O valor sobe com `1 - (1 - u)^3` (rápido no começo, assenta no fim), formatado em pt-BR (1.250,5). Som:
`contagem` no começo e `impacto` quando pousa. O texto nasce com o valor inicial no HTML e com o espaço de
largura zero na frente (pegadinhas 2 e 3).

## 6. Notificação no celular (`notificacao`)

**Quando:** a fala conta que vendas ou mensagens chegam ("as vendas começam a cair"). Um celular sobe e as
notificações caem uma por palavra ("Nova venda", "Pedido aprovado"). Peça grande (400 x 700): moldura.

```json
{"id": "cel", "tipo": "notificacao", "titulo": "Hoje", "depois_de": 33, "sai": 40.3,
 "notas": [{"texto": "Nova venda", "em": "custo"}, {"texto": "Nova venda", "em": "produção"}]}
```
```html
<div class="cel" id="cel" style="left:340px;top:237px;width:400px;height:700px"><div class="tela"><div class="hoje">Hoje</div>
  <div class="nt" id="cel-n0"><span class="ic">(sacola em svg)</span><b>Nova venda</b><span class="ok">(check em svg)</span></div>
</div></div>
```
```js
tl.fromTo("#cel",{opacity:0,y:120},{opacity:1,y:0,duration:0.5,ease:"back.out(1.5)",immediateRender:false},33.925);
tl.fromTo("#cel-n0",{opacity:0,y:-80},{opacity:1,y:0,duration:0.3,ease:"back.out(2)",immediateRender:false},34.425);
tl.to("#cel",{opacity:0,y:60,duration:0.3,ease:"power3.in",immediateRender:false},40.0);
```
Som: `whoosh` quando o celular sobe, `ding` na primeira notificação (chave), `pop` nas outras.

## 7. Foto em polaroide (`polaroide`)

**Quando:** a pessoa cita algo real dela ("meu carro", "minhas viagens", "meu produto"). Só foto
VERDADEIRA da pessoa, copiada para `assets/`. Nada de foto de banco fingindo ser dela. Uma ou duas fotos.

```json
{"id": "fotos", "tipo": "polaroide", "sai": 52.2,
 "fotos": [{"arquivo": "assets/carro.jpg", "legenda": "meu carro", "em": "carro", "foco": "50% 72%"},
           {"arquivo": "assets/viagem.jpg", "legenda": "minhas viagens", "em": "viagem"}]}
```
`foco` é o `object-position` da foto (onde fica o assunto dentro do quadrado).
```html
<div class="pola" id="fotos-f0" style="left:150px;top:335px;width:330px;height:420px;padding:16px 16px 0">
  <img src="assets/carro.jpg" alt="" style="width:298px;height:330px;object-position:50% 72%"/><span style="font-size:46px;line-height:67px">meu carro</span></div>
```
```js
// cai girando e assenta torta: -6° a primeira, +5° a segunda
tl.fromTo("#fotos-f0",{opacity:0,y:-120,rotation:-15.6},{opacity:1,y:0,rotation:-6,duration:0.4,ease:"back.out(1.7)",immediateRender:false},47.095);
tl.to("#fotos > .pola",{opacity:0,y:-40,duration:0.25,stagger:0.05,ease:"power3.in",immediateRender:false},51.9);
```
Som: `camera` 0,12 s depois de cada foto. Legenda na Caveat (letra de mão).

## 8. Logo ou objeto preso na mão (`objeto_na_mao`)

**Quando:** a pessoa fala de uma coisa e "segura" ela no ar ("posso colocar aqui a logo e ela vai
acompanhar"). A imagem (PNG com fundo transparente) nasce na mão e segue a palma quadro a quadro, com um
atraso curto de mola. Precisa do rastreio das mãos antes:

`uv run --python 3.12 --with mediapipe==0.10.21 --with numpy python ferramentas/rastrear_maos.py --edit '<EDIT>' --de 19 --ate 22`

Versão fixa: o mediapipe 1.x (que o uv pega sem versão, no Python 3.13) aborta com "Service is unavailable". A
primeira vez o uv baixa o Python 3.12 e o mediapipe, uns minutos: avise a pessoa antes. Depois fica guardado.

```json
{"id": "logo", "tipo": "objeto_na_mao", "imagem": "assets/logo.png", "mao": "esquerda_da_imagem",
 "em": 19.2, "sai": 21.6, "tamanho": 170, "folga": 30}
```
`mao`: `esquerda_da_imagem`, `direita_da_imagem` ou `qualquer` (o lado é o da IMAGEM; o rótulo Left/Right
do MediaPipe engana). O objeto fica `folga` px acima dos nós dos dedos (a palma costuma encostar na borda
de baixo). Use em tela cheia, dentro de uma tomada só (o rastreio recomeça em cada corte).
```html
<div class="namao" id="logo"><div class="pop" id="logo-p" style="left:-85px;top:-85px;width:170px;height:170px"><img src="assets/logo.png" alt=""/></div></div>
```
```js
// posição por quadro: mola (frequência 3,4 Hz, amortecimento 0,45) calculada no build a partir da palma; inclina com a velocidade
for (const [t, p] of [[19.2,{x:622.7,y:930.5,rotation:0}],[19.233,{x:620.9,y:930.6,rotation:0.53}] /* ... */]) tl.set("#logo", p, t);
// nasce da mão: um círculo que se abre do meio (a sombra cabe no círculo aberto) e sobe um pouco, sem escala
tl.fromTo("#logo-p",{opacity:0,y:24,clipPath:"circle(12% at 50% 50%)"},{opacity:1,y:0,clipPath:"circle(100% at 50% 50%)",duration:0.4,ease:"back.out(2)",immediateRender:false},19.2);
tl.to("#logo-p",{opacity:0,clipPath:"circle(12% at 50% 50%)",duration:0.18,ease:"power2.in",immediateRender:false},21.42);
```
A posição da tela considera o enquadramento da câmera naquele quadro (zoom e deslocamento do `#inner`).
Quadro sem mão segura a última posição (nunca pisca). Som: `pop` na entrada (chave) e na saída. Só logo que
a pessoa tem direito de usar (a dela, ou a de uma ferramenta citada, sem parecer anúncio da marca).

## 9. Painel "editando ao vivo" com timecode (`hud`)

**Quando:** vídeo que mostra o próprio processo (a IA editando, um bastidor com etapas). Pílula escura à
esquerda com bolinha vermelha piscando, "EDITANDO AO VIVO" e o timecode do quadro (MM:SS:QQ); pílula à
direita com a etapa ("01 CORTES"). No fim pode virar "EDIÇÃO CONCLUÍDA" com a bolinha verde.

```json
{"id": "hud", "tipo": "hud", "em": 0.3, "sai": 9.9, "concluido_em": 9.3, "tamanho": 1.4, "topo": 240,
 "secoes": [{"em": 0.4, "nome": "01 GANCHO"}, {"em": "migalhas", "nome": "02 IMPACTO"}]}
```
`tamanho` multiplica a pílula inteira (1 = 52 px de altura com letra de 21 px; 1,4 lê bem no celular, onde o
tamanho 1 é o menor texto do vídeo). `topo` é o y da pílula (padrão 160 no 9:16, 40 no 16:9).
```html
<div id="hud"><div class="pill l"><i id="hud-dot"></i><b id="hud-state">Editando ao vivo</b><span id="hud-tc">00:00:00</span></div>
  <div class="pill r" id="hud-r"><span id="hud-n">&#8203;01</span><b id="hud-name">GANCHO</b></div></div>
```
```css
#hud .pill{position:absolute;top:0;height:52px;border-radius:26px;background:rgba(11,8,25,.88);border:1.5px solid #49445d;
  display:flex;align-items:center;font-family:Mono;font-weight:700;font-size:21px;letter-spacing:.13em;text-transform:uppercase;white-space:nowrap}
```
```js
tl.fromTo("#hud-dot",{opacity:1},{opacity:0.15,duration:0.55,ease:"sine.inOut",yoyo:true,repeat:15,immediateRender:false},0.3);   // repeat ímpar: termina aceso
for (const [t, p] of [[0.3,{textContent:"00:00:09"}],[0.333,{textContent:"00:00:10"}] /* ... um por quadro */]) tl.set("#hud-tc", p, t);
tl.set("#hud-n",{textContent:"​02"},8.337);          // sem o ​ o GSAP escreve "2"
tl.set("#hud-name",{textContent:"IMPACTO"},8.337);
// a pílula abre da direita para a esquerda (recorte com a borda redonda)
tl.fromTo("#hud-r",{opacity:0,clipPath:"inset(0% 0% 0% 22% round 36.4px)"},{opacity:1,clipPath:"inset(0% 0% 0% 0% round 36.4px)",duration:0.16,ease:"power2.out",immediateRender:false},8.357);
tl.set("#hud-state",{textContent:"Edição concluída"},9.3);
tl.set("#hud-dot",{backgroundColor:"#3bc59d",boxShadow:"0 0 12px #3bc59d",opacity:1},9.3);
```
Fonte mono em arquivo (`assets/fontes/JetBrainsMono.ttf`). O painel fica em y 160 de propósito (acima da
zona segura, onde o Reels põe o nome do perfil): se a pessoa achar que some ou que está pequeno, use
`"topo": 240` e `"tamanho": 1.4`. Som: `clique` a cada etapa, `sucesso` no concluído. Timecode sem
`repeat: -1`: um valor por quadro; no fim parado (`segura_fim`) ele fica no último quadro do vídeo.

## 10. Texto atrás da pessoa (`atras_da_pessoa`, 2,5D)

**Quando:** uma palavra forte que merece presença em volta da pessoa, num momento de ênfase ("O FIM",
"EDITOU", "GRÁTIS"): a palavra fica grande atrás dela, a cabeça passa na frente da parte de baixo das letras e
a palavra desliza devagar de lado (paralaxe leve). É composição em planos, 2,5D (vídeo de fundo, texto, pessoa
recortada na frente), não 3D. Uma palavra por vez, curta (até uns 10 letras), só em tela cheia, e o vídeo
volta ao normal depois dela.

**Antes:** o recorte da pessoa daquele trecho, aprovado num teste de uns 5 s (cabelo, mãos, borda):
`python3 ferramentas/pessoa_recortada.py --edit '<EDIT>' --de 5.5 --ate 9.5`. Sem recorte, ou com o recorte de
outro trecho, o build para com o comando certo.

```json
{"id": "editou", "tipo": "atras_da_pessoa", "texto": "EDITOU", "em": "editou", "sai": "foi", "sai_depois_de": 7.5}
```

Campos: `texto` (`*parte*` sai na cor de destaque), `em`, `sai`, `altura` (`cabeca`, o padrão; `topo`; ou o
topo do texto em pixels), `tamanho` (px; padrão: a maior que cabe na zona segura, até 16% do lado maior do
quadro), `cor` (padrão: o papel do estilo), `paralaxe` (1; 0 desliga), `caixa_alta` (true), `recorte`
(`assets/pessoa.webm`; use outro arquivo, com `--saida`, quando o vídeo tem tela verde em outro trecho),
`esconde_legenda`, `nome`, `frente` e `fundo_anda` (os dois abaixo).

**Terceiro plano (`"frente"`).** Uma imagem do projeto na frente da pessoa, para o 2,5D ter os três planos:
fundo, pessoa com a palavra atrás, objeto na frente.

```json
{"id": "editou", "tipo": "atras_da_pessoa", "texto": "EDITOU", "em": "editou", "sai": 7.0,
 "frente": {"imagem": "assets/tesoura.png", "em": "não"}}
```

`frente` aceita só o caminho (`"assets/tesoura.png"`) ou `{"imagem", "em", "sai", "lado", "tamanho", "onde"}`. Ela
entra depois da palavra (padrão 0,35 s depois; nunca antes de 0,2 s: um elemento por vez), num canto de baixo, do
lado oposto ao rosto (`"lado": "esquerda"` ou `"direita"` escolhe), com 26% do lado menor da tela de largura
(`tamanho` em px), e sai com a palavra. PNG com fundo transparente; jpg aparece como retângulo e o build avisa. O
build também avisa quando ela passa na frente do rosto, e a legenda desvia dela. Fica numa sub-composição própria
por cima de tudo (faixa "Frente · tesoura.png"):

```html
<div class="frente" id="editou-frente" style="left:659px;top:1249px;width:281px;height:281px">
  <div class="fr2" id="editou-frente-in"><img src="assets/tesoura.png" alt=""/></div></div>
```
```js
// entra com um pulo; a caixa de fora anda mais que a palavra (±40 px contra ±18), sem escala
tl.fromTo("#editou-frente-in",{opacity:0,y:110},{opacity:1,y:0,duration:0.4,ease:"back.out(1.6)",immediateRender:false},0);
tl.fromTo("#editou-frente",{x:-40,y:8},{x:40,y:-8,duration:0.717,ease:"none",immediateRender:false},0);
```

**O fundo anda um pouco (`"fundo_anda"`, ligado por padrão).** Cada plano tem o seu deslocamento: o fundo menos, a
palavra mais, a frente mais ainda. O fundo é a gravação, e a pessoa recortada sai do mesmo vídeo: os dois andam
juntos (se só um andasse, apareceria a borda dupla). O build põe o `#inner` e o `#innerp` cada um dentro de uma
caixa `.plfundo` e mexe nas duas com o mesmo tween, na timeline principal:

```js
tl.fromTo(".plfundo",{x:0,scale:1},{x:6,scale:1.015,duration:1.487,ease:"sine.inOut",immediateRender:false},5.48);
tl.set(".plfundo",{x:0,scale:1},7.067);   // volta ao normal: no corte, seco; sem corte perto, em 0,5 s
```

A aproximação de 1,5% nunca mostra borda (o vídeo cresce a partir do meio). `"fundo_anda": false` deixa o fundo
parado; dois textos atrás da pessoa ao mesmo tempo: só o primeiro mexe no fundo (o build avisa).

O `build.py` põe o texto numa sub-composição entre o vídeo e a pessoa, e a pessoa recortada numa segunda
câmera (`#innerp`) que recebe o mesmo enquadramento do vídeo (a classe `.camera` anda junto em todos os
cortes):

```html
<div id="frame">
  <div id="inner" class="camera"><video id="cam" ...></video></div>
  <div id="atras">
    <div id="faixa-editou" class="faixa" data-composition-id="peca-editou" data-composition-src="compositions/editou.html"
         data-start="1.14" data-duration="1.84" data-track-index="10" data-timeline-label="Texto atrás · EDITOU" ...></div>
  </div>
  <div id="innerp" class="camera">
    <video id="pa-editou" class="pessoa-atras" src="assets/pessoa.webm" muted playsinline data-start="1.04" data-duration="2.04"
           data-media-start="0.54" data-track-index="40" data-timeline-label="Pessoa recortada · EDITOU"></video>
  </div>
</div>
```
```html
<!-- compositions/editou.html: uma letra por span, o acento junto da letra -->
<div class="atras-txt" id="editou" style="left:140px;top:324px;width:800px;height:171px;font-size:171px;color:#fffdfd">
  <span class="atl" id="editou-l0">E</span><span class="atl" id="editou-l1">D</span>...</div>
```
```css
.atras-txt{position:absolute;display:flex;align-items:center;justify-content:center;font-weight:900;line-height:1;
  white-space:nowrap;letter-spacing:-.01em;text-shadow:0 14px 40px rgba(0,0,0,.28)}
.atras-txt .atl{display:inline-block;opacity:0}
.atras-txt .atl.d{color:#665080}
```
```js
// letras sobem uma a uma na palavra falada
tl.fromTo("#editou .atl",{opacity:0,yPercent:70},{opacity:1,yPercent:0,duration:0.45,ease:"power3.out",stagger:0.05,immediateRender:false},0);
// paralaxe: a palavra desliza de lado enquanto a pessoa fica parada na frente (sem escala)
tl.fromTo("#editou",{x:-18,y:6},{x:18,y:-6,duration:1.74,ease:"none",immediateRender:false},0);
tl.to("#editou .atl",{opacity:0,yPercent:-35,duration:0.28,ease:"power2.in",stagger:0.02,immediateRender:false},1.36);
```

- **Onde:** centrado na cabeça e com o meio das letras no topo da cabeça, medido no próprio recorte
  (`top_median` e `head_cx` do `pessoa.json`, por tomada). A cabeça esconde só a parte de baixo do miolo da
  palavra; a metade de cima continua à vista.
- **Legível:** o build avisa quando o rosto medido (ou o corpo, abaixo do queixo) cobre mais de uns 40% da
  largura das letras, contando só a letra que fica mais da metade atrás da pessoa. Suba o texto
  (`"altura": "topo"`), use uma palavra mais comprida ou outro trecho.
- **Tempo:** a pessoa recortada entra 0,1 s antes da primeira letra e sai 0,1 s depois da última, sempre dentro
  do trecho que o recorte cobre.
- **Conferir:** amostra de uns 5 s com a palavra chegando, a pessoa se mexendo e a palavra saindo; olhe a borda
  do cabelo e a palavra no arquivo renderizado.

Som: `whoosh` na entrada; `pop` na frente.

## 11. Cartão final com botão (`cartao_final`)

**Quando:** sempre que o vídeo termina com chamada (anúncio). Entra no último quadro da fala e fica 1,8 s
depois dela: papel do estilo, logo (opcional), título, frase à mão e o botão, que pulsa uma vez. Vai no topo
do `projeto.json`, fora de `animacoes`, e só aparece no `completo` (ou numa amostra que chega ao fim).

Sem chamada, mas com o fim precisando de tempo de leitura (a síntese é a última palavra, o diagrama tem que
ficar inteiro uns 2 s, o carimbo acabou de bater): `"segura_fim": 1.2` no topo do `projeto.json`. O último
quadro fica parado, a voz já terminou e as peças e cenas continuam até o `sai` delas. Não deixe silêncio no
corte para isso (o corte continua seco). Com os dois ligados, o cartão entra depois do fim parado.

```json
"cartao_final": {"ligado": true, "segundos": 1.8, "titulo": "Conheça o Jarvis", "sub": "aula gratuita",
                 "botao": "Toque em Saiba Mais", "logo": "assets/logo.png"}
```
```html
<div id="fim"><div class="logo" id="fim-logo" style="background-image:url(assets/logo.png);left:260px;top:523px;width:560px;height:300px"></div>
  <div class="tit" id="fim-tit" style="top:883px;font-size:66px">Conheça o Jarvis</div>
  <div class="sub" id="fim-sub" style="top:973px;font-size:64px">aula gratuita</div>
  <div class="bt" id="fim-bt" style="left:190px;top:1093px;width:700px;height:140px;font-size:52px">Toque em Saiba Mais</div></div>
```
```js
tl.fromTo("#fim",{opacity:0},{opacity:1,duration:0.25,immediateRender:false},53.733);          // 0,1 s antes do fim da fala
tl.fromTo("#fim-bt",{opacity:0,y:30},{opacity:1,y:0,duration:0.4,ease:"back.out(2)",immediateRender:false},54.183);
tl.fromTo("#fim-bt",{y:0},{keyframes:[{y:-12,duration:0.3,ease:"power2.out"},{y:0,duration:0.4,ease:"power2.in"}],immediateRender:false},54.733);   // pulsa: um pulinho
```
A logo vai como fundo de uma `div` (a mesma imagem em dois `<img>` sem tempo faz o lint avisar de mídia
duplicada). O botão diz o mesmo que o botão do anúncio. Som: `whoosh` na entrada, `pop` no botão.

## 12. Lettering com ordem de leitura (`lettering`)

**Quando:** a fala tem uma a quatro expressões que mudam o jeito de entender o vídeo e merecem ficar grandes na
tela, com um peso para cada papel (`references/lettering.md`: o que escrever, os três papéis, o estilo aprovado). A
peça cuida da ordem, do tempo, do encaixe e da animação; quem escolhe o texto e o momento é você.

```json
{"id": "melhor", "tipo": "lettering", "principal": "Ficou com a *melhor*", "apoio": "sem regravar nada",
 "chamada": "Veja o resultado", "em": "ficou", "sai": 21.2, "posicao": "topo", "estilo": "bloco"}
```

| campo | o que é |
|---|---|
| `principal` | o texto que manda (obrigatório). `*parte*` sai na cor de destaque. Até 3 palavras grandes por vez; de 4 a 6 viram dois momentos; mais de 6 é erro |
| `apoio` | opcional: o complemento curto, com metade do tamanho, peso 800, palavra por palavra |
| `chamada` | opcional: a pílula do convite, por último |
| `em`, `sai` | palavra ou segundo, como nas outras peças (`depois_de`, `sai_depois_de`). `sai` no fim do corte: fica até o último quadro |
| `antecipa` | quanto cada palavra pode aparecer antes de ser dita: padrão 0,05 s, no máximo 0,15 |
| `posicao` | `topo`, `meio` ou `base`. Sem ela, o build tenta topo, base e meio, nessa ordem, e fica com a primeira que não cobre o rosto. Na moldura vai na área livre |
| `estilo` | `bloco` (Montserrat 900 em caixa alta, branca com contorno forte e o destaque na cor base do estilo) ou `manuscrito` (Caveat 700 à mão, destaque amarelo) |
| `apoio_em`, `chamada_em` | opcionais: a palavra de cada um. Nunca antes do elemento de cima terminar de chegar (o build empurra e avisa) |
| `tamanho` | teto da letra do principal em px (padrão 13% do lado menor no bloco, 16% no manuscrito) |
| `caixa_alta`, `esconde_legenda` | padrão: caixa alta no bloco; a legenda some no trecho (`false` mantém tudo) |
| `legenda_junto` | a legenda continua, mas sem as palavras que a peça escreve grande naquele instante. Padrão: `true` com a legenda `discreta`, `false` com a `padrao` (letra grande em cima e legenda grande embaixo brigam) |

O que o build faz, sem você escrever nada:

- **Ordem de leitura.** O principal chega primeiro e é o maior, o mais pesado e o de mais contraste. O apoio só
  começa 0,12 s depois de o principal terminar de chegar; a chamada, por último. No `bloco`, a batida (a linha
  dá um pulinho para cima, 7% da letra, e volta) marca o fim da chegada do principal.
- **Tempo da fala, palavra por palavra.** Cada palavra do principal é procurada na fala, em ordem, a partir do
  `em` (a muleta que o texto cortou pode ficar no meio), e entra na hora em que é dita, antecipando no máximo o
  `antecipa` (0,15 s): o começo medido na onda quando existe o `palavras_onda.json` (o Whisper adianta até 0,3 s;
  sem o arquivo, o build avisa). As letras dela aparecem dentro do tempo em que ela é dita (0,1 a 0,3 s). O apoio
  também, palavra por palavra, nunca antes de o principal terminar de chegar. Palavra que não está na fala (texto
  encurtado) vem logo depois da anterior. O que precisa aparecer junto (número com a unidade, a negação com a
  palavra de antes) aparece na hora da última delas: nada antes de ser dito.
- **Letra por letra montada no lugar.** Cada letra é um grafema (o acento junto: "NÃO" são três letras, N, Ã e O).
  A linha inteira já está no HTML, com as letras invisíveis, na posição final: nada se recentraliza enquanto
  aparece. O espaço entre palavras é margem em px (o HyperFrames joga fora o texto que é só espaço).
- **Nenhum quadro muda o sentido.** Número aparece inteiro, junto com "R$", "mil", "%" (nunca "R$ 1" antes de
  "R$ 1.000"). A negação que não tem outra antes dela aparece junto com a palavra de antes ("PRECISA NÃO" nunca
  mostra "PRECISA" sozinho); depois de um "não", a frase já é negativa e o resto vem letra por letra.
- **Encaixe por procedimento.** A largura é medida com a fonte de verdade, no peso usado
  (`assets/fontes/larguras.json`). A ordem: 1 a 3 linhas quebrando por sentido (nunca artigo ou preposição longe do
  nome, número longe da unidade, "não" longe do verbo); mais de 3 palavras viram dois momentos (duas telas: o
  primeiro sai inteiro antes de o segundo entrar, na palavra dele); a letra só diminui por último, até caber numa
  área livre sem cobrir o rosto. Abaixo da legenda mais 12 px (72 px no 9:16) não é lettering: erro com o que
  encurtar.
- **Fonte carregada antes de medir.** Na página, a timeline da peça é montada depois do `document.fonts.ready` e
  registrada no fim; antes disso, cada linha é medida no navegador e, se passar da largura, a letra desce o que
  falta.
- **Legenda fora, ou junto.** A legenda some do `em` ao `sai`. Com `legenda_junto` (o padrão com a legenda
  `discreta`), só as palavras que a peça escreve grande saem da legenda; o resto da fala continua legendado ali
  (sem isso, uns 30% da fala ficavam sem texto na tela). Se a peça não acha nem metade do principal na fala, a
  legenda sai do trecho inteiro, com aviso.
- **Tempo de leitura.** Depois de tudo completo, pelo menos 1 s até 4 palavras e mais 0,3 s por palavra: menos
  que isso vira aviso. Quando a próxima expressão entra logo depois (fala rápida), atrasar o `sai` não resolve e o
  aviso diz para tirar uma expressão ou juntar esta com a próxima numa peça só (a segunda vira o apoio). Cada
  expressão ocupa uns 2 s (chegar, 1 s de leitura e sair): em 10 s de fala rápida cabem 3.
- **Até o último quadro.** Com o `sai` no fim do corte (até 0,25 s antes), a peça não tem saída: fica legível até o
  último quadro do vídeo, também no quadro parado do `segura_fim`.
- **Tamanho da letra.** A linha `nota:` do build diz a letra usada e por que não foi maior (a largura da área ou o
  rosto). O `tamanho` é só o teto.

```html
<!-- compositions/errei.html: uma linha por div, uma palavra por .lt-w, uma letra por .lt-u (invisível até a vez dela) -->
<div class="lt-m" id="errei-m0">
  <div class="lt-l lt-p" id="errei-m0l0" style="left:164px;top:1115px;width:752px;height:109px;line-height:109px;
       font-size:92px;-webkit-text-stroke:8px #352c40"><span class="lt-w"><span class="lt-u" id="errei-m0u0">E</span>...</span></div>
  <div class="lt-l lt-a" id="errei-al0" ...><span class="lt-w"><span class="lt-u" id="errei-a0">e</span></span>...</div>
</div>
```
```js
const montar = () => {
  ajustar();                                   // mede cada linha com a fonte carregada; passou da largura, a letra desce
  const tl = gsap.timeline({ paused: true });
  tl.fromTo("#errei-m0u0",{opacity:0,yPercent:55},{opacity:1,yPercent:0,duration:0.24,ease:"power3.out",immediateRender:false},0);
  // ... uma letra a cada 18 a 50 ms; letras que aparecem juntas (número, negação) vão no mesmo tween: "#x-u3,#x-u4"
  window.__timelines["peca-errei"] = tl;       // registrada só no fim
};
document.fonts.ready.then(montar);
```

Som: `pop` na batida do principal (pequeno, -18; na última peça lettering do vídeo, a conclusão, médio, -15),
`whoosh` na entrada do manuscrito, `pop` baixo (-20) no pouso do apoio e `pop` pequeno na chamada. O
`plano_sfx.py --projeto` usa esses níveis.

---

## Pegadinhas do HyperFrames e do GSAP

Todas já custaram um render refeito. O `build.py` já segue; se escrever peça à mão, siga também.

1. **Nada em tempo negativo.** Uma legenda que começava 0,05 s antes da amostra entrou em -0,05 e o GSAP
   empurrou a timeline INTEIRA 0,05 s para frente: a logo presa na mão ficou 1,5 quadro atrás da mão.
   Toda posição passa por `max(0, t - inicio)`.
2. **Texto que muda nunca nasce vazio.** Um `tl.set(el, {textContent: "A"}, 1)` sobre um elemento vazio
   não volta a ficar vazio quando o seek vai para antes de 1 s (testado no GSAP 3.14.2). Escreva o primeiro
   valor no HTML.
3. **'01' vira 1.** O GSAP lê texto numérico como número. Prefixe com o espaço de largura zero:
   `{textContent: "​01"}`.
4. **Estado do quadro 0 com fromTo curto.** `tl.fromTo(sel, P, {...P, duration: 0.04, ease: "none"}, 0)`
   em vez de `tl.set(sel, P, 0)`: o set em 0 pode sumir quando o seek volta ao começo.
5. **`immediateRender: false` depois do primeiro tween** do elemento. Sem isso o `fromTo` de uma saída
   aplica o estado inicial na hora da montagem e a peça some antes de entrar.
6. **Anime transform, opacity e clip-path.** Nada de tween em `left`, `top`, `width`, `height`: o lint
   reclama e o movimento trava no pixel inteiro. A moldura é `clip-path: inset(...)` no `#frame`.
7. **Nada de `transform` no CSS de um elemento que o GSAP vai mover** (`gsap_css_transform_conflict`).
   O estado inicial vai no `from` do `fromTo` (o risco começa com `{opacity: 1, clipPath: "inset(0% 100% 0% 0%)"}`).
8. **Fonte em arquivo, sempre.** `local("SF Mono")` não carrega no navegador do render: o painel saiu com
   outra fonte. Toda `font-family` tem `@font-face` para um arquivo em `assets/fontes/`.
9. **Texto solto em caixa `flex` perde o espaço** antes de um `<b>`. Envolva num `<span>`.
10. **Por quadro, só com dados.** Timecode, contador e objeto na mão têm um valor por quadro; o tempo de
    cada `set` arredonda PARA BAIXO em milissegundos (nunca cai um quadro depois) e entra como lista + laço.
    Nada de `repeat: -1`, `Date.now()` ou `Math.random()`.
11. **Corte dentro da transição da moldura usa o enquadramento da moldura**, e o tempo do corte com 3
    casas pega o rosto da tomada nova com 2 ms de folga (detalhes em `references/enquadramento.md`).
12. **Vídeo e áudio:** `<video>` sempre `muted playsinline` e com `id`; a voz vai num `<audio id>` à parte
    com o mesmo `data-media-start`. Nunca `crossorigin`. Nunca `data-start` numa caixa que tem um
    `<video data-start>` dentro. Não anime o `<video>`: anime a caixa em volta.
13. **Sons:** vão no campo `sons` do `projeto.json`, que o `helpers/plano_sfx.py --projeto` escreve (nunca
    cole `<audio>` no `index.html`: o build reescreve). Cada `<audio>` com `id` e `data-track-index` próprios.
    O início é `pico_na_tela - pico_no_arquivo + inicio_no_arquivo` (o pico do som cai no quadro do
    movimento; `data-media-start` pula o começo do arquivo); volume `10^((nível - pico_do_trecho)/20)`, no
    máximo 3,98; a descida é `data-fade-out`. Som que começaria antes do 0 perde só a cabeça.
    Com `data-automation` de volume, o `data-volume` é ignorado: o ganho vai nos pontos da curva.
14. **A peça sai antes da cena trocar.** Tudo o que está na moldura some antes dela abrir; item cuja
    palavra cai depois da saída fica de fora (senão fica na tela para sempre por cima da cena seguinte).
15. **Palavra repetida pelo Whisper numa pausa** ("aqui aqui" a menos de 0,6 s) vira uma só na legenda.
16. **Caminho de arquivo a partir da pasta do projeto, também dentro de `compositions/`.** O estúdio serve
    tudo a partir da raiz: `assets/fontes/Montserrat.ttf` funciona, `../assets/...` é erro no lint (e some na
    prévia). Toda sub-composição traz o `@font-face` das fontes que usa.
17. **Sub-composição não alcança o `index.html`.** A timeline de uma peça só mexe no que está dentro dela: a
    tremida do carimbo (que mexe no `#palco`) vai na timeline principal. Raiz da peça estilizada por `#root`,
    nunca por uma classe dela.
18. **O lint só confere `index.html` e `compositions/`.** Cena sob medida fora de `compositions/cenas/`
    passaria sem lint.
19. **Pessoa recortada no estúdio:** o webm com transparência (VP9) toca direito no Chrome. No Safari a
    transparência pode não aparecer (fundo preto por cima do texto). E, na prévia, vídeo e pessoa tocam como
    dois vídeos separados: numa emenda a pessoa pode ficar um quadro atrás. O render junta quadro a quadro e
    não tem isso; o arquivo renderizado é a referência.
20. **Espaço entre elementos some.** O HyperFrames descarta o texto que é só espaço entre dois elementos:
    `<span>Não</span> <span>fui</span>` sai "NÃOFUI" no snapshot e no render. Espaço entre palavras em spans
    separados vai como `margin-left` em px (em `em`, um span sem `font-size` mede pelos 16 px da raiz).
21. **Risco em cima de texto reprova o `check`** (`text_occluded`, erro), e o `data-layout-allow-occlusion`
    não resolve. O risco vai por baixo das letras: filho do item, com o texto em `position:relative;
    z-index:1` (ou o item com `z-index:0` e o risco com `z-index:-1`). Continua parecendo riscado.
22. **Troca de estado com os dois textos cruzando** ("Animações." virando "Efeitos sonoros." no mesmo lugar)
    reprova o `check` (`content_overlap`). O velho sai inteiro antes do novo entrar, ou saem por lados
    opostos sem se cruzar.
23. **O estúdio escreve nas cenas próprias.** Ao abrir, ele carimba `data-hf-id` em cada elemento de
    `compositions/cenas/`. Troca de texto exata feita depois disso falha calada; leia o arquivo de novo e
    edite ignorando o `data-hf-id`.
24. **As `palavras` da cena mudam** quando o corte é conferido de novo (o Whisper junta ou separa palavras).
    Procure pela palavra e deixe o desenho aguentar uma a mais; nada de uma linha fixa por palavra.
25. **Peça sem escala, imagem no tamanho.** Tween de `scale` (ou `scaleX`) deixa o Chrome com o desenho de uma
    escala antiga, que depende do que a página já tocou e de quantos workers o render usou: dois renders do
    mesmo projeto saíam diferentes (medido com todas as peças do catálogo: o texto da pílula do hud, o carimbo, o
    celular; 243 quadros com diferença real entre um render com 1 worker e outro com 5). As peças entram por
    deslocamento, opacidade, giro e `clip-path` (o "cresce" virou recorte que abre ou um pulinho em `y`), sem
    escala; depois disso, os dois renders saem iguais, com diferença só de ruído (1 ou 2 níveis de cor, no
    recorte da pessoa e na borda da moldura). Imagem maior que a caixa, reduzida pelo navegador, também mudava a
    borda (o logo na mão): o build reduz uma cópia para `assets/gerado`, no tamanho em que ela aparece. Só a
    câmera e o fundo do 2,5D (o próprio vídeo) continuam com `scale`.
