/* =========================================================
   NORMA — Design Tokens (v2 — Onyx / Platinum / White /
   Dodger Blue / Cool Horizon / Prussian Blue palette)
   Matched 1:1 to norma_DESIGN.md (colors, type scale, spacing,
   radius). Onyx is text/foreground; Prussian Blue is the
   dark-surface background (announcement bar, about, footer) —
   per the doc's own 4.5:1 contrast guardrail, bare blue text on
   platinum/white stays well under threshold.

   Platinum (#E9ECEF) is the secondary surface. It is NOT the hairline
   value, and that split is the point of this block.

   The token has been through two hands. It began as a warm near-white
   (#EFE6E6, luminance 0.8066) that drew every boundary on the site at
   1.23:1 against white — a rounding error away from invisible. That was
   replaced by slate (luminance 0.3575), a mid-grey that fixed
   the boundaries at 2.58:1 but made a poor surface: it sat INSIDE the
   band the two brand blues occupy (dodger 0.3358, cool horizon 0.3856),
   and contrast is luminance-only, so blue-on-slate read ~1.06:1 no
   matter how blue the blue was.

   Platinum reverses the lightness direction: luminance 0.8355, lighter
   than even the original parchment. As a SURFACE that is a clear win —
   onyx body text reads 16.70:1 on it (slate gave 7.68:1) and secondary
   onyx-soft text 7.03:1 (slate gave 4.61:1). But it is lighter than the
   parchment whose invisible boundaries started this audit, so as a
   BORDER it would draw at 1.19:1 on white — worse than the defect being
   fixed.

   So the boundary work came off the surface token entirely. Ruling 10
   names two boundary weights; WCAG 1.4.11 splits the lighter one in two,
   giving three, all of them onyx at low alpha over whatever is behind
   them (the same idiom portal.css already used for .chart-grid-line):

     --color-control-edge  onyx 45%  — anything focusable or clickable.
                                       1.4.11 binds at 3:1; 45% is the
                                       first whole percent that clears it
                                       on BOTH grounds (3.15:1 on white,
                                       3.07:1 on platinum; 44% gives
                                       2.96:1 on platinum and fails).
     --color-hairline      onyx 38%  — decorative rules between peers:
                                       table rows, card edges, dividers.
                                       Not bound by 1.4.11, but pinned to
                                       the weight slate already had so the
                                       dividers do not silently regress:
                                       2.55:1 on white against slate's
                                       2.58:1, and 2.50:1 on platinum.
     --color-onyx-soft               — the emphasis rule, unchanged.
                                       Separates a total from its inputs.
                                       Rare by construction (Ruling 10).

   The brand-blue warning survives the swap in weakened form: dodger on
   platinum is 2.30:1, still short of 3:1. Nothing on a platinum ground
   may rely on a brand blue to be seen — see .btn-primary's onyx border
   below and the chart series in portal.css.
   ========================================================= */

/* =========================================================
   "Poppins Fallback" — a fonte de recurso com métricas alinhadas.

   PORQUE ISTO EXISTE (adicionado em 3 set 2026)
   O token --font-body abaixo já listava "Poppins Fallback" na pilha, mas
   NENHUM @font-face a definia -- não existia um único @font-face em toda a
   pasta css/. Uma família que o navegador não conhece é simplesmente saltada,
   por isso a pilha caía direto em -apple-system/sans-serif, cujas métricas não
   se parecem nada com as da Poppins. Resultado: quando a Poppins chega e
   substitui a fonte de recurso, o texto muda de tamanho e a página salta.

   ISTO ESTAVA MEDIDO, NÃO É TEORIA. Lighthouse 13.4.1 (mobile, slow-4G
   simulado, CPU 4x), 3 set 2026, teste A/B a bloquear fonts.googleapis.com e
   fonts.gstatic.com:
     /planos       CLS 0.1252 -> 0.0000  (a totalidade do CLS medido)
     /             CLS 0.0845 -> 0.0182  (-78%)
     /privacidade  CLS 0.1624 em todas as 3 corridas -- falha o limiar de 0.1
   O `cls-culprits-insight` do Lighthouse aponta explicitamente
   `cause: "Web font"` na /planos e na /privacidade. A referência pendurada
   acima era, portanto, um bug latente: a intenção certa, nunca implementada.

   COMO OS NÚMEROS FORAM OBTIDOS
   Métricas reais da Poppins: unitsPerEm 1000, ascender 1050, descender -350,
   lineGap 0. Em percentagem do em: ascent 105%, descent 35%, line-gap 0%.
   Com `size-adjust: 112.1%` (a razão de largura média Poppins/Arial), as
   percentagens de ascent e descent têm de ser divididas por esse mesmo fator,
   senão a caixa de linha fica escalada duas vezes:
     105 / 1.121 = 93.6      35 / 1.121 = 31.2
   São os mesmos valores que o fontaine/capsize geram para este par.

   VERIFICADO, COM UMA CORREÇÃO PELO MEIO. Medição A/B própria (Chromium
   headless, 375x812, PerformanceObserver de layout-shift, mesmo servidor
   local para as três configurações):
     original (folha bloqueante, sem fallback)      CLS 0.2043
     só a folha não-bloqueante                      CLS 0.1807
     não-bloqueante + este @font-face               CLS 0.1737
   A primeira leitura destes números foi ERRADA: comparou-se o site em
   produção (0.0711) com o build local (0.1737) e concluiu-se uma regressão.
   Eram ambientes diferentes -- em localhost o HTML e o CSS chegam
   instantaneamente mas a fonte continua a vir da CDN da Google, por isso a
   janela entre a primeira pintura e a troca é MAIOR e o CLS medido é
   sistematicamente mais alto. Só a comparação das três configurações no MESMO
   ambiente é válida, e essa mostra as duas mudanças a melhorar o valor.
   Lição para quem vier a seguir: nunca comparar uma medição local com uma
   medição de produção; medir sempre as duas versões no mesmo sítio.

   LIMITE HONESTO: um só `size-adjust` não pode servir Arial, Helvetica e
   Roboto com igual precisão -- as três têm larguras diferentes -- e a Arial
   não tem peso 900, por isso o h1 (Poppins Black) é o pior caso de todos, com
   negrito sintético a fazer de substituto. Isto reduz o salto, não o elimina.
   O ganho aqui é modesto (-15% no CLS); o ganho grande da mesma mudança está
   no LCP (-516ms medidos). Verificar com Lighthouse depois de qualquer mexida
   aqui, e não assumir.

   NÃO altera a identidade visual: a Poppins continua a ser a única fonte que
   o visitante vê depois do carregamento (NORMA_CANONICAL.md §7). Isto governa
   apenas o intervalo antes de ela chegar, que já existia na mesma.
   ========================================================= */
@font-face {
  font-family: "Poppins Fallback";
  src: local("Arial"), local("Helvetica"), local("Roboto");
  size-adjust: 112.1%;
  ascent-override: 93.6%;
  descent-override: 31.2%;
  line-gap-override: 0%;
}

:root {
  /* Color — literal tokens from norma_DESIGN.md */
  --color-onyx: #0a0a0a;
  --color-dodger-blue: #4CA0FA;
  --color-cool-horizon: #62ABF9;
  --color-platinum: #E9ECEF;
  --color-white: #ffffff;
  --color-prussian-blue: #011428;
  --color-onyx-soft: #0a0a0ab3; /* onyx at reduced opacity, for secondary text on light surfaces */

  /* Boundaries — see the block above. These are not new colours: each is
     onyx at an alpha, composited by the browser over whatever ground it
     lands on, so one declaration holds its weight on white and on
     platinum alike. Do not substitute a surface token here. */
  --color-control-edge: color-mix(in srgb, var(--color-onyx) 45%, transparent);
  --color-hairline: color-mix(in srgb, var(--color-onyx) 38%, transparent);
  /* White at an alpha, not onyx: the one boundary drawn on a DARK ground
     (the footer, prussian-blue) instead of a light one. norma_DESIGN.md
     §2.2 names this exact value/role (--boundary-on-inverse, white 15%) and
     flags it as the one dark-band boundary genuinely required. Was a raw
     rgba(255,255,255,0.149) literal on the footer rule itself (already
     correct in value, just not a token) -- promoted here so it's named and
     reusable rather than a one-off number a future dark-band rule would
     have to rediscover. */
  --color-hairline-dark: color-mix(in srgb, var(--color-white) 15%, transparent);

  /* Type */
  --font-body: "Poppins", "Poppins Fallback", -apple-system, sans-serif;

  /* Spacing — literal tokens from norma_DESIGN.md */
  --space-xs: 5px;
  --space-sm: 8px;
  --space-md: 10px;
  --space-base: 15px;
  --space-lg: 20px;
  --space-2xl: 32px;
  --space-3xl: 50px;
  --space-5xl: 70px;
  /* norma_DESIGN.md §5: macro inter-section rhythm on wide viewports only
     (1025px and up), as padding-block on a full-bleed marketing band — never a
     component, card or panel value; those still cap at --space-2xl. Added
     here because the doc already fixes the value (96px) and the role;
     nothing today consumes it — see docs/DESIGN_PROPOSALS.md for where
     Wave 2's markup pass is proposed to use it. */
  --space-6xl: 96px;
  --space-header-desktop: 87px;
  --space-header-mobile: 68px;

  /* Radius — literal tokens from norma_DESIGN.md */
  --radius-badge: 2.5px;
  --radius-card: 8px;
  /* Só para a moldura encaixada da landing page. Uma banda de marketing
     puxada para dentro da aresta da janela lê-se como cartão à escala de
     banda, e 8px sobre 70px de recheio lê-se como artefacto de
     renderização, não como canto. O valor não é novo: é --space-2xl.
     NUNCA num cartão, painel, campo ou controlo -- esses continuam a
     levar --radius-card. */
  --radius-band: 32px;
  --radius-pill: 100px;

  /* Layout */
  /* 1440, não 1160. Um só valor governa a largura de TODAS as
     páginas públicas, e a 1160 sobravam 760px de nada de cada
     lado numa tela de 1920 -- a queixa que abriu esta revisão.
     A tira ficava tão estreita que o herói da página inicial
     deixava o quadrante superior direito inteiramente vazio.

     1440 e não "sem limite": uma linha de texto corrida a
     1900px é ilegível, e o brief separa as duas regras de
     propósito -- conteúdo público até 1440, casca de aplicação
     sem limite nenhum (ver .app-shell em css/shell.css, que
     não passa por aqui). Os limites em `ch` das colunas de
     texto continuam a valer por cima deste: é o menor dos dois
     que ganha. */
  --content-max: 1440px;
  --header-height: var(--space-header-mobile);

  /* Sem sombras de elevação. O ÚNICO box-shadow do código é o anel de
     foco, e ele está declarado em dois sítios com a mesma geometria:
     :focus-visible em enhancements.css (o anel geral) e
     .pay-option-input:focus-visible + .pay-option-label em checkout.css
     (onde o elemento focado está escondido e o anel tem de pintar no
     rótulo irmão). Se mudar um, mude o outro.
     Superfícies separam-se por filete de 1px.
     O --shadow-card antigo (0 30px 70px rgba(0,0,0,.3)) alcançava 100px
     abaixo e 70px para cada lado de uma caixa que só tinha 20px de
     goteira: o efeito visível era uma mancha cinzenta entre os cartões,
     exactamente onde eles deviam separar-se. */

  /* Skeleton — derivado da paleta existente, não de cinzas neutros.
     Platinum já é a superfície secundária do site (fundo do cabeçalho
     das tabelas), então o placeholder lê como parte da marca em vez de
     um bloco cinza genérico.
     NOTA de peso: sobre branco o bloco lê a 1.19:1 — discreto, que é o
     que a paleta original (#EFE6E6, 1.23:1) sempre deu. Dentro de uma
     .portal-section o placeholder é platinum sobre platinum, tal como
     era slate sobre slate: a região inteira é decorativa e quem usa
     leitor de tela recebe o estado por aria-live no #loadStatus, não
     por estes blocos. */
  --skeleton-base: var(--color-platinum);
  --skeleton-sheen: var(--color-white);
  --skeleton-radius: var(--radius-badge);
  --skeleton-block-radius: var(--radius-card);
}

/* Duas alturas de cabeçalho, não três. Antes havia um degrau de tablet de
   106px entre os 68px do celular e os 87px do desktop -- a tela menor
   entregava MAIS altura ao cabeçalho do que o maior, e ao cruzar 1024→1025
   a barra encolhia e a página inteira saltava. O comentário que justifica
   o aperto de tablet (mais abaixo, ~linha 400) fala de largura, nunca de
   altura: os 106px compravam ar na única faixa onde o menu já é o mais
   apertado.
   Igual de importante: `max-width: 1024px` e `min-width: 1025px` deixavam
   um buraco para qualquer largura entre os dois -- reproduzido a 1024.545px
   (janela de 1024px no primeiro passo de zoom do Chrome), onde nenhuma das
   duas media queries batia e --header-height caía para os 68px do celular
   com o menu de desktop desenhado por cima. Com um só limiar não há
   largura que escape. */
@media (min-width: 768px) {
  :root { --header-height: var(--space-header-desktop); }
}

/* =========================================================
   Reset
   ========================================================= */

*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* O cabeçalho é sticky, por isso qualquer navegação por fragmento
     entregava o alvo debaixo da barra -- incluindo o skip link, em todas
     as páginas. O token já carrega a escada responsiva, portanto isto
     está certo em qualquer breakpoint sem media query própria. */
  scroll-padding-top: var(--header-height);
}

/* `scroll-behavior` é movimento accionado pelo agente do usuário, da
   mesma natureza de uma transição, e é o MAIOR movimento do site: uma
   rolagem de viewport inteira. É também o único com potencial vestibular
   real, e sequestra o skip link nas 23 páginas -- um controlo de
   acessibilidade cujo trabalho é entregar a pessoa no conteúdo de
   imediato. js/main.js já honra a preferência para exactamente este
   comportamento no botão voltar-ao-topo (`prefersReducedMotion ? 'auto'
   : 'smooth'`); esta regra faz o mesmo do lado do CSS, que é quem governa
   todos os outros saltos de fragmento. `auto` é o valor inicial da
   propriedade. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* =========================================================
   Transições entre documentos

   O site é multi-página: cada URL é um ficheiro .html real, e toda a
   navegação era um corte seco -- branco entre páginas, e o cabeçalho e
   o rodapé a serem redesenhados de raiz apesar de serem os mesmos em
   todas elas. Isto dá continuidade sem trocar a arquitectura por uma
   SPA: o navegador guarda o estado antigo, pinta o novo e interpola
   entre os dois.

   `view-transition-name` no cabeçalho e no rodapé é o que os faz
   PERSISTIR em vez de desvanecer com o resto: são o mesmo elemento aos
   olhos do navegador, portanto ficam quietos enquanto o conteúdo muda.
   Cada nome tem de ser único no documento, e ambos aparecem uma vez por
   página.

   Só é activado quando há apetite por movimento -- por isso o
   `@media not (prefers-reduced-motion: reduce)` à volta, e não uma
   regra sempre ligada com um override depois. Onde o navegador não
   suporta (Firefox, hoje), a navegação é a de sempre: nada quebra,
   apenas não interpola. */
@media not (prefers-reduced-motion: reduce) {
  @view-transition { navigation: auto; }
}

.site-header { view-transition-name: site-header; }
.site-footer { view-transition-name: site-footer; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  line-height: 24px;
  color: var(--color-onyx);
  background: var(--color-white);
  -webkit-font-smoothing: antialiased;
}

img, svg { display: block; max-width: 100%; }

a { color: inherit; text-decoration: none; }

ul, ol { list-style: none; margin: 0; padding: 0; }

h1, h2, h3, p { margin: 0; }

button { font-family: inherit; cursor: pointer; }

/* NÃO existe aqui uma regra :focus-visible. A que vale é a de
   enhancements.css (anel de dois tons, 2px + box-shadow de 5px), que
   carrega depois em todas as 23 páginas e tem a mesma especificidade.
   A declaração que vivia aqui nunca desenhou um pixel em lado nenhum e
   só servia para a próxima pessoa a editar o anel a editar o ficheiro
   errado. */

/* =========================================================
   Skip link

   Vive aqui, e só aqui. Estava declarado em auth.css, checkout.css e
   portal.css e em NENHUM dos dois ficheiros que todas as páginas
   carregam -- por isso index.html, termos.html, privacidade.html e
   cookies.html imprimiam "Pular para o conteúdo" como texto preto
   normal por cima do logótipo, sem o mecanismo que o esconde.
   styles.css carrega primeiro em todas as páginas, portanto isto tem de
   ser uma MUDANÇA, não uma cópia: qualquer cópia deixada para trás numa
   folha de superfície continuaria a ganhar.
   ========================================================= */

.skip-link {
  position: absolute;
  left: var(--space-lg);
  top: 0;
  z-index: 100;
  transform: translateY(-200%);
  background: var(--color-prussian-blue);
  color: var(--color-white);
  font-weight: 700;
  padding: var(--space-md) var(--space-lg);
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  transition: transform 0.15s ease;
}

.skip-link:focus { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

.container {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--space-lg);
}

@media (min-width: 768px) {
  .container { padding-inline: var(--space-2xl); }
}

/* =========================================================
   Typography scale (literal tokens: hero 72/900/1,
   section 48/800/1, sub-heading 24/700/36, body 16/400/24,
   body-medium 16/500/24, body-bold 16/700/24,
   fine-print 12/200/15.6, nav-label 16/400/24,
   announcement 16/700/24)
   ========================================================= */

.eyebrow {
  font-weight: 700;
  font-size: 1rem;
  line-height: 24px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-onyx);
  margin-bottom: var(--space-sm);
  position: relative;
  padding-left: var(--space-lg);
}

/* Decorative Dodger Blue brand mark — not text, so it carries no contrast
   requirement, keeping "brand highlight color" present on light
   surfaces without breaking the 4.5:1 text-contrast guardrail. */
.eyebrow::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  translate: 0 -50%;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-badge);
  background: var(--color-dodger-blue);
}

h1 {
  font-weight: 900;
  text-transform: uppercase;
  line-height: 1;
  font-size: 2.5rem;
  /* REGRA: os limites de layout deste site estão em px e as escalas de
     tipo estão em rem, e nada guardava a divergência. Com a raiz a 16px
     medimos ZERO estouros em 579 leituras; com a raiz a 20px -- uma
     definição de acessibilidade comum, e a que o norma_DESIGN.md nomeia
     -- "PRIVACIDADE" (a palavra mais longa do site) a 2.5rem/900 dentro
     dos 68ch do .legal-inner passava a medir 353px de texto numa caixa
     de 280px e a página rolava 53px na horizontal a 320px de janela.
     A defesa está na escala: `overflow-wrap` deixa a palavra partir
     quando -- e SÓ quando -- ela não cabe. À raiz de 16px nada muda,
     porque nunca chega a não caber. Nada de hífens: `hyphens: auto`
     precisaria de dicionário e mudaria a leitura de títulos que hoje
     nunca partem. */
  overflow-wrap: break-word;
}

h2 {
  font-weight: 800;
  line-height: 1;
  font-size: 2rem;
  margin-bottom: var(--space-2xl);
  max-width: 20ch;
}

h3 {
  font-weight: 700;
  font-size: 1.5rem;
  line-height: 36px;
  margin-bottom: var(--space-sm);
}

@media (min-width: 768px) {
  h1 { font-size: 3.5rem; }
  h2 { font-size: 2.5rem; }
}

@media (min-width: 1025px) {
  h1 { font-size: 4.5rem; }
  h2 { font-size: 3rem; }
}

p { font-weight: 400; }

.body-medium { font-weight: 500; }
.body-bold { font-weight: 700; }

.fine-print {
  font-weight: 400; /* DS: Poppins 400/500 only (25/09/2026) */
  font-size: 0.75rem;
  line-height: 15.6px;
}

/* =========================================================
   Buttons
   ========================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  font-weight: 700;
  font-size: 1rem;
  line-height: 24px;
  text-align: center;
  border: none;
  /* A base passa a declarar a sua própria geometria. Sem esta linha, .btn
     declarava tudo -- raio, peso, corpo, entrelinha, alinhamento, borda,
     transição, nowrap, largura máxima -- menos preenchimento, e só
     .btn-large e .btn-small o forneciam. Um .btn sem modificador de
     tamanho caía no `1px 6px` da chrome do navegador: 7px de recuo contra
     os 21 do pequeno e os 33 do grande, com a altura ainda a medir 44 por
     causa do piso de alvo de toque e o rótulo ainda centrado, ou seja um
     botão 28px estreito demais que continua a parecer um botão. E `1px
     6px` não está na escala de oito degraus -- seria um quinto valor fora
     da escala a chegar à página, excepto que nunca é DECLARADO, portanto
     nenhuma varredura das folhas o encontra. Pior: um `<a class="btn">`
     não recebe preenchimento nenhum do navegador, logo o mesmo defeito
     dava 0/0 num sítio e 7/7 no outro.
     É a mesma lição que .btn-secondary já pagou uma vez neste selector,
     do lado da cor: uma base que não fornece uma propriedade herda a
     chrome do sistema operativo nessa propriedade. A cor ficou fechada e
     a geometria continuou aberta.
     Hoje isto é latente e foi verificado como tal: os 47 sítios de
     marcação nas 23 páginas e os 4 botões construídos em JS carregam
     todos .btn-large ou .btn-small, e 585 medições em 13 larguras não
     encontram uma única assinatura sem modificador. A base toma o tamanho
     pequeno porque é o tamanho por omissão do produto -- 316 das 585
     medições -- e porque .btn-large continua a vencê-la por ordem de
     origem, como sempre venceu.
     .btn-small MANTÉM a declaração idêntica, e isso é deliberado, não
     esquecimento: é o nome explícito do tamanho por omissão, é a classe
     de que `.primary-nav .btn-small` depende na faixa de tablet, e é o
     que impede os 51 sítios de marcação de mudarem em silêncio no dia em
     que esta omissão mudar. Não a apagar como regra redundante. */
  padding: var(--space-sm) var(--space-lg);
  /* `opacity` entra na lista. O par que marca "a enviar" -- `cursor:
     progress; opacity: 0.7` -- não estava coberto por transição nenhuma,
     portanto o botão saltava para 70% num único frame enquanto o spinner
     aparecia ao lado. É o primeiro frame de feedback a seguir ao clique
     de maior intenção do site ("Entrar", "Ir para o pagamento", "Enviar
     mensagem"), e um salto duro lê-se como falha de repintura onde 150ms
     se lêem como o sistema a aceitar o clique. A máquina e a duração já
     cá estavam para o fundo. Nada mais no site mexe na opacidade de um
     .btn -- verificado. */
  transition: background-color 0.15s ease, transform 0.15s ease, opacity 0.15s ease;
  white-space: nowrap;
  /* Sem isto o botão podia ficar mais largo que a tela: com nowrap, a
     largura intrínseca é a frase inteira. Medido a 375px: os dois
     .btn-large do index tinham 407px e empurravam a página para 427px
     de rolagem horizontal. */
  max-width: 100%;
}

.btn-primary {
  background: var(--color-dodger-blue);
  color: var(--color-onyx); /* white-on-blue fails AA; onyx-on-blue passes */
  /* A borda é o limite do controlo, não decoração. .btn é `border: none`,
     por isso o preenchimento era a única fronteira do botão -- e dodger
     blue sobre platinum dá 2.30:1, ainda abaixo dos 3:1 que o critério
     1.4.11 exige, ou seja, sem esta borda o rótulo volta a flutuar num
     campo claro sem botão nenhum. Onyx lê 16.70:1 sobre platinum e 7.27:1
     sobre o próprio preenchimento, logo a forma sobrevive nos dois fundos.
     A troca slate -> platinum melhorou o número (era 1.06:1) mas NÃO o
     resolveu: esta linha continua a ser o que faz do botão um botão.
     `box-sizing: border-box` (linha ~75) faz com que 1px não altere a
     altura: o controlo continua a medir 40px. */
  border: 1px solid var(--color-onyx);
}

.btn-primary:hover { background: var(--color-cool-horizon); }
.btn-primary:active { transform: scale(0.98); }

/* O .btn é o elemento interactivo mais usado do código -- onze
   combinações de classe, dezenas de instâncias, nas 23 páginas -- e era
   o único que ignorava a preferência. O site honrava-a no skip link, no
   spinner, nos rádios de pagamento e no botão voltar-ao-topo, e depois
   ignorava-a no controlo que a pessoa carrega em todas as telas.
   Acessibilidade meia-feita lê-se pior do que nenhuma: a pessoa ligou a
   definição e o site obedeceu por partes.
   Nada aqui depende de `opacity` ou de `transform` para ser VISÍVEL --
   o estado final do botão é o botão -- portanto `none` é seguro e não
   esconde nada. */
@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn-primary:active,
  .btn-secondary:active { transform: none; }
}

/* .btn declara geometria e `border: none`, sem fundo e sem cor: só
   .btn-primary os fornecia. Dez sítios de marcação mais um construído em
   JS renderizavam com a chrome do sistema operativo -- medido
   rgb(240,240,240) sobre rgb(0,0,0), ou seja `buttonface`/`buttontext`,
   nenhum dos dois na paleta. Entre eles "Solicitar alterações" ao lado de
   "Aprovar", e "Cancelar" ao lado de "Excluir permanentemente": as duas
   metades da única decisão que o cliente toma, uma desenhada e a outra
   não.
   O tratamento não é inventado -- .primary-nav a.nav-account já entrega
   exactamente esta pílula contornada (1px onyx, --radius-pill), e .btn já
   carrega --radius-pill. Isto é uma variante do componente existente, não
   um segundo componente.
   A borda é 1px e não 2px pelo mesmo motivo que a de .btn-primary o é: as
   duas têm de medir igual. MEDIDO, e ao contrário do que a auditoria
   afirmava: `box-sizing: border-box` só anula a borda quando há um
   `height` explícito, e .btn não define nenhum -- a altura vem do padding
   com a linha. Com .btn-primary a ganhar 1px de borda, .btn-large passou
   de 54px para 56px e .btn-small de 40px para 42px, e um .btn nu ao lado
   ficou 2px mais baixo. Dar a mesma borda de 1px ao secundário fecha a
   diferença: ambos a 42px, sem calc(). */
.btn-secondary {
  background: transparent;
  border: 1px solid var(--color-onyx);
  color: var(--color-onyx);
}

/* norma_DESIGN.md §13 recorded this as a documented gap ("no hover fill")
   since v1 -- .btn-primary and .btn-danger-outline both hover, this one
   never did. White matches .btn-danger-outline's own precedent exactly
   and needs no new token; this is the only one of the shipped variants
   where white-on-hover works everywhere it's actually used today
   (platinum auth panels, portal sections) -- see DESIGN_PROPOSALS.md §8. */
.btn-secondary:hover { background: var(--color-white); }
.btn-secondary:active { transform: scale(0.98); }

/* Moved here from auth.css: a danger button is a general component-API
   variant (components/ui/button.tsx's variant="danger"), not an
   auth-page-only pattern, so it needs to resolve on every page rather
   than only the ones that happen to load auth.css. 1px border with an
   `outline` embedded on top, instead of a 2px border -- renders the same
   2px of weight without costing layout: measured, "Excluir
   permanentemente" landed at 44px and the "Cancelar" beside it at 40px,
   because the 2px border never had compensation. This is the destructive
   confirmation pair -- the two buttons someone sees when deleting their
   account -- the product's least-confident moment, at the point it most
   needs confidence. Same technique as Ruling 5, already used on
   .pix-row[data-copied]. */
.btn-danger-outline {
  background: transparent;
  border: 1px solid var(--color-prussian-blue);
  outline: 1px solid var(--color-prussian-blue);
  outline-offset: -1px;
  color: var(--color-prussian-blue);
}
.btn-danger-outline:hover { background: var(--color-white); }

/* Botão grande carrega frase ("Agende Sua Call Estratégica Gratuita"),
   não rótulo, então ele quebra linha. O pequeno continua nowrap: são uma
   ou duas palavras e quebrá-las ficaria pior. */
.btn-large {
  padding: var(--space-base) var(--space-2xl);
  font-size: 1.125rem;
  white-space: normal;
}
.btn-small { padding: var(--space-sm) var(--space-lg); }

/* =========================================================
   Announcement bar (dark surface — blue passes contrast here,
   so it's safe as text/link color on prussian blue)
   ========================================================= */

.announcement-bar {
  background: var(--color-prussian-blue);
  color: var(--color-white);
  text-align: center;
  padding: var(--space-sm) var(--space-md);
  font-weight: 700;
  font-size: 1rem;
  line-height: 24px;
}

.announcement-bar a {
  color: var(--color-dodger-blue);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* =========================================================
   Header / Nav
   ========================================================= */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--color-white);
  border-bottom: 1px solid var(--color-hairline);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  /* Era `height`, fixa. Com o menu sempre visível (o ☰ saiu -- ver
     `.primary-nav`) os sete itens quebram de linha num celular, e uma
     altura fixa cortava-os fora da barra. `min-height` deixa o cabeçalho
     crescer só onde precisa: acima dos 768px a barra cabe numa linha e o
     mínimo é exactamente a altura de antes, pixel por pixel. */
  min-height: var(--header-height);
}

.wordmark {
  position: relative;
  font-weight: 900;
  font-size: 1.375rem;
  letter-spacing: 0.02em;
}

/* O BOTÃO ☰ FOI RETIRADO das 25 páginas. `.nav-toggle` e
   `.nav-toggle span` -- a caixa de 40x22 com piso de 44px e as três
   barras de 24x2 -- saíram com ele, porque um seletor sem marcação é
   uma regra que ninguém pode ler e que a próxima pessoa tem de
   investigar para descobrir que não faz nada. */

/* JÁ NÃO É UM PAINEL. Era um menu de celular que aparecia por baixo do
   cabeçalho quando se tocava no ☰; o botão foi retirado por ser
   redundante, e com ele saiu toda a maquinaria que o painel precisava:
   o `opacity`/`visibility`/`transform` que o escondia, a transição de
   200ms, o `position: absolute` colado ao fundo do cabeçalho, o
   `max-height` com rolagem própria, a classe `.is-open` e a regra de
   movimento reduzido que desligava a transição.

   NENHUMA DELAS PODIA FICAR. Sem um controlo que accione `.is-open`,
   `opacity: 0; visibility: hidden` não é um estado fechado -- é o
   estado permanente. Os sete links ficariam invisíveis e fora da ordem
   de tabulação em qualquer janela abaixo dos 768px, que é precisamente
   onde o botão era a ÚNICA forma de lá chegar. Retirar o botão e deixar
   o painel teria sido retirar a navegação inteira do celular.

   O que fica é o que a barra de desktop já era: uma linha de links que
   quebra para as linhas de que precisar. O cabeçalho cresce com ela --
   ver `min-height` em `.header-inner`. */
.primary-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}

/* =========================================================
   A barra no celular: UMA linha que rola, não três que quebram.

   Com o ☰ fora, os sete itens passaram a quebrar e o cabeçalho subiu
   de 68px para 185px -- 353px de cromo antes do h1, 44% da primeira
   tela a 375x812. O comentário da barra de anúncio, neste mesmo
   ficheiro, já tratava 181px/22% como defeito; o dobro disso não podia
   ficar.

   PORQUE É ROLAGEM E NÃO TIPO MAIS PEQUENO: medido a 375px, os itens
   somam 659px (67+72+46+81+66+155+172) e sobram ~240px ao lado do
   logótipo de 80px. Meter 659 em 240 pede uma escala de 36%, ou seja
   ~5.8px de fonte. Não é uma questão de apertar: não cabe. A tipografia
   também está congelada (§14 do norma_DESIGN.md).

   PORQUE NÃO SE ESCONDEM ITENS: os dois mais largos são "Área do
   cliente" (155px) e "Começar agora" (172px) -- a porta da conta e o
   CTA. São exactamente os dois que não se podem cortar para ganhar
   espaço.

   Fica a rolagem lateral, que devolve os 68px exactos e não perde um
   único link. O degradê à direita é a única affordance: sem ele a
   barra parece cortada em vez de rolável.
   ========================================================= */
.primary-nav a:not(.btn) {
  position: relative;
  font-weight: 400;
  font-size: 1rem;
  line-height: 24px;
  /* Alvo de toque. A linha de 24px sozinha dava 24px de altura, abaixo
     dos 44px que o WCAG pede para toque. O padding vertical só existe no
     menu em coluna (mobile); no desktop o gap horizontal já separa e
     esticar a altura desalinharia a barra. */
  padding-block: 10px;
}

@media (min-width: 768px) {
  .primary-nav a:not(.btn) { padding-block: 0; }
  /* UX_LAW_AUDIT.md #15 (lei de Fitts): 24px medido ao vivo, abaixo do
     piso de 32px no desktop (ainda dentro do mínimo de 24px do WCAG, daí
     a severidade baixa). Mesma técnica de alvo invisível já usada em
     .portal-rail-settings/.info-tip -- estende a área de toque para 32px
     sem alterar padding-block, que é exatamente o que o comentário acima
     evita mudar (desalinharia a barra).
     ::before, não ::after: enhancements.css já usa ::after neste mesmo
     `.primary-nav a` para o sublinhado azul de `.is-active` (bottom:
     calc(...), height: 2px, background dodger-blue). Um elemento só tem
     UM ::after -- as duas declarações se fundiam no mesmo pseudo-elemento,
     e como este bloco carrega depois (enhancements.css vem depois de
     styles.css), `top`/`translate` daqui venciam a posição enquanto
     `height`/`background` de lá venciam a aparência: o resultado media, ao
     vivo, uma barra azul de 2px A MEIO do texto em vez de um sublinhado
     por baixo dele. ::before nunca colide, porque `.is-active` não o usa. */
  .primary-nav a:not(.btn)::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    translate: 0 -50%;
    height: 32px;
  }
  /* Mesmo achado, mesma técnica -- o logótipo "norma" também media 24px. */
  .wordmark::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    translate: 0 -50%;
    height: 32px;
  }
}

/* Porta da área logada. Antes eram dois itens colados -- "Entrar" e
   "Cadastre-se" -- que para quem chega no site parecem a mesma coisa,
   e no index.html o segundo era um btn-primary idêntico ao "Agendar
   uma Call" ao lado, ou seja, dois botões primários competindo.
   Agora é um item só, com peso próprio: nem link de seção, nem botão
   primário. Quem já tem conta e quem ainda não tem entram pela mesma
   porta; login.html oferece os dois caminhos.
   Seletor com `a.nav-account` (e não `.nav-account`) para empatar em
   especificidade com a regra acima e vencer por vir depois. */
.primary-nav a.nav-account {
  font-weight: 700;
  /* Um <a> é inline por omissão e o piso de 44px (ver a regra do alvo de
     toque mais abaixo) não diz onde vai a folga que ele cria: a caixa
     crescia de 34px para 44px e os 10px caíam todos em baixo. Medido: o
     texto ficava 5.5px acima do centro em todas as larguras e 6.5px
     entre 768 e 1024 -- exactamente a faixa em que esta pílula está numa
     linha horizontal ao lado de um .btn-small que mede 0.5px de
     desvio, que é o resíduo da caixa de linha do Poppins.
     `inline-flex` e não `flex`: a pílula tem de continuar a abraçar o
     próprio texto, e é isso que segura os 155.313px do menu de
     celular, onde a coluna é `align-items: stretch`. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm) var(--space-base);
  border: 1px solid var(--color-onyx);
  border-radius: var(--radius-pill);
  line-height: 1;
  white-space: nowrap;
  /* No mobile o menu é uma coluna com align-items: stretch, e a pílula
     esticava de ponta a ponta como se fosse um botão de largura total.
     Aqui ela abraça o próprio texto; no desktop volta a centralizar na
     linha (ver o reset dentro do @media abaixo). */
  align-self: flex-start;
  /* A mesma propriedade, a mesma duração e a mesma curva que .btn já usa
     duas regras acima. Na mesma barra, a 8px de distância, esta pílula
     saltava enquanto o "Agendar uma Call" ao lado atravessava 150ms:
     duas portas do negócio, as duas em forma de pílula, uma a responder
     e a outra a piscar. Ninguém nomeia isto conscientemente; toda a
     gente o lê como desleixo.
     `background-color` é a única excepção não-transform/opacity da regra
     de movimento, e só num controlo interactivo, a <=150ms, em
     hover/focus/checked. Isto cumpre todas as cláusulas. */
  transition: background-color 0.15s ease;
}
.primary-nav a.nav-account:hover { background: var(--color-platinum); }

@media (prefers-reduced-motion: reduce) {
  .primary-nav a.nav-account { transition: none; }
}

/* Alvo de toque, em TODAS as larguras. Estava fechado dentro de
   `@media (max-width: 767px)` com o argumento de que acima disso o
   ponteiro é preciso -- mas 768px é exactamente o iPad em retrato, o iPad
   Air tem 820px e o iPad Pro 11" tem 834px: aparelhos só de toque, todos
   acima do limiar, todos fora da regra. Medido a 768px em account.html:
   os três botões da linha davam 40px.
   `min-width` entrou originalmente por causa do .nav-toggle, que media
   40x44 -- os 24px das barras mais 8px de padding de cada lado -- quando
   o comentário dizia que a intenção era limpar os 44px e limpava um eixo
   só. ESSE BOTÃO JÁ NÃO EXISTE (foi retirado das 25 páginas), mas o piso
   fica: vale para os dois sujeitos que restam, e um alvo de toque com
   menos de 44px num deles seria o mesmo defeito com outro nome.
   .nav-account entra porque é a outra porta do negócio e nunca esteve na
   regra: media 34px contra os 40px do botão ao lado, a 8px de distância.
   Rejeitado `@media (pointer: coarse)`: é uma feature de media query que
   este código nunca usou e é o eixo errado -- um portátil com tela tátil
   reporta coarse e um tablet com caneta reporta fine.

   REGRA: um piso `min-width`/`min-height` TEM DE VIR ACOMPANHADO DE
   CENTRAGEM. O piso não redimensiona o conteúdo, cria folga entre a
   caixa natural e os 44px -- e quem decide para onde vai essa folga é o
   alinhamento por omissão da caixa, não esta regra. Dos três sujeitos,
   o .btn já se safava sozinho porque declara `align-items: center;
   justify-content: center` por conta própria; os outros dois não
   declaravam nada e toda a folga caía em baixo e à direita. A centragem
   deles está agora junto de cada componente, acima.
   O padrão certo já existia duas vezes no repositório: .back-to-top
   (css/enhancements.css) chega aos 48px com `width`/`height` explícitos
   E centra, e .info-tip > summary (css/portal.css) mantém a marca de
   16.797px e faz crescer só o ALVO com um ::after de 44x44 centrado por
   `left/top: 50%` + `translate: -50% -50%`. Três dos cinco componentes
   ícone-em-caixa estavam certos, e os dois errados eram exactamente os
   dois que chegavam aos 44px por este piso partilhado.
   NÃO MEXER no .btn: ele já está certo e partilha esta regra. */
.btn,
.primary-nav a.nav-account { min-height: 44px; min-width: 44px; }

@media (min-width: 768px) {
  /* MUITO MAIS CURTO DO QUE ERA, e a razão vale a pena registar: metade
     desta regra existia só para DESFAZER o painel de celular --
     `opacity`, `transform`, `pointer-events`, `visibility`,
     `transition`, `position`, `flex-direction`, `border`, `padding`,
     `max-height`, `overflow-y`, `background`. Nada disso existe agora na
     regra base, portanto não há nada para desfazer, e um reset que
     desfaz uma declaração que já não é escrita é ruído que a próxima
     pessoa tem de ler para perceber que é inerte. `flex-wrap` e
     `align-items` também subiram para a base: valem nas duas faixas.
     Fica só o que é genuinamente diferente no desktop -- a goteira. */
  .primary-nav {
    /* 32px -> 20px. A 1025px o aperto de tablet caía de uma vez e o menu
       voltava ao tamanho cheio, mas a janela só tinha crescido 1px:
       medido em index.html, a folga entre o logótipo e o menu passava de
       292px a 1024 para 12px a 1025. Com a raiz a 20px -- uma definição
       de acessibilidade comum -- a folga era ZERO de 1025 para cima e a
       página rolava 128px na horizontal.
       Vinte pixéis compram ~72px ao longo dos seis intervalos e é a mesma
       goteira que as grelhas do resto da página já usam. Rejeitado:
       esticar a fonte de 0.8125rem do tablet acima de 1024px (a
       tipografia está congelada) e criar uma faixa 1024-1150 (seria um
       breakpoint novo). */
    gap: var(--space-lg);
  }

  /* Volta ao centro: em linha, align-self mexe no eixo vertical, e o
     flex-start do mobile deixaria a pílula grudada no topo. */
  .primary-nav a.nav-account { align-self: center; }
}

/* Tablet only: the full inline nav (up to 9 items on index.html) is too
   wide for --space-2xl gaps + full 1rem link size at this width -- tighten
   both here without touching desktop (1025px+), which has room to spare. */
@media (min-width: 768px) and (max-width: 1024px) {
  .primary-nav { gap: var(--space-sm); }

  .primary-nav a:not(.btn) { font-size: 0.8125rem; }

  /* Horizontal a --space-base e não a --space-sm. Este bloco re-espaça o
     CTA e a .nav-account ao lado dele, mas só descia o preenchimento de
     um dos dois: medido a 881-1024, o CTA dava 9px de recuo de texto
     contra os 16px da pílula vizinha, e o mesmo botão saltava de 9 para
     21px ao passar de 1024 para 1025 -- 2.3x num pixel de janela. A 8px
     a razão do comprimido caía para 8:5 = 1.6, a mais apertada do site,
     com o rótulo a 9px de uma curva de 22px. A 15px os dois partilham o
     mesmo recuo horizontal em toda a faixa e a razão sobe para 3.0.
     A folga que esta faixa precisa vem da goteira (32px para 8px), não
     do preenchimento do botão: medido depois, a 881px -- a largura mais
     estreita onde o CTA aparece -- sobram 133px entre o logótipo e o
     menu, e scrollWidth continua igual a clientWidth em 768-1120. */
  .primary-nav .btn-small {
    font-size: 0.8125rem;
    padding: var(--space-xs) var(--space-base);
  }

}

/* =========================================================
   O CABEÇALHO ABAIXO DOS 880px: as duas portas, sem os atalhos.

   Sete itens somam 659px. Ao lado de um logótipo de 80px sobram 240px
   a 375px e 190px a 320px. Não é um problema de aperto -- não cabe, em
   nenhuma escala que ainda seja legível, e a tipografia está congelada
   (norma_DESIGN.md §14).

   ROLAR HORIZONTALMENTE FOI TENTADO E É PIOR DO QUE PARECE: a 320px
   escondia 527 dos 719px, e o item que ficava mais longe era
   precisamente o "Começar agora". Um cabeçalho que põe o botão de
   conversão atrás de um gesto que ninguém sabe que existe não é
   compacto -- é um botão perdido.

   O QUE SAI são os cinco atalhos de seção, e nenhum deles desaparece
   do site: #services, #process, #about, #faq e contato.html estão
   TODOS no rodapé, em todas as páginas, e numa landing page a própria
   rolagem já é a navegação. O QUE FICA são as duas portas do negócio,
   a conta e o CTA, que são as únicas coisas do cabeçalho sem segunda
   morada em lado nenhum.

   ESCOLHIDOS POR href, e não por "todo <a> simples": a navegação do
   portal (Portal / CMS / Minha conta) também é feita de links simples,
   é a navegação PRINCIPAL daquelas páginas e cabe à vontade. Uma regra
   por tipo de elemento esvaziava o cabeçalho do portal inteiro.

   SUBSTITUI A REGRA QUE ESCONDIA O CTA ENTRE 768 E 880px. Aquela
   resolvia este mesmo estouro sacrificando a coisa errada: o botão de
   conversão desaparecia ao rodar um tablet e voltava aos 881px, e o
   cabeçalho ficava com uma escada incoerente -- CTA no celular, sem
   CTA no tablet pequeno, CTA outra vez no desktop. Agora é ao
   contrário: o CTA nunca sai, e são os atalhos que só aparecem quando
   há espaço para eles. Uma regra em vez de duas, e a que fica sacrifica
   o item que tem cópia noutro sítio.
   ========================================================= */
@media (max-width: 767px) {
  .primary-nav a[href*="#services"],
  .primary-nav a[href*="#process"],
  .primary-nav a[href*="#about"],
  .primary-nav a[href*="#faq"],
  /* Era a[href$="contato.html"]. contato.html deixou de existir como página
     própria (SEO_AUDIT.md M10 / HANDOFF.md) -- o link virou #contact dentro
     de index.html -- e o selector antigo ficou órfão, sem casar com nada.
     Sem esta troca "Contato" reaparecia como um terceiro item ao lado de
     "Área do cliente" e do CTA, exatamente o estouro que este bloco existe
     para evitar (ver o comentário grande acima: 7 itens não cabem). */
  .primary-nav a[href*="#contact"] { display: none; }
}

/* O celular, onde o espaço é mesmo curto. */
@media (max-width: 767px) {
  /* Sem isto o <nav> desce para uma linha própria e o cabeçalho cresce
     de 68px para 185px -- era este `wrap` que fazia as três linhas. */
  .header-inner { flex-wrap: nowrap; }

  .primary-nav {
    flex-wrap: nowrap;
    /* Rede de segurança, não o mecanismo: depois do corte acima o
       conteúdo cabe. Isto existe para que uma página futura com um item
       a mais degrade a rolar em vez de empurrar o logótipo para fora da
       tela. */
    overflow-x: auto;
    min-width: 0;
    /* A caixa de rolagem corta nas próprias arestas e o anel de foco
       sangra 5px para fora da borda. Esta folga é o que o impede de ser
       decepado ao tabular. */
    padding-block: var(--space-xs);
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    /* 8px -> 5px. Medido: com 8px de goteira e 8px de recuo as duas
       portas somavam 249px e a 360px -- a largura de Android mais comum
       depois da de 375 -- só havia 232px. Faltavam 18px, e são estes
       três mais os doze do recuo abaixo que os pagam. */
    gap: var(--space-xs);
  }

  .primary-nav::-webkit-scrollbar { display: none; }

  .primary-nav > * { flex: 0 0 auto; white-space: nowrap; }

  /* As duas portas ao tamanho de tipo que a faixa de tablet já usa.
     Não é um tamanho novo no sistema: é o mesmo 0.8125rem que o menu
     assume entre 768 e 1024px. VIVE AQUI, depois de
     `.primary-nav a:not(.btn)`, e isso é load-bearing: aquele seletor
     mede (0,2,1) -- o `:not(.btn)` conta como classe -- exactamente o
     mesmo que `.primary-nav a.nav-account`. Com especificidade
     empatada ganha quem vem depois, e a primeira versão desta regra
     estava ANTES: a pílula ficava a 16px e só o botão encolhia. */
  .primary-nav a.nav-account,
  .primary-nav .btn-small {
    font-size: 0.8125rem;
    padding-inline: var(--space-xs);
  }
}

/* =========================================================
   Hero (surface-background role: platinum)
   ========================================================= */

.hero {
  /* 55px era um dos dois únicos degraus fora da escala do sistema, e o
     ritmo vertical das secções partia-se em três: hero 70/55, páginas de
     aplicação 55/70, retornos de pagamento 70/70. checkout.html e
     pagamento/sucesso.html são vistos com trinta segundos de intervalo,
     no momento em que o dinheiro muda de mãos, e o branco no topo mudava
     entre os dois. --space-4xl foi retirado; sobram dois ritmos. */
  padding-block: var(--space-5xl) var(--space-3xl);
  background: var(--color-platinum);
}

.hero-inner { max-width: 46rem; }

.hero-sub {
  font-size: 1.125rem;
  line-height: 1.6;
  margin-top: var(--space-lg);
  max-width: 42ch;
}

.hero-actions {
  margin-top: var(--space-3xl);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-base);
}

/* `.hero-trust` vivia aqui e saiu com a última marcação que a usava.
   Era o papel caption-fine-print, 200/12px/15.6px em onyx-soft, e
   carregava a nota de rodapé do hero. A linha que ela desenhava passou
   a ser a linha da troca, subiu para cima do botão e ganhou o papel
   body-bold em `.page-home .hero-trade`, porque era o argumento
   inteiro da oferta escrito no menor tipo da página. Nenhuma das 23
   rotas voltou a nomear esta classe. */

/* A faixa "Feito para donos de negócios em" (.proof-strip, .proof-label,
   .proof-tags) saiu com ProofStrip.tsx: nota 10 do owner, 26/09/2026. */

/* =========================================================
   Card grid (Services)
   ========================================================= */

.services { padding-block: var(--space-5xl); background: var(--color-white); }

/* REGRA DO MÍNIMO AUTOMÁTICO -- vale para o site inteiro, não só para
   esta grelha. Um item de flex ou de grelha tem, por omissão, um mínimo
   AUTOMÁTICO igual ao seu min-content: `1fr` é `minmax(auto, 1fr)`. Um
   item cuja palavra mais longa não caiba na sua quota não encolhe -- ele
   ROUBA largura aos irmãos, e a fila deixa de ser regular sem que nada
   no CSS diga que pode deixar de ser.
   As duas metades da mesma regra, porque são propriedades diferentes em
   contentores diferentes:
     - numa GRELHA de N colunas iguais, a pista declara-se
       `minmax(0, 1fr)`;
     - num item de FLEX que tem de poder encolher, declara-se
       `min-width: 0`.
   O repositório já tinha resolvido isto uma vez e nunca o escreveu:
   css/checkout.css:51 usa `minmax(0, 1fr) 340px` e o .field-group ao
   lado usa `min-width: 0` com o comentário que explica porquê. Medido a
   768px antes desta regra: .stat-grid dava 219.016 / 219.031 / 225.953
   (6.937px de diferença entre cartões irmãos) porque `meses]` em
   .stat-number não cabia na quota; com depoimentos reais a
   .testimonial-grid chegava aos 13.7px pela mesma via, e a folga da
   .process-list eram 4.8px. `minmax(0, ...)` só RETIRA um piso: nada
   encolhe abaixo do que já encolhia, e o que não couber passa a
   partir a linha em vez de empurrar o vizinho.
   Nota: isto cobre grelhas. Os contentores `flex-wrap` do site
   precisam da outra metade e não são tocados aqui. */
.card-grid {
  display: grid;
  gap: var(--space-lg);
  /* Também é a declaração que impede o .card de estourar a própria
     pista com a raiz a 20px -- medido: 317.9px de cartão numa pista de
     280px a 320px de janela. */
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 768px) {
  .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.card {
  background: var(--color-white);
  border-radius: var(--radius-card);
  padding: var(--space-2xl);
  /* Substitui --shadow-card. Onde uma sombra sai, entra um filete de 1px
     num token que já existe -- e este é o mesmo filete que .proof-tags li
     já usa na mesma página. Um cartão branco sobre a faixa branca de
     .services não tinha fronteira nenhuma sem isto.
     .faq-item saiu deste grupo: é um <details>, logo a sua borda delimita
     um controlo e sobe para --color-control-edge. Um <article> não. */
  border: 1px solid var(--color-hairline);
}

/* D7: 52ch de medida para texto de interface. A grelha vai de 1 para 2
   colunas a 768px e pára -- medido a 1280 e 1440, cada cartão fica com
   538px e o corpo corre ~59ch. Limita-se a MEDIDA dentro do cartão, não
   o número de colunas: mudar a contagem de colunas mudaria a composição
   da secção, que é estrutura de layout. */
.card p { max-width: 52ch; }

.card-number {
  display: inline-block;
  font-weight: 800;
  font-size: 0.875rem;
  color: var(--color-onyx-soft);
  margin-bottom: var(--space-base);
}

/* =========================================================
   Process (subtle surface differentiation: platinum)
   ========================================================= */

.process { padding-block: var(--space-5xl); background: var(--color-platinum); }

.process-list {
  display: grid;
  /* Uma goteira de grelha por página: .card-grid, .stat-grid e
     .testimonial-grid usam todas --space-lg e só esta corria a 32px. */
  gap: var(--space-lg);
  margin-top: var(--space-2xl);
}

/* `minmax(0, 1fr)`: ver a regra do mínimo automático em .card-grid.
   Quatro colunas a partir de 768px, e ficam quatro. Medidas as pistas:
   161px a 768 · 169 a 800 · 189 a 880 · 225 a 1024 · 259 de 1160 para
   cima, com desvio de 0.000px entre irmãs em todas as larguras -- não é
   um defeito de regularidade, é densidade. A 768px estas são as pistas
   mais estreitas do site (a .card-grid dá 342, a .testimonial-grid 221,
   a .stat-grid 219 e a .kpi-grid 208 pelo seu próprio mínimo de 13rem),
   23% abaixo da única medida confortável que o sistema declara.
   Passar a duas colunas em 768-1024 punha todas as pistas acima de 208px
   sem inventar um breakpoint, mas mudava a COMPOSIÇÃO da secção, que é
   uma decisão de desenho e não uma correcção mecânica: a pilha de passos
   ancorada à esquerda é um padrão de leitura. Aceite como está, medido e
   por escrito, para que a densidade seja uma decisão e não um acidente.
   `minmax(0, ...)` só retira o piso de min-content; não alarga a pista,
   e não alargou: os números acima são iguais antes e depois dele. */
@media (min-width: 768px) {
  .process-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.process-list li {
  display: flex;
  flex-direction: column;
  gap: var(--space-base);
}

.process-index {
  font-weight: 900;
  font-size: 2rem;
  line-height: 1;
  color: var(--color-onyx); /* a blue text-stroke here fails AA, removed */
}

/* =========================================================
   Results (placeholder section)
   ========================================================= */

.results { padding-block: var(--space-5xl); background: var(--color-white); }

.results-note {
  max-width: 60ch;
  color: var(--color-onyx-soft);
  font-style: italic;
  margin-top: calc(var(--space-2xl) * -1 + var(--space-base));
  margin-bottom: var(--space-2xl);
}

.stat-grid {
  display: grid;
  gap: var(--space-lg);
}

/* `minmax(0, 1fr)`: ver a regra do mínimo automático em .card-grid.
   Esta é a instância que a media mostrou a falhar hoje -- e os números
   reais de cliente a 2.5rem/900 (`R$ 1.240.000`, um único token
   inquebrável) têm min-content MAIOR que o `[X meses]` do exemplo, por
   isso sem esta declaração a secção piora no dia do lançamento. */
@media (min-width: 768px) {
  .stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Enquanto a secção está `hidden` isto não muda nada -- e é por isso que
   este é o momento mais barato para o fazer. No dia em que os resultados
   reais entrarem, os cartões que provam o crescimento de um cliente não
   podem estar desenhados como um rectângulo tracejado, que é a convenção
   nativa do navegador para "vazio, nada aqui ainda". O tracejado fica
   reservado ao único sítio onde significa literalmente rascunho:
   .status-badge[data-status="draft"]. */
.stat-card {
  border: 1px solid var(--color-hairline);
  border-radius: var(--radius-card);
  background: var(--color-white);
  padding: var(--space-2xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.stat-number {
  font-weight: 900;
  font-size: 2.5rem;
  line-height: 1;
}

.stat-label { font-size: 0.875rem; color: var(--color-onyx-soft); }

/* =========================================================
   About (dark surface — prussian blue bg per "surface-dark"
   role; blue eyebrow mark passes contrast here)
   ========================================================= */

.about { padding-block: var(--space-5xl); background: var(--color-prussian-blue); color: var(--color-white); }

.about .eyebrow { color: var(--color-white); }
.about .eyebrow::before { background: var(--color-dodger-blue); }

.about-inner { max-width: 48rem; }

/* launch-review.md P2-62: 768px a 18px media 78.9-85.3 caracteres,
   acima do teto de 75 que norma_DESIGN.md §5 fixa -- a medida mais larga
   do site, e o defeito já resolvido uma vez no hero (.hero-inner larga
   46rem, mas é .hero-sub, o parágrafo, que fecha a medida real a 42ch).
   Mesmo remédio aqui: a medida sobe para o parágrafo, em ch, sem tocar em
   .about-inner. 62ch ≈ 69 caracteres a 18px. */
.about p { font-size: 1.125rem; line-height: 1.6; max-width: 62ch; }

/* =========================================================
   FAQ
   ========================================================= */

.faq { padding-block: var(--space-5xl); background: var(--color-white); }

.faq-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-base);
  margin-top: var(--space-2xl);
}

.faq-item {
  /* Controlo, não cartão: .faq-item é o <details> e o <summary> lá dentro
     recebe foco e clique, portanto esta borda é o que delimita o controlo
     e o critério 1.4.11 aplica-se a 3:1. Daí --color-control-edge (onyx a
     45%, 3.15:1 sobre branco) e não o filete decorativo dos cartões. */
  border: 1px solid var(--color-control-edge);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
  transition: border-color 0.15s ease;
}

/* norma_DESIGN.md §13 registava isto como "gap (sem preenchimento de hover
   documentado)" -- a mesma classe de falta que .btn-secondary tinha. Onyx
   sólido, não um alpha novo: já é o valor de .btn/.field ao focar, e mede
   19.80:1 sobre branco e 16.70:1 sobre platinum, portanto continua a
   clarear 1.4.11 em qualquer fundo onde este controlo já vive hoje. */
.faq-item:hover { border-color: var(--color-onyx); }

@media (prefers-reduced-motion: reduce) {
  .faq-item { transition: none; }
}

.faq-item summary {
  font-weight: 700;
  cursor: pointer;
  list-style: none;
  /* Mesma convenção que .primary-nav a:not(.btn) já carrega, e pelo mesmo
     motivo escrito lá: a linha de 24px sozinha dava um alvo de 24px, e o
     acordeão é a interacção principal da landing page abaixo da dobra. */
  padding-block: var(--space-md);
}

.faq-item summary:hover { color: var(--color-onyx-soft); }

/* Sem isto o <h3> herda o h3{} global (1.5rem/36px), o que muda a altura do
   <summary> medida no comentário acima. `font: inherit` copia o peso/tamanho
   do próprio <summary>; `display: inline` mantém a pergunta na mesma linha
   do marcador em ::after, que depende do <summary> ser um único bloco de
   texto. */
.faq-item summary h3 {
  display: inline;
  font: inherit;
  margin: 0;
}

.faq-item summary::-webkit-details-marker { display: none; }

/* O marcador flutua, logo vive na PRIMEIRA linha do <summary>, e quando a
   pergunta quebra fica acima do centro da caixa. Medido: com 3 de 4
   perguntas a quebrar a 320px e 2 de 4 a 375px, a caixa de conteúdo passa
   de 24px para 48px e o marcador fica a -12.5px do centro; de ~460px para
   cima nenhuma quebra e o desvio é -0.5px, que é só o resíduo da caixa de
   linha da Poppins. Fica assim, de propósito. Centrá-lo numa pergunta
   quebrada exige tirar o marcador do ::after e tornar o <summary> um flex
   com `justify-content: space-between` -- isso é mudar o componente e o
   contrato de marcação do acordeão, não mudar um valor, e um marcador
   preso à primeira linha de uma pergunta longa é uma convenção
   defensável. A altura do <summary> é idêntica aberto e fechado em todas
   as larguras medidas (68/68 a 320 e 375, 44/44 de 460 a 1440): abrir um
   item não desloca geometria nenhuma. */
.faq-item summary::after {
  content: "+";
  float: right;
  font-weight: 400;
}

.faq-item[open] summary::after { content: "\2212"; }

.faq-item p {
  margin-top: var(--space-base);
  color: var(--color-onyx-soft);
}

/* =========================================================
   Final CTA (surface-background role: platinum)
   ========================================================= */

.final-cta {
  padding-block: var(--space-5xl);
  background: var(--color-platinum);
  text-align: center;
}

/* --space-6xl consumido pela primeira vez (DESIGN_PROPOSALS.md §6, tratamento
   C): esta é a última seção antes do rodapé e o último pedido da página --
   a única exceção, deliberada, ao ritmo uniforme de --space-5xl que toda
   banda irmã (hero/process/faq/about) usa. Não vira regra geral: nenhuma
   outra banda ganha este espaçamento maior. */
@media (min-width: 1025px) {
  .final-cta { padding-block: var(--space-6xl); }
}

/* A outra metade da regra do invólucro interno. Um invólucro numa secção
   alinhada à esquerda OMITE `margin-inline: auto` de propósito -- é isso
   que segura uma única aresta esquerda de conteúdo nas 23 rotas, e
   enhancements.css:233 mede o que custa quebrá-la (308px entre a marca e
   a tela de pagamento). Um invólucro numa secção CENTRADA precisa dela.
   .final-cta é a única secção `text-align: center` do site, portanto
   .final-cta-inner é o único dos dez invólucros que a leva; os outros
   nove ficam como estão.
   Medido antes, dentro da caixa de conteúdo do .container: gapL/gapR de
   0/0 a 375, 0/64 a 768, 0/320 a 1024 e 0/456 a 1280 e a 1440 -- cada
   filho centrado numa caixa de 640px encostada à esquerda de um
   contentor de 1160px, com o bloco de conversão final 228px à esquerda
   do centro real da secção enquanto se anunciava centrado.
   O defeito é a COLUNA, não o texto: `text-align: center` fica. */
.final-cta-inner {
  max-width: 40rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
}

.final-cta-note {
  font-weight: 400; /* DS: Poppins 400/500 only (25/09/2026) */
  font-size: 0.75rem;
  line-height: 15.6px;
  color: var(--color-onyx-soft);
}

/* =========================================================
   Footer (dark surface — prussian blue)
   ========================================================= */

.site-footer {
  background: var(--color-prussian-blue);
  color: var(--color-white);
  padding-top: var(--space-3xl);
}

@media (min-width: 1025px) {
  /* Alvo do briefing era 64px; 64 não está na escala de espaçamento
     (5·8·10·15·20·32·50·70·96, norma_DESIGN.md §5) e este documento
     proíbe literais fora da escada. --space-5xl/70px é o degrau mais
     próximo (6px de distância, contra 14px de --space-3xl). */
  .site-footer { padding-top: var(--space-5xl); }
}

.footer-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xl);
  padding-bottom: var(--space-2xl);
  /* Era `#ffffff26` — o único hex cru fora do bloco :root em toda a
     folha. Mesmo valor exacto (0x26 = 38/255 = 0.149), agora o token
     nomeado (--color-hairline-dark) em vez de uma segunda vez como
     rgba() solto -- um valor, um nome, uma declaração para reencontrar.
     É também, por coincidência de valor (branco 15% ~ platina 12-16%
     sobre este mesmo fundo, ambos ~1.5:1), o pedido do briefing do
     rodapé 2026-09-03 de uma linha platina 12-16% -- reaproveitado em
     vez de um segundo token para o mesmo papel decorativo. */
  border-bottom: 1px solid var(--color-hairline-dark);
}

.wordmark-footer { color: var(--color-white); }

.footer-tagline {
  font-weight: 400;
  font-size: 1rem;
  line-height: 24px;
  color: var(--color-white);
  margin-top: var(--space-sm);
}

.footer-brand p, .footer-social p {
  font-weight: 400; /* DS: Poppins 400/500 only (25/09/2026) */
  font-size: 0.75rem;
  line-height: 15.6px;
  color: var(--color-platinum);
  margin-top: var(--space-sm);
}

.footer-whatsapp { margin-top: var(--space-base); }

/* .btn-secondary's onyx border/text (styles.css ~L555) assumes a light
   ground; on the footer's prussian ground it's onyx-on-near-black, which
   fails contrast outright. Recolour to the footer's own bright tier (white
   -- the same tier .wordmark-footer and .footer-tagline already use, one
   step above the platinum body text) rather than inventing a new token. */
.footer-email {
  margin-top: var(--space-sm);
  border-color: var(--color-white);
  color: var(--color-white);
}

.footer-email:hover { background: var(--color-white); color: var(--color-onyx); }

/* UX_LAW_AUDIT.md #8 (lei de Miller): 10-11 links soltos numa fila só,
   misturando navegação de página e links legais sem separação nenhuma.
   .footer-nav agora empilha 3 grupos rotulados (.footer-nav-group), cada
   um um <h2> + <ul> real -- o briefing de 2026-09-03 pediu cabeçalhos
   acessíveis e listas semânticas em vez das <div aria-label> que serviam
   o mesmo papel visualmente mas não anunciavam a um leitor de tela
   quantos links tinha cada grupo. */
.footer-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xl);
}

/* Rodapé 2026-09-03 (briefing de redesenho): grelha de 4 colunas a
   >=1025px, 2x2 a >=768px, empilhado numa coluna só abaixo disso -- a
   pilha vertical de cima é o estado BASE (mobile-first), sem precisar
   de query própria. `.footer-nav` fica com `display: contents` nos dois
   patamares de grelha para que os seus 3 `.footer-nav-group` entrem
   directamente como itens da grelha ao lado de `.footer-brand` -- 4
   itens de grelha ao todo, não 2 -- mantendo o <nav aria-label="Rodapé">
   na árvore de acessibilidade sem lhe dar caixa própria (suportado nos
   motores actuais; `role`/nome do landmark continuam expostos). ESTA
   regra tem de vir DEPOIS da declaração base de `.footer-nav` acima --
   mesma especificidade (uma classe), a ordem da folha decide, e a base
   já sobrepôs esta regra uma vez por vir depois no ficheiro. */
@media (min-width: 768px) {
  .footer-inner {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2xl);
  }
  .footer-nav { display: contents; }
}

@media (min-width: 1025px) {
  .footer-inner {
    grid-template-columns: minmax(0, 1.2fr) repeat(3, minmax(0, 1fr));
  }
}

.footer-nav-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.footer-nav-heading {
  /* Papel de micro-label já documentado (norma_DESIGN.md §3): 0.75rem,
     700, 0.08em, maiúsculas -- a excepção a "nunca all-caps" que §15 já
     nomeia. Cor platina = --fg-on-inverse-muted, o mesmo par já usado
     por .footer-service-area. */
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-platinum);
  margin: 0;
}

.footer-nav-group ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* O cabeçalho recebeu alvo de toque e escreveu porquê; o rodapé nunca o
   recebeu. Medido, igual em todas as 23 páginas: os links do rodapé
   davam 24px e os dois links de contacto do bloco da marca -- o e-mail e
   o WhatsApp -- davam 17px, os elementos interactivos mais pequenos do
   site, e são precisamente aqueles que alguém no celular procura,
   porque em 20 das 23 páginas o cabeçalho não oferece caminho nenhum
   para uma pessoa. Mesmo token, mesma correcção. */
.footer-nav a,
.footer-brand p a,
.footer-social p a,
.footer-backtotop,
.wordmark-footer {
  display: inline-flex;
  align-items: center;
  padding-block: var(--space-md);
  /* O padding sozinho leva os links do rodapé (linha de 24px) aos 44px,
     mas os dois links de contacto correm a 15.6px de linha e parariam a
     35.6px. `min-height` é o mesmo mecanismo, com o mesmo literal de
     44px, que a regra de alvo de toque lá em cima já aplica a .btn, ao
     hambúrguer e a .nav-account -- um chão, não um segundo valor de
     espaçamento. */
  min-height: 44px;
}

.footer-bottom {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  padding-block: var(--space-lg);
  font-size: 0.75rem;
  color: var(--color-platinum);
}

.footer-copyright { margin: 0; }

.footer-bottom-actions {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.footer-social {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  color: var(--color-platinum);
  transition: color 160ms ease;
}

.footer-social a:hover { color: var(--color-white); }

@media (min-width: 768px) {
  .footer-bottom { flex-direction: row; align-items: center; justify-content: space-between; }
  /* .back-to-top (css/enhancements.css) é fixed, 48px, right/bottom
     var(--space-lg)/20px -- ocupa 700-748px de uma tela de 768px na
     página que o mostra (index.html, único ficheiro com .hero, que é o
     que liga a visibilidade do disco). MEDIDO: sem esta goteira, "Voltar
     ao topo" media até x=726 e ficava 26px por baixo do disco. Não se
     mexe em .back-to-top -- fora do escopo deste rodapé -- por isso o
     rodapé é que abre espaço para o vizinho fixo. */
  .footer-bottom-actions { padding-right: var(--space-3xl); }
}

/* =========================================================
   PRIMITIVAS PARTILHADAS

   .field, .form-status e .spinner estavam declarados de raiz em
   auth.css, checkout.css e portal.css, e as cópias já tinham divergido
   de formas que o cliente via: a <textarea> do formulário de contacto
   renderizava em Courier dentro de um rectângulo cinzento do sistema,
   o estado inválido não existia em SEIS páginas do portal nem em
   nenhuma textarea do site, e os campos de contato.html ficavam ao
   dobro da distância dos de login.html.
   A resposta certa para um styles.css grande é parti-lo em base +
   componentes numa passagem de engenharia, não espalhar primitivas por
   um ficheiro chamado "enhancements" para manter um número baixo. Um
   skip link, um spinner e um campo de formulário são primitivas de
   base.
   Tem de ser uma MUDANÇA, não uma cópia: styles.css carrega primeiro em
   todas as páginas, portanto qualquer cópia deixada numa folha de
   superfície continua a ganhar.
   ========================================================= */

/* Precisa de vencer os `display` declarados nas primitivas acima
   (.spinner é inline-block, .field é flex). Vive aqui porque as
   primitivas que guarda vivem aqui. */
[hidden] { display: none !important; }

/* ---------- Layout de formulário ---------- */

/* Sobe junto com .field, e não por arrumação: portal-dashboard.html e
   portal-project.html escrevem `class="auth-form"` e nunca carregaram
   auth.css, portanto o `gap` que separa os campos dessas páginas nunca
   existiu -- o espaçamento vinha do `margin-bottom` que a cópia de .field
   em portal.css tinha, e que a fusão retirou. O mesmo vale para
   .auth-actions em portal-deliverable.html, onde a linha de dois botões
   traz um `style=` inline com `flex-direction: row` sobre um elemento que
   nunca teve `display: flex`. */
.auth-form { display: flex; flex-direction: column; gap: var(--space-lg); }

.auth-actions { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-base); }
/* O mesmo par opacity/cursor passa a alcançar os botões que vivem
   dentro de uma .form-status -- na prática, o "Tentar de novo" que
   js/auth-common.js acrescenta à caixa de erro de carregamento.
   Essa caixa não fica dentro de .auth-actions nem de .checkout-actions
   em nenhuma das seis páginas de portal e de admin, e css/portal.css
   não declarava [disabled] de todo, portanto o ÚNICO caminho de
   recuperação de todas essas páginas desactivava-se sem mudar um
   pixel. Um selector a mais numa regra que já existe, e não uma
   terceira cópia da mesma declaração: `cursor: progress` continua a
   só apanhar botões que estão mesmo à espera de uma resposta, e não
   todo e qualquer [disabled] do site. */
/* Era `opacity: 0.7` aqui, e `0.45` no equivalente de .pix-copy mais abaixo.
   Medido (launch-review.md P3-20): opacity compõe o rótulo com o fundo por
   trás, e um botão azul sobre platinum a 70% cai para 3.85:1 -- abaixo do
   4.5:1 que §16 exige, na exacta frase ("Tentando…") que mais precisa de
   ser lida. A regra geral logo abaixo (.btn:disabled) resolve a cor; esta
   regra mais específica continua a existir só para o cursor -- "progress"
   é um sinal genuinamente diferente de "not-allowed" (um botão à espera de
   resposta, não um botão permanentemente indisponível), e essa distinção
   já vinha comentada aqui antes desta mudança. */
.auth-actions .btn[disabled],
.form-status .btn[disabled] { cursor: progress; }

/* Trata qualquer botão desativado do site, não só os dois contextos acima
   -- o mesmo gap que .btn-secondary:hover tinha (nenhuma regra geral,
   só cópias por contexto). Cor e borda substituem opacity/alpha: os dois
   tokens já existem, os dois já medem 4.5:1/3:1 sozinhos, e nenhum dos
   dois compõe o rótulo até à invisibilidade do jeito que opacity fazia
   (fecha P3-20 como efeito colateral, não como o objetivo desta regra). */
/* `:not([aria-busy])`: um botão A CARREGAR também leva `disabled` (é a
   guarda contra duplo envio), mas não é um botão indisponível e não deve
   parecer um -- ver o estado de carregamento logo abaixo. */
.btn:disabled:not([aria-busy="true"]),
.btn[disabled]:not([aria-busy="true"]) {
  color: var(--color-onyx-soft);
  border-color: var(--color-hairline);
  cursor: not-allowed;
}

/* ESTADO "A CARREGAR" -- distinto de desactivado. Posto por
   NormaAuth.setButtonLoading (js/auth-common.js), o único sítio que o
   escreve, em todos os formulários que enviam.

   - O preenchimento da variante FICA: nenhuma cor é redeclarada aqui, e a
     regra de desactivado acima exclui [aria-busy]. Um .btn-primary a enviar
     continua azul -- "recebi, estou a trabalhar", e não "recusado".
   - Opacidade 0.8, não 0.7. A 0.7 um .btn-primary sobre platinum media
     3.85:1 (launch-review.md P3-20, o motivo de a opacidade ter saído da
     regra de desactivado). A 0.8 o rótulo onyx sobre o azul composto fica
     acima de 4.5:1 sobre platinum e sobre o painel escuro da casca --
     medido em e2e/button-loading.spec.ts.
   - pointer-events: none impede o :hover de trocar o preenchimento a meio
     do envio. O clique já está bloqueado por `disabled`.
   - Spinner num ::before: nenhum elemento extra no HTML, portanto nenhum
     script tem de o mostrar e esconder, e trocar o rótulo nunca o apaga.
     .shell-btn já tem `gap`, por isso não leva margem. */
.btn[aria-busy="true"],
.shell-btn[aria-busy="true"] {
  opacity: 0.8;
  pointer-events: none;
}

.btn[aria-busy="true"]::before,
.shell-btn[aria-busy="true"]::before {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: norma-spin 0.7s linear infinite;
}

.btn[aria-busy="true"]::before { margin-right: var(--space-sm); }

@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::before,
  .shell-btn[aria-busy="true"]::before { animation: none; }
}

/* Uma linha de dois botões, em vez de `style=` inline a responder à mesma
   pergunta com dois valores diferentes: a linha de acções do CMS usava
   8px e a linha de revisão do entregável usava 15px, para o mesmo par de
   botões renderizado por dois ficheiros JS quase idênticos -- 7px de
   diferença que o cliente vê. Herda o gap de 15px que .auth-actions já
   declara. */
.auth-actions[data-layout="row"] { flex-direction: row; flex-wrap: wrap; align-items: center; }

/* Espaço acima de um sub-formulário ou botão revelado. Eram quatro
   respostas (15px, 20px, 32px) espalhadas por seis ficheiros de marcação
   em `style=`; 20px é a mediana e é o valor que .auth-form já usa como
   gap. */
.revealed-below { margin-top: var(--space-lg); }

/* ---------- Campo de formulário ---------- */

/* Sem `margin-bottom`: o espaçamento vem do `gap` do pai, que é o
   padrão que compõe. checkout.css declarava as duas coisas ao mesmo
   tempo e contato.html -- que carrega auth.css E checkout.css -- ficava
   com 40px entre campos contra os 20px de login.html. */
.field { display: flex; flex-direction: column; }

.field label {
  font-weight: 500;
  font-size: 1rem;
  line-height: 24px;
  margin-bottom: var(--space-xs);
  max-width: 52ch;
}

/* `select` e `textarea` entram no selector. Um selector de elemento
   `input` não casa com uma <textarea>, e era só isso que separava a
   caixa de mensagem de contato.html -- a maior peça do único formulário
   que a maioria dos visitantes alguma vez preenche -- de ser Poppins a
   16px com borda onyx em vez de monospace a 13.333px numa caixa
   rgb(118,118,118) do sistema. Os 16px também removem o zoom automático
   do Safari do iOS em campos abaixo desse tamanho.
   max-width de 440px: a largura que .auth-panel já estabelece. Sem
   limite, uma senha, um CPF ou o nome de um projeto eram
   desenhados numa caixa de 1.032px. Não se cria token: 440px passa a ter
   dois usos, e mintar um token de layout é uma decisão de design system
   que esta passagem não toma. */
.field input,
.field select,
.field textarea {
  font-family: inherit;
  font-size: 1rem;
  line-height: 24px;
  font-weight: 400;
  color: var(--color-onyx);
  background: var(--color-white);
  border: 1px solid var(--color-onyx);
  border-radius: var(--radius-card);
  padding: var(--space-md) var(--space-base);
  transition: border-color 0.15s ease;
  max-width: 440px;
}

/* Mesmo comportamento da outra textarea do produto (o editor do CMS). */
.field textarea { resize: vertical; }

.field input::placeholder,
.field textarea::placeholder { color: var(--color-onyx-soft); font-weight: 400; /* DS: Poppins 400/500 only (25/09/2026) */ }

.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--color-prussian-blue); }

/* O peso de ênfase são 2px RENDERIZADOS, entregues por `outline` num
   offset negativo sobre a fronteira de 1px -- nunca por mudar
   `border-width` num estado. O `border-width: 2px` anterior empurrava o
   texto dentro do campo 1px para dentro e não era coberto por transição
   nenhuma; `outline` não é propriedade de layout, portanto nada se mexe.
   Não é padrão novo: .pix-row[data-copied] em checkout.css já faz
   exactamente isto. Prussian blue lê 18.55:1 sobre branco. */
.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--color-prussian-blue);
  outline: 1px solid var(--color-prussian-blue);
  outline-offset: -1px;
}

/* UX_LAW_AUDIT.md #10 (lei de Jakob): alternador de visibilidade de senha.
   O wrapper isola o posicionamento absoluto do botão do resto de .field;
   o padding-right no input evita que o texto digitado fique atrás do
   botão. */
.password-field { position: relative; max-width: 440px; }
.password-field input { width: 100%; max-width: none; padding-right: 4.75rem; }
.password-toggle {
  position: absolute;
  right: 2px;
  top: 50%;
  translate: 0 -50%;
  height: 44px;
  padding: 0 var(--space-base);
  background: transparent;
  border: none;
  font-family: inherit;
  font-weight: 500; /* DS: Poppins 400/500 only (25/09/2026) */
  font-size: 0.8125rem;
  color: var(--color-prussian-blue);
  cursor: pointer;
}
.password-toggle:hover,
.password-toggle:focus-visible { text-decoration: underline; }

.field-hint {
  font-weight: 400; /* DS: Poppins 400/500 only (25/09/2026) */
  font-size: 0.75rem;
  line-height: 15.6px;
  color: var(--color-onyx-soft);
  margin-top: var(--space-xs);
  max-width: 52ch;
}

.field-error {
  font-weight: 700;
  font-size: 0.875rem;
  line-height: 20px;
  color: var(--color-onyx);
  margin-top: var(--space-sm);
  padding-left: var(--space-md);
  border-left: 3px solid var(--color-prussian-blue);
}
.field-error::before { content: "! "; font-weight: 900; }

@media (prefers-reduced-motion: reduce) {
  .field input,
  .field select,
  .field textarea { transition: none; }
}

/* ---------- Caixa de aviso "barra prussian à esquerda" ---------- */

/* Sobrevive a versão de auth.css: a mais completa, a mais usada e
   BRANCA. checkout.html usava a variante preenchida com o token de
   superfície e contato.html a variante branca, e as duas páginas
   carregam checkout.css -- ou seja,
   dois tratamentos do mesmo componente a uma navegação de distância na
   mesma superfície pública. */
.form-status {
  font-weight: 500;
  font-size: 1rem;
  line-height: 24px;
  padding: var(--space-base) var(--space-lg);
  background: var(--color-white);
  border-left: 3px solid var(--color-prussian-blue);
  border-radius: 0 var(--radius-card) var(--radius-card) 0;
}
.form-status[data-tone="error"]::before { content: "! "; font-weight: 500; }

/* ---------- Chegada das respostas do site ---------- */

/* Estas quatro caixas carregam TODAS as respostas que o site alguma vez
   dá: "E-mail ou senha incorretos", "Preparando seu pagamento…",
   "Recebemos sua mensagem", o campo de código MFA e a confirmação de
   apagar a conta. Entravam e saíam pelo atributo `hidden` contra
   `[hidden] { display: none !important }` -- ou seja, display none <->
   block, sem transição possível nesse mecanismo. A pessoa carregava em
   "Entrar", o botão baixava, um spinner rodava até 15 segundos, e a
   resposta materializava-se sem nada a ligar o clique à resposta.

   200ms (o fundo da banda de entrada), `ease`, opacidade e transform
   apenas, 4px de percurso -- um assentar, não uma entrada.

   O atributo `hidden` FICA. js/checkout.js e js/contato.js registam os
   dois que uma região aria-live alterada enquanto está `hidden` costuma
   não ser anunciada; a classe é aditiva, tirar o atributo regredia um
   bug já corrigido. */
.form-status,
.field-error,
.delete-confirm {
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.form-status.is-shown,
.field-error.is-shown,
.delete-confirm.is-shown {
  opacity: 1;
  transform: none;
}

/* ATENÇÃO -- este bloco põe `opacity: 1; transform: none`, e NÃO apenas
   `transition: none`. Não é estilo: é a diferença entre o site funcionar
   e não funcionar.
   As quatro regras acima escondem estes elementos a `opacity: 0` por
   omissão e revelam-nos por classe. Um bloco de movimento reduzido que
   só matasse a transição deixá-los-ia parados no estado ANTERIOR à
   transição -- e todas as mensagens de estado, todos os erros de
   validação, o campo de MFA e o painel de apagar a conta ficariam
   PERMANENTEMENTE INVISÍVEIS, em todas as páginas, para quem tiver
   "reduzir movimento" ligado. Incluindo os erros de login, de checkout e
   do portal.
   Com estas três declarações, quem pediu menos movimento volta a ter
   exactamente o comportamento antigo: o atributo `hidden` governa, e a
   caixa aparece de uma vez, visível. */
@media (prefers-reduced-motion: reduce) {
  .form-status,
  .field-error,
  .delete-confirm {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---------- Spinner ---------- */

/* Um laço, um nome. Eram três regras byte a byte idênticas a accionar
   três @keyframes de corpo idêntico e nomes diferentes (auth-spin,
   checkout-spin, portal-spin); contato.html carregava duas delas ao
   mesmo tempo e definia .spinner duas vezes. Um site que quer ler como
   técnico não carrega o mesmo laço sob três alcunhas.
   O nome segue a convenção que este ficheiro já usa para o seu outro
   keyframe global (norma-skeleton-sheen). */
.spinner {
  display: inline-block;
  width: 16px;
  height: 16px;
  margin-right: var(--space-sm);
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: norma-spin 0.7s linear infinite;
}

@keyframes norma-spin {
  to { transform: rotate(360deg); }
}

/* Sem animação: o rótulo do botão já diz "Processando…". */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}

/* ---------- Painel de secção (conta + portal) ---------- */

/* .account-section e .portal-section eram byte a byte o mesmo
   componente -- mesmo fundo, mesmo raio, mesmo padding, mesma margem --
   separados apenas por 5px na margem do cabeçalho. Sobrevive
   .portal-section (seis páginas contra uma) com os 20px do portal.
   Vive aqui e não em portal.css porque account.html carrega
   styles + enhancements + auth e NÃO carrega portal.css; fazer
   account.html carregar uma quarta folha é exactamente o que dá a
   contato.html os defeitos que esta passagem acabou de corrigir. */
.portal-section {
  background: var(--color-platinum);
  border-radius: var(--radius-card);
  padding: var(--space-2xl);
  margin-bottom: var(--space-2xl);
}

/* REGRA: a margem de baixo do ÚLTIMO bloco de uma secção não lhe
   pertence -- ela colapsa para fora do .container (que não tem padding
   de bloco nenhum) e aterra por cima do padding-bottom da própria
   casca. O ritmo 50/70 que o sistema declara passa a ser 50 em cima e
   qualquer coisa em baixo.
   Medido antes desta regra, com a janela baixa o suficiente para que
   seja o CONTEÚDO -- e não o `min-height: 60vh` do .portal -- a definir
   a altura da casca: o topo dava 50 em todas as páginas de aplicação, e
   o fundo saía em três valores: 70 onde o último bloco já tem margem 0,
   85 nas três páginas legais (.legal-inner p, 15px) e 102 na conta, no
   admin-cms, no portal-desempenho, no portal-dashboard e no
   portal-project. Depois desta regra são 70 nas nove.

   O selector é `:not(:has(~ :not([hidden])))` e NÃO só `:last-child`, e
   a diferença é a razão de ser da segunda linha: `:last-child` é ordem
   de DOCUMENTO, e a margem que escapa é ordem de LAYOUT. No
   portal-dashboard e no portal-project o último filho do #portalContent
   é uma secção `hidden` -- o painel de criar que o JS revela -- por
   isso o reset aterrava na secção invisível e a visível ficava com os
   32px. Medido com só o `:last-child`: 102px de fundo nessas duas
   páginas contra 70 nas outras sete, e são exactamente as duas páginas
   que a auditoria apontou como as piores.
   `:not(:has(~ :not([hidden])))` lê-se "não tenho a seguir nenhum irmão
   visível", que é a definição de último EM LAYOUT, e mantém-se correcto
   nos DOIS estados do painel: escondido, a secção de cima perde a
   margem; revelado, recupera-a e é o painel que fica sem a sua.
   As duas linhas ficam SEPARADAS de propósito, e não numa lista com
   vírgula: uma lista inteira é descartada se um dos selectores não for
   entendido, e assim, onde `:has()` não existir, cai-se no
   comportamento do `:last-child` em vez de se perder a regra toda.

   Isto NÃO mexe no 50/70 nem nas inversões 70/50: só faz render o que
   já estava declarado. As folgas que continuam a variar com a largura
   no portal-dashboard e no portal-project quando a janela é alta são o
   `min-height: 60vh` do .portal a empurrar o rodapé para baixo numa
   página curta -- é a casca a fazer o seu trabalho, não uma margem a
   fugir, e não é o que esta regra corrige.
   E não substitui o `.kpi-card { margin-bottom: 0 }` do portal.css --
   essa é outra regra, contra a dupla marcação dentro de uma grelha, e
   continua a fazer falta. */
.portal-section:last-child { margin-bottom: 0; }
.portal-section:not(:has(~ :not([hidden]))) { margin-bottom: 0; }

.portal-section h2 { font-size: 1.25rem; font-weight: 800; margin-bottom: var(--space-lg); }

/* =========================================================
   Badges de status

   REGRA: um componente cujos consumidores atravessam fronteiras de
   folha TEM DE SER DECLARADO NA FOLHA QUE TODOS OS CONSUMIDORES
   CARREGAM. Este bloco vivia em css/portal.css e era o ÚNICO conjunto
   de regras de .status-badge no repositório -- mas account.html carrega
   styles + enhancements + auth e NÃO carrega portal.css. Resultado
   medido nas onze larguras: o badge de dois factores da conta saía
   `p=0/0/0/0 b=0/0/0/0`, 23px de altura, texto de corpo a 16px dentro
   de um <p>. Sem pílula, sem borda, sem raio, sem preenchimento, nos
   quatro estados que js/account.js conduz. O facto mais relevante da
   página inteira renderizava como uma palavra solta.
   A ironia está escrita no próprio repositório: foi a consolidação que
   apagou o .badge do auth.css e mudou o tratamento para uma folha que
   esta página não carrega.
   Esta é a correcção que o repositório já aplicou CINCO vezes -- ver
   css/auth.css, onde .auth-form, .auth-actions, .field, .field-error,
   .form-status e .spinner registam a mesma mudança pela mesma razão --
   e uma sexta com o .portal-section aqui acima. Faltou esta.

   Porquê styles.css e não enhancements.css, quando as 23 páginas
   carregam as duas: (1) é a folha que as cinco aplicações anteriores
   desta mesma correcção escolheram, e o .portal-section acabou aqui
   pelo mesmo argumento; (2) o .skeleton-badge -- que existe só para
   medir exactamente o mesmo que este componente, e cujo comentário diz
   que copia daqui o padding, a borda, a fonte, o raio e o line-height
   -- vive nesta folha, umas dezenas de linhas abaixo. Separar os dois
   gémeos por duas folhas é como a paridade se perde. A alternativa
   (pôr um <link> para portal.css em account.html) puxa 32KB por um
   badge e deixa a armadilha armada para a página seguinte.
   ========================================================= */

.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: var(--space-xs) var(--space-md);
  border-radius: var(--radius-pill);
  font-weight: 700;
  font-size: 0.8125rem;
  border: 1px solid var(--color-onyx);
  /* O badge -- não o texto -- é o que define a altura da linha nestas
     tabelas. Com `line-height: normal` media 36px e a linha inteira
     media 53px, um terço acima do que a regra de densidade permite: numa
     tela de portátil de 900px isso são quatro linhas a menos por tela.
     13px x 1.3 = 16.9px, o que dá um badge de ~28.9px; com o padding de
     célula em --space-xs a linha aterra em ~40px. Não há outra combinação
     dentro da escala que lá chegue.
     .skeleton-badge mais abaixo carrega o mesmo line-height, senão a
     tabela volta a saltar ao trocar o placeholder pelos dados. */
  line-height: 1.3;
}

/* "pendente" e "nao_verificado" entram na lista existente: o Onboarding
   (norma website/js/portal-onboarding.js, js/admin-onboarding.js) usa os
   mesmos três vocabulários de estado que o resto do portal já usa aqui --
   "ainda não" (draft), "em curso/reconhecido" (bucket seguinte) e
   "concluído" (bucket depois desse) -- só com os nomes em português que a
   API de kickoff devolve (data-status ainda usa os valores literais do
   backend, como "pendente"/"nao_verificado" -- não renomeados). Zero
   regras novas, mesma lista ganhando valores. */
.status-badge[data-status="draft"],
.status-badge[data-status="pendente"],
.status-badge[data-status="nao_verificado"] {
  border-style: dashed;
  color: var(--color-onyx-soft);
}

/* "dispensado" entra aqui, não no bucket "concluído" abaixo: uma dispensa
   é a conta destravada por decisão administrativa, não um onboarding que
   terminou por ter sido preenchido -- a distinção visual entre os dois
   estados é a mesma que já existia entre "em curso" e "concluído". */
.status-badge[data-status="submitted"],
.status-badge[data-status="active"],
.status-badge[data-status="upcoming"],
.status-badge[data-status="PENDING"],
.status-badge[data-status="dispensado"] {
  border-color: var(--color-dodger-blue);
  background: var(--color-platinum);
}

/* "on" entra na lista existente: é o estado de dois factores em
   account.html, que antes usava um componente .badge à parte SEM
   selector de estado nenhum -- "Ativada" e "Desativada" saíam na mesma
   caixa, com o facto mais relevante da página inteiro entregue por uma
   palavra a 13px. Aqui, ligado é cheio e desligado é a base contornada,
   que é o mesmo vocabulário que quem usa o portal já aprendeu a ler.
   Zero regras novas: uma lista de selectores ganha um valor. E remove um
   componente em vez de acrescentar um -- de dois badges para um. */
/* "completo"/"concedido"/"nao_se_aplica"/"verificado" join the same
   "settled, nothing left to do" bucket -- concedido and não se aplica are
   deliberately styled identically, because requireKickoffComplete's gate
   (server/src/lib/portal-auth.ts) already treats them identically: either
   one closes out that platform's row. (These data-status values are the
   backend's literal onboarding status vocabulary -- not renamed.) */
.status-badge[data-status="approved"],
.status-badge[data-status="completed"],
.status-badge[data-status="done"],
.status-badge[data-status="on"],
.status-badge[data-status="PAID"],
.status-badge[data-status="completo"],
.status-badge[data-status="concedido"],
.status-badge[data-status="nao_se_aplica"],
.status-badge[data-status="verificado"] {
  background: var(--color-onyx);
  color: var(--color-white);
  border-color: var(--color-onyx);
}

.status-badge[data-status="changes_requested"],
.status-badge[data-status="missed"],
.status-badge[data-status="OVERDUE"],
.status-badge[data-status="rejeitado"] {
  /* `border: 1px solid transparent` e não `border: none`. MEDIDO, e ao
     contrário do que a auditoria previa: o line-height de 1.3 NÃO iguala
     as alturas sozinho, porque este badge perdia a borda de 1px em cima e
     em baixo e ficava 2px mais baixo que os vizinhos (26.89px contra
     28.89px), o que deixava a linha dele 2px mais curta que as outras.
     Uma borda transparente devolve a caixa exacta sem desenhar nada e sem
     um `padding-block` fora da escala. É o mesmo truque que
     .skeleton-badge já usa mais abaixo, pela mesma razão. */
  border: 1px solid transparent;
  border-left: 3px solid var(--color-prussian-blue);
  /* Todos os outros blocos com barra à esquerda no repositório usam
     `0 8px 8px 0` -- .form-status, .method-notice, .legal-todo,
     .delete-confirm. Este era a única excepção, a 2.5px, e numa coluna de
     pílulas aparecia como um separador de cantos rectos, lido como falha
     de renderização em vez de ênfase. É a linha que o cliente mais
     precisa de encontrar. */
  /* Aqui esteve um `padding-left: var(--space-md)` que não fazia nada: a
     regra base acima já declara `padding: var(--space-xs) var(--space-md)`,
     portanto a linha repetia os mesmos 10px. Estava escrita como se
     compensasse a barra de 3px, e é por isso que sai em vez de ficar --
     sentada exactamente onde alguém procura a lógica de compensação,
     dizia que ela existe.
     Não existe, e a família está certa como está. Nenhum dos sete membros
     da barra prussiana de 3px compensa: a barra fica FORA do
     preenchimento da caixa, logo o recuo interno é sempre mais pesado à
     esquerda pela largura da barra menos a borda que já lá estava (+3 em
     .form-status, .method-notice, .legal-todo e .delete-confirm; +2 aqui
     e em .item-editor). Fechar a conta pedia 17px em dois deles, 12px em
     outros dois, 18px e 8px nos de borda de 1px -- e de todos esses só o
     8 está na escala de oito degraus. Como a compensação não pode ser
     feita dentro da escala, a decisão é não a fazer em lado nenhum, e
     esta linha era a única que sugeria o contrário. Medido dos dois
     lados da remoção: `padding-left` computado 10px antes e 10px depois,
     recuo interno 13/11 nos três estados com barra, altura 28.891px nos
     treze. Não mexer na `border: 1px solid transparent` acima: é ela que
     mantém estes três estados à altura dos vizinhos. */
  border-radius: 0 var(--radius-card) var(--radius-card) 0;
}
.status-badge[data-status="changes_requested"]::before,
.status-badge[data-status="missed"]::before,
.status-badge[data-status="OVERDUE"]::before,
.status-badge[data-status="rejeitado"]::before {
  content: "! ";
  font-weight: 900;
}

/* =========================================================
   Linha de identificador copiável (.pix-row)

   MUDOU DE FOLHA. Vivia inteira em css/checkout.css (a chave PIX do
   checkout) e passou para cá porque norma website/portal-onboarding.html
   passou a usar o mesmo componente para o identificador que o cliente
   cola em cada etapa de Acessos do Onboarding, e essa página NÃO carrega
   checkout.css -- mesmo motivo, e mesma regra, que já valia para
   .honeypot e para .field/.form-status/.spinner acima: uma classe usada
   por páginas com folhas diferentes vive na folha que todas carregam.
   Comportamento e valores inalterados pela mudança.
   ========================================================= */

.pix-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "label  button"
    "value  button";
  align-items: center;
  gap: 2px var(--space-base);
  background: var(--color-white);
  border: 1px solid var(--color-hairline);
  padding: var(--space-md) var(--space-base);
  border-radius: var(--radius-card);
  outline: 2px solid transparent;
  outline-offset: -2px;
  transition: outline-color 0.15s ease;
}

.pix-row-label {
  grid-area: label;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-onyx-soft);
}

/* Dígitos de largura fixa: o identificador é para conferir caractere a
   caractere, seja uma chave PIX ou um ID de Business Manager. */
.pix-row-value {
  grid-area: value;
  font-size: 1.0625rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  word-break: break-all;
}

.pix-row .pix-copy { grid-area: button; white-space: nowrap; }

.pix-row[data-copied="true"] {
  outline-color: var(--color-onyx);
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .pix-row { transition: none; }
}

/* .pix-copy:disabled vivia aqui como opacity: 0.45 -- 2.06:1, a pior das
   duas medições de P3-20. `.pix-copy` já é `.btn` (checkout.html), então a
   regra geral acima (.btn:disabled) já cobre cor, borda e cursor sem
   precisar de uma cópia própria. */

@media (max-width: 480px) {
  .pix-row {
    grid-template-columns: 1fr;
    grid-template-areas: "label" "value" "button";
    gap: var(--space-xs);
  }
  .pix-row .pix-copy { justify-self: stretch; text-align: center; }
}

/* =========================================================
   Onboarding -- cabeçalho de cartão (título + status-badge lado a lado)

   Um único par título+badge, reaproveitado por portal-onboarding.html
   (cada plataforma de acesso) e admin-onboarding.html (cada cliente na
   fila). .portal-card já resolve o cartão em si (css/portal.css); isto
   só resolve o alinhamento do cabeçalho, que nenhum cartão existente
   antes do Onboarding precisava fazer.
   ========================================================= */
.onboarding-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}
.onboarding-card-head h3 { margin: 0; font-size: 1rem; }

/* =========================================================
   Skeleton — placeholders de carregamento

   Vive aqui (e não em portal.css/auth.css) porque as 7 páginas
   com carregamento de dados se dividem entre os dois arquivos e
   duplicar isto significaria manter duas cópias divergentes.

   Regra de ouro: o skeleton espelha a FORMA do conteúdo que está
   chegando, para a página não pular quando os dados chegam.
   ========================================================= */

/* Só para leitores de tela. O #loadStatus das páginas com carregamento
   usa isto: ele continua sendo a região aria-live que anuncia
   carregando/erro/pronto, enquanto o visual fica por conta do skeleton.
   Escondê-lo com [hidden] silenciaria o anúncio. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Armadilha de robô. Fora da tela em vez de display:none -- parte dos
   robôs pula campo escondido com display/hidden, mas preenche este.
   aria-hidden + tabindex="-1" no HTML mantêm pessoa e leitor de tela
   longe dele.

   VIVE AQUI, e não em css/checkout.css onde nasceu, porque tem dois
   consumidores que carregam folhas diferentes: contato.html (que carrega
   checkout.css) e signup.html (que não carrega). Enquanto a regra esteve
   só na outra folha, o campo do cadastro teria sido desenhado na tela,
   com o rótulo "Não preencha este campo" à vista e sem servir de
   armadilha nenhuma. Esta folha é a única que as 23 páginas carregam.

   NÃO é .sr-only acima: aquela esconde do olho mas mantém no leitor de
   tela, que é o oposto do que uma armadilha precisa. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Nota curta debaixo das ações de um formulário de conta: a divulgação
   pré-cadastro em signup.html. Papel caption fine print, medida presa ao
   próprio parágrafo como o resto do sistema faz. */
.auth-note {
  max-width: 52ch;
  margin-top: var(--space-base);
  font-size: 0.875rem;
  line-height: 20px;
  color: var(--color-onyx-soft);
}

/* UX_LAW_AUDIT.md #9 (Fitts's law): "Termos de uso" e "Política de
   Privacidade" mediam 103x20 / 260x40px ao vivo -- abaixo do piso de 44px
   no mobile, e o texto é uma divulgação legal obrigatória bem em cima do
   botão principal. padding-block estende a área de toque sem mudar a
   altura de linha visível; margin-block negativo cancela o deslocamento. */
.auth-note a {
  padding-block: 12px;
  margin-block: -12px;
}

.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--skeleton-base);
  border-radius: var(--skeleton-radius);
}

/* O brilho que atravessa o bloco. Fica no ::after para não exigir um
   elemento extra por linha de skeleton. */
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--skeleton-sheen) 70%, transparent),
    transparent
  );
  /* `linear`, não `ease-in-out`. Este era o terceiro traçado de
     aceleração do repositório, contra uma especificação que registava
     dois -- e um indicador em laço é uma máquina a dizer que está a
     trabalhar, o que `linear` lê honestamente. O outro laço do site, o
     .spinner, já é `linear` nas suas declarações, e no
     portal-dashboard.html os dois estão na tela ao mesmo tempo a bater
     ritmos diferentes: exactamente a imprecisão que esta passagem
     existe para remover. Fecha a questão RETIRANDO uma curva em vez de
     ratificar mais uma, que é a única direcção que estreita o sistema. */
  animation: norma-skeleton-sheen 1.4s linear infinite;
}

@keyframes norma-skeleton-sheen { to { transform: translateX(100%); } }

/* Mesmo tratamento que .spinner já usa: sem movimento, mas o
   placeholder continua visível e legível. */
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; opacity: 0; }
}

/* A chegada dos dados. js/portal-common.js limpava o skeleton e
   construía a tabela real no mesmo frame: o brilho parava a meio do
   ciclo e era substituído de uma vez. E porque o skeleton foi
   deliberadamente construído para medir a mesma altura do conteúdo real
   (ver a nota acima), nada se mexia -- a troca era SILENCIOSA além de
   instantânea, e numa ligação rápida o olho não recebia sinal nenhum de
   que alguma coisa aconteceu. É o momento pelo qual o cliente paga:
   abriu o portal para saber se o entregável foi aprovado.

   Opacidade apenas, 200ms, `ease`. Sem transform e SEM escalonamento --
   uma entrada linha a linha numa tabela de dados é exactamente a
   decoração que a regra rejeita.

   A classe entra em clearSkeleton() e sai em renderTableSkeleton(), no
   mesmo elemento que leva o `aria-hidden`, para o anúncio do #loadStatus
   continuar a liderar o visual: quem usa leitor de tela não fica à
   espera de um fade de 200ms. Sair em renderTableSkeleton() é o que
   permite à animação repetir-se num "Tentar de novo". */
.is-loaded { animation: norma-content-in 0.2s ease; }

@keyframes norma-content-in { from { opacity: 0; } to { opacity: 1; } }

/* `animation: none` e NADA de opacity aqui: o conteúdo é desenhado pelo
   seu próprio estado, não pela animação, portanto desligar a animação
   deixa-o visível. Pôr `opacity` neste bloco seria a armadilha do
   bloco de reveal acima, ao contrário. */
@media (prefers-reduced-motion: reduce) {
  .is-loaded { animation: none; }
}

/* Linha de texto. A barra tem 1em (fica fina, não um bloco), mas a
   CAIXA precisa medir uma linha inteira -- 1lh -- senão o skeleton
   fica mais baixo que o texto que substitui e a tabela pula. Medido:
   com margem de 0.25em a linha dava 40px contra 41,5px do texto real.
   A margem em em é o fallback para quem não tem a unidade lh. */
.skeleton-line {
  height: 1em;
  margin-block: 0.25em;
  margin-block: calc((1lh - 1em) / 2);
}

.skeleton-line[data-width="full"]  { width: 100%; }
.skeleton-line[data-width="wide"]  { width: 78%; }
.skeleton-line[data-width="half"]  { width: 52%; }
.skeleton-line[data-width="short"] { width: 34%; }

/* Título de página: mesma altura do <h1> que substitui. */
.skeleton-heading {
  height: 2rem;
  width: 46%;
  margin-bottom: var(--space-base);
  border-radius: var(--skeleton-block-radius);
}

.skeleton-block { border-radius: var(--skeleton-block-radius); }

/* Placeholder de badge de status. Existe porque é o BADGE, não o texto,
   que define a altura da linha nessas tabelas (36px contra ~22px de uma
   linha de texto): um skeleton de linha de texto na coluna de status
   deixava a linha 13,5px mais baixa que a real, e a tabela pulava na
   troca. Copia padding, borda, fonte e raio de .status-badge, e usa um
   espaço rígido para herdar exatamente a mesma caixa de linha. */
.skeleton-badge {
  display: inline-flex;
  align-items: center;
  width: 84px;
  padding: var(--space-xs) var(--space-md);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: 0.8125rem;
  /* Tem de acompanhar .status-badge line-height a line-height, senão a
     tabela volta a saltar na troca do skeleton pelos dados reais -- que é
     o defeito que este bloco existe para eliminar. */
  line-height: 1.3;
}
.skeleton-badge::before { content: "\00a0"; }

/* Par rótulo/valor, como os campos de conta e os detalhes de
   entregável. */
.skeleton-field { margin-bottom: var(--space-lg); }
.skeleton-field .skeleton-line:first-child { height: 0.875em; width: 28%; }

/* A região inteira é decorativa: quem usa leitor de tela recebe o
   estado por aria-live no #loadStatus, não por estes blocos. */
.skeleton-region { margin-block: var(--space-base); }

/* =========================================================
   PADRÃO DE DETALHE — vale em todas as páginas.
   ========================================================= */

/* Selecção de texto deliberada. Sem isto o azul do sistema entra na
   página e é a única cor fora da paleta que o visitante vê. Onyx sobre
   dodger mede 7.27:1, o mesmo par do CTA primário. */
::selection {
  background: var(--color-dodger-blue);
  color: var(--color-onyx);
}

/* ALVO DE TOQUE do link secundário do hero (Fitts's law — Lidwell,
   Universal Principles: o tempo para atingir um alvo cai à medida que o
   alvo cresce).
   Medido antes: 150x24. A área cresce por padding com margem negativa a
   compensar, portanto NADA se move no layout — só o alvo invisível
   aumenta.

   DELIBERADAMENTE NÃO INCLUI a navegação nem o wordmark. `.primary-nav
   a:not(.btn)` zera o padding vertical no desktop de propósito, e a
   regra traz a razão escrita: o padding existe no menu em coluna, que é
   onde há toque; no desktop o espaçamento horizontal já separa e o
   ponteiro é um rato. Isso é uma decisão tomada, não um esquecimento, e
   inflar a altura ali também empurrava o cabeçalho 10px para cima. */
.hero-cta-secondary {
  padding-block: 10px;
  margin-block: -10px;
}

/* O foco desenha-se em volta do texto e não da área inflada, senão o
   anel salta para longe da palavra. */
.hero-cta-secondary:focus-visible { outline-offset: -6px; }

/* =========================================================
   PLANOS (planos.html)
   Vive em styles.css e não em home.css porque home.css está
   inteiro sob `.page-home` e só a landing o carrega. Aqui
   tudo sai dos tokens: seis cores, escala de espaçamento,
   raio de cartão, e os pesos/entrelinhas do bloco de tipo.
   ========================================================= */

.plans-intro {
  background: var(--color-prussian-blue);
  color: var(--color-white);
  padding-block: var(--space-5xl) var(--space-3xl);
}

.plans-intro h1 { color: var(--color-white); }

/* O rótulo desaparecia. `.eyebrow` fixa `color: var(--color-onyx)` na sua
   própria regra, por isso o branco que `.plans-intro` passa por herança
   nunca chegava lá: onyx sobre prussian mede 1.07:1, ou seja, texto
   invisível. Os outros dois painéis prussian do site já corrigiam isto
   um a um — `.about .eyebrow` aqui em cima e `.page-home .hero .eyebrow`
   em home.css, cujo comentário diz exatamente a mesma coisa ("sobre
   prussian desaparece"). Este painel foi o único que ficou por fazer.
   Não é uma decisão nova de estilo: é a terceira cópia de uma regra que
   já existe, e leva o rótulo de 1.07:1 para 18.55:1.
   O ponto decorativo não precisa de nada: `.eyebrow::before` já nasce
   dodger. */
.plans-intro .eyebrow { color: var(--color-white); }

.plans-lede {
  max-width: 62ch;
  margin-top: var(--space-base);
  font-weight: 400;                  /* token: body-regular */
  line-height: 24px;                 /* token: body-regular */
  color: var(--color-platinum);
}

.plans {
  background: var(--color-white);
  padding-block: var(--space-3xl) var(--space-5xl);
}

.plans__list {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 768px) {
  .plans__list { grid-template-columns: repeat(3, 1fr); gap: var(--space-2xl); }
}

.plan {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-2xl);
  border: 1px solid rgba(10, 10, 10, 0.12);
  border-radius: var(--radius-card);
}

.plan__name {
  margin: 0;
  font-weight: 700;                  /* token: sub-heading */
  line-height: 36px;                 /* token: sub-heading */
  font-size: 1.5rem;
  color: var(--color-onyx);
}

.plan__what {
  margin: 0;
  font-weight: 400;                  /* token: body-regular */
  line-height: 24px;                 /* token: body-regular */
  color: rgba(10, 10, 10, 0.72);
}

/* O preço é a razão de a página existir: sobe para o peso de display e
   usa dígitos tabulares para as três colunas alinharem verticalmente. */
.plan__price {
  margin: auto 0 0;
  padding-block-start: var(--space-base);
  border-block-start: 1px solid rgba(10, 10, 10, 0.12);
  font-weight: 900;                  /* token: hero-heading */
  line-height: 1;                    /* token: hero-heading */
  font-size: 2rem;
  color: var(--color-onyx);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.plan__period {
  font-weight: 400;                  /* token: body-regular */
  font-size: 1rem;
  color: rgba(10, 10, 10, 0.72);
}

/* A letra miúda que corrige a expectativa (setup, plano Enterprise) fica
   colada ao preço que ela corrige, não solta no fim do cartão. */
.plan__note {
  margin: 0;
  font-weight: 400;                  /* token: body-regular */
  line-height: 24px;                 /* token: body-regular */
  font-size: 0.875rem;
  color: rgba(10, 10, 10, 0.72);
}

.plans__cta {
  margin-top: var(--space-3xl);
  text-align: center;
}

.plans__cta-note {
  margin-top: var(--space-base);
  font-weight: 400;                  /* token: body-regular */
  line-height: 24px;                 /* token: body-regular */
  color: rgba(10, 10, 10, 0.72);
}

/* =========================================================
   IMPRESSÃO
   Não existia nenhuma folha de impressão no site. Privacidade,
   Termos e Cookies são exactamente as páginas que as pessoas
   imprimem ou guardam em PDF -- e sem esta folha saíam com a
   navegação, a barra de anúncio, os botões de conversão e o
   fundo escuro a gastar tinta.
   Aplica-se a todas as páginas porque nenhuma delas ganha
   alguma coisa em ser impressa com o cabeçalho colado.
   ========================================================= */
@media print {
  /* Fundo branco e texto preto. Um bloco prussian a sangrar numa
     folha A4 gasta tinta e não acrescenta nada em papel. */
  *, *::before, *::after {
    background: transparent !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }

  body { font-size: 12pt; line-height: 1.5; }

  /* Cromo de tela. Nada disto é accionável em papel. */
  .site-header,
  .primary-nav,
  .announcement-bar,
  .skip-link,
  .site-footer nav,
  .btn,
  .hero-cta,
  .hero-cta-secondary,
  .back-to-top,
  .proof { display: none !important; }

  /* O endereço de um link é a informação que o papel perde. Só para
     links absolutos: imprimir "(#top)" ao lado de uma âncora interna
     é ruído. */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 10pt;
    word-break: break-all;
  }

  /* Um título órfão no fim da página, ou uma cláusula partida a meio,
     é o defeito clássico de um documento legal impresso. */
  h1, h2, h3 { break-after: avoid-page; page-break-after: avoid; }
  p, li, blockquote { break-inside: avoid-page; page-break-inside: avoid; orphans: 3; widows: 3; }

  /* A medida de leitura de tela não serve numa folha. */
  .container { max-width: none !important; padding: 0 !important; }

  @page { margin: 2cm; }
}
