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

30 KiB
Raw Blame History

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:

// 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.
  • 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).
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.