Uma faixa discreta colada no fundo da janela: título curto, três linhas dizendo o que de fato acontece, e dois botões — Recusar não essenciais e Aceitar todos — desenhados na mesma primitiva, do mesmo tamanho, lado a lado. Ao lado deles, um terceiro caminho, Preferências, que abre um <dialog> nativo com um interruptor por categoria e uma frase explicando o que cada uma faz. A barra não escurece a página, não trava a rolagem e não prende o foco; o painel, esse sim, é modal.
O que resolve editorialmente: todo site que a biblioteca gerar precisa de um, e sem um componente catalogado ele seria improvisado em cada projeto — que é exatamente como se escreve um banner errado. Aqui a forma carrega a regra: o desenho não é só bonito, ele é o que separa um consentimento válido de um padrão escuro.
Qual irmão ele substitui: nenhum. É o primeiro componente de consentimento da casa, e nada no catálogo resolvia a interação — overlay/card-expansivel-fullscreen e navegacao/drawer-fullscreen são sobreposições de conteúdo, com foco preso e página travada, e usar qualquer um dos dois para pedir consentimento produziria justamente o bloqueio de leitura que a peça precisa evitar. navegacao/botao-flutuante é o outro elemento colado à janela, mas é um atalho de contato, não uma escolha.
Quando usar
- Sempre que o site carregar qualquer coisa que não seja estritamente necessária: medição de audiência, mapa de calor, pixel de anúncio, vídeo incorporado de terceiro, chat.
- Site de assessoria, escritório ou gestora — onde o visitante tem patrimônio e o custo reputacional de um banner mal feito é maior que o ganho de três pontos de taxa de aceitação.
- Quando existe uma política de privacidade publicada. Sem ela no ar, a barra promete uma leitura que não existe, e é o primeiro item que uma fiscalização confere.
- Junto de
rodape/quatro-colunas, que é onde mora o link[data-lgpd-reabrir]— o caminho de volta é parte da peça, não um extra.
Quando não usar
- Quando o site não carrega nada de terceiro e não usa cookie além do de sessão. Aí não há consentimento a pedir, e pedir mesmo assim treina a pessoa a clicar em "aceitar" sem ler. Uma linha na política basta.
- Como caixa de aviso genérica ("usamos IA", "novidade no site"). Para isso existe um toast, não este componente: aqui cada detalhe do desenho existe por causa da LGPD e viraria peso morto.
- Quando o projeto já usa uma plataforma de consentimento (CMP) contratada. Duas peças gravando escolhas em endereços diferentes produzem dois estados divergentes na mesma página; escolha uma.
- Como bloqueio de leitura. Se a exigência do cliente é "a pessoa não lê nada até escolher", o pedido está errado, e é essa conversa que precisa acontecer — não uma variação do componente.
Padrão editorial
| Slot | Regra |
|---|---|
Título da barra (.lgpd-bar-titulo) |
3 a 5 palavras, descritivo e sem simpatia forçada: "Cookies e dados de navegação". Nada de "Sua privacidade importa" — quem escreve isso costuma estar prestes a pedir tudo. |
Corpo da barra (.lgpd-bar-corpo) |
30 a 55 palavras. Diga o que acontece, não o benefício: "os de desempenho e os de marketing só são ligados se você permitir — até lá, nada dispara". Proibido "para melhorar sua experiência": não é informação, é enchimento. |
| Link da política | Sempre presente, na barra e no painel, sempre com o texto "Política de privacidade". Nunca escondido só dentro do painel. |
| Texto dos botões | "Recusar não essenciais" e "Aceitar todos". Verbos simétricos, comprimento comparável, nenhum dos dois em diminutivo ou hesitação ("agora não", "talvez depois"). O terceiro é "Preferências", e não "Gerenciar cookies" — quem gerencia é o site, quem escolhe é a pessoa. |
Nome da categoria (.lgpd-nome) |
1 palavra: Necessários, Desempenho, Marketing. A travada leva o rótulo "sempre ativos" ao lado. |
Descrição da categoria (.lgpd-desc) |
20 a 35 palavras, em português de gente e no presente: "Contam quantas pessoas abriram cada página e em que ponto a leitura para". Diga também o limite: "os números são agregados; não identificam você". |
Nota do painel (.lgpd-nota) |
O que fica gravado, onde, por quanto tempo e como trocar. É o parágrafo que transforma o banner em transparência de verdade. |
Duas a quatro categorias. Com uma só não há o que escolher e o painel é teatro. Acima de quatro, a lista vira formulário e a taxa de leitura cai — se o site precisa de seis, o problema é o número de terceiros que ele carrega, não o desenho do painel.
Toda categoria da lista tem de existir de verdade. Uma categoria "Marketing" num site que não veicula anúncio nenhum é declaração falsa, e não custa nada apagar o <li>.
Especificação dos assets
Nenhum. A peça não usa imagem, ícone de terceiro nem fonte própria — o único SVG é o X de fechar, desenhado em currentColor no próprio markup.
O que ela exige que exista fora dela:
| Política de privacidade | página publicada, com endereço estável. Os dois href="#" do markup são placeholders e estão marcados com comentário HTML |
| Versão da política | o texto de data-lgpd-versao. Use o identificador que a área jurídica reconhece no documento — é ele que aparece no registro de consentimento quando alguém pedir prova |
| Um lugar no rodapé | para o link [data-lgpd-reabrir]. Sem ele, o consentimento deixa de ser revogável, e aí a peça inteira perde a validade |
Compliance
Esta peça é um instrumento jurídico com aparência de componente, e o que ela tem de errado não aparece no navegador. Antes de publicar, o consumidor tem de:
- Trocar os dois
href="#"pelo endereço real da política de privacidade. - Ligar as tags ao estado — e ligar de verdade, lendo
data-lgpd-estado/ o evento (ver "Como usar"). Um banner bonito com o Google Analytics carregando no<head>de qualquer jeito é pior que nenhum banner: registra um consentimento que o site não respeita. - Conferir a lista de categorias contra o que o site realmente carrega. Cada categoria declarada tem de corresponder a scripts que existem; cada script de terceiro tem de cair em alguma categoria.
- Preencher a política de privacidade com a mesma lista, nos mesmos nomes. Banner e política que discordam é a divergência que uma fiscalização acha primeiro.
- Levar o texto à área de compliance do cliente antes de publicar.
O que a peça já garante, e não deve ser desfeito:
- Recusar é tão fácil quanto aceitar. Os dois botões leem a mesma primitiva (
.btn .btn-ghost) e dividem uma grade de duas colunas iguais, então têm a mesma caixa medida: 230×43 os dois a 1440, 350×43 os dois a 390. Vivem no primeiro nível, na barra e no painel. Promover só um a.btn-primaryé o padrão escuro que a ANPD reprova. - Nada não essencial dispara antes da escolha. O componente não carrega script de terceiro, não injeta
<script>e não conhece ferramenta nenhuma. Ele não tem como. - A barra não bloqueia a leitura. Sem scrim, sem trava de rolagem, sem foco preso: forçar a escolha para poder ler é o outro padrão escuro.
- O consentimento é granular e revogável, e o caminho de volta (
[data-lgpd-reabrir]) faz parte da entrega. - Nenhum interruptor não essencial nasce marcado. Consentimento pré-marcado não é consentimento.
Dado pessoal: o registro gravado é { versao, data, categorias } e não identifica ninguém — não há id, e-mail, IP nem impressão digital de navegador. Esta é a única exceção da biblioteca ao "nada de localStorage para dado de pessoa" (§4 do brief da casa), e ela existe porque uma escolha que não sobrevive ao recarregamento não é consentimento: é a mesma pergunta para sempre.
Como usar
<link rel="stylesheet" href="componentes/overlay/aviso-cookies/estilo.css">
<script src="componentes/overlay/aviso-cookies/script.js"></script>
Cole markup.html logo depois de <body>, antes do cabeçalho do site. A barra desenha embaixo de qualquer jeito (position:fixed), mas a posição no DOM decide a ordem do Tab — medido: com o bloco no começo, a primeira parada do teclado é a própria barra.
Depende de core/tokens.css e core/base.css, e de mais nada: sem motion.css, sem motion.js, sem lib. O script consulta LIB.reduce quando o core está na página e lê a preferência na mão quando não está.
E ponha o caminho de volta no rodapé:
<a href="#" data-lgpd-reabrir>Preferências de cookies</a>
Como ligar as tags ao estado
O estado sai daqui por dois canais, e os dois são necessários. O §7 do contrato proíbe qualquer coisa no escopo global, então não existe window.lgpd.consentiu().
| Canal | O quê | Para quê |
|---|---|---|
data-lgpd-estado no <html> |
lista de categorias ligadas separada por espaço ("necessarios desempenho"), ou "pendente" |
é o canal com memória: quem carrega depois lê o estado pronto |
lgpd:consentimento no document |
CustomEvent com { estado, categorias, versao, data, origem } no detail |
é o canal que avisa: reagir à troca sem recarregar |
Um só não resolve: o atributo não notifica ninguém, e o evento não tem memória — um ouvinte registrado depois do disparo nunca saberia. A receita é sempre a mesma:
(function () {
function ligarMedicao() {
if (window.__medicaoLigada) return; // idempotente: o evento repete
window.__medicaoLigada = true;
/* aqui, e só aqui, entra o <script> da ferramenta */
}
// 1. o que já estava decidido quando esta página abriu
if (document.documentElement.matches('[data-lgpd-estado~="desempenho"]')) ligarMedicao();
// 2. e o que a pessoa decidir daqui para a frente
document.addEventListener('lgpd:consentimento', function (e) {
if (e.detail.categorias.desempenho) ligarMedicao();
});
})();
O separador por espaço não é capricho: é o que faz o seletor ~= funcionar, no JS e no CSS. Uma categoria desligada não é desligada retroativamente — script carregado não se descarrega; o caminho honesto para "desligar" é recarregar a página, e é por isso que o painel também aceita apagar os cookies do próprio domínio no navegador (fora do escopo do componente).
O evento dispara também na carga, com origem: "gravado" ou "pendente", para o consumidor ter um caminho de código só se preferir.
Ganchos
| Gancho | Papel |
|---|---|
[data-lgpd] |
a raiz; é dela que toda busca parte. Leva o escopo de tom (.sec-deep) porque o bloco não mora em nenhuma <section> |
data-lgpd-versao |
versão da política. Trocar o valor faz a barra voltar para todo mundo |
[data-lgpd-bar] |
a barra. role="region" com aria-label, [hidden] no repouso |
[data-lgpd-painel] |
o <dialog>; leva a primitiva .dialog-casca do core antes da classe local. showModal() traz foco preso, Esc, ::backdrop e devolução de foco |
[data-lgpd-anuncio] |
região role="status" que confirma a escolha e recebe o foco quando a barra some |
[data-lgpd-cat="…"] |
o <input type="checkbox"> de cada categoria; o valor é o nome dela no registro e no evento |
[data-lgpd-aceitar], [data-lgpd-recusar], [data-lgpd-salvar] |
as três gravações. Aceitar e recusar existem na barra e no painel |
[data-lgpd-abrir], [data-lgpd-fechar] |
abrem e fecham o painel |
[data-lgpd-reabrir] |
o caminho de volta, no rodapé. Único gancho fora do bloco e único listener no document |
--lgpd-z |
camada da barra; 150, entre a nav fixa (100) e o --z-overlay (200) do core. A barra não adota o --z-overlay: ver o gotcha da camada |
--lgpd-veil-mix |
grau do véu do ::backdrop do painel; 84%, contra os 78% do --veil da casa |
--lgpd-dur, --lgpd-saida, --lgpd-atraso |
entrada .7s, saída .32s (bate com SAIDA no script), atraso .45s |
.lgpd-par |
a grade de duas colunas iguais dos botões do primeiro nível. É ela que garante a mesma caixa, e não só o mesmo desenho |
--lgpd-larg |
largura do painel; 660px |
.is-in |
a barra visível. Duração e atraso moram neste estado, não na regra base |
.is-fixa |
a categoria travada; só troca o cursor do rótulo |
Variações
- Barra clara — troque
.sec-deeppor.sec-paperou.sec-lightna raiz. Nenhuma regra do CSS muda; confira só o contraste do--accentdo tema contra o fundo claro. - Duas categorias, ou quatro — acrescente ou tire um
<li class="lgpd-item">. A lista sai do DOM: o registro, o atributo e o evento acompanham sozinhos. Mantenha exatamente um campodisabled. - Sem o painel — só para sites com duas categorias de verdade. Apague o
<dialog>e o botão "Preferências", e deixe os<input data-lgpd-cat>num bloco[hidden]; o script continua lendo deles a lista. Com três categorias, esconder a granularidade é o padrão escuro de volta. (experimental) - Cartão no canto em vez de faixa — na barra,
inset-inline-endno lugar deinset-inline:0einline-size:min(420px,calc(100vw - 32px)). Ocupa menos página; empurra o texto para cinco linhas. (experimental) - Versão por número —
data-lgpd-versaoaceita qualquer texto:"v3","politica-2026.2".
Gotchas
-
A barra é
position:fixede fica FORA de todo[data-rv]. O reveal do core termina emfilter:blur(0), eblur(0)continua sendo um filtro — elemento com filtro é bloco de contenção para descendentefixed(§5 do contrato, e o caso está medido noMANUAL.md). Dentro de um[data-rv]já revelado, a barra colaria no fundo da caixa do pai em vez de colar na janela, sem um erro sequer no console. Vale para qualquer ancestral: nada acima dela pode tertransform,filter,perspective,backdrop-filterouwill-changedesses. Por isso o bloco é filho direto do<body>, e por isso a entrada da barra é transição própria em vez dedata-rv— o reveal do core é observador de entrada em viewport, e uma barra fixa já nasce em viewport: ele dispararia na carga, antes de o script saber se há escolha gravada. -
.lgpd-bar[hidden]{display:none}está lá antes de precisar. Hoje.lgpd-barnão declaradisplaye o[hidden]do agente do usuário bastaria. Basta alguém acrescentar umdisplay:flexinocente para o atributo virar decoração e a barra continuar na tela depois da escolha — o JavaScript põehidden, o atributo aparece no inspetor, e o elemento não sai (§6.5). A linha custa nada e fecha a armadilha antes de ela existir. -
O anel de foco do painel passa uma vez pelo
<body>a cada volta, e não é vazamento. Medido no Chrome comshowModal(): fechar → desempenho → marketing → recusar → aceitar → salvar → link da política →<body>→ fechar → … Nenhum foco cai em conteúdo da página; a parada no<body>é o ponto em que o navegador fecha o ciclo do diálogo modal (em navegador com janela, é onde o foco passa pela barra de endereço).Shift+Tabpercorre o mesmo anel ao contrário. Não tente "consertar" com um trap escrito à mão: você perderia o que o elemento nativo dá de graça e ganharia o mesmo comportamento. -
O interruptor de "Necessários" é
disabled, e o script não confia nocheckeddele. As duas coisas juntas, e a segunda é a que importa. Medido: um clique real no rótulo ou no interruptor não muda nada, e o campo nunca é alcançado porTab— mas umdispatchEvent(new MouseEvent('click'))disparado por outro script da página troca ocheckedno DOM. Por issomapa()eescolhido()noscript.jsdevolvemtruepara todo campodisabled, sem olhar ochecked: o DOM pode mentir e o registro continua correto. Foidisablede nãoaria-disabledporque aqui não há nada que possa mudar de estado nunca, e um controle assim é ruído na ordem de tabulação — diferente da seta de carrossel do core, que usaaria-disabledsó para não jogar o foco para fora da dobra. -
O
autofocusno botão de fechar existe porque o quadro do painel rola.showModal()entrega o foco inicial ao primeiro focável do diálogo, e o Chrome considera focável um container comoverflow-y:auto— medido: semautofocus, o foco ao abrir caía emdiv.lgpd-quadro, não num controle. Com ele, cai no botão de fechar, e o anel de tabulação continua igual. Se o painel deixar de rolar (menos categorias, tela alta), oautofocusnão atrapalha nada: só torna a escolha explícita em vez de depender da altura do conteúdo. -
.lgpd-catéposition:relativepor causa do.visually-hidden. O<input>real leva a primitiva do core, que éposition:absolute. Sem um ancestral posicionado no próprio rótulo, ele se ancora no<dialog>inteiro — e aí oTabque chega ao interruptor da última categoria rola o painel de volta ao topo, porque é para lá que o navegador acha que o foco foi. -
Fechar não é escolher.
Esc, o X e o clique no fundo do painel fecham sem gravar nada: medido,localStoragecontinua vazio e a barra continua visível. Só os três botões gravam. -
Quando a barra some, o foco vai para a região de anúncio. Quem clicou "Aceitar todos" pelo teclado está com o foco num botão que vai virar
[hidden]; sem o desvio, o foco cai no<body>e a próxima tabulação recomeça do topo da página. A regiãorole="status"fica no mesmo ponto do DOM em que a barra estava, então oTabseguinte continua de onde parou — medido: "Limpar escolha" → link do rodapé. Ofocus({preventScroll:true})é o que impede a página de saltar para o topo por causa de um foco que ninguém vê (medido com a página emscrollY:1200: continuou em 1200). -
A região de anúncio mora FORA da barra. A barra vira
[hidden]no mesmo instante da escolha, e conteúdo escondido não é anunciado por leitor de tela nenhum. Se ela estivesse dentro, a confirmação nunca sairia. -
Trocar
data-lgpd-versaoderruba a escolha de todo mundo, e a antiga não é reaproveitada. Medido: com um registro de versão2024-01-01gravado e2026-09-06no markup, a barra volta e os interruptores nascem desmarcados — não com o que a pessoa tinha escolhido antes. É deliberado: se o texto do que se consente mudou, marcar de antemão o que a pessoa disse sobre o texto anterior é responder por ela. -
localStoragepode falhar, e o erro cai para o lado seguro. Janela privada de alguns motores, política de empresa, cota cheia: leitura e escrita estão emtry/catch, e o que não pôde ser lido vira "pendente" — perguntar de novo, nunca assumir consentimento. A escolha ainda vale para a visita (atributo e evento saem do mesmo jeito) e a barra volta na próxima. Ocatchvazio é deliberado: um console vermelho em cima de quem só clicou "recusar" não ajuda ninguém. -
O
::backdroplêvar(--veil)por herança. Quem pinta o véu é a primitiva.dialog-cascado core, com o token--veil; o grau vem daqui, porque::backdropherda custom property do elemento que o originou — declarar--veilna classe do painel basta. Medido no Chrome atual: resolve para--darka 84% maisblur(6px). Em navegador anterior ao Chrome 122,::backdropnão herdava do elemento de origem, e ali o fundo do modal sai transparente — o painel continua legível (tem fundo próprio), só perde o escurecimento. Mesmo compromisso domidia/showreel-expansivel. -
O véu é 84%, e o padrão da casa é 78%. Não é descuido: este painel cobre uma barra que já está na tela, e o véu é o que separa as duas camadas — sem ele a barra continuaria lendo como parte do que está por cima dela. O número: com uma dobra
.sec-paperpor trás, o composto vai dergb(64,64,64)a 78% parargb(49,49,49)a 84% — quase 7 pontos deL*em CIELAB, várias vezes o limiar do perceptível, então padronizar seria mudança visual e não faxina. Sobre dobra escura os dois graus dão praticamente o mesmo pixel: o grau do véu só se vê quando há claro atrás. Quem quiser o padrão da casa apaga a linha--veildo.lgpd-painel. -
Os dois botões dividem uma grade de duas colunas
1fr, e isso não é enfeite de layout. Numa fila comum cada pílula fica do tamanho do próprio texto: medido antes da grade, "Aceitar todos" saía com 158px contra os 230px de "Recusar não essenciais" — mesmo desenho, caixa menor, e a mais curta lendo como a secundária. Comgrid-template-columns:repeat(2,1fr)numa caixa de largura automática, as duas colunas ficam do tamanho da maior: 230×43 as duas. É a diferença entre "mesmo estilo" e "mesmo peso". -
Em 390×844 a barra ocupa 311px, ~37% da tela. É o custo de manter os dois botões com exatamente a mesma caixa: lado a lado em 350px de coluna, "Recusar não essenciais" quebraria em duas linhas e "Aceitar todos" não, e a igualdade — que é o ponto inegociável da peça — iria embora na primeira quebra. Por isso abaixo de 720px a grade vira de coluna única e os botões empilham. Quem precisar de menos altura usa a variação "cartão no canto", e paga em largura de leitura.
-
A camada é 150, e a barra não adota o
--z-overlaydo core. O core passou a nomear o degrau da sobreposição de página inteira:--z-overlay, 200 — a peça que cobre tudo, trava a rolagem e prende o foco. A barra não é uma delas. Ela é uma faixa que não bloqueia nada, e o número dela existe para ficar abaixo do que é sobreposição de verdade: com 200 ela empataria com um drawer de menu aberto e, sendo o último filho do<body>, ganharia o desempate por ordem de documento — a barra de cookies grudada por cima do menu. Daí 150, entre a nav fixa do core (100) e o--z-overlay. E o painel, que é sobreposição, é<dialog>em top layer: não participa dez-indexnenhum, então não há onde o--z-overlayentrar nele. O token fica sem uso neste componente, e isso é a resposta certa. -
O botão "Limpar escolha e recarregar" é do preview. Ele mora no
preview-extra.htmlporque, gravada a escolha, o preview reaberto não mostraria mais a barra — o componente estaria certo e pareceria quebrado. Não copie para o site. O painel de estado ao lado dele é a demonstração da API: lê o atributo na carga e escuta o evento depois. -
Sem JavaScript não há barra, e isso é o certo. Medido com o script desligado: a página lê inteira (as quatro dobras do preview aparecem), e nem a barra nem o painel aparecem. Uma barra visível cujos botões não gravam nada seria pior que nenhuma — pediria uma escolha que não seria registrada.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | — | 2026-09-06 | Construído do zero na biblioteca. Sem referência única e sem uma linha copiada. |
O padrão e de onde ele vem. Barra discreta em dois níveis com aceitar, recusar e um terceiro caminho, mais um modal de categorias com interruptores: é o mesmo desenho de três famílias — as plataformas de consentimento que os sites europeus passaram a usar depois do GDPR, os bancos e corretoras brasileiros depois da LGPD, e os veículos de imprensa. Da família vem a forma; nada mais.
O que foi decidido na tradução para o contrato, e o que na forma é regulação e não gosto:
| Decisão | Por quê |
|---|---|
Os dois botões do primeiro nível na mesma primitiva (.btn-ghost) e na mesma grade de duas colunas iguais |
Um deles em .btn-primary é o padrão escuro clássico: recusar tem de ser tão fácil quanto aceitar. E mesmo estilo não basta — numa fila comum a pílula mais curta fica menor e lê como secundária |
| Recusar no primeiro nível; "Preferências" como terceiro caminho | Esconder o "não" dentro de um segundo clique é a versão educada do mesmo padrão escuro |
| Barra sem scrim, sem trava de rolagem, sem foco preso | Bloquear a leitura para forçar a escolha é o outro padrão escuro. Quem bloqueia é só o painel, e só depois de a pessoa pedir |
role="region" rotulada, e não role="dialog" não-modal |
Um dialog promete um limite de foco que a barra deliberadamente não tem; anunciar "diálogo" sem gerenciar foco descreve errado o que está na tela. Uma região é achável pela lista de marcos do leitor de tela e é o que a barra de fato é |
Painel em <dialog> + showModal() |
Mesmo critério do midia/showreel-expansivel: foco preso, Esc, ::backdrop, devolução de foco e página inerte vêm do elemento nativo — quatro comportamentos que não se reimplementam bem |
Bloco fora de todo [data-rv], com transição própria |
§5 do contrato: blur(0) é filtro, e filtro prende descendente fixed. E o reveal do core é observador de entrada, que dispararia antes de o script saber se há escolha |
Interruptor com <input type="checkbox"> real + .visually-hidden do core, desenho num irmão |
Teclado e leitor de tela de graça; o desenho num <span> irmão em vez de ::after do próprio input porque pseudo-elemento em controle de formulário depende de appearance:none e ainda varia entre motores |
Estado por atributo no <html> + CustomEvent no document |
§7 proíbe escopo global. O atributo é o canal com memória, o evento é o que avisa; um sem o outro deixa metade dos consumidores de fora |
| Categorias lidas do DOM, nunca de uma constante no script | Acrescentar categoria é acrescentar um <li>; registro, atributo e evento acompanham sozinhos |
versao + data no registro gravado |
A versão é o que torna o consentimento revogável pelo lado de cá; a data é o que a área jurídica pede quando alguém exige prova |
| Nada de identificação no registro | Consentimento é o único caso em que a biblioteca grava algo, e ele para exatamente aqui: sem id, sem e-mail, sem impressão digital |
Normalizações aplicadas: prefixo lgpd- em todas as classes; zero cor crua (o véu é color-mix sobre --dark); primitivas do core consumidas em vez de recopiadas (.wrap, .btn/.btn-ghost/.btn-primary, .visually-hidden, .nota, escopo .sec-deep); media queries de largura no fim do arquivo e prefers-reduced-motion colada a cada regra que ela desliga; script em IIFE, ES5, com guard silencioso.
2026-09-06 — a casca do <dialog> virou primitiva. Os quatro componentes com <dialog> da biblioteca escreviam a mesma zeragem de caixa e o mesmo véu. Isso subiu para .dialog-casca no core/base.css, e o véu para o token --veil. Saíram deste arquivo padding, border, background, color, max-inline-size e o overflow do painel, mais a regra ::backdrop inteira; ficaram a largura, o teto de altura que anda em par com o .lgpd-quadro, e o grau do véu. Medido antes e depois com bin/medir.js, em 1440×900, 1440×420 e 390×844: a caixa do painel (660×774, 660×361.2 e 370×776.5), a do quadro, o teto de altura, o ::backdrop (0.84 de alfa, blur(6px)), a camada da barra (z-index:150), o foco inicial no botão de fechar e a rolagem interna do quadro saem idênticos. Mudou um computado, e era o objetivo: o max-width do painel, de 100% para none — a primitiva remove o limite que o navegador dá em vez de neutralizá-lo, e a largura usada continua a mesma nas três larguras medidas. Conferido depois com teclado de verdade: Tab circula dentro do painel e volta ao começo, Esc fecha, e o foco volta para o botão "Preferências".
Calibragem no primeiro uso. O texto é o que mais vai mudar: cada cliente carrega um conjunto diferente de terceiros, e a lista de categorias tem de sair da auditoria real do site, não deste exemplo. O desenho e a mecânica não deveriam precisar de ajuste — mas a altura da barra em celular (311px em 390×844) é o número a olhar primeiro em projeto cujo conteúdo principal fica no fim da tela.