← Biblioteca MANUAL.md CatálogoContratoManualLeia-me
manual

Manual da biblioteca

O CLAUDE.md diz o que vale. Este arquivo diz por quê, e guarda os casos que já custaram uma noite para alguém.

A separação existe por economia: o contrato é lido em toda sessão de trabalho e precisa ser curto. O manual é consultado quando você está construindo, e pode ser longo à vontade. As seções aqui têm a mesma numeração do contrato.


§1 — Por que a stack é essa, e o que ela cobra

O alvo é um snippet que cola em três lugares sem adaptação: página estática, .astro, ou bloco HTML do Elementor. É isso que decide todo o resto. Qualquer coisa que exija passo de build quebra o terceiro caso, que é justamente o mais usado no dia a dia.

GSAP entra só quando a interação depende de timeline encadeada ou de ScrollTrigger com scrub. Reveal, parallax simples e entrada de seção são IntersectionObserver + requestAnimationFrame, já resolvidos no core/motion.js. Nenhum dos onze componentes autorais precisou de GSAP.

O 404 de fonte não cai no fallback de graça

Custou o showroom inteiro rodando em fonte de sistema sem ninguém notar.

O tema declarava @font-face para Qanelas e Neue Haas Display. Os arquivos são licenciados e nunca estiveram no repositório — o .gitignore os barra, e está certo. Mas a declaração ficou.

O que a intuição diz que acontece: o navegador não acha, usa a próxima fonte da pilha, fim. O que de fato acontece: o navegador pede o arquivo, toma 404, e só então desiste. Em toda página. E o showroom carrega dezenas de previews em <iframe>, cada um com a própria cópia do CSS — o que multiplicou o pedido morto por dezenas. Medido na home: mais de 400 requisições de fonte, todas 404, e 212 erros de console.

Nenhum validador viu, e os dois estavam certos para o que sabiam fazer: o valida.py conferia caminho em HTML, e o conformidade.py não olha fonte. url() dentro de CSS era ponto cego dos dois — hoje não é mais: o valida.py resolve todo url() de CSS a partir da própria folha, e ignora o que está dentro de comentário, porque regra comentada não roda.

A regra que ficou: declare @font-face só quando o arquivo estiver lá. Sem o arquivo, a pilha de --display/--sans nomeia a fonte assim mesmo e cai limpo na seguinte — que é o que o core/temas/kirvano.css sempre fez.

Por que as fontes da casa são auto-hospedadas

Inter e Manrope vivem em core/temas/fontes/biblioteca/, servidas pelo próprio site. Não por <link> para o Google, por duas razões que sozinhas já decidiriam: o §1 pede @font-face local, e a CSP em _headers declara font-src 'self' — um pedido ao fonts.googleapis.com seria bloqueado do mesmo jeito silencioso que o 404.

Elas podem ser versionadas porque são SIL Open Font License 1.1, livre para redistribuir. Qanelas e Neue Haas não podem. É a licença que decide, não a conveniência.

São woff2 variáveis: um arquivo por família cobre a faixa inteira de peso. Por isso não têm o par .woff — woff não carrega fonte variável, e todo navegador que entende fonte variável entende woff2. O par continua valendo para fonte estática. Créditos: core/temas/fontes/biblioteca/LICENCA.md.


§2 — Por que o idioma é misturado

Português na documentação, inglês no código. O motivo é portabilidade: os sites que já rodam (Tarvos, Liberta Wealth) foram escritos assim. Trocar agora quebraria a cópia nos dois sentidos — daqui para lá e de lá para cá.


§3 — Tokens

Componente que lê só token é o que permite o mesmo hero rodar preto-e-sage na Liberta e claro-e-glow em outro cliente, sem tocar no arquivo do componente.

Global pede conversa, local não

Token global entra no vocabulário que todo componente enxerga e todo tema tem de preencher. Criar um é decisão de arquitetura, e o custo de errar é alto: um nome mal escolhido fica.

Custom property do próprio componente é outra coisa. --cine-dur, --wheel-radius, --sync-band e --fold-spread não são vocabulário — são o jeito de o CSS e o JS trocarem um número sem cravá-lo em dois lugares. O midia/roda-de-cards mede o raio no script e escreve em --wheel-radius; o CSS lê. Se isso precisasse de aprovação, ninguém construiria nada.

A regra prática: se outro componente pode querer ler, é global. Se só o dono entende o que o número significa, é dele.

A cor de marca não é cor de texto

--accent existe para o botão, o filete do olho, o ponto aceso, a borda que acende. Em nenhum desses casos ele é texto pequeno sobre fundo claro — e foi exatamente ali que ele falhou.

O core pinta o realce em itálico do título com a cor de marca: h2.sec-title em{color:var(--accent)}. Sobre o escuro da casa isso dá 12:1, folgado. Sobre o creme do .sec-paper, com o sage da Liberta, dá 1.36:1 — o mínimo é 3:1 para texto grande e 4,5:1 para texto normal. O realce ficava praticamente invisível, e já era assim em quatro componentes catalogados: conversao/calculadora-sucessao, efeitos/quebra-papel-rasgado, listas/leque-paineis e prova/manchetes-imprensa.

A saída foi um token de escopo, --accent-ink, e a escolha do valor tem uma razão que a conta mostrou: misturar accent com --fg só serve no claro.

escopo fundo accent puro 45% accent + --fg
.sec-dark #0a0a0a 12,08 14,86
.sec-paper #ede9e3 1,36 4,59

No claro a mistura escurece e resolve. No escuro ela clarearia, e aí o problema é outro: 45% de accent com uma tinta quase branca devolve um cinza esverdeado pálido — o contraste sobe e a cor de marca desaparece. Por isso o escopo escuro mantém --accent-ink: var(--accent), e só o claro mistura.

Duas consequências práticas:

A regra que decide é qual fundo está atrás do glifo: fundo do escopo → --accent-ink; foto com véu → --accent. - A correção é do tema de graça. As duas pontas da mistura são tokens, e um tema que troque o --accent leva o ajuste sem escrever nada.

O que não se resolve com isso: --accent como fundo de botão em seção clara continua sendo decisão de contraste do tema, porque ali quem tem de mudar é a cor do texto por cima, não a do fundo.

A tinta de apoio tinha o mesmo defeito, e ninguém tinha medido

Um dia depois de .nota virar primitiva — a ressalva de 12,5px, que a promoção levou a dezessete componentes de uma vez —, a conta foi refeita para --fg-muted. Texto pequeno pede 4,5:1, e o cinza cravado não fechava em nenhum dos três temas da casa:

escopo fundo --fg-muted cravado
.sec-light #f4f2ee (default do core) 4,63 — passa raspando
.sec-light #f1ece1 (kirvano) 4,39
.sec-light #ede9e3 (liberta-wealth) 4,28
.sec-light #e4e4e7 (liberta-eventos) 4,08
.sec-paper #ede9e3 (liberta-wealth) 4,37

A causa é a mesma do --accent-ink, e vale como regra: valor cravado num escopo é calibrado contra o fundo do default, e todo tema que escurece --light-2 invalida a calibragem sem avisar. Os três escurecem — é o que um tema faz.

A saída também é a mesma: derivar das duas pontas que o tema controla. --fg-muted:color-mix(in srgb,var(--fg) 62%,var(--bg)) nos dois escopos claros devolve 4,62 a 4,95:1 nos três temas, medido no Chrome com bin/medir.js trocando o href do tema em tempo de execução.

62% é o menor valor que fecha 4,5 no pior fundo da casa (#e4e4e7) com folga. Acima disso a tinta de apoio começa a disputar com --fg-2 e a hierarquia de três níveis achata.

O escuro ficou de fora de propósito: lá o cinza cravado dá 6,05:1 no .sec-dark e 5,43:1 no .sec-deep, e derivar só tiraria a calibragem de quem já está certo.

Consequência para quem escreve tema: core/temas/liberta-wealth.css deixou de redeclarar --fg-muted no .sec-paper dele. Redeclarar é legítimo — mas quem redeclara volta a assinar a conta sozinho, e a conta é contra o fundo daquele tema, não contra o do core.

A exceção do escopo de tom

"Componente jamais faz override de --fg" continua valendo. Mas existe um padrão que parece violação e não é: declarar .sec-dark ou .sec-deep no markup do próprio bloco.

A diferença é real. Override é o componente redefinir --fg com um valor próprio, e aí ele deixa de ser tematizável. Escopo no markup é o componente entrar num escopo que o core já define, continuando a ler os mesmos nomes — o tema segue mandando nas cores dos dois escopos.

Serve para inverter o tom dentro de uma dobra de tom oposto: quatro cards escuros numa seção clara. Precedente: conteudo/bento-recursos-expansivel.

O tom vizinho

Fundos escuros adjacentes têm de ser quase idênticos (#0a0a0a / #131313). Salto forte de tom entre seções suja a página. Quando precisa de contraste de verdade, use uma seção .sec-paper inteira como respiro, e a seguinte volta direto ao escuro.


§4 — Grid

.wrap sozinha controla a coluna. A tentação é sempre a mesma: um componente que precisa sangrar até a borda ganha margin-inline: calc(50% - 50vw). Não faça. 100vw inclui a barra de rolagem em alguns navegadores, e o componente passa a estourar a página na horizontal em vez de sangrar.

A saída é o elemento sair de dentro do .wrap. Quando não dá — trilho que sangra mas cujo primeiro card tem de alinhar à coluna — o padrão da casa é uma custom property de recuo calculada com a mesma conta do .wrap, aplicada ao trilho. Ver --pfl-inset no midia/carrossel-perfis e --depo-inset no prova/carrossel-depoimentos.


§5 — Movimento

Nada entra seco

Toda entrada carrega filter: blur(14px) indo a zero, junto do fade e da subida. Translate puro não é aceito. É a assinatura de movimento da casa: o elemento não aparece, ele entra em foco.

O caso que custou uma noite: blur(0) ainda é um filtro

[data-rv].in termina em filter: blur(0), e não em filter: none. Isso não é descuido, e a diferença é grande: um elemento com filtro é bloco de contenção para descendente position: fixed.

Consequência: tudo que já foi revelado por [data-rv] segue prendendo um filho fixed à própria caixa, para sempre — não só durante a entrada. Medido no Chrome: filho fixed com top:0 dentro de um pai com blur(0px) pinta no topo do pai; com filter:none no pai, pinta no topo da janela.

Trocar por filter:none não resolve: a transição sai de blur(14px), e none não interpola a partir de um filtro — a entrada perderia o desfoque. Então a regra fica, e quem precisa de um elemento fixed (uma imagem que segue o cursor, um painel colado à janela) põe esse elemento fora de qualquer [data-rv], como filho direto da seção. É o que o listas/lista-imagem-flutuante faz.

sticky não sofre disso, e a diferença importa. A leitura natural da regra acima sugere que todo posicionamento quebra dentro de um elemento filtrado — não quebra. position:sticky gruda no contêiner de rolagem mais próximo, e filtro nenhum interfere nisso. Medido no mesmo pai com filter:blur(0px): o filho fixed ficou ancorado ao contêiner (topo em 1253, rolando junto com ele, em vez de parar no topo da janela), e o filho sticky grudou certinho no topo do seu scroller (1653). Um cabeçalho de tabela sticky dentro de uma linha revelada por data-rv funciona. Precedente: listas/tabela-comparativa.

O outro caso que custou uma noite: a especificidade do reveal

O core termina o reveal com [data-rv].in{transform:none}. Esse seletor vale (0,2,0) — mais que [meu-atributo] ou .minha-classe, que valem (0,1,0).

Um componente que gerencie o próprio transform de repouso no mesmo nó que leva o data-rv perde a regra em silêncio assim que a dobra entra. O sintoma é cruel: o JS continua rodando, o style inline até aparece certo no inspetor, e o computado volta a none.

Medido: com o core carregado, [data-tilt]{transform:…} e .classe{transform:…} no mesmo nó computam transform:none depois do .in; o mesmo transform num filho, com o data-rv no pai, computa a matriz certa.

A saída é sempre a mesma: data-rv num invólucro, elemento que se move por dentro. É o que efeitos/inclinacao-3d faz com .tilt-card-slot.

A armadilha é com transform, e só com ele. As propriedades individuais translate, rotate e scale são outras propriedades, e o reset do core não as toca. Medido em 2026-10-01: um [data-rv].in com translate:0 30% computa transform:none e o translate intacto. Um deslocamento de repouso escrito em translate pode morar no mesmo nó do data-rv — e às vezes precisa: no hero/letreiro-marca a capa carrega a sombra no invólucro deslocado, e com o data-rv num filho a sombra aparecia pousada um segundo antes da capa. Quem "simplificar" um desses para transform: translateY() reabre o caso desta seção.

Princípios da leva autoral


§6 — CSS

Por que prefixo em tudo

Classe solta (.n, .full, .veil) colide no primeiro projeto que juntar dois componentes na mesma página. Como a biblioteca existe justamente para juntar componentes, a colisão não é hipótese: é questão de tempo.

Por que seletor de elemento é proibido solto — e onde não é

h3 { } dentro do CSS de um componente vaza para todo h3 da página no momento em que a folha é colada, e o §6.4 diz que ela pode ser colada em qualquer posição. Por isso a estrutura do componente é sempre classe.

O que não é problema: elemento descendente do próprio prefixo, em conteúdo de prosa. .fan-body p não vaza — está preso ao bloco. E prosa é justamente o lugar onde exigir classe não paga: o texto vem de quem escreve conteúdo, não de quem escreve markup, e obrigar .fan-body-paragraph em cada parágrafo transforma edição de texto em edição de código.

A fronteira: estrutura e peça de interface pedem classe; corpo de texto corrido (p, li, strong, em, a) pode ser elemento, sempre descendente do prefixo.

O [hidden] que não esconde

O [hidden] do navegador é display:none na folha do agente do usuário, e qualquer regra de autor com display vence essa — inclusive um .minha-classe{display:flex} inocente.

O sintoma é cruel: o JavaScript põe hidden, o atributo está lá no inspetor, e o elemento continua na tela. A linha que resolve é uma só, e vive no próprio componente:

.meu-bloco[hidden]{display:none}

Já foi preciso em quinze componentes (contados em 2026-09-06): agendamento-reuniao, aviso-cookies, card-expansivel-fullscreen, carrossel-foco-central, comparador-cenarios, diagnostico-perfil, drawer-fullscreen, drawer-lateral, galeria-scroll-horizontal, modal-dialogo, planos-honorarios, progresso-leitura, select-custom, simulador-carteira, slideshow-cinematico. Não vira primitiva porque o seletor tem de ser do próprio bloco — o que se promove aqui é a regra de escrevê-la.

Que a lista tenha ido de sete a quinze em cinco dias diz o de sempre: toda peça que alterna estado esbarra nisso, e cada uma esbarra sozinha. Se um dia alguém achar um jeito de a regra valer sem o seletor do bloco, é a maior economia de linha repetida que a biblioteca tem para fazer.

Por que mobile fica no fim do bloco

Media query de largura em arquivo separado obriga a abrir dois arquivos para entender uma regra. No fim do próprio bloco, a leitura é linear: desktop, depois o que muda.

@media(prefers-reduced-motion) é a exceção e fica colada à regra que desliga — ali a proximidade é o que torna a intenção legível.

Véu sobre imagem

backdrop-filter em caixa reta produz borda dura e lê "chapado". Sempre acompanhado de máscara em gradiente:

mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);

Texto sobre imagem

Legenda ou título de card de mídia vai dentro da imagem, ancorado embaixo, com gradiente mais blur mascarado. Não vira parágrafo abaixo da foto — a legenda embaixo transforma uma peça gráfica em duas.

Hover não mexe em nada

Decisão do dono em 2026-09-29, com a razão dele: "sempre acaba quebrando as páginas". E quebra mesmo, por três caminhos que a biblioteca já tinha pisado:

O que saiu, na varredura do dia: o zoom de foto de dez componentes (pilares-texto-na-foto, carrossel-foco-central, carrossel-perfis, faixa-videos, galeria-scroll-horizontal, grade-cases-foto, showreel-expansivel, card-expansivel-fullscreen, depoimento-video e o card que subia no drawer-lateral), o salto de cinco botões (stage-arrow, fab e os três discos de play) e a abertura por hover do leque e da pasta. O .btn do core já dizia a mesma coisa desde o começo — "o botão não salta nem move o layout" —, e cinco peças o desobedeciam.

O retorno de hover mora onde não há geometria: filter (brilho, saturação), cor de borda e de texto, opacidade de um véu. Quase todos os componentes já tinham os dois — o zoom e a cor —, e a cor sozinha basta.

O que continua permitido, e por quê: a seta de 2–4px dentro do .btn (é o .btn svg do core) e o filete de link que cresce por ::after. Os dois são tipografia: não mudam a caixa de nada e não tocam imagem. E os componentes cujo assunto é o ponteiro — efeitos/inclinacao-3d, efeitos/cursor-disco, listas/lista-imagem-flutuante — não são hover incidental; quem os cola está pedindo exatamente aquele efeito.

O bin/conformidade.py acusa transform sob :hover quando o sujeito da regra é o próprio elemento sob o ponteiro ou um descendente que não seja svg, pseudo-elemento ou ícone.

Quando promover ao core

Uma regra que aparece em três ou mais componentes é candidata a primitiva. Antes de recopiar pela terceira vez, promova e ajuste os componentes. O .display-xl entrou assim: conteudo/manifesto-com-imagens era a terceira cópia, depois de conteudo/apresentacao-foto-alternada e midia/carrossel-perfis.

O ul,ol do reset entrou pelo mesmo caminho em 2026-09-06: conteudo/rolagem-sincronizada, conversao/agendamento-reuniao e listas/linha-do-tempo-etapas escreviam as mesmas três declarações, cada um com o próprio comentário explicando que o core zerava ul e não ol.

A primitiva que quase entrou errada

O .field nasceu de três cópias idênticas, e a receita foi copiada da mais antiga delas — que trazia outline:none no controle. Passou para o core junto, e o comentário da primitiva chegou a afirmar que "o outline do :focus-visible continua de pé".

Não continuava. .field input vale (0,1,1) e o ::focus-visible do core vale (0,1,0): o anel de foco morreria em todo campo que vestisse a primitiva. Dois dos três componentes de origem já zeravam o anel localmente, então para eles nada mudaria — o erro era invisível justamente onde nasceu. Quem o pegou foi o terceiro, o conversao/agendamento-reuniao, o único que mantinha o anel e o único que tinha escrito por quê.

Duas lições, e a segunda é a que interessa:

Adotar uma primitiva cria uma dependência silenciosa

O caso, inteiro, aconteceu em uma tarde:

  1. A .dialog-casca nasceu com color:inherit, para o <dialog> não cair no CanvasText do navegador.
  2. O overlay/aviso-cookies adotou a primitiva e apagou o color local, porque a primitiva agora fazia aquilo. Correto, e é o objetivo da promoção.
  3. Mediu-se que color:inherit vale (0,1,0) — o mesmo que .sec-deep{color:var(--fg)} — e, carregando depois, vencia o escopo de tom declarado no markup, que é a exceção nomeada do §3. Foi removido.
  4. O painel de cookies ficou com texto preto sobre fundo rgb(24,24,24). Nada acusou: o CSS é válido, a conformidade passa, o console fica limpo.

A regra que fica: um componente que apaga uma declaração local porque a primitiva a cobre passa a depender daquela linha da primitiva. Mexer na primitiva depois quebra o componente em silêncio, e o silêncio é o problema — não há validador para "esta cor herdada ficou ilegível".

Duas consequências práticas:

Seis armadilhas que a leva de 2026-09-09 mediu

Todas apareceram construindo, nenhuma lendo o código. Ficam aqui porque a próxima pessoa vai esbarrar nas mesmas.

1. <dialog> modal ESCAPA da contenção do filter. O §5 diz que elemento com filtro prende descendente position:fixed, e o sticky já ganhou a ressalva dele. Agora o diálogo: com um [data-rv] de blur(10px) real por cima, um <dialog> aberto com showModal() mediu [0,0,1425,900] e pintou nítido. É a primeira coisa fixed da casa que escapa da armadilha — porque o diálogo modal vive na top layer, fora da árvore de pintura do ancestral. Precedente: prova/depoimento-video.

2. width/height no <img> transforma aspect-ratio em no-op silencioso. Uma capa com width="1600" height="900" e aspect-ratio:16/9 saiu 348×900 — o atributo ganha, e o card foi a 902px. block-size:auto resolve. Precedente: midia/faixa-videos.

3. Véu de legenda se mede pela altura da LEGENDA, não do card. Véu com altura em porcentagem do card é ultrapassado por título de três linhas em tela estreita, e o texto sai do gradiente. O véu vira ::before da própria legenda. Precedente: midia/grade-cases-foto.

4. list-style:none custa a semântica de lista no Safari. O reset ul,ol do core remove o marcador, e o VoiceOver deixa de anunciar "lista de N itens". Onde a ordem é a informação, escolher uma das duas saídas é obrigatório: role="list" no <ol>, ou número audível (sem aria-hidden no contador). O listas/linha-do-tempo-etapas tinha as duas faltando — cinco etapas que não entregavam ordem a ninguém que ouve a página.

5. O [hidden] do §6.5 tem um segundo buraco: a decoração do vizinho. .meu-bloco[hidden]{display:none} esconde o bloco, e deixa de pé a seta que o penúltimo item desenha apontando para ele. A saída é :has(+ .meu-item[hidden]), que a casa já usa em três componentes. Precedente: dados/fluxo-conectado.

6. Na ficha, código inline com um atributo de caminho vaza caminho resolvível. (E sim: a primeira versão deste próprio parágrafo caiu nela, escrevendo o atributo em span inline — o valida.py acusou o MANUAL.html.) O ficha.html gerado escapa as aspas dentro de bloco cercado (``), mas **não** dentro de span inline — e ovalida.pypassa a acusar um caminho que só existia como exemplo. Escreva exemplo de markup em bloco cercado. Precedente:midia/faixa-videos`.

E uma de ambiente, ao lado da nota dos quatro autofocus do §8: legenda .vtt não carrega por file://. O <track> é buscado com CORS e cada arquivo local é origem opaca. Conferir legenda pede servidor.

A sétima, medida em 2026-09-21: o comentário que se fecha sozinho

Custou 885px de rolagem horizontal num celular de 390, com o conformidade.py passando limpo.

O componente de efeitos delimita a peça no markup com um comentário de abertura e outro de fechamento, como moldura-secao e cursor-anotacao fazem. O cabeçalho do arquivo, que também é comentário, citava esses delimitadores por escrito — e a citação carregava a marca de fechar comentário. O HTML não tem comentário aninhado: a marca fecha o comentário ali, no meio da explicação. Da linha seguinte em diante o cabeçalho deixou de ser comentário e virou conteúdo — a prosa foi parar na página, e o <em> que o texto mencionava virou um elemento de verdade.

Por que nada acusou: o auditor lê CSS e JS, não interpreta HTML; o navegador não reclama de comentário mal fechado, ele só decide onde o comentário acaba; e o preview abre normalmente, com uma barra de rolagem horizontal que só aparece em tela estreita.

A regra: dentro de comentário, escreva o delimitador por extenso — "o comentário de início e o de fim da peça" —, nunca a marca literal. Vale para o HTML e vale para o CSS, onde a marca de fechar bloco tem o mesmo poder. É primo do caso 6 acima: as duas nascem de escrever, dentro do arquivo, um exemplo do próprio arquivo.

Precedente: efeitos/destaque-texto-scroll.

E a oitava: grid-template-rows:0fr não fecha nada

A receita de acordeão sem altura fixa é uma pista de grade indo de 0fr a 1fr, e ela está espalhada por toda a internet escrita assim:

.item{display:grid;grid-template-rows:0fr;transition:grid-template-rows .4s}
.item.aberto{grid-template-rows:1fr}

0fr sozinho é minmax(auto,0fr) — o mínimo automático é o conteúdo, e o conteúdo não encolhe. Medido no listas/etapas-em-abas: a linha "fechada" tinha 103,938px, exatamente a altura da aberta, e a transição não tinha o que animar. Com min-height:0 no item de dentro sobraram 10px (o padding). Só minmax(0,0fr) na pista fechou em 0px.

O erro é silencioso das duas pontas: o CSS é válido, o navegador não reclama, e num acordeão com pouco texto a diferença passa por decisão de desenho. Quem escrever a receita, escreva minmax(0,0fr) e meça a altura fechada.

Precedente: listas/etapas-em-abas.

A fila de promoção — o que saiu dela e o que continua

A fila é contada à mão, e envelheceu duas vezes: em 2026-09-21 e de novo em 2026-09-29 ela listava como pendente o que já estava no core. Quem promover algo daqui, risca a linha na mesma mudança — e quem for contar de novo relê o core/ antes de acreditar na tabela.

Já promovidas — a primitiva existe, e o número entre parênteses é quantos componentes a consomem hoje:

Primitiva Onde Consumidores
Trava de rolagem com contador LIB.travarRolagem/soltarRolagem, core/motion.js 6 — drawer-fullscreen, card-expansivel-fullscreen, drawer-lateral, modal-dialogo, faixa-videos, depoimento-video. O bug que esperava — dois overlays se destravando mutuamente — morreu com o contador
Casca do <dialog> .dialog-casca, core/base.css 6. Só a casca neutra; a coreografia de cada um continua local
Campo com régua .field, core/base.css 4, com a conversao/faixa-newsletter
.sec-head centrar os filhos core/base.css, 2026-09-29 todo .sec-head. Ver abaixo
Tokens --veil e --z-overlay core/tokens.css 2 leem hoje; os <dialog> vivem na top layer e não precisam
.nota, o parágrafo de ressalva core/base.css, 2026-09-21 17 migrados, mais os que nasceram depois. Ver "Saíram da fila — 2026-09-21"
.btn-play + LIB.pausavel core/base.css e core/motion.js, 2026-09-21 o botão de pausar o que anda sozinho (WCAG 2.2.2)
Hora num fuso LIB.relogio(fuso), core/motion.js, 2026-10-01 3 — contato-escritorios, cta-com-assessor e hero/letreiro-marca, que foi a terceira cópia. Só o relógio subiu; a regra de cada selo continua local. Ver abaixo

O .sec-head, e por que a contagem estava errada. A fila dizia "doze componentes escrevem margin-inline:auto para corrigir". Medido no Chrome antes da promoção, a 1440px, eram dezenove os que não corrigiam: o título saía 103px à esquerda do eixo do cabeçalho e o apoio 65px, em comparador, FAQ, tabela, modal, drawer, manchetes, painel e mais doze. A conta certa não era "quantos remendam" — era "quantos precisam". Duas declarações no core consertaram os dezenove e tornaram nove remendos redundantes, apagados na mesma mudança; dois (diagrama-hub, baralho-sticky) ficaram porque a mesma regra faz outra coisa.

Continuam na fila:

Candidata Cópias O que promover
Véu-legenda sobre mídia 18 linear-gradient + backdrop-filter + a mesma mask-image, caractere a caractere. Promova a casca, nunca a forma: altura, ancoragem e conteúdo são de cada peça
Reveal por estado, disparado por classe do ancestral em vez de interseção 3 — card-expansivel-fullscreen, showreel-expansivel, drawer-lateral o data-rv é IntersectionObserver e revela uma vez; painel que abre e fecha repetidamente não pode usá-lo
Disco de fechar sobre mídia 3 44px, color-mix de --light/--dark e backdrop-filter. O base.css já registra que o .btn-disc não cobre este caso de propósito
Selo concedido pelo relógio 7 — cards-cotacoes, grade-artigos, agenda-eventos, contato-escritorios, e em 2026-09-29 cta-com-assessor, selo-prazo, programacao-evento o padrão é o mesmo (a peça só afirma o que a data sustenta), mas as regras são cinco — validade, janela, início/fim, horário por fuso, prazo com vagas. Promover a função de relógio, nunca a regra. ~~O horário por fuso via Intl~~ saiu da fila em 2026-10-01 como LIB.relogio (ver a tabela de promovidas). Continua com segunda cópia literal o início/fim de sessão (agenda-eventos → programacao-evento); a terceira é o gatilho
Loop de vídeo com pausa 2 — hero/video-fundo e midia/case-ficha-flutuante (2026-10-01) não é a mesma função, e a diferença vai importar na promoção: o hero segue os eventos do <video> (ninguém o pausa por fora), e o case segue o pedido da pessoa, porque o próprio script pausa o vídeo ao sair da tela e trocar de aba. A primitiva, se vier, é a do case — toca só na tela, nasce parado sob reduce/saveData, botão pelo LIB.pausavel —, com o hero como caso particular sem a escuta de visibilidade

E as que ainda não bateram, anotadas para a contagem: .pill-sm de ressalva (2 — painel-patrimonio, lista-beneficios-palco), .visually-hidden que alterna por media query (2 — galeria-pasta, tabela-comparativa), o par de ações de peso igual em grade (2 — aviso-cookies, cta-dois-caminhos), e o token --veil-media: showreel-expansivel, prova/depoimento-video e midia/faixa-videos chegaram independentemente aos mesmos 88% — "vídeo pede sala escura" —, e três peças convergindo no mesmo número é o sinal de que o número tem nome. Token global pede conversa antes (§3).

Saíram da fila — 2026-09-21

Promovida Cópias na hora O que entrou no core
.nota, o parágrafo de ressalva 17 componentes sob 25 nomes de classe — a fila acima dizia 11, e a recontagem achou dezessete só o invariante tipográfico (corpo, entrelinha, letter-spacing:normal, --fg-muted, medida), com --nota-size e --nota-max como parafuso. Margem, alinhamento, filete e recuo ficaram locais, que é o que impediu a primitiva de ser desobedecida por doze delas
.btn-play + LIB.pausavel, o botão de pausar o que anda sozinho 2 na casa (hero/video-fundo, hero/slideshow-cinematico) e mais três chegando na mesma leva a troca de ícone sai de [data-pausado] no próprio botão, e não de classe de estado na raiz — duas instâncias na mesma página compartilhariam o seletor. E a copy dos rótulos saiu do JavaScript: data-rot-pausar/data-rot-tocar no markup, que é onde um redator procura

Sobre a segunda: o gatilho do §6 é três cópias, e aqui eram duas — o que decidiu foi a leva que estava sendo escrita no mesmo dia, com mais três botões de pausa. Promover antes da terceira cópia é o que a própria regra manda quando a terceira já está à vista.

Quando esta seção foi escrita, a fila ainda listava como pendentes o .field, a .dialog-casca e a trava de rolagem, que já estavam no core. A tabela de promovidas acima foi reescrita em 2026-09-29 para refletir isso.

Saiu da fila — 2026-10-01: a hora num fuso

O hero/letreiro-marca precisava mostrar a hora do escritório, e a função que dá dia e minuto num fuso (Intl.DateTimeFormat com formatToParts) já estava copiada ao caractere em conversao/contato-escritorios e conversao/cta-com-assessor. Seria a terceira cópia, e subiu como LIB.relogio(fuso), devolvendo {dia, min, h, m} ou null.

O que subiu é o relógio, não a regra. "Aberto até 18h", "volta amanhã às 9h" e "agora são 17h08" continuam cada um no seu componente — são três textos e três decisões. O null também é contrato: fuso ilegível ou navegador sem Intl, e quem chama não afirma nada, que é a regra de toda peça concedida pelo relógio. Os dois consumidores antigos passaram a sair no guard sem LIB.relogio, o mesmo comportamento de sem JavaScript.


§7 — JS

As seis regras do contrato são todas sobre a mesma coisa: o componente pode aparecer duas vezes na mesma página, em qualquer posição, com ou sem os irmãos. Guard clause silenciosa em vez de erro, busca a partir da raiz em vez do documento, e passive para não travar a rolagem de uma página que você não controla.

requestAnimationFrame para qualquer coisa presa ao scroll não é otimização prematura: cálculo direto no listener roda dezenas de vezes por quadro e produz jank visível em celular médio.


§8 — Ordem e preview

destaque: por que a ordem não sai da complexidade

A vitrine abre para impressionar, e a ordem em que os componentes aparecem é metade disso. Ela sai de dois lugares: bin/categorias.json decide a sequência dos blocos, e o destaque de cada componente decide a ordem dentro do bloco.

Antes, dentro da categoria, ordenava por complexidade — e complexidade é um proxy ruim para interesse. O conteudo/manifesto-com-imagens é o título gigante em caixa alta com imagens dentro da frase, um dos mais bonitos da casa, e é de complexidade baixa: caía em nono de dez. O conteudo/pilares-texto-na-foto, que é imagem com véu e texto por cima, era o último. Enquanto isso o conversao/select-custom, um <select> estilizado, subia por ser de complexidade média.

A escala é curta de propósito, porque escala longa vira discussão:

3 vitrine — para a rolagem de quem está só passando o olho
2 forte — bom componente, mas não é o cartão de visita da categoria
1 normal — o padrão de quem entra
0 bastidor — existe porque é necessário, não porque impressiona

A complexidade continua no critério, como desempate, e o id fecha. Componente novo nasce em 1 e sobe quando merecer — o _modelo/componente.json já traz assim.

O preview é gerado, nunca escrito duas vezes

preview.html sai do markup.html. Nunca escreva o markup duas vezes à mão.

Os quatro avisos de autofocus no console do showroom local são do file://, não do seu componente. Abrindo index.html com duplo-clique, o Chrome trata cada <iframe> como origem opaca e recusa o foco automático: "Blocked autofocusing on a <button> element in a cross-origin subframe" — hoje, vindo de modal-dialogo, drawer-lateral e aviso-cookies, que usam autofocus dentro do <dialog> de propósito. Medido nos dois modos: por file:// os avisos aparecem; servido por HTTP (same-origin, que é como o site roda no ar) o console fica limpo, com os mesmos 70 cards. O preview isolado de cada um também é limpo nos dois. Não persiga esses quatro.

bash bin/previews.sh                          # todos
bash bin/previews.sh hero/full-bleed-media    # um só
TEMA=<projeto> bash bin/previews.sh           # com outro tema

Precisa de mais que o markup? Cinco arquivos opcionais na pasta do componente, todos respeitados pelo gerador:

Arquivo Para quê
preview-antes.html markup antes do componente — um rodapé só existe no fim de uma página, e sem dobras em cima não há o que ele revele
preview-extra.html markup depois do componente — uma seção falsa para o parallax ter para onde rolar, um botão que dispara o estado
preview-tema.txt nome do tema só deste componente, uma linha — clone de referência costuma só fazer sentido na cor da referência, e assim uma regeração geral no tema padrão não o desmancha
preview-css.txt folhas extras, um caminho por linha — para componente que compõe outro
preview-js.txt scripts extras, um por linha, inseridos antes do próprio

§9 — Fluxo para catalogar um componente novo

  1. Copie _modelo/ para componentes/<categoria>/<nome>/.
  2. Extraia o markup da origem e normalize os prefixos de classe.
  3. Troque toda cor crua por token. Faltando token global, discuta antes (§3).
  4. Se a regra já apareceu em três componentes, promova ao core antes de recopiar.
  5. Preencha componente.json — inclusive o destaque, que nasce em 1 — e a ficha.md inteira — inclusive o padrão editorial (que conteúdo entra ali) e o histórico.
  6. Registre a origem no arquivo certo de projetos/ (§11).
  7. Rode os quatro comandos do contrato §9, nesta ordem, e o node bin/varredura.js <categoria>/<nome> — ele mede o componente no navegador nos quatro cenários que ninguém lembra de testar à mão: celular, sem JavaScript, movimento reduzido e o console.

§11 — Status e caminhos de entrada

Os três status, e o que foi aposentado

Existia um quarto, externo, para "nunca rodou em produção". Foi aposentado, e a razão é que ele não informava nada: 33 dos 54 componentes o carregavam. Quando a maioria tem o mesmo selo, o selo para de discriminar.

O erro de origem foi medir o componente pelo histórico em vez de pelo código. Histórico não prevê uso: cada projeto pede uma peça diferente, e "já rodou na Liberta" não diz se serve na próxima. Pior, o rótulo misturava duas coisas que não coincidem — nunca rodou e veio de fora sob licença de terceiro —, e a ficha do conversao/cta-fechamento-foto chegou a precisar de três parágrafos para explicar qual das duas valia no caso dela.

Os três experimental de então caíram junto: os três diziam "rodou uma vez", que é histórico com outro nome. Em variacoes[], porém, experimental tem sentido próprio e ficou — ali marca a variante que ainda não foi exercitada, o que é estado de código.

A procedência não se perdeu: ela vive onde sempre viveu, no histórico da ficha e nos registros de projetos/, que é onde alguém vai procurar.

Clone de referência

O caminho padrão de entrada: um site no ar, uma seção dele, e um clone reescrito do zero aqui dentro.

Licença de site aleatório nunca é clara, então a regra decide sozinha: não se copia — se reescreve. Da referência vem a ideia da interação, a proporção, o timing e a curva; o arquivo é nosso, em token, prefixo, primitiva do core e gancho do motion.js. Na prática o original quase nunca serviria mesmo: vem em Tailwind, GSAP ou React.

Na ficha, o histórico traz a URL como referência visual — não como origem de código — mais o que mudou na tradução. No componente.json:

"origem": [{ "projeto": "referência: <site>", "arquivo": "<url>", "ancora": "<seção>", "data": "<AAAA-MM-DD>" }]

Registro consolidado: projetos/referencias-externas.md, seção Clones.

Origem externa de código

Caso distinto: não um site no ar, mas um componente publicado como código, com autor nomeado e licença explícita. Licença clara é condição de entrada — sem ela, reescreva do zero ou deixe passar.

O crédito é obrigatório na ficha mesmo quando a licença dispensa: de onde veio, quem escreveu, sob qual licença. Crédito não custa nada.

O código continua sendo reescrito. Licença permissiva autoriza copiar; é o contrato desta biblioteca que impede — um arquivo em React e Tailwind não caberia aqui de nenhum jeito. Precedente: midia/galeria-pasta.

Registro consolidado: projetos/referencias-externas.md, seção Origem externa de código.

Componente autoral

Um padrão de interação que aparece em muitos sites premium (anel 3D arrastável, faixa horizontal pinada, lista com imagem que segue o cursor) e que aqui é construído do zero, sem referência única e sem uma linha copiada.

A diferença para o clone é só de registro: não há URL. No componente.json, origem[].projeto é "autoral", ancora nomeia o padrão, e nota diz quais famílias de exemplo inspiraram a forma e o que foi decidido na tradução.

Autoral não é licença para inventar: a regra de ouro continua. Um autoral só existe quando nenhum componente catalogado resolve a interação, e a ficha tem de dizer qual irmão ele substitui e em que caso.

Registro consolidado: projetos/autorais.md.

O que nunca atravessa

Fonte de terceiro, ícone proprietário, logo, foto e qualquer asset com direito de imagem — mesmo que o original tenha. O preview usa os placeholders de showroom/demo/, e a ficha especifica o que o asset real precisa ser.