Files
portifolio-evandrodevbr/specs/2026-11-09-blocos-3d-motion-pwa.md
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento
- cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py):
  marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao
  cursor e, parada no topo, passa sozinha pelos estados com legenda
- três níveis de aparelho (full, lite, static); sem WebGL fica o pôster
- título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha."
- faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções
- rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa
  compartilhada entre a lista de cases e o case

Ofertas
- atendimento no WhatsApp com IA e nota fiscal para a reforma tributária
  (páginas de serviço, cartões da home, formulário e aviso com data de validade)
- página /ia atualizada, com fontes
- peças 3D em loop nos cartões e topos de página (3d/pecas.py)

Design
- hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta
- um destino por rótulo: "Falar sobre meu projeto" leva ao formulário,
  "Chamar no WhatsApp" abre o WhatsApp
- logo montado em blocos, 404 redesenhado, grades sem célula vazia
- ícones do PWA e manifesto novos

Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
2026-11-10 03:48:25 -03:00

261 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Blocos: hero 3D feito no Blender, movimento e ícone do app
**Autor:** Evandro · **Data:** 2026-11-09 · **Porte:** grande · **Status:** em execução
## 1. Objetivo
Trocar o fundo de partículas do hero por uma peça 3D modelada no Blender (os blocos da marca, que se remontam em tela, integrações e dados), dar ao site uma linguagem de movimento única (escrita, entrada e saída de componentes, troca de tela) e corrigir o ícone do aplicativo instalado. Hoje o 3D é um fundo discreto, só no computador, e o celular não recebe nada.
Leitura (frontend-orchestrator): refino com mundo visual novo no hero · Persuade (suposição S1) · web · `3d`, `premium` → impeccable, awwwards, threejs-fundamentals/geometry/materials/shaders/animation.
## 2. Escopo
- Dentro: ícones do manifesto; pipeline Blender → web em `3d/`; hero em três níveis (tempo real, tempo real leve, pôster); escrita, entrada/saída e troca de tela em CSS; peças 3D pré-renderizadas nos cartões de Soluções e no 404.
- Fora (não fazer): biblioteca de animação (GSAP, Motion) ou React Three Fiber, removidos em 2026-10-14 por desempenho; mudar texto, SEO ou estrutura das seções; service worker; modelos gerados por IA ou de bibliotecas de terceiros; vídeo de rolagem no celular; merge dos branches `wip/*`.
## 3. Critérios de aceite
- CA1 (ícone): o manifesto serve 4 ícones em URLs novas (`any` e `maskable`, 192 e 512) e `id: "/"`; no maskable a marca cabe na zona segura (caixa ≤ 57% do lado, centrada ±2%) → `pnpm test -- app/manifest.test.ts` e `node scripts/build-icons.mjs --check`.
- CA2 (ativos): `blender -b --python 3d/blocos.py` recria do zero `public/3d/` (`bloco.glb` ≤ 40 KB, `blocos.json` ≤ 40 KB em gzip, 2 matcaps ≤ 30 KB cada, `hero-poster.webp` ≤ 90 KB), com N ≤ 320 blocos e ≤ 60 mil triângulos → o comando sai com `OK` e código 0.
- CA3 (pôster): sem JavaScript ou com movimento reduzido o hero mostra o pôster da marca em blocos, todo o conteúdo visível, sem pedido a `/3d/bloco.glb` nem ao pacote do Three.js → `pnpm test:e2e -- hero-3d`.
- CA4 (computador): em aparelho `full` os blocos montam a marca em até 2,6 s e a rolagem leva marca → tela → integrações → dados dentro dos trechos de `hero-captions.ts` → `pnpm test -- lib/blocos.test.ts` e captura local com GPU nos progressos 0, 0,35, 0,62 e 0,85.
- CA5 (celular): em aparelho `lite` a cena roda com DPR ≤ 1,5 e 30 quadros por segundo sob demanda, trocando de estado sozinha enquanto está à vista; se a média dos 60 primeiros quadros passar de 28 ms, volta ao pôster e lembra na sessão → `pnpm test -- lib/device.test.ts BlocosScene` e inspeção num celular real.
- CA6 (desempenho): em 390×844 com rede e CPU limitadas, LCP < 2,5 s e continua sendo o H1, CLS < 0,05, os dois botões inteiros na primeira dobra → `shoot.mjs` do awwwards (`mobile.vitals`) sobre `pnpm build && pnpm start`.
- CA7 (escrita): rótulos em fonte mono entram digitados (cursor de bloco) e o trecho verde do H1 entra como marca-texto; o texto está no HTML desde o primeiro quadro; só `transform`, `opacity` e `clip-path` animam → `pnpm test -- Hero` e inspeção com movimento reduzido.
- CA8 (entrada e saída): menu do celular, vídeo ampliado, aviso de cookies, botão do WhatsApp e perguntas frequentes animam ao abrir e ao fechar, sem JavaScript novo; navegador sem suporte troca na hora → `pnpm test:e2e -- mobile-menu keyboard` verdes e inspeção.
- CA9 (troca de tela): a troca de página usa a cortina de blocos (≤ 700 ms) e a capa do projeto vira o topo do case; com movimento reduzido não há animação → `pnpm test:e2e -- navigation routes` verdes e captura dos quadros da troca.
- CA10 (peças): os 3 cartões de Soluções e o 404 mostram peças do mesmo kit renderizadas no Blender (loop ≤ 3 s, ≤ 250 KB por arquivo, pôster ≤ 40 KB), tocando só à vista, sem contexto WebGL extra → `ls -l public/3d` e `pnpm test:e2e`.
- CA11 (fechamento): `pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm test:e2e` verdes; impeccable critique sem heurística abaixo de 3/4; audit sem falha; jury com 8 ou mais em cada categoria.
## 4. Contexto verificado
Código lido no `main` do Gitea (`4671837`, 2026-10-24), que é o que está no ar. O checkout local está em `b2f3899` (2026-10-05), 54 commits atrás, com `origin` num repositório do GitHub que não existe mais.
- `package.json`: só `three` 0.182 e `lenis`; animação é CSS (`animation-timeline`, View Transitions).
- `app/components/cinematic/HeroFieldScene.tsx:26,422,692,969,999`: nuvem de pontos procedural em três camadas, fundo ambiente. Padrão de referência do motor (renderizador, descarte, desenho por software).
- `app/components/cinematic/HeroField.tsx:195,201` e `lib/device.ts:42-53`: toque, ≤ 4 núcleos, ≤ 4 GB, economia de dados ou movimento reduzido não recebem cena nem baixam o Three.js.
- `app/components/Hero.tsx:64-66,92,143`: palco, H1 (LCP) e `HeroWork` (navegador e celular com trabalho real, 40% da largura).
- `app/components/hero-captions.ts`: legendas "A tela", "As integrações", "Os dados" com trechos 0,18–0,49, 0,51–0,75, 0,79–1,07.
- `app/manifest.ts:17-28`: dois ícones (`/brand/icon-192.png`, `/icon.png`), sem `purpose` nem `id`. Arquivos iguais aos do ar (md5) e com o símbolo de blocos atual. A marca ocupa 395×392 de 512 (77%), fora da zona segura.
- `app/template.tsx:29` e `app/globals.css:1038-1054`: troca de página por `<ViewTransition name="page">`, fade com escala.
- `app/globals.css:755-767,963-977,1075`: entrada do título, das seções (`ds-scene-in`, `view()`) e dos títulos por palavra. Não há saída animada.
- `app/components/WorkModalities.tsx:265,348`, `app/not-found.tsx:34-46` (404 fora da marca), `Faq.tsx:43`, `Header.tsx:247`, `showcase/ProjectLightbox.tsx:253`, `showcase/ProjectList.tsx:84`, `app/cases/_components/CaseCover.tsx:57`.
- Máquina: Blender 5.2.2 LTS (`io_scene_gltf2`, Cycles com OptiX na RTX 5060 Ti 16 GB, addon `blender_mcp`), `ffmpeg`, `magick`. Não há outro programa de 3D instalado.
## 5. Desenho
**Conceito.** Ideia: os blocos da marca são as peças com que se monta o site ou o sistema de cada cliente. Assinatura: os blocos se soltam e se remontam com a rolagem. Verbo: encaixar (chega rápido, assenta curto, sem quique). Quadro para compartilhar: a marca em porcelana com os quatro cubos verdes ao lado do título.
Decisões:
- Um kit, quatro estados (`marca`, `tela`, `integracoes`, `dados`) com o mesmo número de blocos. As três legendas atuais ficam como estão.
- Fonte é o script `3d/blocos.py` (mapas de blocos, chanfro, materiais, luz, exportação). O `.blend` é gerado em `3d/out/` (ignorado no git). Descartado: `.blend` modelado à mão e versionado, binário que ninguém revisa nem reconstrói.
- Tempo real com uma `InstancedMesh` e matcaps renderizados dos materiais do Blender; a troca de estado é interpolada no vertex shader. Descartado: luz e sombra em tempo real (peso no celular) e vídeo de rolagem (o projeto já o tirou do celular).
- `GLTFLoader` do próprio `three`, sem Draco nem meshopt (a geometria é um bloco só).
- Três níveis: `full` (regra atual), `lite` (toque, ≥ 6 núcleos, ≥ 4 GB ou não informado), `static` (o resto, e qualquer aparelho que estoure o orçamento de quadro).
- Movimento 2D só com CSS: `steps()` nos detalhes (cursor, cortina), ease-out no resto, `@starting-style` com `allow-discrete` para saída.
- Peças secundárias pré-renderizadas no Cycles (vídeo curto com pôster), nunca WebGL por cartão.
Interfaces:
```ts
// public/3d/blocos.json
{ n: number; estados: ["marca","tela","integracoes","dados"]; dados: number[] } // [estado][bloco] = x, y, z, escala, tom (0 porcelana, 1 lima)
// lib/blocos.ts
export function stateAt(p: number): { from: number; to: number; t: number }
// lib/device.ts
export type SceneTier = "full" | "lite" | "static";
export function sceneTier(): SceneTier
// app/components/cinematic/BlocosScene.tsx: mesmas props de HeroFieldSceneProps
export function prepareBlocos(): Promise<void>
export default function BlocosScene(props: HeroFieldSceneProps)
```
Mapa de arquivos. Criar: `3d/blocos.py`, `3d/pecas.py`, `public/3d/*`, `public/brand/pwa-*.png`, `scripts/build-icons.mjs`, `lib/blocos.ts`, `app/components/cinematic/BlocosScene.tsx`, `e2e/hero-3d.spec.ts`, `DESIGN.md`. Modificar: `app/manifest.ts`, `lib/device.ts`, `HeroField.tsx`, `Hero.tsx`, `app/globals.css`, `WorkModalities.tsx`, `not-found.tsx`, `ProjectList.tsx`, `CaseCover.tsx`. Remover no fim: `HeroFieldScene.tsx` e seus testes, `public/brand/icon-192.png`.
## 6. Restrições, riscos, suposições
- Restrições: Next 16 com APIs diferentes (ler `node_modules/next/dist/docs/` antes de codar, `AGENTS.md`); trabalho em branch, nunca na `main`; nenhum servidor de desenvolvimento em segundo plano (`pnpm build && pnpm start`, encerrado ao fim).
- Riscos: o CI roda sem GPU, então a cena em tempo real só se confere em captura local; o celular pode perder LCP (o orçamento de quadro e o CA6 seguram); sincronizar o checkout exige trocar o `origin`. Reverter é desfazer o branch; nada toca dados.
- O worktree `~/.cache/security-fixes/portifolio-evandrodevbr` (branch `chore/security-updates-2026-11-09`) nasceu do mesmo commit velho e vai conflitar com o `main` real.
- S1: o site é de venda (Persuade), com vitrine de portfólio.
- S2: há um celular Android e um iPhone para a inspeção dos CA5 e CA1.
- P1 (bloqueia T2): qual é a "logo nova"? O repositório e o site no ar já usam o símbolo de blocos nos ícones; nada mais novo foi encontrado. Resposta recomendada: é esse símbolo, e o que se vê no celular é ícone sem versão maskable mais o ícone guardado da instalação antiga.
- P2 (bloqueia T7): no computador, a peça 3D ocupa a coluna da direita e as molduras de `HeroWork` saem do hero (os projetos reais começam uma rolagem abaixo)? Resposta recomendada: sim.
## 7. Microtasks
### Fase 0 · base e ícone
### T0 · Sincronizar o checkout com o Gitea · N0
- Serve: todos · Depende de: "ok" do usuário
- Fazer: apagar `AGENTS.md` e `CLAUDE.md` não rastreados (idênticos aos do Gitea); `git remote set-url origin https://git.evandro.dev.br/evandro/portifolio-evandrodevbr.git`; `git pull --ff-only`; `git switch -c feat/blocos-3d-motion`; `pnpm install --frozen-lockfile`.
- Verificar: `git log -1 --format=%h` → `4671837` ou mais novo; `pnpm test` verde.
- [x] feito
### T1 · Escrever o teste do manifesto · N2
- Serve: CA1 · Depende de: T0 · Arquivos: criar `app/manifest.test.ts` · Padrão: `app/robots.test.ts`
- Fazer: afirmar `id === "/"` e exatamente estes ícones: `/brand/pwa-192.png` e `/brand/pwa-512.png` (`purpose: "any"`), `/brand/pwa-maskable-192.png` e `/brand/pwa-maskable-512.png` (`purpose: "maskable"`), todos `image/png`, e que cada arquivo existe em `public/`.
- Verificar: `pnpm test -- app/manifest.test.ts` → falha (ainda não implementado).
- [x] feito
### T2 · Gerar os ícones a partir do símbolo · N2
- Serve: CA1 · Depende de: T1, bloqueada por P1 · Arquivos: criar `scripts/build-icons.mjs` e os 4 PNG em `public/brand/`
- Fazer: com `sharp`, compor `public/brand/mark-dark.svg` sobre fundo `rgb(8,10,9)`. `any`: marca com 77% do lado. `maskable`: marca com 56% do lado, centrada. `--check` relê os PNG e falha se o tamanho ou a caixa da marca saírem do CA1.
- Verificar: `node scripts/build-icons.mjs && node scripts/build-icons.mjs --check` → `OK`.
- [x] feito
### T3 · Apontar o manifesto para os ícones novos · N2
- Serve: CA1 · Depende de: T2 · Arquivos: modificar `app/manifest.ts:17-28`; remover `public/brand/icon-192.png`
- Fazer: trocar a lista de ícones pela do T1 e acrescentar `id: "/"`.
- Verificar: `pnpm test -- app/manifest.test.ts` → passa; `grep -rn "icon-192" app lib scripts e2e` → vazio.
- [x] feito
### CP1 · após T0–T3 · N0
- Verificar: `pnpm lint && pnpm typecheck && pnpm test && pnpm build`. Depois do deploy, reinstalar o app no Android e no iPhone (o iPhone só troca o ícone reinstalando) e conferir o ícone.
### Fase 1 · Blender e hero
### T4 · Registrar conceito e movimento em DESIGN.md · N4
- Serve: CA4, CA7, CA9 · Depende de: T0 · Arquivos: criar `DESIGN.md`
- Fazer: cartão do conceito da seção 5, tokens que já existem em `globals.css`, durações e curvas do verbo "encaixar", os três níveis de aparelho.
- Pronto quando: `impeccable context` lê o arquivo sem aviso.
- [x] feito
### T5 · Montar o kit e os estados no Blender · N3
- Serve: CA2 · Depende de: T0 · Arquivos: criar `3d/blocos.py`; `.gitignore` com `3d/out/`
- Fazer: bloco chanfrado (2 segmentos); os 4 estados como mapas de blocos no próprio script, com `marca` derivado dos retângulos de `public/brand/mark-dark.svg`; exportar `bloco.glb` e `blocos.json` no formato da seção 5; afirmar no script o mesmo N em todos os estados e os limites do CA2.
- Verificar: `blender -b --python 3d/blocos.py` → `OK blocos=<N> tris=<T>` e código 0.
- [x] feito
### T6 · Acertar material, luz e pôster · N4
- Serve: CA2, CA3 · Depende de: T5 · Arquivos: modificar `3d/blocos.py`
- Fazer: porcelana fosca `#f4f3ef` e esmalte `#d5fa00` com emissão leve, luz de estúdio, Cycles com OptiX; renderizar os 2 matcaps (256 px) e `hero-poster.webp` (1200 px, fundo transparente). Ajuste fino pelo Blender MCP (`execute_blender_code`, `look`), com os valores copiados para o script.
- Pronto quando: pôster sem ruído, com as proporções de `mark-dark.svg` e 4 cubos verdes; arquivos dentro do CA2.
- [x] feito
### CP2 · após T4–T6 · N0
- Verificar: mostrar o pôster ao usuário e seguir só com o "ok" dele.
### T7 · Compor o hero com o pôster · N4
- Serve: CA3, CA6 · Depende de: T6, bloqueada por P2 · Arquivos: `Hero.tsx:64-66,143`, `app/globals.css` (bloco `.hero-*`), criar `e2e/hero-3d.spec.ts`
- Fazer: pôster via `next/image` (lazy, prioridade baixa) no lugar de `.hero-glow::after`; no computador, coluna da direita; em 390 px, sem cobrir texto e com ≥ 96 px visíveis na primeira dobra. Teste e2e do CA3 primeiro.
- Verificar: `pnpm test:e2e -- hero-3d` → passa; capturas em 1440×900 e 390×844 aprovadas pelo usuário.
- [x] feito
### T8 · Classificar o aparelho em três níveis · N2
- Serve: CA5 · Depende de: T0 · Arquivos: `lib/device.ts:42-53`, `lib/device.test.ts`
- Fazer: `sceneTier()`: `static` com movimento reduzido, economia de dados, ≤ 4 núcleos ou memória informada < 4; senão `lite` com ponteiro `coarse` (exige ≥ 6 núcleos quando informado); senão `full` pela regra atual. Remover `isLowPowerDevice` e trocar a chamada em `HeroField.tsx:195`. Testes primeiro.
- Verificar: `pnpm test -- lib/device.test.ts` → passa; `grep -rn isLowPowerDevice app lib` → vazio.
- [x] feito
### T9 · Escrever o motor da cena de blocos · N3
- Serve: CA4, CA5 · Depende de: T5, T6, T8 · Arquivos: criar `lib/blocos.ts`, `lib/blocos.test.ts`, `app/components/cinematic/BlocosScene.tsx` · Padrão: `HeroFieldScene.tsx:692-999`, `FramePulse.ts`
- Fazer: `stateAt(p)` com os trechos de `hero-captions.ts` (teste primeiro); `InstancedMesh` com os estados em atributos por instância e mistura no vertex shader; entrada em 2,6 s; `full` segue `progressRef`, `lite` troca de estado a cada 4 s à vista; orçamento de 28 ms nos 60 primeiros quadros chama `onError` e grava na sessão.
- Verificar: `pnpm test -- lib/blocos.test.ts BlocosScene` → passa.
- [x] feito
### T10 · Ligar a cena ao hero · N2
- Serve: CA3, CA4, CA5 · Depende de: T7, T9 · Arquivos: `HeroField.tsx`, `app/globals.css` (`.hero-field`)
- Fazer: importar `BlocosScene` e `prepareBlocos` no lugar de `HeroFieldScene` e `prepareField`; `static` não carrega nada; `lite` sem `trackDive`; o canvas cobre o pôster com fade quando `data-ready`.
- Verificar: `pnpm test -- HeroField` e `pnpm test:e2e -- hero-3d scroll` → passam.
- [x] feito
### CP3 · após T7–T10 · N0
- Verificar: suíte e build; captura local com GPU (CA4); celular real (CA5); `shoot.mjs` (CA6).
### T11 · Remover a cena de partículas · N2
- Serve: CA11 · Depende de: CP3 · Arquivos: remover `HeroFieldScene.tsx` e `HeroFieldScene*.test.tsx`; ajustar o comentário de `hero-captions.ts`
- Verificar: `grep -rn HeroFieldScene app lib` → vazio; `pnpm typecheck && pnpm test` → passam.
- [x] feito
### Fase 2 · movimento 2D
### T12 · Animar a escrita · N2
- Serve: CA7 · Depende de: T4 · Arquivos: `app/globals.css` (junto de `:755-767`), `Hero.tsx:92`, `Section.tsx`
- Fazer: classe `.type-line` (largura em `ch` com `steps(var(--n))`, cursor de bloco verde que pisca 3 vezes) nos rótulos mono; marca-texto do trecho verde do H1 por `scaleX` num pseudo-elemento; tudo dentro de `prefers-reduced-motion: no-preference`.
- Verificar: `pnpm test -- Hero Section` → passa; inspeção com movimento reduzido: texto inteiro, sem cursor.
- [x] feito
### T13 · Animar entrada e saída dos componentes · N2
- Serve: CA8 · Depende de: T4 · Arquivos: `app/globals.css`, `app/components/showcase/lightbox.css`, `app/components/conversion.css`
- Fazer: `@starting-style` com `transition-behavior: allow-discrete` no menu do celular, no `<dialog>` e no `::backdrop`, no aviso de cookies e no botão do WhatsApp; `interpolate-size: allow-keywords` com `::details-content` nas perguntas frequentes; 180 ms para sair, 280 ms para entrar.
- Verificar: `pnpm test:e2e -- mobile-menu keyboard contact` → passam.
- [x] feito
### T14 · Trocar de página com a cortina de blocos · N3
- Serve: CA9 · Depende de: T4 · Arquivos: `app/globals.css:1038-1054`
- Fazer: substituir `page-out` e `page-in` por uma máscara em grade que fecha e abre na diagonal em `steps()`, 240 ms para sair e 460 ms para entrar; manter o cabeçalho parado e o bloco de movimento reduzido.
- Verificar: `pnpm test:e2e -- navigation routes` → passam; captura de 3 quadros da troca.
- [x] feito
### T15 · Levar a capa do projeto ao topo do case · N3
- Serve: CA9 · Depende de: T14 · Arquivos: `showcase/ProjectList.tsx:84`, `app/cases/_components/CaseCover.tsx:57` · Padrão: `app/template.tsx:29`
- Fazer: envolver as duas imagens em `<ViewTransition name={"case-" + slug}>`, conforme a documentação do Next 16 instalada.
- Verificar: `pnpm test -- ProjectList CaseCover` e `pnpm test:e2e -- navigation` → passam.
- [x] feito
### CP4 · após T12–T15 · N0
- Verificar: suíte, build e revisão do diff de CSS (só `transform`, `opacity`, `clip-path`, `mask`).
### Fase 3 · peças 3D
### T16 · Renderizar as quatro peças · N3
- Serve: CA10 · Depende de: T6 · Arquivos: criar `3d/pecas.py`
- Fazer: com o kit do T5, montar `site` (vitrine com balão de conversa), `sistema` (grade que vira barras), `revisao` (escudo) e `404` (marca com um bloco caindo); loop de 3 s a 24 quadros por segundo no Cycles, 640 px, fundo `--ink`; `ffmpeg` gera `.webm` e `.mp4` mais pôster `.webp`; o script afirma os limites do CA10.
- Verificar: `blender -b --python 3d/pecas.py` → `OK`.
- [x] feito
### T17 · Usar as peças nos cartões de Soluções · N2
- Serve: CA10 · Depende de: T16 · Arquivos: `WorkModalities.tsx:265,348` · Padrão: `showcase/useShowcaseMedia.ts`
- Fazer: trocar a ilustração de cada cartão por `<video muted loop playsInline preload="none" poster>` que toca só à vista; com movimento reduzido ou economia de dados fica o pôster.
- Verificar: `pnpm test -- WorkModalities` → passa.
- [x] feito
### T18 · Refazer o 404 com a marca · N2
- Serve: CA10 · Depende de: T16 · Arquivos: `app/not-found.tsx:34-46`, `app/not-found.test.tsx`
- Fazer: tokens do site (`--ink`, `--slash`, fonte display) no lugar das cores `slate`, e o loop `404` no lugar do ícone `Code2`; manter título e links.
- Verificar: `pnpm test -- not-found` → passa.
- [x] feito
### CP5 · fechamento · N0
- Verificar: cada CA da seção 3, com o resultado anotado aqui; impeccable critique e audit; jury; `Status: concluída`.
## 8. Registro
2026-11-09 — spec escrita sobre o `main` do Gitea (`4671837`); o checkout local está 54 commits atrás.
2026-11-09 — aprovada pelo usuário: P1 = o símbolo que o site já usa; P2 = a peça 3D ocupa a coluna da direita e as molduras de `HeroWork` saem do hero.
2026-11-09 — T0 feito: `origin` no Gitea, branch `feat/blocos-3d-motion`, base `4671837`, 150 suítes verdes antes de qualquer mudança.
2026-11-09 — CP1: `pnpm lint`, `pnpm typecheck`, `pnpm test` (151 suítes, 1654 testes) e `pnpm build` verdes; `build-icons.mjs --check` → `OK`. Falta só conferir o ícone no celular depois do deploy.
2026-11-09 — T2: o `sharp` é resolvido a partir do pacote `next`, porque não é dependência direta do site.
2026-11-09 — T8: `lib/device.paths.test.ts` também citava `isLowPowerDevice` e foi convertido.
2026-11-09 — interface de `blocos.json` mudou de 5 para 7 números por bloco (`x, y, z, sx, sy, sz, tom`): a marca não cabe numa grade única, então cada retângulo dela é ladrilhado com blocos de escala própria e o contorno fica exato.
2026-11-09 — o `next dev` reescreve uma linha do `AGENTS.md` (nível do título); não é mudança deste trabalho.
2026-11-09 — o projeto não tem `PRODUCT.md`; o impeccable pede que ele seja escrito com o usuário antes do T7 (composição do hero).
2026-11-09 — T5: N = 238 blocos, 25.704 triângulos no total; `bloco.glb` 2,7 KB, `blocos.json` 3 KB em gzip; mesma saída em duas execuções.
2026-11-09 — T6: materiais, luz de três pontos, pôster (38 KB) e matcaps (2 a 3 KB) saem do próprio `3d/blocos.py` em Cycles com OptiX, em 6 s. O ajuste foi feito por renders sem interface, não pelo Blender MCP: dá o mesmo resultado e fica reproduzível. Os matcaps se chamam `matcap-porcelana.webp` e `matcap-lima.webp`.
2026-11-09 — CA8 não fecha como escrito: o menu do celular, o aviso de cookies e o botão do WhatsApp são desmontados pelo React ao fechar (`Header.tsx`, `GoogleAnalytics.tsx`, `FloatingWhatsApp.tsx`), e CSS não anima a saída de elemento que some do DOM. Só o vídeo ampliado (`<dialog>`) e as perguntas frequentes (`<details>`) aceitam saída sem JavaScript. T13 fica à espera da decisão do usuário.
2026-11-09 — T12: rótulos das seções digitados (`.ds-type`) e bloco verde que recolhe sobre as palavras em verde do H1 (`.hero-mark`). O rótulo do hero ficou de fora: ele usa Space Grotesk de propósito (não baixar fonte antes do LCP) e não é mono. `Reveal.test.tsx` também afirmava `ds-reveal` no rótulo e foi atualizado. Conferido no navegador em 1440×900 com as animações pausadas em 700, 1150 e 4000 ms.
2026-11-09 — `public/3d` saiu da checagem do Biome (`biome.json`): `blocos.json` é gerado e compacto de propósito.
2026-11-09 — aprovado pelo usuário: o pôster (CP2); entrada em todos os componentes e saída só onde o elemento continua no DOM (CA8 passa a valer assim); os três ganchos recomendados, em spec própria (`specs/2026-11-09-ganchos-e-ofertas.md`).
2026-11-09 — `PRODUCT.md` escrito com as respostas do usuário; o site também serve a recrutadores e empresas de tecnologia.
2026-11-09 — T7: a peça fica na coluna da direita de 1024 px em diante (até 46% da largura) e, abaixo disso, no canto, na linha do rótulo (112 a 200 px). As molduras de navegador e celular saíram do hero e a prop `workUrl` foi removida da cadeia; o cartão do site da SEV continua abaixo de 1024 px. A malha de pontos estática (`.hero-glow::after`) saiu. Conferido em 1440×900 e 390×844; a aprovação visual do usuário fica para o CP3, com o site rodando em local.
2026-11-09 — T13: entrada por `@starting-style` no menu do celular, no aviso de cookies e no botão do WhatsApp; abrir e fechar animados nas perguntas frequentes. A saída do vídeo ampliado não aparece: `ShowcaseProvider.tsx:74-79` desmonta o `<dialog>` no `close`. A altura das perguntas frequentes é a única propriedade de leiaute animada.
2026-11-09 — entrada da cena (CA4): como o pôster já mostra a marca montada, a cena assume com os blocos a até 1,2 unidade do lugar e assenta em 1,4 s, em vez de montar de longe em 2,6 s.
2026-11-09 — Fase 3 (T16 a T18) adiada para depois da spec dos ganchos: os cartões de Soluções vão mudar de 3 para 4, e as peças dependem de quais cartões existem.
2026-11-09 — T9: `lib/blocos.ts` (`parseBlocos`, `stateAt`, `createFrameBudget`, `framePiece`) e `BlocosScene.tsx` (uma chamada de desenho, estados em atributos por instância, matcaps do Blender). Conferido com GPU real (ANGLE Vulkan, RTX 5060 Ti): os quatro estados batem com as prévias do Blender.
2026-11-09 — T10: `HeroField` trata os três níveis. No `full` o canvas cobre o palco e a peça descansa sobre o pôster (`placementRef`); no mergulho ela vai para a área útil entre o cabeçalho e as legendas (`stageRef`: 6% a 94% da largura, do cabeçalho + 24 px a 70% da altura). No `lite` o canvas ocupa só a caixa do pôster. `HeroField` passou para depois de `.hero-scrim`, para a peça não ficar sob o véu. `--hero-open` e a máscara do fundo ambiente saíram.
2026-11-09 — o primeiro trecho de `stateAt` é 0,16 a 0,30 (era 0,10 a 0,26): os blocos só se mexem com o título quase apagado. Em p = 0,20 os blocos mais à esquerda encostam no fim do subtítulo, que está com 16% de opacidade.
2026-11-09 — a caixa da marca na cena é 15,25 unidades (o enquadramento real do pôster), não 13. Canvas contra pôster em 1440×900: centro a 1,4%, tamanho a 3,1%; a porcelana do canvas sai 11% mais clara que a do pôster (vem do matcap, que é de uma esfera vista de frente).
2026-11-09 — T11: `HeroFieldScene.tsx` e seus testes removidos.
2026-11-09 — T14: cortina de blocos em 600 ms (`@property --page-wipe`, 8 colunas, `round(down, …, 12.5vw)`); a página que sai fica parada. `app/template.tsx` passou a usar `default="page"` em vez de `name="page"`: com nome comum, a página antiga pulava para o topo ao sair rolada. `jest.setup.ts` repassa os filhos de `ViewTransition`.
2026-11-09 — CP3 parcial: captura com GPU em 1440×900 nos progressos 0, 0,23, 0,35, 0,63 e 0,93 vista e aprovada pela sessão principal. Faltam celular real (CA5) e a medição de LCP com rede limitada (CA6).
2026-11-09 — T15: a capa viaja com o nome `case-<id>` (`cover-transition.ts`) na lista da home no celular, em `/cases` e, no computador, a partir do projeto em exibição na vitrine (`PinnedReel`, pelo pôster e não pelo vídeo). No primeiro quadro há um pequeno salto, porque o pôster mostra o topo do site e o vídeo estava no meio da rolagem.
2026-11-09 — CP3 e CP4: `biome check`, `pnpm typecheck`, `pnpm test` (150 suítes, 1612 testes), `pnpm build` e `pnpm test:e2e` (53 testes, com axe e os três de `hero-3d`) verdes sobre `pnpm start -p 3001`, encerrado ao fim. Para rodar a suíte e2e foi baixado o Chromium do Playwright (`playwright install chromium`).
2026-11-09 — CA6 medido em 390×844 com CPU 4× mais lenta e rede de 1,6 Mbps: LCP em 812 ms, no H1; CLS 0. O `shoot.mjs` do awwwards não roda sem o pacote `playwright`; a medição foi feita com um script descartável sobre `@playwright/test`.
2026-11-09 — pendente: celular real (CA5 e ícone do CA1), aprovação visual do usuário da composição do hero, Fase 3 e fechamento (CP5).
2026-11-09 — Fase 3 revista (T16 e T17): as peças em loop vão para onde não há prova real para mostrar. São três: `sistema` (no cartão, no lugar do esquema em CSS), `whatsapp` (cartão novo) e `404`. O cartão do site continua com a captura do site real e o da revisão com o trecho de relatório: trocar trabalho real por escultura iria contra o `PRODUCT.md`. A T17 foi feita junto com a T4 da spec dos ganchos.
2026-11-09 — T16: `3d/pecas.py` gera `peca-sistema`, `peca-whatsapp` e `peca-404` (webm de 18 a 51 KB, mp4 de 21 a 49 KB, pôster de 5 a 7 KB; 640×400, 3 s, sem áudio). Em `3d/blocos.py` a luz virou `luz_de_estudio()` e `main()` ficou sob `if __name__ == "__main__"`.
2026-11-09 — T17: `OfferVideo.tsx` toca o loop só à vista e fica no pôster com movimento reduzido ou economia de dados; usado nos cartões `sistema` e `whatsapp`.
2026-11-09 — T18: 404 com os tokens do site, os botões de `button-styles.ts` e o loop da marca; sem ícones.
2026-11-09 — o matcap da porcelana sai com 0,3 ponto de exposição a menos: o centro foi de 222 para 203, contra 201 na frente dos blocos do pôster.
2026-11-09 — Impeccable atualizado para 4.8.2 pelo gate com `--force`, depois da revisão dos achados (1 alto e 29 médios, todos lidos como falsos positivos). Cópia da 4.7.0 em `~/tmp/impeccable-4.7.0-backup`.
2026-11-09 — CP5, fechamento. Resultado por critério:
- CA1: passa nos testes e no `--check`; falta ver o ícone instalado num celular depois do deploy.
- CA2: passa (`OK blocos=238 tris=25704`, todos os arquivos dentro dos limites).
- CA3: passa (`e2e/hero-3d.spec.ts`, 3 testes).
- CA4: passa, com a entrada revista (assenta em 1,4 s); visto com GPU em 1440×900.
- CA5: passa nos testes e em celular emulado com GPU; falta celular real.
- CA6: passa. LCP no título em 340 ms (computador com GPU), 116 ms (sem GPU) e 868 ms (celular, CPU 4× mais lenta e rede de 1,6 Mbps); CLS 0.
- CA7: passa.
- CA8: passa como revisto (saída só nas perguntas frequentes).
- CA9: passa; medido à parte: a cortina roda de 150 a 850 ms em home → /cases, home → /servicos e /cases → case.
- CA10: passa, com três peças em vez de quatro.
- CA11: NÃO passa. Suíte toda verde (biome, tipos, 152 suítes e 1631 testes, build, 57 testes e2e), mas a crítica do Impeccable deu 29/40 com Consistência em 2 (o botão "Falar sobre meu projeto" leva a lugares diferentes, o item "IA" do menu é ambíguo, o verde do WhatsApp não tem token), a auditoria deu 15/20 e o crítico cego do jury deu 6,4 (Design 6, Usabilidade 7, Criatividade 6, Conteúdo 7). Relatórios em `~/tmp/avaliacao/impeccable.md` e `~/tmp/.jury/127.0.0.1-3001/scorecard.md`. Diagnóstico rápido das regras de design: 9 (falha só nos rótulos em caixa alta).
2026-11-09 — achado da crítica corrigido: em computador com GPU o LCP era a imagem do pôster aos 2,3 s, porque a troca de opacidade ao esconder o pôster o repintava. O pôster agora carrega junto com a página e some por `visibility`; o LCP voltou ao título.
2026-11-09 — 404 alinhado à grade do site (`ds-section-shell`) e com a peça menor no celular, para os botões não ficarem sob o botão do WhatsApp.