/* OVEYON Mail — PORTAL DO CLIENTE. Folha PRÓPRIA, self-hosted, sem recurso externo
   (nenhuma fonte/CDN — CSP-ready). Superfície distinta do painel do operador
   (admin.css): esta é a porta de entrada do cliente, com visual de produto.

   ── RODADA 2 DA DÍVIDA DE CSS (05/08/2026) ─────────────────────────────────
   1. VOCABULÁRIO ÚNICO. As duas folhas passam a definir os MESMOS nomes de
      token. Antes, regras copiadas de lá para cá referenciavam nomes que aqui
      não existiam (--surface, --ink, --line…) e caíam num fallback CRAVADO:
      `var(--surface, #fff)`. Isso não é uma rede de segurança — é um bug
      silencioso: o fallback é fixo, então o modal de mensagem do cliente
      renderizava BRANCO no tema escuro. Agora os nomes resolvem e o componente
      acompanha o tema.
   2. VALORES DE SAÚDE unificados pela MEDIDA de contraste, não por qual folha é
      dona. O caso que motivou: --ok #1e7e46 sobre --ok-bg dava 4.49:1 e REPROVAVA
      AA em TODO sinal de sucesso do cliente (flash, toast, badge, checklist).
      Com o valor do painel são 5.12:1.
   3. TEMA MANUAL. O portal ganhou os seletores [data-theme] que só o painel
      tinha: o cliente deixa de ficar preso à preferência do sistema.
   O que NÃO foi unificado, de propósito: --accent (a ação de produto, não o
   navy do operador) e --radius (12px de produto vs 8px de painel denso).
   São divergências DECLARADAS, com motivo — não dívida.

   ── RODADA 6 — A PALETA DO REDESENHO (22/08/2026) ──────────────────────────
   O portal adota a paleta do mockup: trocam VALORES, nunca NOMES. A folha já
   falava só por token, então a mudança inteira cabe em quatro blocos.
   O número que sustenta a decisão: as duas folhas definem 72 tokens em comum e
   65 resolvem no MESMO valor. As SETE que sobram são IDENTIDADE (--accent,
   --accent-hover, --focus) e DENSIDADE (--btn-pad-y/-x, --btn-fs, --radius) —
   e são as sete que continuam divergindo, de propósito, cada uma marcada
   «DIVERGENTE» na sua linha.
   Os outros 65 são SIGNIFICADO — tinta, superfície, os quatro estados de saúde,
   a borda de campo — e mudaram IGUAIS no admin.css, no mesmo commit. Um --ok
   que é um verde na tela do cliente e outro na do operador é a divergência
   SILENCIOSA: ninguém decidiu, ninguém revisa.
   AGORA COM VETO: test/portalContraste.test.js lê os tokens das DUAS folhas,
   mede os pares contra a superfície declarada de cada um (nunca sobre branco) e
   reprova divergência nova. Divergência declarada passa; divergência inventada,
   não. Documento não veta — teste veta. */

/* ── A FONTE DO REDESENHO (22/08/2026) — AUTO-HOSPEDADA ─────────────────────
   O mockup sempre pediu Figtree (corpo) e JetBrains Mono (dado), na primeira
   linha do <head>:

     <link href="…css2?family=Figtree:wght@400;500;600;700
                        &family=JetBrains+Mono:wght@400;500&display=swap">

   O port nunca as trouxe. Não foi decisão: nenhum documento do redesenho cita
   família, e a folha ficou em system-ui/ui-monospace por OMISSÃO. Isso não é
   detalhe de acabamento — é a montante de toda medida de fidelidade: cada
   largura de coluna e cada quebra de linha ajustada na fonte do sistema muda
   de novo quando a fonte certa entra. Por isso ela entra PRIMEIRO.

   PESOS: os do <link> do mockup e mais nenhum — medido no HTML, `font-weight`
   aparece em 500 (17×), 600 (367×) e 700 (161×), mais o 400 do corpo. Os dois
   arquivos são VARIÁVEIS (fvar wght: Figtree 300–900, JetBrains Mono 400–800),
   então os quatro pesos saem de UM arquivo por subset — não há economia em
   cortar peso, e o `font-weight: 400 700` declara a faixa que o produto usa.

   SUBSETS: latin + latin-ext, e a razão do segundo NÃO é o português —
   medido: os acentos de pt-BR («ç» U+00E7, «ã» U+00E3, «é» U+00E9) moram todos
   em U+0000-00FF, ou seja, no subset `latin`, e varrendo as 15 telas
   renderizadas não há um único caractere na faixa do `latin-ext`. Hoje o
   navegador baixa só os dois arquivos `latin`: 51,6 KB.
   O `latin-ext` entra como SEGURO, e o seguro tem sinistro nomeado: assunto de
   e-mail, nome de remetente e domínio de cliente são texto de terceiro. Um
   «ș» romeno, um «ł» polonês ou um «ā» num nome faria AQUELA letra cair na
   fonte de sistema no meio da palavra — o defeito tipográfico mais visível que
   existe, e o que menos aparece em teste. 21,9 KB que só descem quando a letra
   aparece (é para isso que serve o `unicode-range`).
   Os outros quatro subsets que o Google serve (cyrillic, cyrillic-ext, greek,
   vietnamese) não vêm. Total em disco: 4 arquivos, 73 KB.
   Fora dos dois subsets ficam, de propósito, as setas e os símbolos que o
   produto usa (→ ✓ ⊘ ↻ ✕ ↗): 172 ocorrências nas 15 telas, e nenhuma das duas
   famílias os desenha — eles resolvem na fonte de sistema, como sempre
   resolveram.

   POR QUE AUTO-HOSPEDADA e não o <link> do mockup: o produto não depende de
   terceiro em runtime (é a primeira linha desta folha desde sempre), e a
   privacidade do cliente não passa por fonts.gstatic.com a cada carga.

   URL RELATIVA e não /public/fonts/…: a folha é servida em /public/portal.css
   em produção (resolve para /public/fonts/…) E é copiada para a raiz do
   snapshot de design por scripts/designSnapshot.js (resolve para fonts/… ao
   lado). Uma URL absoluta funcionaria só no primeiro caso, e o snapshot — o
   instrumento com que se MEDE esta fidelidade — mediria com a fonte errada.

   CACHE: o ?v=<mtime> do portalAssetVersion carimba portal.css e portal.js,
   não estes arquivos, e está certo assim. Fonte é asset IMUTÁVEL: se um dia
   uma face precisar mudar, ela entra com NOME NOVO e a folha passa a apontar
   para ele — o que busta o cache com garantia, e não por efeito colateral do
   mtime de outro arquivo.

   LICENÇA: as duas são SIL OFL 1.1. Os textos vieram junto, em
   public/fonts/OFL-Figtree.txt e public/fonts/OFL-JetBrainsMono.txt. */
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/figtree-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/figtree-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/jetbrainsmono-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/jetbrainsmono-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  color-scheme: light dark;
  /* Marca OVEYON Mail (mesma família do painel, tom mais quente/produto). */
  --brand: #16405f;
  --brand-2: #1f6699;
  /* ── DIVERGÊNCIA DECLARADA 1 de 2: IDENTIDADE ─────────────────────────────
     Estas TRÊS linhas (--accent, --accent-hover, --focus) e as quatro de
     DENSIDADE lá embaixo (--btn-pad-y/-x, --btn-fs, --radius) são as ÚNICAS
     sete que divergem do admin.css. Medido, não estimado: as duas folhas
     definem 72 nomes em comum e 65 resolvem no MESMO valor.
     A regra que separa as duas categorias, e o motivo de a lista ser curta:
       • SIGNIFICADO — tinta, superfície, ok/warn/bad/info, borda de campo —
         é IGUAL nas duas folhas. Um --ok que é um verde na tela do cliente e
         outro na do operador é divergência SILENCIOSA: ninguém decidiu, ninguém
         revisa, e o dia em que uma das duas sair do AA a outra continua verde.
       • IDENTIDADE e DENSIDADE são divergências que alguém DECIDIU. O portal é
         a porta de entrada do cliente (azul de produto, folgado); o painel é
         sala de controle (navy do operador, denso).
     RODADA 6 (22/08/2026) — o valor mudou, a divergência ficou. O azul do
     portal passa a ser o do redesenho: #2563eb, medido 5,17:1 com --accent-ink
     branco (era #2f6df0). O painel segue em #1f6699. Continuam duas identidades
     — agora a do cliente é a que o mockup desenhou. */
  --accent: #2563eb;          /* ação primária — DIVERGENTE (painel: #1f6699) */
  --accent-ink: #ffffff;
  --accent-hover: #1d4fd7;    /* DIVERGENTE (painel: #16405f) */
  /* Anel de foco em cor CHEIA (antes era color-mix a 55%, que media 2.93:1
     sobre --bg e ficava abaixo do 3:1 exigido de indicador de foco).
     Herda a divergência de --accent por construção: 4,74:1 sobre --bg. */
  --focus: var(--accent);     /* DIVERGENTE por herança de --accent */

  /* ── SUPERFÍCIE — RODADA 6: valores do redesenho, nomes intactos ───────────
     A folha inteira já fala por token, então trocar a paleta é trocar SÓ estas
     linhas. As duas bordas movem em DIREÇÕES OPOSTAS de propósito: --border
     afrouxa (1,29 → 1,21:1 sobre --panel) e --border-strong aperta (1,53 →
     1,65:1). Abrir o vão entre elas é o ponto — se toda linha grita, nenhuma
     informa. Nenhuma das duas está sob 1.4.11: divisor de cartão é enfeite, e a
     norma escreve a exceção. O CONTROLE tem token próprio (--field-border, logo
     abaixo) exatamente para não depender disto. */
  --bg: #f4f5f8;
  /* O fundo do mockup é chapado; aqui ele continua lavado por dois radiais.
     Medido para não virar buraco: --muted dá 4,56:1 sobre --bg-grad-a e 4,73:1
     sobre --bg-grad-b — os dois cantos ficam ACIMA do 4,5 do fundo liso. */
  --bg-grad-a: #e7eef7;
  --bg-grad-b: #f4f1ec;
  --panel: #ffffff;
  --panel-2: #fbfcfd;
  --border: #e7e9ee;
  --border-strong: #c3cad4;
  /* ── A BORDA DO CAMPO ──────────────────────────────────────────────────────
     Token novo, gêmeo do de admin.css, e pelo mesmo motivo medido: a borda de
     input/select era `--border-strong`, que sobre --panel dá **1,53:1**. WCAG
     1.4.11 pede 3:1 para elemento não-textual, e aqui incide com força total
     porque a borda é a ÚNICA fronteira do campo — o preenchimento do campo é
     --panel e o cartão em volta também.
     O portal estava um degrau MELHOR que o painel (1,53 contra 1,29) porque já
     usava a linha «forte» em vez da comum. Um degrau numa escala que precisava
     de dois: mais perto do certo é indistinguível de errado quando o mínimo é
     um número.
     #79879a mede 3,66 · 3,49 · 3,38:1 sobre --panel, --panel-2 e --bg. Mesmo
     valor nas duas folhas DE PROPÓSITO: o campo é o componente que o cliente e
     o operador usam para a mesma coisa, e divergir aqui seria a «divergência
     silenciosa» que o dono apontou na linha de base. */
  /* RODADA 6 — o mockup desenha a borda de campo em #dde1e7, que sobre --panel
     mede 1,4:1. É o MESMO defeito que a rodada 5 pagou aqui, e ele não volta:
     este token não acompanha o redesenho. 3,66 · 3,56 · 3,35:1 sobre --panel,
     --panel-2 e --bg — folga nas três, com a paleta nova. */
  --field-border: #79879a;
  --text: #0f172a;
  /* RODADA 6 — o mockup escreve a tinta secundária em CINCO valores (#52607a,
     #5f6c80, #667283, #4e617f, #5b6979) para um papel só. Vence #5f6c80, e por
     medida: #667283 sobre --panel-2 dá 4,48:1 e REPROVA — é a substituição que
     o próprio HANDOFF-deltas §5 registra. #5f6c80 dá 5,33 · 4,88 · 5,18:1 sobre
     --panel, --bg e --panel-2, e ainda 4,7:1 no pior tint (--neutral-bg). */
  --muted: #5f6c80;
  /* Tinta TERCIÁRIA: só objeto gráfico (ícone). Mede 3.11:1 sobre --panel —
     cruza o 3:1 de objeto gráfico e NÃO chega aos 4.5:1 de corpo. Texto pequeno
     que usava este token migrou para --muted na rodada 2.
     RODADA 6 — NÃO adota o mockup. O gêmeo de lá (#8492a6) é o valor que o
     HANDOFF-deltas §5 reprovou como texto (3,10:1), e o nosso é o mesmo tom
     medido: trocar não compraria nada e perderia a medição. Teto honesto: os
     3,11:1 valem sobre --panel; sobre --bg são 2,85:1. Ícone sobre cartão, não
     sobre a página. */
  --muted-2: #8494a5;

  /* ── SAÚDE — RODADA 6: os quatro estados adotam a paleta do redesenho ───────
     Valor NOVO, nome e papel intactos, e cada par medido contra o tint em que
     é usado (nunca sobre branco — foi assim que o próprio handoff errou uma vez
     e registrou o erro). ok 4,87 · warn 4,93 · bad 4,85 · info 6,03:1.
     Estes doze valores são de SIGNIFICADO: mudam IGUAIS no admin.css, no mesmo
     commit. Ver a divergência declarada lá em cima para a regra. */
  --ok: #0b7a47;      --ok-bg: #e7f7ef;   --ok-bd: #bfe7d2;
  --warn: #8a6304;    --warn-bg: #fdf3de;  --warn-bd: #f0e2b4;
  --bad: #b4442f;     --bad-bg: #fdedea;   --bad-bd: #f0cfc8;
  /* INFO É DA FAMÍLIA DA MARCA, não um azul solto. Até 28/08 era #1d4fd7 sobre
     #eef4ff — o «azul do nada» que o dono viu em /app/regras, fora de uma
     paleta que ele lê como terrosa. O papel (informar sem alarmar) fica; o
     tom passa a ser o navy da marca sobre cinza-azulado quase neutro. */
  --info: #1f4b70;    --info-bg: #eef2f6;  --info-bd: #cfdae6;
  /* --neutral-line NÃO acompanha: é a borda do selo cinza, e o mockup não tem
     um valor para esse papel — só para --border e --border-strong. #d8dee6
     continua entre os dois (1,21 · 1,35 · 1,65:1), que é a ordem que ele
     precisa manter. Inventar aqui seria canonizar acidente. */
  --neutral: #5f6c80; --neutral-bg: #eef1f6; --neutral-line: #d8dee6;
  /* ── ÂMBAR SÓLIDO — a única lacuna de cor REAL do redesenho ────────────────
     Das três «lacunas» que o HANDOFF-deltas §4 alegou, duas não existem: --sh-lg
     (elevação de peça flutuante) está definido logo abaixo, e --info/-bg/-bd
     estão completos nos quatro blocos. Sobrou uma.
     «Retida» não é aviso passivo: é um estado ATIVO que pede decisão humana, e
     por isso o mockup a preenche a ~55% de saturação em vez de tingir a 8–10%
     como os outros. Preenchimento sólido exige TINTA PRÓPRIA, e é aqui que
     reusar o token vizinho quebraria em silêncio:
        --on-solid (branco) sobre #f7e2a8 …… 1,28:1  (reprova por 3,5×)
        --warn     (#8a6304) sobre #f7e2a8 …… 4,24:1  (reprova)
        --on-warn  (#6b5410) sobre #f7e2a8 …… 5,65:1  (passa)
     --on-warn é da família de --on-solid — tinta sobre PREENCHIMENTO. Nunca use
     um pelo outro. E o estado entra pelo SSOT ([data-health="held"], lá
     embaixo), não por componente: selo, chip, faixa, stat e tool-box ganham
     «retida» sem uma linha a mais. */
  --warn-solid: #f7e2a8;
  --on-warn: #6b5410;
  --sw-knob: #fff; /* knob do switch: objeto branco nos DOIS temas (peça física) */
  /* Aliases `-line` do SSOT de saúde (replicados do admin.css). A borda de cada
     estado reusa a cor de borda já definida acima — zero cor crua nova. */
  --ok-line: var(--ok-bd); --warn-line: var(--warn-bd); --bad-line: var(--bad-bd); --info-line: var(--info-bd);
  --faint: var(--muted-2);

  /* ── APELIDOS DO PAINEL ────────────────────────────────────────────────────
     Os nomes que o admin.css usa, apontando para os tokens desta folha. Existem
     para matar os fallbacks cravados (`var(--line, #e6e9ef)`) que ignoravam o
     tema escuro. Regra copiada de uma folha para a outra agora RESOLVE. */
  --surface: var(--panel);
  --surface-2: var(--bg);
  --surface-3: var(--panel-2);
  --ink: var(--text);
  --line: var(--border);
  --line-2: var(--border-strong);
  /* RODADA 6 — o corpo de texto do redesenho. 8,97:1 sobre --panel e nunca
     abaixo de 7,89:1 sobre qualquer um dos quatro tints de saúde. */
  --tint-ink: #3c4a60;      /* texto secundário sobre tint */

/* Tinta sobre preenchimento sólido/gradiente (marca). Mesmo nome e mesmo
     papel do admin.css — NÃO é --accent-ink (que é escuro no tema escuro). */
  --on-solid: #ffffff;

  /* ── ENGAJAMENTO — o token que FALTAVA nesta folha ─────────────────────────
     `statusPresentation.js:62` emite a classe `badge-engaged` para «Lido» e
     «Clicado», e o handler do portal a entrega para a view. Mas a regra e o
     token só existiam no admin.css: no PORTAL a pílula saía **sem cor nenhuma**
     (o `.badge` base só tem forma), enquanto todo status vizinho tinha.
     Achado em 22/08 pelo mapa de classes do redesenho; independente dele.
     Mesmo valor do admin.css de propósito — é a convenção das classes
     compartilhadas, e divergir aqui seria a divergência silenciosa. */
  --engaged-bg: #0d7d78;


  /* ── BOTÃO — a FORMA vira token; o PAPEL continua sendo de cada superfície ──
     Ver o comentário longo em admin.css. O que é IGUAL nos dois arquivos:
     --btn-*-sm, --btn-radius e --btn-min-h. É isso que faz `.btn-sm` ser a
     mesma coisa na tela do cliente e na do operador. */
  /* ── DIVERGÊNCIA DECLARADA 2 de 2: DENSIDADE ──────────────────────────────
     Quatro linhas (--btn-pad-y, --btn-pad-x, --btn-fs e --radius, abaixo). O
     portal é folgado porque a tela do cliente empilha três ações de mesmo peso;
     o painel é denso porque o operador clica dezenas por dia. Isso é decisão de
     produto, não dívida — e é por isso que estas quatro carregam a marca no
     comentário: quem for «unificar» tem de ler o motivo antes. */
  --btn-pad-y: 12px;   --btn-pad-x: 16px;      /* DIVERGENTE (painel: 8px/14px) */
  --btn-pad-y-sm: 6px; --btn-pad-x-sm: 12px;   /* IGUAL no admin.css */
  --btn-radius: 6px;                            /* IGUAL no admin.css */
  --btn-fs: var(--fs-lg);                       /* 15px — DIVERGENTE (painel: 14px) */
  --btn-fs-sm: var(--fs-sm);                    /* 13px — IGUAL no admin.css */
  --btn-gap: 10px;
  --btn-min-h: 44px;                            /* alvo de toque do primário */

  --radius: 12px;           /* cartão/painel — DIVERGENTE (painel: 8px) */
  --radius-sm: 8px;         /* bloco interno, controle */
  /* --radius-lg: 14px foi PEDIDO pelo redesenho e RECUSADO. Não é gosto: o
     scanner de test/helpers/cssRampa.js aceita literalmente
     /^var\(--radius(-sm)?\)$/ — um token NOVO reprova junto com o literal. Os
     70 border-radius:14px do mockup cedem para var(--radius). */
  /* --shadow/--shadow-sm VARIAM com o tema (ver os blocos de tema): a elevação
     do portal é azulada no claro e preta no escuro. Ficam onde estão. */
  --shadow: 0 1px 2px rgba(16,32,52,.06), 0 8px 28px rgba(16,32,52,.10);
  --shadow-sm: 0 1px 2px rgba(16,32,52,.06), 0 2px 8px rgba(16,32,52,.06);
  /* ── RODADA 3 — o vocabulário que as DUAS folhas passam a falar ────────────
     `--sh-lg` (peça FLUTUANTE) e `--scrim` (o véu atrás dela) existem aqui com
     os MESMOS valores do admin.css. Não é duplicação por descuido: o modal é o
     mesmo componente nas duas superfícies e o valor já era o mesmo, escrito à
     mão dos dois lados. Nomeado, uma regra copiada de lá resolve aqui.
     INVARIANTES de tema (só no :root) — o véu de um modal não clareia no tema
     claro, e a sombra de uma peça flutuante sobre o véu também não.
     Os outros degraus da rampa do painel (--sh-sm/--sh-md/--sh-pop) NÃO vêm
     junto de propósito: aqui a elevação de cartão é --shadow/--shadow-sm, que
     varia com o tema. Duas rampas de sombra no mesmo arquivo seriam duas
     respostas para a mesma pergunta — e nenhuma delas teria consumidor. */
  --sh-lg: 0 20px 60px rgba(0,0,0,.25);
  --scrim: rgba(0,0,0,.5);
  /* RODADA 3 — `"SF Mono"` entra na lista. Ele já estava escrito à mão em CINCO
     atalhos `font:` das duas folhas, que assim mantinham uma SEGUNDA pilha
     mono quase igual a esta. Acrescentar o apelido aqui torna --mono um
     superconjunto e deixa aqueles cinco lugares resolverem pelo token sem
     perder nenhuma face. Inerte para quem já usava --mono: no macOS
     `ui-monospace` resolve primeiro e os apelidos nunca são consultados. */
  /* RODADA 7 (22/08/2026) — a família do mockup entra NA FRENTE da pilha. O
     token não muda de NOME nem perde uma face: as pilhas antigas viram, na
     íntegra, o fallback de quem ainda não baixou o woff2 (font-display:swap)
     e do navegador que não suporta woff2. Ver o bloco @font-face acima. */
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: Figtree, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* ── ESCALA TIPOGRÁFICA — IDÊNTICA à do admin.css ──────────────────────────
     14 degraus, conjunto fechado. Ver o comentário longo no admin.css. */
  --fs-3xs: 10px;  --fs-2xs: 11px;  --fs-xs: 12px;  --fs-sm: 13px;
  --fs-md: 14px;  --fs-lg: 15px;  --fs-xl: 16px;  --fs-2xl: 18px;
  --fs-3xl: 20px;  --fs-4xl: 22px;  --fs-5xl: 24px;  --fs-6xl: 26px;
  --fs-7xl: 32px;  --fs-8xl: 40px;

  /* ── ESCALA DE ESPAÇO ──────────────────────────────────────────────────────
     0 · 1 (fio) · 2 4 6 8 10 12 14 16 18 20 22 24 26 28 · 32 40 48 64 */
}

/* ── MODO ESCURO ────────────────────────────────────────────────────────────
   TRÊS blocos, exatamente como o admin.css, e nesta ordem:
     @media                  → default pela preferência do SISTEMA
     :root[data-theme=dark]  → o toggle força escuro
     :root[data-theme=light] → o toggle força claro, REAFIRMANDO os valores base
   O terceiro é o que faz o botão vencer nos DOIS sentidos. Sem ele, um cliente
   com o sistema no escuro que pedisse claro não veria nada mudar: a media query
   continuaria valendo, porque tirar o atributo não desliga a preferência do SO.
   Só os valores mudam; os apelidos (--surface, --ink, --line…) herdam via var(). */
@media (prefers-color-scheme: dark) {
  :root {
    --brand: #cfe0f0;
    --brand-2: #7fb2e6;
    --accent: #5b8df5;
    --accent-ink: #0b1420;
    --accent-hover: #7aa3f7;
    --bg: #0d141d;
    --bg-grad-a: #101a26;
    --bg-grad-b: #16130f;
    --panel: #151f2b;
    --panel-2: #1b2735;
    --border: #263548;
    --border-strong: #34465c;
    --field-border: #6b7f95;   /* 4,03:1 sobre --panel · 3,67:1 sobre --panel-2 */
    --text: #e7eef6;
    --muted: #9fb0c2;
    --muted-2: #74869a;
    --ok: #4cc07d;   --ok-bg: #12271b;  --ok-bd: #234c33;
    --warn: #e6b855; --warn-bg: #2a2210; --warn-bd: #4d3f18;
    --bad: #f08a92;  --bad-bg: #2a1315;  --bad-bd: #522328;
    --info: #8fb6ee; --info-bg: #131f31; --info-bd: #26364f;
    --neutral: #9fb0c2; --neutral-bg: #1b2735; --neutral-line: #33465c;
    --ok-line: var(--ok-bd); --warn-line: var(--warn-bd); --bad-line: var(--bad-bd); --info-line: var(--info-bd);
    /* ÂMBAR SÓLIDO no escuro: o par INVERTE. No claro é tinta escura sobre
       preenchimento claro; aqui é o fundo da página sobre o âmbar de texto —
       10,00:1. Reusar #6b5410 aqui daria tinta escura sobre tinta escura. */
    --warn-solid: var(--warn); --on-warn: var(--bg);
    --tint-ink: #c3d3e6;
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.45);
    --shadow-sm: 0 1px 2px rgba(0,0,0,.35), 0 2px 10px rgba(0,0,0,.35);
  }
}
/* Toggle força escuro (vence a preferência do SO por especificidade). */
:root[data-theme="dark"] {
  --brand: #cfe0f0;
  --brand-2: #7fb2e6;
  --accent: #5b8df5;
  --accent-ink: #0b1420;
  --accent-hover: #7aa3f7;
  --bg: #0d141d;
  --bg-grad-a: #101a26;
  --bg-grad-b: #16130f;
  --panel: #151f2b;
  --panel-2: #1b2735;
  --border: #263548;
  --border-strong: #34465c;
  --field-border: #6b7f95;
  --text: #e7eef6;
  --muted: #9fb0c2;
  --muted-2: #74869a;
  --ok: #4cc07d;   --ok-bg: #12271b;  --ok-bd: #234c33;
  --warn: #e6b855; --warn-bg: #2a2210; --warn-bd: #4d3f18;
  --bad: #f08a92;  --bad-bg: #2a1315;  --bad-bd: #522328;
  --info: #8fb6ee; --info-bg: #131f31; --info-bd: #26364f;
  --neutral: #9fb0c2; --neutral-bg: #1b2735; --neutral-line: #33465c;
  --ok-line: var(--ok-bd); --warn-line: var(--warn-bd); --bad-line: var(--bad-bd); --info-line: var(--info-bd);
  --warn-solid: var(--warn); --on-warn: var(--bg);   /* par INVERTIDO — ver a media acima */
  --tint-ink: #c3d3e6;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.45);
  --shadow-sm: 0 1px 2px rgba(0,0,0,.35), 0 2px 10px rgba(0,0,0,.35);
}
/* Toggle força claro (re-afirma os valores base, vencendo a media escura). */
:root[data-theme="light"] {
  --brand: #16405f;
  --brand-2: #1f6699;
  --accent: #2563eb;
  --accent-ink: #ffffff;
  --accent-hover: #1d4fd7;
  --bg: #f4f5f8;
  --bg-grad-a: #e7eef7;
  --bg-grad-b: #f4f1ec;
  --panel: #ffffff;
  --panel-2: #fbfcfd;
  --border: #e7e9ee;
  --border-strong: #c3cad4;
  --field-border: #79879a;
  --text: #0f172a;
  --muted: #5f6c80;
  --muted-2: #8494a5;
  --ok: #0b7a47;      --ok-bg: #e7f7ef;   --ok-bd: #bfe7d2;
  --warn: #8a6304;    --warn-bg: #fdf3de;  --warn-bd: #f0e2b4;
  --bad: #b4442f;     --bad-bg: #fdedea;   --bad-bd: #f0cfc8;
  --info: #1f4b70;    --info-bg: #eef2f6;  --info-bd: #cfdae6;
  --neutral: #5f6c80; --neutral-bg: #eef1f6; --neutral-line: #d8dee6;
  --ok-line: var(--ok-bd); --warn-line: var(--warn-bd); --bad-line: var(--bad-bd); --info-line: var(--info-bd);
  --warn-solid: #f7e2a8; --on-warn: #6b5410;
  --tint-ink: #3c4a60;
  --shadow: 0 1px 2px rgba(16,32,52,.06), 0 8px 28px rgba(16,32,52,.10);
  --shadow-sm: 0 1px 2px rgba(16,32,52,.06), 0 2px 8px rgba(16,32,52,.06);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  background-image: radial-gradient(1200px 600px at 100% -10%, var(--bg-grad-b), transparent 60%),
    radial-gradient(1000px 500px at -10% 0%, var(--bg-grad-a), transparent 55%);
  background-attachment: fixed;
  color: var(--text);
  font: var(--fs-lg)/1.55 var(--sans);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}
/* Link segue a AÇÃO, não a marca. O mockup usa #2563EB em link — que é o
   próprio --accent — e manter o navy da marca deixaria o link de uma cor e o
   botão de outra na mesma frase. Mede 5,17:1 sobre --panel (o navy media 6,14;
   os dois passam, e o desenho escolheu). */
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3 { line-height: 1.25; }

/* Foco visível e consistente (acessibilidade).
   RODADA 2 — o anel era `color-mix(--accent 55%, transparent)`: medido, dava
   2.93:1 contra --bg e ficava ABAIXO do 3:1 que a WCAG pede de indicador de
   foco. Em cor cheia (--focus) são 4.09:1 sobre --bg e 4.60:1 sobre --panel. */
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ============================ AUTH (login/signup) ======================== */
.auth {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.auth-card {
  width: 100%;
  max-width: 400px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px 28px 26px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 22px;
}
.brand .mark {
  width: 34px; height: 34px;
  flex: 0 0 34px;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--brand-2), var(--accent));
  display: grid; place-items: center;
  color: var(--on-solid); font-weight: 800; font-size: var(--fs-2xl);
  box-shadow: var(--shadow-sm);
}
.brand .name { font-size: var(--fs-2xl); font-weight: 700; letter-spacing: .2px; color: var(--text); }
/* RODADA 2 — era var(--muted-2) (3.11:1, REPROVA AA em texto). --muted mede
   5.46:1 sobre --panel. É o subtítulo da marca na primeira tela do cliente. */
.brand .name small { display: block; font-size: var(--fs-2xs); font-weight: 500; letter-spacing: 1.5px; color: var(--muted); text-transform: uppercase; }

.auth-card h1 { font-size: var(--fs-4xl); margin: 0 0 4px; }
.auth-lead { color: var(--muted); margin: 0 0 22px; font-size: var(--fs-md); }

/* Botões */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--btn-gap);
  width: 100%;
  padding: var(--btn-pad-y) var(--btn-pad-x);
  border-radius: var(--btn-radius);
  border: 1px solid var(--border-strong);
  background: var(--panel);
  color: var(--text);
  font: 600 var(--btn-fs)/1 var(--sans);
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .05s;
}
.btn:hover { background: var(--panel-2); text-decoration: none; }
.btn:active { transform: translateY(1px); }
/* OCUPADO (aria-busy) mantém `pointer-events: none`, e isso é uma TRAVA, não
   estilo: o botão está no ar esperando resposta e o segundo clique mandaria a
   segunda requisição. DESABILITADO perdeu a trava de propósito — `disabled` já
   impede a ativação nativamente, e `pointer-events: none` só tirava do usuário
   o cursor de recusa, que é o único aviso que chega ANTES do clique. Era
   proteção duplicada num caso e mordaça no outro. */
.btn[aria-busy="true"] { opacity: .6; cursor: progress; pointer-events: none; }
.btn .ico { width: 18px; height: 18px; flex: 0 0 18px; display: inline-block; }
/* Botão de copiar por ÍCONE (pedido do dono, 24/08: «Copiar» → dois retângulos
   sobrepostos, #ic-copy do sprite). O feedback de cópia é por CLASSE (.copied,
   aplicada por wireCopy em portal.js) — trocar o texto apagaria o <svg>. Mesmo
   padrão do .dom-copy/.ip-copy do painel (admin.css). */
.btn .ic { width: 15px; height: 15px; flex: none; }
.btn.copied { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: transparent;
}
.btn-primary:hover { background: var(--accent-hover); }
/* Botão pequeno — regra IDÊNTICA à do admin.css, palavra por palavra. Ver lá o
   porquê da escolha (o acabamento daqui venceu; a escrita virou token). */
.btn-sm {
  padding: var(--btn-pad-y-sm) var(--btn-pad-x-sm);
  font-size: var(--btn-fs-sm);
  line-height: 1.2;
  border-radius: var(--btn-radius);
  width: auto;
}
/* ALVO DE TOQUE (norma de UX §8) — ver admin.css. */
@media (pointer: coarse) {
  .btn-sm { min-height: 44px; }
}
.btn-ghost { background: transparent; }

/* ── ESTADO DESABILITADO — reposição de affordance ──────────────────────────
   Bloco GÊMEO do de admin.css, palavra por palavra no raciocínio; ver lá a
   explicação longa. O resumo: as regras base declaram `background` e `color`
   em `.btn` e nos campos, isso cancela o desenho que o agente de usuário faz
   de graça, e a rodada 4 mediu 893 de 1117 controles desabilitados sem UMA
   propriedade visível diferente do ativo.
   O PORTAL ESTAVA PIOR QUE O PAINEL, ao contrário do resto desta rodada: aqui
   havia UMA regra (`.btn[disabled] { opacity: .6 }`) e ela cobria só a classe
   `.btn`. O `<button class="code-copy">` de /docs — 82 ocorrências numa página
   só, a referência de API que é a primeira coisa que um cliente dev-first
   abre — não é `.btn`, e não tinha estado nenhum. Campo desabilitado no portal
   também não tinha.
   O critério NÃO é contraste: WCAG isenta componente inativo. É «dá para ver
   que está desabilitado sem clicar», e quem responde é `sonda-estado.js`. */
:root:root :is(button, .btn, [role="button"]):is(:disabled, [disabled]) {
  filter: grayscale(1) opacity(.5);
  box-shadow: none;
  cursor: not-allowed;
}
:root:root :is(input, select, textarea):is(:disabled, [disabled]) {
  background: var(--neutral-bg);
  color: var(--muted);
  border-color: var(--border);
  cursor: not-allowed;
}
:root:root label:has(+ :is(input, select, textarea):disabled) { color: var(--muted); }
/* ── VERMELHO É PARA O IRREVERSÍVEL — a regra do dono, 07/08/2026 ───────────
   Bloco GÊMEO do de admin.css; o raciocínio longo está lá e vale palavra por
   palavra aqui. O resumo: a pergunta que decide a cor é «depois de clicar, dá
   para voltar ao estado anterior SEM PERDA?». Volta → `.btn` puro, com a
   confirmação intacta. Não volta → `.btn-danger`.

   O QUE MUDA ENTRE AS DUAS SUPERFÍCIES é só quanto o vermelho pesa, e isso é
   deliberado: no painel a variante é PREENCHIMENTO sólido (`button.danger`
   pinta `--bad` de fundo) e aqui é CONTORNO (tinta e borda vermelhas sobre o
   painel). O operador vê o vermelho dele dezenas de vezes por dia; o cliente vê
   o dele nas duas ou três vezes na vida em que apaga um domínio. Mesma regra,
   pesos diferentes — que é a mesma razão de `.btn` ser contornado aqui e
   preenchido lá.

   POR ISSO O PORTAL QUASE NÃO MUDOU NESTE CICLO, e é um resultado, não uma
   omissão: das doze ocorrências, ONZE são remoção de linha (passkey, endereço,
   webhook, chat, encaminhamento, host de tracking, domínio) ou fim de
   credencial (desativar/revogar, que não têm rota de volta). A única que caiu
   foi o «Apagar conta…» que apenas REVELA o formulário — o submit continua
   vermelho. Um cliente que vê vermelho no portal está mesmo prestes a perder
   alguma coisa.

   ONDE ISTO É VETADO: test/vermelhoIrreversivel.test.js, o mesmo das duas
   superfícies. */
.btn-danger { color: var(--bad); border-color: var(--bad-bd); background: transparent; }
.btn-danger:hover { background: var(--bad-bg); }
.w-auto { width: auto; }

.stack > * + * { margin-top: 10px; }

.divider {
  display: flex; align-items: center; gap: 12px;
  color: var(--muted); font-size: var(--fs-xs);   /* rodada 2: era --muted-2, 3.11:1 */
  margin: 18px 0;
}
.divider::before, .divider::after {
  content: ""; height: 1px; flex: 1; background: var(--border);
}

.auth-foot {
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--fs-xs);
  text-align: center;
}

/* Mensagens/flash */
/* ── MOMENTO DE ERRO — o mesmo objeto nas duas superfícies ──────────────────
   Até a rodada 2, cliente e operador viam tratamentos DIFERENTES do mesmo
   evento: o portal com borda, 13.5px e fundo #fbe6e8; o painel sem borda,
   14px e fundo #fbe4e6. A unificação NÃO seguiu "de quem é a classe" e sim o
   que se LÊ melhor, medido (WCAG):
     fundo  #fbe6e8 → 5.41:1 contra --bad, contra 5.33:1 do #fbe4e6. Vence o
            mais legível — e é o do portal.
     corpo  14px → o portal lia a mensagem de ERRO meio pixel menor que o resto
            do texto da página. Erro não é nota de rodapé.
     borda  1px --bad-bd → sem ela o bloco é uma mancha de cor que se dissolve
            no fundo; com ela o aviso tem contorno e o olho acha onde começa.
            Mede 1.63:1 contra --panel: é reforço de FORMA, não o portador do
            significado (esse é o par texto/fundo, a 5.41:1) — por isso não é
            uma reprovação de 1.4.11.
   Raio: 8px nos dois. Aqui `var(--radius)` do painel e `var(--radius-sm)` do
   portal resolvem no MESMO 8px; o token é o do cartão de cada superfície. */
.flash {
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: var(--fs-md);
  margin-bottom: 16px;
  border: 1px solid transparent;
}
.flash-error { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-bd); }
.flash-ok    { background: var(--ok-bg);  color: var(--ok);  border-color: var(--ok-bd); }
.flash-info  { background: var(--info-bg); color: var(--info); border-color: var(--info-bd); }
/* Deu certo COM ressalva (ex.: credencial reabilitada que volta retida). Verde
   mandaria seguir em frente; vermelho diria que falhou, e não falhou. */
.flash-warn  { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-bd); }
/* área de status do WebAuthn (aria-live); some quando vazia */
.wa-status { margin-top: 14px; }
.wa-status:empty { display: none; }

/* ============================ APP SHELL ================================== */
.topbar {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
/* RODADA 7 — a MESMA caixa do <main> e das duas faixas de nav: 1280 de teto e
   24 de recuo. Se uma das quatro divergir, a marca deixa de nascer na mesma
   vertical que o h1 da página logo abaixo, e o desalinhamento aparece em toda
   tela do portal ao mesmo tempo. */
.topbar-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 12px 24px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.topbar .brand { margin: 0; }
.topbar .spacer { flex: 1; }
.topbar .whoami { color: var(--muted); font-size: var(--fs-sm); text-align: right; }
.topbar .whoami strong { color: var(--text); font-weight: 600; }

/* ── ALTERNADOR DE TEMA (rodada 2) ──────────────────────────────────────────
   Até aqui o OPERADOR escolhia claro/escuro e o CLIENTE ficava preso à
   preferência do sistema operacional — este arquivo não tinha um único seletor
   [data-theme]. Quem lê e-mail num escritório claro com o notebook no escuro
   (ou o contrário) não tinha saída.
   O mecanismo é o mesmo do painel, e a peça que o faz funcionar nos DOIS
   sentidos é o bloco `:root[data-theme="light"]` lá em cima: sem ele, tirar o
   sistema do escuro seria impossível, porque a media query continuaria valendo.
   O atributo vence a media por ESPECIFICIDADE (0,1,0 contra 0,0,0), não por
   ordem — então funciona esteja onde estiver no arquivo.

   DIVERGÊNCIA DECLARADA: `.theme-toggle` não é igual à do admin.css e não deve
   ser. Lá o controle mora sobre a barra NAVY (cores fixas nos dois temas, tinta
   --nav-ink-2); aqui mora sobre a topbar clara, que acompanha o tema. Mesmo
   nome, mesma função, chrome diferente porque a superfície é diferente. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; min-height: 36px; padding: 0; flex: none;
  background: transparent; border: 1px solid var(--border);
  border-radius: var(--btn-radius); color: var(--muted);
  cursor: pointer; font: inherit;
}
.theme-toggle:hover { background: var(--neutral-bg); color: var(--text); }
.theme-toggle svg { width: 18px; height: 18px; flex: 0 0 auto; }
/* Alvo de toque: no dedo o botão cresce para 44px (norma de UX §8). */
@media (pointer: coarse) { .theme-toggle { width: 44px; height: 44px; min-height: 44px; } }
/* O ícone mostra o tema que o clique VAI trazer: lua no claro, sol no escuro.
   O `:not([data-theme="light"])` dentro da media é o que impede o sol de
   aparecer quando o cliente já forçou o claro por cima de um sistema escuro. */
.tt-sun { display: none; } .tt-moon { display: inline-flex; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tt-sun { display: inline-flex; }
  :root:not([data-theme="light"]) .tt-moon { display: none; }
}
:root[data-theme="dark"] .tt-sun { display: inline-flex; }
:root[data-theme="dark"] .tt-moon { display: none; }
:root[data-theme="light"] .tt-sun { display: none; }
:root[data-theme="light"] .tt-moon { display: inline-flex; }
/* ── A LARGURA DO SHELL (rodada 7, 22/08/2026) — 920 → 1280 ─────────────────
   MEDIDO, não arbitrado. O mockup declara `max-width: {{ shellW }}` com
   shellW = 1280px e `padding: 26px 24px 90px` no <main> — 1232px úteis — e usa
   esse mesmo shellW na topbar, na faixa de nível 1 e na de nível 2. O port
   estava em 920 com recuo de 20 (880 úteis): 352px a menos, em TODA tela.

   O QUE OS 352px CUSTAVAM (medido no navegador, 15 telas × 2 larguras):
     · cinco das quinze telas transbordavam o cartão a 1280px;
     · /app/messages tinha tabela de 1324px numa caixa de 838 — 486px atrás de
       rolagem horizontal, e o que ficava fora eram as colunas «Desfecho» e
       «Detalhe», exatamente as que respondem «chegou? por quê não?»;
     · em Credenciais a altura de linha ia de 53px uniformes (mockup) para
       SETE alturas distintas, de 42 a 90px, porque tudo quebrava.

   CORREÇÃO DA NOTA DE 22/08 (MAPA-CLASSES §5.7). A resposta anterior — «o
   shell não sobe, o mockup usa larguras por tela» — leu três telas estreitas
   como prova de shell estreito. Era o contrário: o shell é 1280 em TODAS as
   quinze, e três telas põem um bloco mais estreito DENTRO dele. Ver os
   modificadores logo abaixo.

   O 90px do mockup cede para 64: a rampa de espaço vai 48 → 64 e não tem 90.
   É recuo de respiro no fim da página, não geometria — o degrau serve. */
.wrap { max-width: 1280px; margin: 0 auto; padding: 26px 24px 64px; }

/* ── MODIFICADOR POR TELA — três telas estreitam o CONTEÚDO, não a caixa ────
   No mockup o <main> continua 1232 e a tela envolve o próprio conteúdo num
   `<div style="max-width:900px">` SEM `margin:0 auto`. Medido em m-conta-1280:
   o bloco está em x=24 com 900 de largura — encostado à ESQUERDA, e não
   centrado (centrado daria x=190). Estreitar o <main> em vez do conteúdo
   centraria o bloco e erraria 166px de posição em toda linha da tela.

   Por isso o alvo é `.wrap > *` e não `.wrap`: cada bloco de primeiro nível
   ganha o teto e, sendo bloco, fica à esquerda — a mesma geometria do mockup,
   sem tocar em nenhuma view de conteúdo.

   O gancho é `data-tela`, emitido pelo portal-nav (a SSOT dos dois níveis, e o
   único elemento que precede o <main> em toda página do portal). O `.flash`
   entra no teto junto com o resto: ele fica ACIMA do conteúdo, e um aviso de
   1232 sobre uma coluna de 900 seria a única peça desalinhada da tela. */
.subnav[data-tela="conta"] ~ .wrap > * { max-width: 900px; }
.subnav[data-tela="rede"] ~ .wrap > * { max-width: 1000px; }
.subnav[data-tela="timeline"] ~ .wrap > * { max-width: 1120px; }

/* CELULAR — o mockup aperta o recuo do <main> de 24 para 12 (`mainPad` =
   `16px 12px 80px` a 402px). São 16px de conteúdo a mais num aparelho de
   402px, que é onde 16px pesam. O teto por tela some junto: a 402 não há o que
   estreitar. 80 → 64 pelo mesmo motivo do desktop. */
@media (max-width: 640px) {
  .wrap { padding: 16px 12px 64px; }
  .subnav[data-tela] ~ .wrap > * { max-width: none; }
}

/* ── `.toast` SAIU DAQUI (rodada 2, aprovado pelo dono) ─────────────────────
   Existiam DUAS coisas diferentes com o mesmo nome de classe: no admin.css
   `.toast` é uma notificação FLUTUANTE criada por JS (opacity/transform/
   pointer-events, container #toasts, entra e sai sozinha); aqui era um banner
   ESTÁTICO de sucesso, renderizado no HTML. Mesmo nome, contratos opostos —
   copiar um bloco de uma superfície para a outra dava um componente que não
   funcionava, e nenhum código avisava.
   O banner era, na prática, uma variante de `.flash-ok`: mesmas cores, mesmo
   papel (mensagem de resultado no topo do conteúdo). Passou a usar
   `class="flash flash-ok"` nas 4 ocorrências (portal-app.ejs e
   portal-account.ejs ×3). O `display:flex; gap:8px` não foi levado junto porque
   não fazia nada: o "✓" daquelas mensagens é um CARACTERE do texto, não um
   elemento irmão.
   `test/portalUniformity.test.js` veta `class="toast"` em view do portal — sem
   veto, uma view nova traria de volta um banner sem estilo na tela do cliente,
   que é pior que a colisão de nome que acabamos de resolver. */

.page-head { margin-bottom: 22px; }
/* RODADA 6 — o h1 de tela sobe um degrau, de --fs-5xl (24) para --fs-6xl (26).
   O redesenho pediu 29px; a escala tem 14 degraus e é um conjunto FECHADO, e
   abrir um 15º só para este título obriga a nomeá-lo antes (o scanner reprova
   qualquer tamanho fora de token). 26 é o degrau que existe, e o salto para o
   h2 fica preservado porque o h2 sobe junto: 26 → 18. */
.page-head h1 { font-size: var(--fs-6xl); margin: 0 0 4px; }
.page-head p { color: var(--muted); margin: 0; }
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 20px;
  margin-bottom: 18px;
}
/* ── RODADA 7 — TÍTULO DE CARTÃO: 18 → 14, e o peso sobe para 700 ───────────
   A rodada 6 leu «19px» num h2 do mockup e derivou 18 para TODO título de
   cartão. Medido tela a tela, os 19px são DOIS casos; o título de cartão do
   mockup é 14/700 — e a diferença não é de 4px, é de RELAÇÃO. Com o h1 em 26,
   quatro títulos de 18 na mesma tela quase empatam com ele, e a página deixa
   de ter um começo: o olho não sabe se está lendo o assunto da tela ou o de um
   bloco dentro dela. Em 14/700 o título de cartão volta a ser rótulo de
   seção — mais pesado que o corpo, muito menor que o assunto.
   O peso sobe de 700 herdado do <h2> (que já era 700) e o tamanho desce; o
   par 26/14 dá quase o dobro, que é o salto que o mockup desenha. */
.card h2 { font-size: var(--fs-md); font-weight: 700; margin: 0 0 4px; }
.card .card-sub { color: var(--muted); font-size: var(--fs-sm); margin: 0 0 16px; }

/* status da conta */
.status-banner {
  display: flex; gap: 14px; align-items: flex-start;
  border-radius: var(--radius); padding: 18px 20px; margin-bottom: 22px;
  border: 1px solid transparent;
}
.status-banner .st-ico { font-size: var(--fs-4xl); line-height: 1; }
.status-banner h2 { margin: 0 0 4px; font-size: var(--fs-2xl); }  /* mesmo degrau de .card h2 */
.status-banner p { margin: 0; font-size: var(--fs-md); }
.status-pending { background: var(--warn-bg); border-color: var(--warn-bd); color: var(--text); }
.status-suspended { background: var(--bad-bg); border-color: var(--bad-bd); color: var(--text); }

/* ============================ PASSKEYS ================================== */
.pk-list { list-style: none; margin: 0; padding: 0; }
.pk {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 0; border-top: 1px solid var(--border);
}
.pk:first-child { border-top: none; }
.pk .pk-ico { font-size: var(--fs-4xl); flex: 0 0 auto; }
.pk .pk-main { flex: 1; min-width: 0; }
.pk .pk-name { font-weight: 600; }
.pk .pk-meta { color: var(--muted); font-size: var(--fs-xs); margin-top: 2px; }
.pk .pk-actions { display: flex; gap: 8px; flex: 0 0 auto; }
/* ── BADGE — o mesmo objeto nas duas superfícies (rodada 2) ──────────────────
   Divergiam em duas coisas: o painel não tinha BORDA (o portal tinha) e usava
   12px contra 11px. Agora: borda como no portal — ela é o que separa o badge
   do fundo quando o tint é claro (o par texto/fundo continua sendo quem carrega
   o significado, a 5.1–5.8:1) — e 11px, o acabamento do portal. `white-space:
   nowrap` e `vertical-align: 1px` passam a existir nos dois: uma classe que
   quebra linha num arquivo e não no outro é a divergência silenciosa que o dono
   apontou. */
/* ── RODADA 7 — A FORMA DO SELO: pílula → RETÂNGULO ─────────────────────────
   É a mudança que mais altera a TEXTURA de uma lista, e por isso ela vale mais
   que o 1px que parece. O mockup usa retângulo de raio 5, 18–19px de altura e
   10,5/600 — 45 ocorrências, sempre `border-radius:5px` com `padding` de 2–3px
   por 6–8px. O port usava pílula de raio 999 com 23px de altura: numa tabela
   de dez linhas são dez cápsulas gordas competindo com o texto que elas
   qualificam.

   O QUE DAVA OS 23px, medido: `display:inline-block` herdava o
   `line-height:1.55` do <body>, e 11px × 1.55 = 17px de caixa de linha, mais
   2+2 de recuo e 1+1 de borda. A altura de um selo é, então, uma conta sobre a
   ENTRELINHA — e é ali que ela se conserta, não no recuo: 11 × 1,2 = 13,2, mais
   2+2 e 1+1, dá 19px, o alto medido no mockup. O recuo vertical fica em 2, que
   é degrau da rampa; 3px (o do mockup) não é, e não precisava ser — a rampa
   está certa e a entrelinha é que estava solta.

   RAIO: 5px não existe na rampa (4 · 6 · 999 · 50% · 0). Cede para 4px — o
   degrau de baixo, porque num selo de 19px o 6 já começa a arredondar de volta
   para cápsula, que é justamente o que estamos saindo.
   ISTO SUBSTITUI a linha «selo retângulo r5 → .badge pílula» do MAPA §5.6: a
   divergência que cedia era o RAIO (5 não existe), não a FORMA. Ceder a forma
   inteira foi ler o impedimento maior do que ele era.
   DIVERGÊNCIA ABERTA, e declarada: o `.badge` do admin.css NÃO muda junto. A
   rodada 2 tinha deixado os dois idênticos e a partir daqui eles divergem em
   forma. É deliberado — o redesenho é DO PORTAL, o mockup só cobre o portal, e
   restilizar todo selo do painel do operador sem desenho aprovado seria mudar
   uma superfície que ninguém revisou. Fica como pendência nomeada: quando o
   painel tiver a sua rodada, o degrau já está escolhido e é este. */
.badge {
  display: inline-block; font-size: var(--fs-2xs); font-weight: 600;
  line-height: 1.2;
  padding: 2px 8px; border-radius: 4px; vertical-align: 1px; white-space: nowrap;
}
/* Badge + carimbo de tempo («drift · há 26h») como UMA unidade: em célula
   estreita (~1400px) o «há Xh» quebrava sozinho para baixo do badge. Sem cor —
   só layout; existe nos DOIS arquivos (convenção das classes compartilhadas). */
.badge-since { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.badge-synced { background: var(--info-bg); color: var(--info); border: 1px solid var(--info-bd); }
.badge-device { background: var(--panel-2); color: var(--muted); border: 1px solid var(--border); }
/* Engajamento (Lido/Clicado): sucesso ALÉM da entrega — teal preenchido, salta
   do verde de «Entregue» sem gritar. Independe de var de tema. Gêmea da regra
   de admin.css:762. */
.badge-engaged { background: var(--engaged-bg); color: var(--on-solid); }

.empty {
  text-align: center; color: var(--muted);
  padding: 26px 16px; border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm); background: var(--panel-2);
}

.field-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* ── O RÓTULO DE CAMPO, e o formulário empilhado ────────────────────────────
   `.field-label` MORAVA em `.create-form` (bloco de Credenciais) e desceu para
   cá sem uma declaração alterada. Ele não era «o rótulo do form de criar
   chave»: é o rótulo de campo do portal — nome em tinta plena, e a explicação
   em cinza DENTRO do rótulo, num `<span class="muted">`. Ficar escondido atrás
   de `.create-form` é o que fez as duas telas novas nascerem sem ele.
   ATENÇÃO À ARMADILHA que este movimento paga: a catraca `classeSemRegra` casa
   o NOME da classe contra a folha, não o alcance do seletor. `class="field-label"`
   dentro de uma `.tb-sub` passava no gate com `.create-form .field-label` na
   folha — e chegava na tela SEM PINTAR. Só o navegador acusa isso. */
.field-label { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 2px 0 4px; }
.field-label .muted { font-weight: 400; }
/* `.field-stack` é o gêmeo VERTICAL do `.field-row` acima: rótulo visível em
   cima, campo em bloco embaixo, largura limitada — a anatomia que a
   `.create-form` já desenha em /app/credentials, que é a régua desta tela.
   POR QUE NÃO REUSAR `.create-form` COMO CASCA: ela carrega `padding` e um
   `border-top` que só fazem sentido debaixo do `<summary>` de um
   `.create-item`, dentro de uma `.create-bar`. Numa `.tb-sub` — que já recebe
   o respiro da `.tool-box` — o filete apareceria sozinho, sem nada acima para
   separar, e o respiro viria em dobro. Medido antes de assumir.
   O SELETOR É POR EXCLUSÃO, NÃO POR LISTA, e essa é a lição das três rodadas
   anteriores: `.create-form` dá largura a `input[type=text]`, `.cnl-form` dá a
   `text`+`url`+`select`, e o bloco de borda enumerou `text`/`search`/`url`/
   `email` antes de descobrir `password`, `number` e `date`. Toda lista de
   INCLUSÃO esquece o próximo tipo. A lista de exclusão — o que não é campo de
   digitar — não cresce: caixa de marcar, botão de rádio e campo oculto. */
.field-stack :is(input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select, textarea) {
  display: block; width: 100%; max-width: 340px;
}
.field-stack .field-label { margin-top: 14px; }
.field-stack .btn { margin-top: 10px; }
/* `search` entra junto com `text`: /app/suppressions traz um
   `<input type="search">` que não casava com seletor NENHUM desta folha e vinha
   cru do agente de usuário — outro raio, outro respiro, outra borda que o resto
   do portal. Aqui não houve a colisão de especificidade que o painel teve com
   `.ipfilter-search`: nenhuma classe desta folha declara largura de campo de
   busca. Conferido antes de acrescentar, não depois. */
/* `url` e `email` entram pelo MESMO motivo que `search` entrou, e a lacuna era
   a mesma: a folha estilizava `text` e deixava os irmãos crus. Enquanto o único
   `type="url"` do portal era o do assistente, `.wz-input` tapava o buraco — e
   isso escondia a falta em vez de pagá-la. A biblioteca de canais (106) trouxe
   campos de URL fora do assistente, e um deles teria vindo com a borda do agente
   de usuário no meio de um formulário estilizado. `type="url"` não é capricho:
   é a validação NO BLUR que a norma pede, de graça e sem um segundo validador
   para divergir do servidor — então o tipo certo tem de ser também o tipo
   bonito, senão o próximo autor escolhe `text` pelo acabamento e perde a
   validação. Nenhum valor novo: é o MESMO bloco, com a lista de tipos
   completa. */
/* 26/08 — `number`, `date` e `password` entram pelo MESMO motivo que `search`,
   `url` e `email` entraram antes, e a lacuna era a mesma: a folha estilizava
   alguns irmãos e deixava os outros CRUS, com a borda e o raio do agente de
   usuário no meio de um formulário estilizado. É a TERCEIRA vez que este bloco
   cresce por descoberta em vez de por varredura — então desta vez foi varrido:
   `grep type="…"` em todas as views do portal dá 12 tipos, e os que precisam
   desta regra são exatamente estes sete. `hidden`, `submit`, `button`,
   `checkbox` e `radio` ficam de fora porque são outro controle, não um campo
   de digitar.

   O DONO VIU nos formulários de Importação e Pesquisas — «ficou sem
   formatação» — porque ali o campo cru É o controle principal: os dois tokens
   do provedor (`password`) e o anti-fadiga (`number`). Nas outras telas os 17
   campos afetados estavam diluídos e ninguém tinha reparado.

   Nenhum valor novo: é o MESMO bloco, com a lista de tipos completa. */
input[type="text"], input[type="search"], input[type="url"], input[type="email"],
input[type="number"], input[type="date"], input[type="password"] {
  padding: 10px 12px; border: 1px solid var(--field-border);
  border-radius: var(--radius-sm); background: var(--panel);
  color: var(--text); font: var(--fs-md) var(--sans); min-width: 0;
}
input[type="text"]:focus-visible,
input[type="search"]:focus-visible,
input[type="url"]:focus-visible,
input[type="email"]:focus-visible,
input[type="number"]:focus-visible,
input[type="date"]:focus-visible,
input[type="password"]:focus-visible { border-color: var(--accent); }
select {
  padding: 10px 12px; border: 1px solid var(--field-border);
  border-radius: var(--radius-sm); background: var(--panel);
  color: var(--text); font: var(--fs-md) var(--sans); min-width: 0;
}
select:focus-visible { border-color: var(--accent); }
/* 26/08 — `textarea` entra pela MESMA porta, e é a lacuna que a rodada dos sete
   tipos de `input` NÃO fechou: aquela varredura procurou por `input[type=…]`, e
   um campo de digitar que não é `input` ficou fora do alcance da busca.
   Nesta folha o `textarea` não tinha regra NENHUMA — só a de `:disabled` lá em
   cima, que é estado, não forma. Ele vinha com a borda, o raio E A FONTE
   MONOESPAÇADA do agente de usuário: em /app/pesquisas «a pergunta» aparecia em
   Courier, com outra caixa, ao lado de campos em Figtree. É o mesmo defeito que
   o dono viu, na mesma tela, por um caminho que a correção anterior não cobriu.
   `resize: vertical` porque o arrasto horizontal estoura a coluna do formulário
   e não há layout que sobreviva a isso.
   Nenhum valor novo: são as MESMAS declarações do bloco de `input` acima. */
textarea {
  padding: 10px 12px; border: 1px solid var(--field-border);
  border-radius: var(--radius-sm); background: var(--panel);
  color: var(--text); font: var(--fs-md) var(--sans); min-width: 0;
  resize: vertical;
}
textarea:focus-visible { border-color: var(--accent); }
label.lbl { font-size: var(--fs-sm); color: var(--muted); font-weight: 600; display: block; margin-bottom: 6px; }

/* rename inline: escondido até o usuário pedir */
.pk-rename { margin-top: 10px; }
.pk-rename[hidden] { display: none; }

/* seção de login social vinculado */
.link-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--border); }
.link-row:first-child { border-top: none; }
.link-row .lr-main { flex: 1; }
.link-row .lr-name { font-weight: 600; }
.link-row .lr-state { font-size: var(--fs-xs); color: var(--muted); }
.link-row .linked { color: var(--ok); font-weight: 600; }

.muted { color: var(--muted); }
.small { font-size: var(--fs-xs); }

/* NOTA AUXILIAR — o componente que faltava (28/08/2026).
   `.hint` já era o vocabulário do sistema, mas só existia ESCOPADO
   (.wizard-panel .hint, .sub-opt .hint, .rc-howto .hint). Fora desses três
   containers cada nota inventava a própria margem inline: medido antes de
   mexer, 57 parágrafos `small muted` com 19 margens diferentes para o MESMO
   objeto — é o que o dono viu como «falta de padrão». Aqui a nota vira
   componente global com quatro papéis, e só quatro; tudo na rampa. `.hint` só
   carrega ESPAÇAMENTO: tamanho segue de `.small`, cor de `.muted`, e as regras
   escopadas continuam vencendo por especificidade.

   E NÃO SÃO SÓ ESSAS TRÊS. Toda regra `X p { margin }` tem especificidade
   (0,1,1) e vence `.hint` (0,1,0) — o QA achou SEIS vivas na folha em 28/08:
   `.page-head p`, `.status-banner p`, `.rate-form p`, `.in-notice p`,
   `.pol-sim-out p` e `.doc-section p`. Nota dentro desses containers NÃO ganha
   os 6px: ou o container já resolve o espaço (e aí `.hint-flush` diz isso em
   voz alta), ou é a regra do container que precisa mudar. Foi assim que uma das
   49 conversões prometeu 6px e entregou 0. */
.hint { margin: 6px 0 0; }
.hint-loose { margin-top: 12px; }
.hint-before { margin: 0 0 12px; }
.hint-flush { margin: 0; }
.inline-form { display: inline; }
.back-link { display: inline-block; margin-bottom: 14px; color: var(--muted); font-size: var(--fs-sm); }

@media (max-width: 480px) {
  .auth-card { padding: 26px 20px 22px; }
  .topbar .whoami { display: none; }
  .pk { flex-wrap: wrap; }
  .pk .pk-actions { width: 100%; }
}

/* ============================ SUBNAV (recursos) ========================== */
/* ── NÍVEL 1: ABA COM PREENCHIMENTO, não sublinhado (rodada 7, 22/08/2026) ──
   O port desenhava sete links com um fio azul de 2px sob o ativo, raio 0, e
   TODOS em peso 600. O mockup desenha ABAS: retângulo de topo arredondado que
   se ENCHE quando ativo, encostando na faixa de nível 2 logo abaixo — a aba e
   a faixa têm o MESMO preenchimento (#EEF1F6 = var(--neutral-bg)), e é isso
   que faz a faixa parecer o interior da aba aberta, e não outra barra.

   O PESO É METADE DA MENSAGEM. No mockup o inativo é 500 e o ativo é 700; o
   port punha 600 nos dois e perdia o contraste de peso inteiro, deixando a cor
   como único sinal de «você está aqui». Peso + preenchimento + cor: três
   sinais, e nenhum deles sozinho.

   O SUBLINHADO AZUL NÃO SUMIU — ele MUDOU DE CONDIÇÃO. No mockup o
   `inset 0 -2px 0 #2563EB` aparece só quando a área ativa NÃO tem nível 2
   (`mark: on && !sub.length`), que é o caso de Painel e Domínios. Faz sentido:
   sem faixa embaixo, a aba cheia flutua sem destino e precisa do fio para se
   fechar; com faixa, o fio cortaria justamente a costura que a faixa faz. Em
   CSS isso é uma pergunta sobre o irmão seguinte, e `:has()` responde sem JS e
   sem a rota precisar contar quantas pílulas a área tem.

   RAIO: os 9px do mockup cedem para var(--radius-sm) (8px) — a rampa aceita
   4 · 6 · 999 · 50% · 0 mais os dois tokens, e 9 não existe nela. Um pixel de
   raio no topo de uma aba de 38px é a divergência mais barata deste lote.
   ALTURA (46 na faixa, 38 na aba) não é medida pelo scanner de espaço — é
   geometria de controle, e sai literal como no mockup. */
.subnav {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.subnav-inner {
  max-width: 1280px; margin: 0 auto; padding: 0 24px;
  display: flex; align-items: flex-end; gap: 2px;
  /* `min-height` e não `height`: entre ~700 e 1280px as sete abas passam a
     rolar, e uma altura FIXA cortaria a barra de rolagem contra o texto. Onde
     não há rolagem — que é onde este desenho foi medido — as duas rendem o
     mesmo 46. */
  min-height: 46px;
  overflow-x: auto;
}
.subnav a {
  display: inline-flex; align-items: center; gap: 8px;
  flex: none; height: 38px; padding: 0 14px;
  color: var(--muted); font-size: var(--fs-md); font-weight: 500;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  white-space: nowrap;
}
/* Ícone de nav (sprite <use>): escala com a fonte, herda a cor do link. Decorativo. */
.subnav a .ic { width: 1.05em; height: 1.05em; flex-shrink: 0; }
.subnav a:hover { color: var(--text); text-decoration: none; }
.subnav a.active {
  background: var(--neutral-bg);
  color: var(--text);
  font-weight: 700;
}
/* A aba ativa de uma área SEM nível 2 (Painel, Domínios) fecha com o fio de
   acento; a de uma área COM nível 2 fecha na própria faixa. Sem `:has()` o
   fio não aparece e a aba continua marcada por preenchimento + peso — degrada
   para «menos um sinal de três», nunca para «nenhum». */
.subnav:not(:has(+ .subnav2)) a.active {
  box-shadow: inset 0 -2px 0 var(--accent);
}
/* LÁPIDE. `.subnav-end` marca a última área e portalUniformity.test.js:191
   afirma, por nome, que ela é a Conta — a classe é contrato, não enfeite, e
   por isso fica. O que ela FAZIA saiu: `margin-left: auto` empurrava Conta
   para a direita, herança da nav FLAT em que «Minha conta» era o único item
   de conta no meio de telas de recurso. O mockup mantém as sete abas em
   fluxo, com 2px entre elas; com sete ÁREAS, empurrar uma abre um vão de
   ~500px que o desenho não tem e insinua que Conta é de outra natureza.
   A declaração explícita não é decorativa: sem ela, uma media query futura
   que reintroduzisse o empurrão não teria nada neste arquivo a contradizer. */
.subnav a.subnav-end { margin-left: 0; }
/* ── O ANEL DE FOCO DA ABA, PARA DENTRO ─────────────────────────────────────
   MEDIDO depois de a aba virar aba: o anel global (`outline: 3px solid
   var(--focus); outline-offset: 2px`) perde 5px na aba de nível 1 — os 2 de
   afastamento mais os 3 de traço, o lado de baixo inteiro. Quem corta é o
   `overflow-x: auto` de `.subnav-inner`: pedir rolagem num eixo faz o
   navegador tratar o outro como `auto` também, e a aba encosta na borda de
   baixo do trilho por construção (`align-items: flex-end`), que é o que a faz
   parecer aba. Sobrava um «⊓» em vez de um retângulo — indicador de foco
   parcial, e o teclado é o único jeito de alguém ver isso.

   `outline-offset: -2px` desenha o anel POR DENTRO do próprio botão: nada a
   cortar, e o raio de 8px do topo é acompanhado pelo anel. Contraste medido do
   traço: 5,17:1 sobre a aba em repouso (branco) e 4,57:1 sobre a aba ativa
   (--neutral-bg) — os dois acima do 3:1 de indicador não-textual (1.4.11).

   A pílula de nível 2 NÃO precisa disto: ela tem 26px numa faixa de 38 com o
   eixo cruzado centrado, sobram 6px de cada lado e o anel cabe — medido, zero
   pixels perdidos. Regra estreita para o caso estreito. */
.subnav a:focus-visible { outline-offset: -2px; }

/* ============================ TABELAS ==================================== */
.table-wrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
.tbl th, .tbl td { text-align: left; padding: 10px 12px; border-top: 1px solid var(--border); vertical-align: top; }
.tbl thead th { border-top: none; border-bottom: 1px solid var(--border-strong); color: var(--muted); font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .3px; }
.tbl code { font-family: var(--mono); font-size: var(--fs-xs); }
.tbl .truncate { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Célula que não pode quebrar em duas linhas (data+hora, IP, versão). Gêmea de
   admin.css: a coluna «Quando» da tabela de recusas na porta
   (portal-domain-detail) partia entre «27/07,» e «16:24», dobrando a altura de
   cada linha e fazendo o olho perder a coluna. A regra existia só na folha do
   operador — a mesma tabela, o mesmo dado, e só uma das duas telas certa. */
.cell-nowrap { white-space: nowrap; }
.wrap-code { font-family: var(--mono); font-size: var(--fs-xs); word-break: break-all; }

/* Badges de estado (ok/warn/bad/muted/info) — vocabulário CANÔNICO completo, o
   mesmo do admin. O portal usa o SSOT de apresentação (lib/statusPresentation),
   que mapeia frozen→badge-muted e rescued→badge-info; sem estas duas classes o
   badge sairia sem estilo. Reusam os tokens já validados (neutral/info), em
   light e dark (definidos no :root e no @media dark). */
.badge-ok    { background: var(--ok-bg);      color: var(--ok);      border: 1px solid var(--ok-bd); }
.badge-warn  { background: var(--warn-bg);    color: var(--warn);    border: 1px solid var(--warn-bd); }
.badge-bad   { background: var(--bad-bg);     color: var(--bad);     border: 1px solid var(--bad-bd); }
.badge-muted { background: var(--neutral-bg); color: var(--neutral); border: 1px solid var(--neutral-line); }
.badge-info  { background: var(--info-bg);    color: var(--info);    border: 1px solid var(--info-bd); }

/* ============================ REVEAL (segredo único) ==================== */
.card.reveal { border-color: var(--warn-bd); background: var(--warn-bg); }
.card.reveal h2 { color: var(--warn); }
.secret {
  display: inline-block; font-family: var(--mono); font-size: var(--fs-sm);
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: 6px; padding: 6px 10px; word-break: break-all; user-select: all;
}

/* ── `.reveal-box` — o segredo que aparece UMA vez ────────────────────────────
   A dívida de 22/08: `portal-domain-detail.ejs` renderiza esta caixa para o
   link de conexão do Telegram (nonce de uso único, ~15 min) e a regra só
   existia em admin.css. O cliente via o segredo mais crítico da tela como
   parágrafo solto no meio do cartão — sem a moldura âmbar que diz «isto é
   temporário, copie agora».

   NÃO DÁ PARA RENOMEAR: `public/portal.js:421` faz
   `swap.querySelector('.reveal-box')` para rolar o segredo até a tela depois
   da troca por AJAX (senão ele nasce fora da área visível e morre sem ninguém
   ter visto). O nome é contrato com o JS; portar a regra é a única saída.

   Espelho de admin.css. Duas diferenças, as duas por token e nenhuma por
   pixel decidido aqui: `--warn-bd` no lugar de `--warn-line` (resolvem no
   MESMO #f0e2b4 — é só o dialeto local, o `.card.reveal` logo acima já fala
   assim) e `var(--radius)`, que aqui é o 12px de produto e lá o 8px denso.
   Medido nos quatro blocos de tema: o corpo dá 16,2:1 (claro) e 13,45:1
   (escuro) sobre --warn-bg; o `strong` em --warn dá 4,93:1 e 8,5:1. */
.reveal-box {
  background: var(--warn-bg);
  border: 1px solid var(--warn-bd);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 18px;
}
.reveal-box strong { color: var(--warn); }
.reveal-box code {
  display: inline-block;
  background: var(--surface);
  border: 1px dashed var(--warn-bd);
  padding: 4px 8px;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: var(--fs-md);
  word-break: break-all;
  margin: 6px 0;
}

/* ============================ CHECKLIST (verificação DNS) =============== */
.check-list { list-style: none; margin: 0; padding: 0; }
.check-list li { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--border); }
.check-list li:first-child { border-top: none; }
.check-list .ck-ico { flex: 0 0 auto; font-weight: 700; width: 20px; text-align: center; }
.check-list .ck-ok  .ck-ico { color: var(--ok); }
.check-list .ck-bad .ck-ico { color: var(--bad); }

/* ============================ TIMELINE ================================== */
.timeline { list-style: none; margin: 0; padding: 0; }
.tl-item { display: flex; gap: 12px; padding: 10px 0; }
.tl-dot { flex: 0 0 auto; width: 10px; height: 10px; margin-top: 6px; border-radius: 50%; background: var(--brand-2); }
.tl-track .tl-dot { background: var(--info); }
.tl-body { flex: 1; min-width: 0; }

/* ============================ PAGER ==================================== */
.pager { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
/* Legenda do selo «DBL» na lista de mensagens — texto visível para quem não
   sabe o que a Spamhaus é (o title do selo continua para o hover). Só entra
   quando há selo na tela (condição na view). */
.dbl-legend { margin: 10px 0 0; }
.dbl-legend .badge { margin-right: 4px; }

/* Barra de exportação de /app/messages (CSV imediato + auditoria por período).
   Um POÇO dentro do cartão: fundo --surface-2 (a cor da PÁGINA) sobre o
   --panel do cartão, com um fio de --line em volta — recuado do conteúdo,
   como quem diz «isto não é a lista, é o que você faz com ela».
   A regra inteira morava no atributo `style` da div, com fallbacks
   (`var(--surface-2, #f5f7fa)`) herdados de quando estes tokens não existiam
   nesta folha. Hoje existem, então o hex era letra morta — mas letra morta que
   voltaria a valer no dia em que alguém renomeasse o token, e aí a barra
   ficaria BRANCA no tema escuro sem nenhum teste reclamar. O `8px` do raio
   virou `var(--radius-sm)`, que é o mesmo 8px com nome. */
.export-bar {
  display: flex; gap: 20px; flex-wrap: wrap; align-items: flex-end;
  margin: 0 0 16px; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* ═══════════════════════════════════════════════════════════════════════════
   BIBLIOTECA DE COMPONENTES (Fase 1) NO PORTAL — replicada do admin.css.
   Linguagem de saúde FIXA (data-health): ok=na meta · warn=atenção não-erro ·
   bad=crítico/bloqueando · neutral=desligado/N-A · info=informativo. Mapeada aos
   tokens do portal (--panel/--border/--text/--muted-2). Claro E escuro herdam via
   os tokens já definidos nos dois temas acima.
   ═══════════════════════════════════════════════════════════════════════════ */

/* SSOT saúde→token: qualquer elemento com data-health ganha o trio --h-ink/-bg/
   -line; todo componente abaixo consome essas vars — cada estado num lugar só. */
[data-health="ok"]      { --h-ink: var(--ok);      --h-bg: var(--ok-bg);      --h-line: var(--ok-line); }
[data-health="warn"]    { --h-ink: var(--warn);    --h-bg: var(--warn-bg);    --h-line: var(--warn-line); }
[data-health="bad"]     { --h-ink: var(--bad);     --h-bg: var(--bad-bg);     --h-line: var(--bad-line); }
[data-health="neutral"] { --h-ink: var(--neutral); --h-bg: var(--neutral-bg); --h-line: var(--neutral-line); }
[data-health="info"]    { --h-ink: var(--info);    --h-bg: var(--info-bg);    --h-line: var(--info-line); }
/* SEXTO termo (rodada 6): «retida» — o estado que PEDE DECISÃO HUMANA. É a
   única linha que este estado custa: todo consumidor do trio --h-* já o fala.
   Repare que ele é o único cujo --h-ink NÃO é o token de texto do próprio
   estado: --warn sobre --warn-solid mede 4,24:1 e reprova. Ver --on-warn. */
[data-health="held"]    { --h-ink: var(--on-warn); --h-bg: var(--warn-solid); --h-line: var(--warn-bd); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── comp/stat — cartão de métrica ─────────────────────────────────────────── */
/* RODADA 7 — `auto-fill` → `auto-fit`. Com 1232px de caixa e trilhas de
   mínimo 160, o `auto-fill` declara CINCO trilhas e as preenche com os quatro
   cartões que existem: a quinta fica vazia e some como 179px de buraco à
   direita, em duas telas. `auto-fit` colapsa a trilha vazia e os quatro
   dividem a linha inteira — que é o que o mockup desenha (`repeat(4,1fr)`), e
   o que o olho espera de uma faixa de estatísticas.
   A diferença só aparece quando sobra trilha; com a grade cheia as duas se
   comportam igual, e é por isso que o defeito atravessou o port inteiro. */
.c-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }
/* Stat clicável (número + link pra "casa" da métrica): o <a> embrulha o card
   inteiro — alvo de toque generoso — sem mudar a cara do c-stat. */
.c-stats .stat-go { display: block; color: inherit; text-decoration: none; border-radius: var(--radius-sm); }
.c-stats .stat-go:hover .c-stat { border-color: var(--border-strong); }
.c-stats .stat-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.c-stats .stat-go .c-stat { height: 100%; }
/* ── RODADA 8 — O FILETE DE SAÚDE SAI, A SOMBRA ENTRA ───────────────────────
   Duas correções que andam juntas, e a ordem entre elas importa.

   1. O FILETE ESTAVA MORTO. `.c-stat[data-health] { border-left-color:
      var(--h-ink) }` dependia de o atributo casar com `[data-health="ok"]`
      (linha 1179), e ele NUNCA casa: comp/stat.ejs monta o atributo com
      `<%= ' data-health="' + h + '"' %>`, e o `<%=` do EJS escapa as aspas —
      o valor que chega ao DOM é `"ok"` COM as aspas literais dentro. Sem
      casar, --h-ink não é publicado, `border-left-color: var(--h-ink)` é
      inválido em tempo de valor computado e cai para currentColor. Medido no
      chromium: os quatro cartões do painel renderizam o MESMO filete
      rgb(15,23,42) — o navy do texto — para ok, neutral, ok e ok.
      Um sinal que não varia com o que sinaliza não é sinal; é tinta.
      (O defeito é da view compartilhada, não desta folha. Enquanto ele viver,
      qualquer regra daqui pendurada em [data-health] neste componente é
      decoração — por isso nenhuma entra no lugar.)

   2. O MOCKUP NÃO TEM ACENTO NENHUM neste cartão, e tem sombra. O port fazia o
      oposto exato: filete de 3px que o desenho não pede, e nenhuma sombra
      enquanto o cartão VIZINHO (.card) carrega --shadow-sm. Na mesma tela,
      dois retângulos brancos com a mesma borda e elevações diferentes leem
      como dois materiais, e o olho procura a diferença que não existe. */
.c-stat {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 12px 16px;
}
.c-stat-ic { position: absolute; top: 12px; right: 14px; color: var(--faint); line-height: 0; }
.c-stat-ic .ic { width: 18px; height: 18px; }
.c-stat-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .5px; color: var(--muted); font-weight: 600; }
/* 26 → 24. O número do stat empatava com o `h1` da tela (os dois em --fs-6xl),
   e empate não é hierarquia: com quatro cartões na faixa, quatro números do
   tamanho do assunto da página disputam o primeiro olhar e a tela perde o
   começo. O mockup abre 29/27; a rampa não tem esses degraus (o pedido de
   `h1: 29` foi recusado com catraca), então o que se porta é a RELAÇÃO, não o
   par de valores: --fs-6xl no h1 contra --fs-5xl aqui reproduz o degrau único
   de diferença que o desenho tem. */
.c-stat-value { font-size: var(--fs-5xl); font-weight: 700; line-height: 1.15; margin-top: 4px; font-variant-numeric: tabular-nums; }
.c-stat-sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 4px; }
/* ── CARTÃO-ESTATÍSTICA CLICÁVEL DE TRIAGEM (Mensagens, 5-up) ───────────────
   MAPA-CLASSES §1.2 diz «`.c-stat` + `[aria-pressed]`, nada novo» — e é isso:
   a caixa, o rótulo, o número e a linha de apoio são as MESMAS regras acima.
   O que esta classe acrescenta é só o que um <button> precisa para parar de
   parecer um <button>: o agente de usuário aplica fonte própria, centralização
   e `align-items:center` ao conteúdo, e sem o reset os cinco cartões saem com
   texto centralizado e tipografia do sistema no meio de uma tela que não usa
   nenhuma das duas.

   POR QUE <button> E NÃO <a>. O recorte é um submit GET dentro de um <form>: é
   assim que o motor de AJAX-first da casa (public/portal.js, `form[data-ajax]`
   com method=get) troca só a seção sem recarregar, e é assim que ele continua
   funcionando sem JS. Um <a> exigiria montar a query string na view em dois
   lugares (aqui e no pager) e perderia o `aria-pressed`, que é o que diz ao
   leitor de tela que o cartão é um filtro LIGADO — não um link para outra tela.

   A COR VEM DO SSOT DE SAÚDE. `data-health` publica o trio --h-* (bloco lá em
   cima) e aqui só se CONSOME: rótulo e número no tom do estado, borda e sombra
   marcando o ativo. Nenhum valor de cor é escrito nesta regra — é o que a
   catraca cssRampa cobra, e é o que faz o tema escuro funcionar de graça. */
.c-stat-pick {
  display: block; width: 100%;
  text-align: left;
  font: inherit; color: inherit;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
/* Os filhos são <span> (o conteúdo de <button> é phrasing content, então <div>
   ali seria HTML inválido); sem isto eles ficariam em linha, um do lado do
   outro, e as três alturas do cartão viram uma frase só. */
.c-stat-pick > span { display: block; }
.c-stat-pick > .c-stat-ic { display: block; }
.c-stat-pick .c-stat-label { color: var(--h-ink, var(--muted)); }
.c-stat-pick .c-stat-value { color: var(--h-ink, var(--text)); }
.c-stat-pick:hover { border-color: var(--border-strong); }
.c-stat-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ATIVO: a borda escurece e a sombra sobe um degrau — o mesmo idioma do filtro
   de Domínios no mockup (preenchimento/contorno escuro sobre cartão branco).
   Não é cor de estado: um cartão «Entregues» ativo não pode ficar verde-escuro
   e passar por «tudo certo», ele está SELECIONADO, que é outra informação. */
.c-stat-pick[aria-pressed="true"] {
  border-color: var(--text);
  box-shadow: var(--shadow);
}
/* BARRA DE COTA — o consumo como forma antes de virar número. Só o painel a
   usa (a cota mora lá, casa única), e ela é o único lugar do stat em que o
   estado tem cor: o traço é `currentColor` do bloco, que a view tinge pelo
   token de saúde. Altura é GEOMETRIA de fio, não espaço de rampa. */
.c-stat-bar {
  height: 6px; margin-top: 8px;
  border-radius: 999px;
  background: var(--neutral-bg);
  overflow: hidden;
  color: var(--h-ink, var(--accent));
}
.c-stat-bar > span {
  display: block; height: 6px;
  border-radius: 999px;
  background: currentColor;
}

/* ── comp/badge — pílula de status ────────────────────────────────────────── */
.c-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 10px 2px 8px; border-radius: 999px;
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; vertical-align: 1px;
  background: var(--h-bg); color: var(--h-ink); border: 1px solid var(--h-line);
}
.c-badge-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }

/* ── comp/chip — semáforo inline p/ tabela ────────────────────────────────── */
.c-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-sm); font-weight: 600; white-space: nowrap; color: var(--h-ink);
}
.c-chip-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.c-chip-ic { line-height: 0; }
.c-chip-ic .ic { width: 14px; height: 14px; }

/* ── comp/empty — estado vazio ────────────────────────────────────────────── */
.c-empty { text-align: center; padding: 32px 20px; }
/* MEDALHÃO (MAPA §4.8) — o ícone solto de 40px em --faint media 3.11:1 e não
   sobrevivia como objeto gráfico. Agora é uma caixa de 44px tingida pelo trio
   --h-*: azul em «primeiro contato», neutro em «filtro sem resultado», verde em
   «boa notícia». O fallback existe porque um .c-empty sem data-health nem
   data-empty NÃO tem --h-bg computado — e var() não resolvido descarta a
   declaração inteira, deixando o medalhão transparente. */
.c-empty-ic {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-bottom: 14px; line-height: 0;
  border-radius: var(--radius);
  background: var(--h-bg, var(--neutral-bg));
  color: var(--h-ink, var(--muted));
}
.c-empty-ic .ic { width: 21px; height: 21px; stroke-width: 1.8; }
.c-empty-title { margin: 0 0 8px; font-size: var(--fs-xl); }
.c-empty-msg { max-width: 52ch; margin: 0 auto 16px; color: var(--muted); }
.c-empty-cta { margin: 0; }

/* ── comp/banner — alerta full-width semântico ────────────────────────────── */
.c-banner {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 12px 16px; margin-bottom: 18px; font-size: var(--fs-md);
  border-radius: var(--radius);
  background: var(--h-bg); color: var(--h-ink);
  border: 1px solid var(--h-line); border-left-width: 4px;
}
.c-banner-body { flex: 1; min-width: 220px; }
.c-banner-body a { color: var(--h-ink); text-decoration: underline; }
.c-banner-action {
  flex: none; font-weight: 600; color: var(--h-ink);
  border: 1px solid var(--h-line); border-radius: 6px; padding: 6px 12px;
  text-decoration: none; background: color-mix(in srgb, var(--panel) 45%, transparent);
}
.c-banner-action:hover { text-decoration: none; background: var(--panel); }

/* ═══════════════════════════════════════════════════════════════════════════
   FASE 5 — SAÚDE DE ENTREGA (WS1)
   ═══════════════════════════════════════════════════════════════════════════ */
/* ── PAINEL — A GRADE DE DUAS COLUNAS ───────────────────────────────────────
   O mockup abre o painel em 750/468 sobre 1232 de caixa: à esquerda o score e
   «O que melhorar» (a coluna que se lê), à direita o gráfico e os Atalhos (a
   coluna que se olha). Medido no chromium, `1.6fr / 1fr` com 14 de gap devolve
   749,5 / 468,5 — os números do mockup, sem cravar largura nenhuma.

   POR QUE NÃO DUAS COLUNAS FIXAS: uma coluna de 468px cravada vira 468px numa
   janela de 900, e o score espremido a 400 quebra o medalhão. Em fr as duas
   encolhem juntas até o ponto em que a de baixo assume.

   O PONTO EM QUE ASSUME é 900, e não 640: a coluna estreita carrega o gráfico
   de 7 barras, que abaixo de ~330px de largura útil deixa de ter forma. Em 900
   a coluna direita vale 336 — o último degrau em que a barra ainda é barra. */
.dash-grid { display: grid; gap: 14px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 900px) {
  /* `align-items: start` porque as duas colunas têm alturas diferentes por
     natureza (o score cresce com o nº de pendências) e esticar a curta faria
     o cartão de Atalhos crescer com buraco embaixo. */
  .dash-grid { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); align-items: start; }
}
/* A coluna direita é uma pilha de cartões; o gap da grade não alcança dentro
   dela, e `.card` já traz margin-bottom 18 — que aqui viraria um respiro
   diferente do da grade (14). Zerar a margem e deixar o gap mandar é o que
   mantém os dois eixos no mesmo degrau. */
.dash-side { display: grid; gap: 14px; align-content: start; }
.dash-side > .card { margin-bottom: 0; }

/* ── ATALHOS — o cartão de partidas ─────────────────────────────────────────
   Decisão do dono (22/08). NÃO é o cartão «Ir para minha conta» que saiu no
   passado e cuja remoção test/portalUniformity.test.js:353 trava: aquele era um
   cartão inteiro para UM destino que a navegação já servia, no mesmo nível dela.
   Este é um bloco de PARTIDAS — três tarefas que começam em outro lugar,
   agrupadas onde a pessoa acabou de ler que tem pendência. A diferença que
   importa é de cardinalidade e de intenção: um duplicava a nav, três abrem
   trabalho. */
.quick { display: grid; gap: 8px; }
.quick-btn {
  display: flex; align-items: center; gap: 10px;
  min-height: 38px; padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--text);
  font-size: var(--fs-sm); font-weight: 600;
  text-decoration: none;
}
.quick-btn:hover { background: var(--panel-2); border-color: var(--border-strong); color: var(--text); text-decoration: none; }
.quick-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* O ícone é o único ponto de cor da linha: ele diz PARA ONDE sem competir com
   o rótulo, que é o que se lê. */
.quick-btn .ic { flex: none; width: 16px; height: 16px; color: var(--accent); }
/* 38px é a altura do mockup e passa o mínimo de 24px do WCAG 2.5.8 — mas no
   dedo o alvo confortável é 44, e o resto da folha já trata `pointer: coarse`
   como a régua de toque (--btn-min-h). Aqui vale o mesmo: o desenho denso fica
   no mouse, o alvo cresce onde o dedo manda. */
@media (pointer: coarse) { .quick-btn { min-height: 44px; } }

.health-card { display: grid; gap: 18px; grid-template-columns: 1fr; }
@media (min-width: 620px) { .health-card { grid-template-columns: auto 1fr; align-items: center; } }

/* Semáforo circular — o valor em ink-de-saúde, o anel colorido pela banda. */
.gauge {
  --g-size: 128px;
  width: var(--g-size); height: var(--g-size);
  border-radius: 50%;
  display: grid; place-items: center;
  background: radial-gradient(closest-side, var(--panel) 71%, transparent 72% 100%),
              conic-gradient(var(--h-ink) calc(var(--g-pct, 0) * 1%), var(--h-line) 0);
  margin: 0 auto;
}
.gauge-inner { text-align: center; }
.gauge-score { font-size: var(--fs-7xl); font-weight: 800; line-height: 1; color: var(--h-ink); font-variant-numeric: tabular-nums; }
.gauge-of { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; }
/* ── O ESTADO É PÍLULA; O TÍTULO É O NOME DA MÉTRICA ────────────────────────
   O port punha o ESTADO no lugar do título — o `<h2>` dizia «Ação necessária»
   e o nome da métrica não aparecia em lugar nenhum. Quem chegava na tela lia
   uma sentença sem saber sentença sobre o quê, e o «57 de 100» ao lado ficava
   órfão de rótulo.

   O mockup separa os dois papéis, e a separação é o que faz a leitura fechar:
   pílula tingida com o ESTADO (varia, chama o olho, é o alarme) e título fixo
   com o NOME (não varia, ancora o número). Título não deve mudar de texto
   conforme o valor: é ele que diz o que se está medindo. */
.health-state {
  display: inline-flex; align-items: center; gap: 6px;
  margin-bottom: 8px; padding: 4px 10px;
  border-radius: 999px;
  background: var(--h-bg); color: var(--h-ink);
  font-size: var(--fs-2xs); font-weight: 600;
}
.health-state .ic { width: 13px; height: 13px; flex: none; }
.health-headline { margin: 0 0 4px; font-size: var(--fs-2xl); }
.health-sub { margin: 0; color: var(--muted); font-size: var(--fs-md); }

/* "O que melhorar" — lista de findings acionáveis. */
.findings { list-style: none; margin: 14px 0 0; padding: 0; }
.findings li {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 0; border-top: 1px solid var(--border);
}
.findings li:first-child { border-top: none; }
.finding-rail { flex: none; width: 8px; height: 8px; margin-top: 6px; border-radius: 50%; background: var(--h-ink); }
.finding-body { flex: 1; min-width: 0; }
.finding-signal { font-weight: 600; }
.finding-impact { color: var(--muted); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); }
.finding-next { color: var(--muted); font-size: var(--fs-sm); margin-top: 2px; }
.finding-body .finding-cta { font-size: var(--fs-sm); font-weight: 600; margin-top: 4px; display: inline-block; }
.health-clean { display: flex; gap: 10px; align-items: center; color: var(--ok); font-weight: 600; margin-top: 6px; }

/* ═══════════════════════════════════════════════════════════════════════════
   FASE 5 — ONBOARDING (WS3): checklist "Primeiros passos"
   ═══════════════════════════════════════════════════════════════════════════ */
.checklist { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 2px; }
.checklist li { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0; border-top: 1px solid var(--border); }
.checklist li:first-child { border-top: none; }
.ck-mark {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: grid; place-items: center; font-weight: 700; font-size: var(--fs-sm);
  border: 1.5px solid var(--border-strong); color: var(--muted); background: var(--panel-2);
}
.checklist li.is-done .ck-mark { background: var(--ok-bg); border-color: var(--ok-bd); color: var(--ok); }
.ck-main { flex: 1; min-width: 0; }
.ck-label { font-weight: 600; }
.checklist li.is-done .ck-label { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--border-strong); }
.ck-hint { color: var(--muted); font-size: var(--fs-sm); margin-top: 1px; }
.ck-go { flex: none; align-self: center; font-size: var(--fs-sm); font-weight: 600; }
.onboard-progress { height: 6px; border-radius: 999px; background: var(--panel-2); overflow: hidden; margin-top: 4px; border: 1px solid var(--border); }
.onboard-progress > span { display: block; height: 100%; background: var(--accent); border-radius: inherit; transition: width .3s; }

/* ═══════════════════════════════════════════════════════════════════════════
   FASE 5 — ANALYTICS (WS2)
   ═══════════════════════════════════════════════════════════════════════════ */
.analytics-toolbar { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 18px; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg a {
  padding: 8px 14px; font-size: var(--fs-sm); font-weight: 600; color: var(--muted);
  border-left: 1px solid var(--border);
}
.seg a:first-child { border-left: none; }
.seg a:hover { background: var(--panel-2); text-decoration: none; }
/* O segmento ESCOLHIDO é estado, não ação: mesma tinta do chip de filtro
   selecionado («Todas») das listas, e não o azul do botão primário. */
.seg a.active { background: var(--text); color: var(--bg); }
.toolbar-field { display: flex; flex-direction: column; gap: 4px; }
.toolbar-field .lbl { margin: 0; }

/* Gráfico de área SVG inline (grade fraca, endpoint destacado). */
.chart-card { padding: 18px 20px 14px; }
.chart-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.chart-head h2 { margin: 0; font-size: var(--fs-lg); }
.chart-scope { color: var(--muted); font-size: var(--fs-sm); }
.chart-svg { width: 100%; height: auto; display: block; overflow: visible; }
.chart-grid { stroke: var(--border); stroke-width: 1; }
.chart-axis { fill: var(--muted); font-size: var(--fs-3xs); font-variant-numeric: tabular-nums; }
.series-sent   { --s-ink: var(--info); }
.series-delivered { --s-ink: var(--ok); }
.series-opened { --s-ink: var(--brand-2); }
.series-bounced { --s-ink: var(--warn); }
.chart-area { fill: var(--s-ink); opacity: .12; }
.chart-line { fill: none; stroke: var(--s-ink); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-end { fill: var(--s-ink); }
.chart-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; font-size: var(--fs-xs); }
.chart-legend span { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
.chart-legend i { width: 10px; height: 10px; border-radius: 4px; background: var(--s-ink); display: inline-block; }
.chart-empty { text-align: center; color: var(--muted); padding: 40px 20px; }

/* ── Facetas do Analytics: «Para onde você envia» · «Por que não chegou» ──────
   Medidor rotulado, o padrão que o MAPA-CLASSES §1.8 já prescreve: `.dgrp-head`
   (label · valor · %) + `.onboard-progress` (o trilho). Nada de barra nova —
   duas geometrias para a mesma ideia é como as duas telas divergem sem ninguém
   notar. Só o espaçamento entre linhas e o tom da barra de falha são novos.

   Sem media query: `auto-fit` + `min(100%, 280px)` dá duas colunas onde cabe e
   uma onde não cabe, que é a mesma receita das outras faixas desta folha. */
.an-duo { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 14px; margin-top: 14px; }
.an-meter + .an-meter { margin-top: 12px; }
/* Números alinham na coluna: tabular-nums impede que a lista «dance» entre 9 e
   10 e destrua a comparação visual que as barras existem para permitir. */
.an-meter .dgrp-state { font-variant-numeric: tabular-nums; }
/* A barra de FALHA é vermelha, e a cor vem do token — nunca de um literal: a
   catraca de contraste trava nos dois sentidos, e `--bad` é o único vermelho
   desta folha que passa o mínimo de 3:1 para objeto gráfico. */
.an-meter[data-tone="bad"] .onboard-progress > span { background: var(--bad); }

/* ═══════════════════════════════════════════════════════════════════════════
   FASE 1 — WIZARD DE ONBOARDING (/app/start)
   Mobile-first, max-width 720, barra de ação sticky com safe-area. Sobre os
   tokens já definidos (claro + escuro herdam). Alvos de toque ≥44px.
   ═══════════════════════════════════════════════════════════════════════════ */
.wizard { max-width: 720px; margin: 0 auto; }
.wizard-panel { padding: 22px 20px; }
.wizard-panel h1 { font-size: var(--fs-4xl); margin: 0 0 6px; }
.wizard-panel .lead { color: var(--muted); font-size: var(--fs-md); margin: 0 0 18px; }
.wizard-panel .hint { color: var(--muted); font-size: var(--fs-sm); margin: 8px 0 0; }
.wizard-panel .lbl { margin-top: 4px; }
.wizard-panel input[type="text"], .wizard-panel input[type="email"] { width: 100%; padding: 12px 12px; }

/* ── progress rail (6 passos) ─────────────────────────────────────────────── */
.wizard-rail {
  list-style: none; margin: 0 0 18px; padding: 0;
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px;
}
.wr-step { display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; min-width: 0; }
/* linha conectora entre os dots */
.wr-step::before {
  content: ""; position: absolute; top: 15px; left: -50%; width: 100%; height: 2px;
  background: var(--border-strong); z-index: 0;
}
.wr-step:first-child::before { display: none; }
.wr-step.is-done::before, .wr-step.is-current::before { background: var(--accent); }
.wr-dot {
  position: relative; z-index: 1;
  width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: var(--fs-sm); font-weight: 700;
  background: var(--panel-2); color: var(--muted);
  border: 2px solid var(--border-strong);
  text-decoration: none;
}
a.wr-dot:hover { text-decoration: none; border-color: var(--accent); }
.wr-step.is-done .wr-dot { background: var(--ok-bg); border-color: var(--ok-bd); color: var(--ok); }
.wr-step.is-current .wr-dot { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.wr-label {
  font-size: var(--fs-2xs); color: var(--muted); text-align: center; line-height: 1.2;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.wr-step.is-current .wr-label { color: var(--text); font-weight: 600; }
@media (max-width: 520px) { .wr-label { display: none; } }

/* ── barra de ação sticky ─────────────────────────────────────────────────── */
.action-sticky {
  position: sticky; bottom: 0;
  display: flex; gap: 10px; justify-content: space-between; align-items: center;
  flex-wrap: wrap;
  margin: 22px -20px -22px; padding: 14px 20px;
  padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(6px);
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius) var(--radius);
}
.action-sticky .btn { width: auto; min-height: var(--btn-min-h); }
.action-sticky .btn-primary { margin-left: auto; }

/* ── S2: escolha de apex ──────────────────────────────────────────────────── */
.apex-pick {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 14px 16px; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); background: var(--panel-2);
}
.apex-pick .ap-key { color: var(--muted); font-size: var(--fs-sm); }
.apex-pick .ap-val { font-family: var(--mono); font-size: var(--fs-xl); font-weight: 600; color: var(--text); }
.sub-opt { margin-top: 14px; }
.sub-opt summary { cursor: pointer; color: var(--brand-2); font-size: var(--fs-sm); font-weight: 600; }
.sub-opt .hint { margin-bottom: 10px; }

/* ── S3: record-cards (mobile) → degrada visualmente no desktop ───────────── */
.records-cards { display: grid; gap: 12px; margin-top: 6px; }
.record-card {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel-2); padding: 14px;
}
.record-card[data-level="required"] { border-left: 3px solid var(--bad); }
.record-card[data-level="recommended"] { border-left: 3px solid var(--warn); }
.record-card[data-level="optional"] { border-left: 3px solid var(--neutral-line); }
/* Detalhe do domínio (portal-domain-detail): o rail passa a refletir o STATUS de
   verificação — verde = publicado/ok, âmbar = pendente — sobrepondo o rail por
   nível. Só ESTA view seta data-status; os cards do wizard (/app/start) têm só
   data-level e seguem intactos. Mesma especificidade do bloco data-level acima,
   então declarado DEPOIS para vencer a cor (largura/estilo 3px solid herdados). */
.record-card[data-status="ok"]   { border-left-color: var(--ok); }
.record-card[data-status="warn"] { border-left-color: var(--warn); }
/* «Quebrou» (o monitor confirmou que parou de resolver) é vermelho no filete,
   não âmbar: o selo já dizia vermelho e o rail dizia âmbar — dois sinais de
   severidade diferente no mesmo cartão. */
.record-card[data-status="bad"]  { border-left-color: var(--bad); }
/* Nota auxiliar (hostname completo / valor atual): quebra código longo sem estourar. */
.rc-note { margin: 6px 0 0; }
.rc-note code { word-break: break-all; }
.rc-head { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.rc-field { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.rc-key { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .4px; color: var(--muted); font-weight: 600; }
.rc-val { display: flex; gap: 8px; align-items: flex-start; }
.rc-val code { flex: 1; min-width: 0; padding: 8px 10px; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; }
/* Alvo de toque ≥44px (o botão de copiar antigo, 2px/8px, reprovava). */
.rc-copy {
  flex: 0 0 auto; width: auto; min-height: 44px; padding: 10px 14px;
  font-size: var(--fs-sm); font-weight: 600;
}
.rc-what { color: var(--muted); font-size: var(--fs-sm); margin: 10px 0 0; }
.rc-howto { margin-top: 10px; }
.rc-howto summary { cursor: pointer; color: var(--brand-2); font-size: var(--fs-sm); font-weight: 600; }
.rc-howto .hint { margin-top: 8px; }
.rc-bind-link { font-size: var(--fs-sm); font-weight: 600; }

/* No desktop, os cards ficam lado a lado (grade densa, mas ainda cartão). */
@media (min-width: 640px) { .records-cards { grid-template-columns: 1fr 1fr; } }
/* Cartão SOZINHO ocupa a largura toda: a seção de recepção publica UM registro
   (o MX), e num grid de duas colunas ele ficava na metade esquerda com metade
   da linha vazia. `:has()` resolve sem classe modificadora e sem JS — o grid
   se adapta ao que tem dentro, que é o comportamento certo tanto aqui quanto
   em qualquer seção futura de registro único. */
@media (min-width: 640px) {
  .records-cards:has(> .record-card:only-child) { grid-template-columns: 1fr; }
}

/* ── O CONJUNTO DE DECISÕES (.dset) ──────────────────────────────────────────
   ESPELHO EXATO de admin.css. Muda só a fonte dos tokens (--border/--accent/
   --ok-bd). O kernel aplicado no TOPO, não um nível abaixo: cada .dgrp já
   colapsava, o CONJUNTO delas não — e com tudo verde sobravam quatro/cinco
   linhas dizendo «ok». Tudo verificado ⇒ UMA linha; qualquer item fora do verde
   ⇒ o conjunto abre e o grupo problemático abre dentro dele.

   NÃO é um .dgrp maior: sem caixa e sem fundo. É o CABEÇALHO da seção que por
   acaso abre — o peso vem da tipografia (o <h2> mora dentro do <summary>, então
   o outline do documento não perde a seção), nunca de mais uma moldura. */
/* COERÊNCIA (dono, 14/08): «Registros DNS» e «Host de tracking» convivem no
   mesmo cartão — os dois em CAIXINHA, mesma moldura do .dgrp (borda +
   filete de saúde à esquerda). O data-health que o .dset já carrega pinta o
   filete pelas MESMAS cores do .dgrp, abaixo. */
.dset {
  display: block;
  border: 1px solid var(--border); border-left: 3px solid var(--neutral-line);
  border-radius: var(--radius-sm); background: var(--panel); margin: 0 0 10px;
}
.dset[data-health="ok"]   { border-left-color: var(--ok-line); }
.dset[data-health="warn"] { border-left-color: var(--warn); }
.dset[data-health="bad"]  { border-left-color: var(--bad); }
/* min-height 44px é alvo de toque, não decoração — o <summary> inteiro é o alvo. */
/* row-gap curto de propósito: quando a linha quebra no celular, 9px de respiro
   vertical fazem as partes lerem como TRÊS blocos separados em vez de uma frase
   que continuou. O respiro horizontal segue 9px. */
.dset-sum {
  display: flex; align-items: center; column-gap: 10px; row-gap: 4px; flex-wrap: wrap;
  min-height: 44px; padding: 2px 12px; cursor: pointer; list-style: none;
}
.dset-sum::-webkit-details-marker { display: none; }
.dset-sum::marker { content: ""; }
.dset-sum:hover .dset-more { text-decoration: underline; }
.dset-sum:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* Glifo de estado — a FORMA que acompanha a cor. Mesmo desenho do .dgrp-mark,
   um grau maior: é o pai. */
.dset-mark {
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-sm); font-weight: 700; line-height: 1;
  background: var(--neutral-bg); color: var(--neutral); border: 1px solid var(--neutral-line);
}
.dset[data-health="ok"]   .dset-mark { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-bd); }
.dset[data-health="warn"] .dset-mark { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-bd); }
.dset[data-health="bad"]  .dset-mark { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-bd); }

/* DOIS nomes de classe, não um: `.card h2` (0,1,1) é mais específico que
   `.dset-name` (0,1,0) e estava vencendo — o <h2> vinha com margem inferior,
   que num item de flex vira espaço morto e desalinha o glifo. */
.dset-sum .dset-name { margin: 0; font-size: var(--fs-xl); line-height: 1.3; font-weight: 700; letter-spacing: -.01em; }
/* --muted, nunca o terciário: ele reprova AA em texto pequeno. */
.dset-state { font-size: var(--fs-sm); color: var(--muted); }
.dset-when  { font-size: var(--fs-xs); color: var(--muted); }
/* Separador ENTRE os itens de texto, no fim de cada um — e NUNCA antes do
   «ver detalhes», que é empurrado para a direita e deixaria o «·» órfão no meio
   da linha. `:has()` resolve sozinho: o ponto só existe se houver vizinho. */
.dset-name::after { content: " ·"; color: var(--muted); font-weight: 400; }
.dset-state:has(+ .dset-when)::after { content: " ·"; color: var(--muted); }
.dset-more  { margin-left: auto; font-size: var(--fs-xs); font-weight: 600; color: var(--brand-2); white-space: nowrap; }
.dset-caret { display: inline-block; transition: transform 140ms cubic-bezier(.16,1,.3,1); }
.dset[open] .dset-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .dset-caret { transition: none; } }
/* No celular: o «ver detalhes» flui atrás do texto em vez de ser jogado sozinho
   na direita de uma linha só dele, e o nome cede 1px para o resumo caber ao
   lado — a linha vira DUAS e não três. */
@media (max-width: 520px) {
  .dset-more { margin-left: 0; }
  .dset-sum .dset-name { font-size: var(--fs-lg); }
}

/* Aberto: um fio separa o cabeçalho do conteúdo — mesma hierarquia do cartão. */
.dset[open] > .dset-sum { border-bottom: 1px solid var(--border); margin-bottom: 12px; padding-bottom: 10px; }
.dset-sub  { margin: 0 0 12px; color: var(--muted); font-size: var(--fs-sm); line-height: 1.55; }
.dset-foot { margin: 12px 0 0; color: var(--muted); font-size: var(--fs-xs); line-height: 1.55; }

/* A EVIDÊNCIA DO MONITOR dentro do registro a que ela pertence (espelho de
   .chk-fix do painel do operador). Existia uma tabela paralela «Monitor de DNS»
   que, com tudo verde, dizia «5 de 5 resolvendo» ao lado de «2/2 · 3/3 · 1/1» —
   o mesmo fato contado duas vezes, ocupando linha própria. O que ela tinha de
   único (há quantas horas quebrou e como corrigir) mudou de casa para cá. */
.chk-fix { margin: 8px 0 0; padding: 8px 12px; border-left: 3px solid var(--brand-2);
           background: var(--info-bg); border-radius: 0 6px 6px 0; font-size: var(--fs-sm); line-height: 1.5; }

/* ── GRUPO DE DECISÃO (.dgrp) ────────────────────────────────────────────────
   ESPELHO EXATO de admin.css (mesma família, mesmos nomes, mesmo comportamento).
   O cliente não pode ver diferente do operador — quem mexer lá mexe aqui.
   Muda só a fonte-da-verdade dos tokens (portal usa --border/--panel/--panel-2).

   O KERNEL do UX (UX-FLUXO-CONFIG): «só mostra quando precisa mostrar». Sete
   registros verificados não são sete cartões — são QUATRO decisões (Envio,
   Assinatura, Política, Recepção), e a decisão já resolvida vale UMA LINHA.
   Colapsado ≠ apagado: o <details> nativo guarda nome, valor e botão de copiar a
   um clique, sem JS. Cor nunca é o único portador de significado: filete +
   glifo + palavra. */
.dgrp {
  border: 1px solid var(--border); border-left: 3px solid var(--neutral-line);
  border-radius: var(--radius-sm); background: var(--panel); margin: 0 0 10px;
}
.dgrp:last-child { margin-bottom: 0; }
.dgrp[data-health="ok"]      { border-left-color: var(--ok-line); }
.dgrp[data-health="warn"]    { border-left-color: var(--warn); }
.dgrp[data-health="bad"]     { border-left-color: var(--bad); }
.dgrp[data-health="info"]    { border-left-color: var(--info-line); }
.dgrp[data-health="neutral"] { border-left-color: var(--neutral-line); }

/* Resumo = a linha única. min-height 44px é alvo de toque, não decoração. */
.dgrp-sum {
  display: flex; align-items: center; gap: 10px; padding: 12px 14px;
  min-height: 44px; cursor: pointer; list-style: none;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.dgrp-sum::-webkit-details-marker { display: none; }
.dgrp-sum::marker { content: ""; }
/* Hover NÃO tinge o fundo de propósito: um fundo tingido derruba o contraste do
   texto secundário. A affordance vem da borda e do sublinhado. */
.dgrp:hover { border-color: var(--border-strong); }
.dgrp-sum:hover .dgrp-more { text-decoration: underline; }
.dgrp-sum:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Glifo de estado — a FORMA que acompanha a cor. */
.dgrp-mark {
  flex: none; width: 21px; height: 21px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 700; line-height: 1;
  background: var(--neutral-bg); color: var(--neutral); border: 1px solid var(--neutral-line);
}
.dgrp[data-health="ok"]   .dgrp-mark { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-bd); }
.dgrp[data-health="warn"] .dgrp-mark { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-bd); }
.dgrp[data-health="bad"]  .dgrp-mark { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-bd); }
.dgrp[data-health="info"] .dgrp-mark { background: var(--info-bg); color: var(--info); border-color: var(--info-bd); }

/* Nome · estado · quando — envolve sozinho no celular (sem media query). */
.dgrp-head { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
/* Separador no FIM do item, não no começo do seguinte: quando a linha quebra no
   celular, um «·» sobrando no início da linha lê como marcador de lista; no fim
   lê como continuação. */
.dgrp-head > span:not(:last-child)::after { content: " ·"; color: var(--muted); }
.dgrp-name  { font-weight: 600; }
/* --muted (#5b6b7d), nunca --muted-2: o terciário reprova AA em texto pequeno. */
.dgrp-state { font-size: var(--fs-sm); color: var(--muted); }
.dgrp-when  { font-size: var(--fs-xs); color: var(--muted); }
.dgrp-more  { flex: none; font-size: var(--fs-xs); font-weight: 600; color: var(--brand-2); white-space: nowrap; }
.dgrp-caret { display: inline-block; transition: transform 140ms cubic-bezier(.16,1,.3,1); }
.dgrp[open] .dgrp-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .dgrp-caret { transition: none; } }

.dgrp-body { padding: 10px 14px 14px; border-top: 1px solid var(--line); }
/* Dentro do grupo o cartão já herda o rail do grupo: sem grade de duas colunas
   (um grupo tem 1–3 registros; empilhado lê melhor e não deixa meia linha vazia). */
.dgrp-body .records-cards { grid-template-columns: 1fr; margin-top: 0; }
.dgrp-note { margin: 10px 0 0; font-size: var(--fs-sm); line-height: 1.55; color: var(--muted); }

/* Grupo de REFERÊNCIA (Recepção): estado + porta, nunca o registro duplicado. */
.dgrp-ref .dgrp-sum { cursor: default; }
.dgrp-ref .dgrp-sum:hover { background: transparent; }
.dgrp-link { flex: none; font-size: var(--fs-xs); font-weight: 600; color: var(--brand-2); white-space: nowrap; }

/* ── BIBLIOTECA: CRIAR UM CANAL — três meios, três cards de peso IGUAL ──────
   O dono olhou a primeira versão e concluiu que só existia webhook: o webhook
   tinha formulário completo e os outros dois, quando a pré-condição faltava,
   viravam uma linha cinza (06/08/2026). Ele estava certo — o peso visual É a
   afirmação de existência. Aqui os três têm a mesma moldura, o mesmo título e
   a mesma presença SEMPRE; a pré-condição que falta é `data-state`
   (todo/dormant, o MESMO vocabulário dos .wz-card do assistente, inclusive a
   borda tracejada — sinal que não depende de cor), nunca o encolhimento.
   Grid auto-fit: três colunas onde cabe, uma no celular, sem breakpoint.
   Bloco IDÊNTICO em conceito no admin.css (raio e rótulo são o dialeto). */
.cnl-new {
  display: grid; gap: 10px; margin-top: 12px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  align-items: stretch;
}
.cnl-card {
  min-width: 0; display: flex; flex-direction: column; gap: 6px;
  padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel);
}
.cnl-card[data-state="todo"], .cnl-card[data-state="dormant"] { border-style: dashed; }
.cnl-card-t { margin: 0; font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.cnl-card[data-state="todo"] .cnl-card-t, .cnl-card[data-state="dormant"] .cnl-card-t { color: var(--muted); }
.cnl-card-tag {
  display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  font-size: var(--fs-3xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  white-space: nowrap; vertical-align: middle;
  background: var(--neutral-bg); color: var(--muted); border: 1px solid var(--neutral-line);
}
.cnl-card-d, .cnl-card-falta { margin: 0; font-size: var(--fs-sm); line-height: 1.5; }
.cnl-card-d { color: var(--muted); }
/* A frase do que falta é CONTEÚDO do card, não legenda: tinta plena. É ela que
   responde «como eu crio?» — apagá-la repetiria o defeito que este bloco paga. */
.cnl-card-falta { color: var(--text); }
.cnl-form { display: flex; flex-direction: column; gap: 8px; flex: 1; margin: 0; }
.cnl-form .lbl { margin: 0 0 4px; }
.cnl-form input[type="text"], .cnl-form input[type="url"], .cnl-form select { width: 100%; }
/* A ação desce ao pé do card: três cards da mesma altura terminam com as três
   ações alinhadas — o peso igual fica visível até na linha de base. Vale para
   o submit (criar) e para o CTA de pré-condição (o caminho do que falta). */
.cnl-form .cnl-cta, .cnl-card > .cnl-cta { margin-top: auto; align-self: flex-start; }
.cnl-card > a.cnl-cta { text-decoration: none; }
.cnl-pend { margin: 6px 0 0; }

/* Evidência NÃO-COPIÁVEL: o que o monitor gravou como esperado quando aquilo não
   é um valor publicável (linha antiga com prosa em expectedValue). Texto corrido
   de propósito — sem <code>, sem botão de copiar. */
.rc-monitor { margin: 8px 0 0; font-size: var(--fs-xs); line-height: 1.5; color: var(--muted); }
.rc-monitor b { font-weight: 600; color: var(--text); }

/* ── S4: check-rows do self-check ─────────────────────────────────────────── */
.check-rows { display: grid; gap: 2px; margin: 4px 0 6px; }
.check-row { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--border); align-items: flex-start; }
.check-row:first-child { border-top: none; }
.cr-chip { flex: 0 0 auto; padding-top: 1px; }
.cr-body { flex: 1; min-width: 0; }
.cr-pair { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-xs); color: var(--muted); }
.cr-pair code { font-size: var(--fs-xs); }
.cr-next { font-size: var(--fs-sm); color: var(--text); margin-top: 4px; }
.cr-fix { font-weight: 600; }
.cr-ok { font-size: var(--fs-sm); color: var(--muted); }

/* ── S5/S6: caixa de conexão ──────────────────────────────────────────────── */
.conn-box { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel-2); overflow: hidden; }
.cb-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 12px 14px; border-top: 1px solid var(--border); font-size: var(--fs-sm); }
.cb-row:first-child { border-top: none; }
.cb-row .cb-k { flex: 0 0 108px; color: var(--muted); font-weight: 600; font-size: var(--fs-xs); }
.cb-row code { font-family: var(--mono); font-size: var(--fs-sm); }

/* ── S6: celebração (respeita prefers-reduced-motion) ─────────────────────── */
.celebrate { text-align: center; }
.celebrate-mark { font-size: var(--fs-8xl); line-height: 1; margin-bottom: 6px; animation: pop .5s ease-out; }
.celebrate .action-sticky { justify-content: center; }
.celebrate .action-sticky .btn-primary { margin-left: 0; }
@keyframes pop { 0% { transform: scale(.4); opacity: 0; } 70% { transform: scale(1.15); } 100% { transform: scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .celebrate-mark { animation: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   CREDENCIAIS — remodelação jul/2026
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Tabela responsiva .table-cards (utilitário canônico, espelho do admin):
   aplique na <table> e ponha data-label="Rótulo" em cada <td>. No mobile
   (≤640px) cada <tr> vira um cartão empilhado "rótulo: valor". ────────────── */
@media (max-width: 640px) {
  /* Escopo por FILHO (> tbody > tr > td): sem isto o padrão desce para tabelas
     ANINHADAS dentro de uma célula e as transforma em lista sem rótulo sem
     ninguém pedir (bug que apareceu no card de IP do painel). Espelha admin.css. */
  table.table-cards > thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
  }
  table.table-cards,
  table.table-cards > tbody,
  table.table-cards > tbody > tr,
  table.table-cards > tbody > tr > td { display: block; width: 100%; }
  table.table-cards > tbody > tr {
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--panel-2); margin-bottom: 10px; padding: 4px 12px;
  }
  table.table-cards > tbody > tr > td {
    border: 0; padding: 8px 0; display: flex; gap: 14px;
    justify-content: space-between; align-items: baseline; text-align: right;
    /* wrap: célula com badge + carimbo + link de saída estourava o cartão e o
       conteúdo era CORTADO na borda. Regra idêntica no admin.css (classe
       compartilhada nos dois arquivos). */
    flex-wrap: wrap;
  }
  table.table-cards > tbody > tr > td::before {
    content: attr(data-label);
    font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .4px;
    color: var(--muted); font-weight: 600; text-align: left; flex: none;
  }
  table.table-cards > tbody > tr > td:not([data-label])::before { content: ""; }

  /* Célula que ocupa a LINHA INTEIRA (td com colspan: o bloco de avisos de um
     endereço, o estado vazio). Sem isto o par «rótulo | valor» do cartão
     espremia o conteúdo numa coluna de três palavras e empurrava o formulário
     para fora da tela. Espelha admin.css — a promessa do comentário lá em cima
     é que este utilitário é o MESMO nos dois painéis. */
  table.table-cards > tbody > tr > td[colspan],
  table.table-cards > tbody > tr > td.cell-block {
    display: block; text-align: left; white-space: normal;
  }
  table.table-cards > tbody > tr > td.cell-block::before { display: block; margin-bottom: 4px; }

  /* Célula de AÇÕES no modo card: rótulo em cima, botões com alvo ≥44px. */
  table.table-cards > tbody > tr > td.actions-cell { display: block; text-align: left; }
  table.table-cards > tbody > tr > td.actions-cell::before { display: block; margin-bottom: 6px; }
  table.table-cards > tbody > tr > td.actions-cell .inline-form { display: inline-block; margin: 2px 6px 2px 0; }
  table.table-cards > tbody > tr > td.actions-cell .btn { min-height: var(--btn-min-h); }

  /* Linha de detalhe (caixas-ferramenta) gruda no cartão da credencial acima —
     um cartão só por credencial, não dois soltos. */
  table.table-cards > tbody > tr:has(+ tr.cred-detail) {
    margin-bottom: 0; border-bottom: 0;
    border-bottom-left-radius: 0; border-bottom-right-radius: 0;
  }
  table.table-cards > tbody > tr.cred-detail {
    border-top: 0;
    border-top-left-radius: 0; border-top-right-radius: 0;
  }
  table.table-cards > tbody > tr.cred-detail > td { display: block; text-align: left; }
  .cred-tools { grid-template-columns: 1fr; }
  .scopes-editor > summary,
  .rate-editor > summary { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ── Box de AÇÃO no topo: criar credencial / chave de API (details name=
   create-cred → nativamente exclusivos, sem JS). ─────────────────────────── */
.create-bar {
  display: grid; gap: 12px; margin-bottom: 18px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}
.create-item {
  min-width: 0;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.create-item[open] { grid-column: 1 / -1; border-color: var(--accent); }
.create-item > summary {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; min-height: 44px; cursor: pointer; list-style: none;
  border-radius: var(--radius);
}
.create-item > summary::-webkit-details-marker { display: none; }
.create-item > summary:hover { background: var(--panel-2); }
.ci-plus {
  flex: none; width: 28px; height: 28px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink);
  font-size: var(--fs-xl); font-weight: 700; line-height: 1;
  transition: transform .15s ease;
}
.create-item[open] .ci-plus { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) { .ci-plus { transition: none; } }
.ci-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ci-main strong { font-size: var(--fs-md); }
.ci-main small { color: var(--muted); font-size: var(--fs-xs); }
.create-form { padding: 12px 16px 16px; border-top: 1px solid var(--border); }
.create-form input[type=text] { width: 100%; max-width: 340px; display: block; }
.create-form .btn { margin-top: 10px; }

/* ── Permissões (escopos) de chave de API ─────────────────────────────────── */
.scope-groups {
  display: grid; gap: 10px; margin: 12px 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
.scope-groups fieldset {
  margin: 0; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel-2); padding: 6px 12px 10px;
}
.scope-groups legend {
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--muted); padding: 0 4px;
}
.scope-opt {
  display: flex; gap: 10px; align-items: flex-start;
  font-weight: 400; font-size: var(--fs-sm); margin: 0; padding: 8px 0; cursor: pointer;
}
.scope-opt input { flex: none; margin-top: 4px; }
.scope-opt code {
  font-family: var(--mono); font-size: var(--fs-2xs); color: var(--muted);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: 4px; padding: 0 6px; white-space: nowrap;
}
.scope-pills { display: inline-flex; flex-wrap: wrap; gap: 4px; max-width: 300px; }
.scope-pill {
  display: inline-block; font-family: var(--mono); font-size: var(--fs-2xs);
  padding: 2px 8px; border-radius: 999px; white-space: nowrap;
  background: var(--info-bg); color: var(--info); border: 1px solid var(--info-bd);
}

/* ── Linha de detalhe por credencial/chave: caixas-ferramenta QUIETAS lado a
   lado (MENSAGENS antes de PERMISSÕES). Superfície neutra + hairline; o SSOT
   data-health segue no markup, mas aqui é só ACENTO pontual: azul no ícone e
   na seta do link de mensagens (navegação), fio âmbar de 2px nas permissões
   (mexer ali é sensível). Cola na linha de cima (o .tbl usa border-top).
   Editor aberto expande pra linha inteira via :has(). ─────────────────────── */
tr.cred-detail td { border-top: 0; padding-top: 0; }
.cred-tools {
  display: grid; gap: 10px; align-items: stretch;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}
.tool-box {
  min-width: 0; padding: 12px 14px; border-radius: var(--radius-sm);
  background: var(--panel-2); border: 1px solid var(--border);
}
/* A TOOL-BOX QUE É BLOCO DE PÁGINA precisa do respiro que a de GRADE recebe de
   graça. Dentro de `.cred-tools` quem separa é o `gap` do grid; sozinha no
   fluxo da página ela não tem nada embaixo, e encosta no cartão seguinte —
   medido em /app/protecao-rede: 0px de vão onde todos os outros vãos da tela
   são 18. O seletor é `details.tool-box` porque é exatamente essa a forma que
   aparece solta no fluxo (três telas do portal), e nunca dentro da grade. */
details.tool-box { margin-bottom: 18px; }
/* Único resto de cor de estado no bloco: fio de atenção nas Permissões. */
.tool-box[data-health="warn"] { border-left: 2px solid var(--h-ink); }
a.tool-box {
  display: flex; flex-direction: column; gap: 2px; justify-content: center;
  min-height: 44px; text-decoration: none; color: inherit;
}
a.tool-box:hover { text-decoration: none; border-color: var(--border-strong); }
.tb-head { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: var(--fs-sm); color: var(--text); }
.tb-head .ic { width: 15px; height: 15px; flex: none; color: var(--h-ink, var(--muted)); }
.tb-sub { font-size: var(--fs-xs); color: var(--tint-ink); }
.tb-arrow { color: var(--h-ink); }
/* Summary neutro (o peso é a hierarquia); chevron cinza diz "abre". */
.tool-box .scopes-editor > summary,
.tool-box .rate-editor > summary { color: var(--text); }
.tool-box .scopes-editor > summary::after,
.tool-box .rate-editor > summary::after {
  content: "▾"; color: var(--muted); font-size: var(--fs-2xs); line-height: 1;
  transition: rotate .15s ease;
}
.tool-box .scopes-editor[open] > summary::after,
.tool-box .rate-editor[open] > summary::after { rotate: 180deg; }
@media (prefers-reduced-motion: reduce) {
  .tool-box .scopes-editor > summary::after,
  .tool-box .rate-editor > summary::after { transition: none; }
}
.tool-box:has(.scopes-editor[open]),
.tool-box:has(.rate-editor[open]) { grid-column: 1 / -1; }
/* Nome/tag da chave — mesma caixa neutra, mas é um form inline. */
.tool-rename { display: flex; flex-direction: column; gap: 8px; }
.rename-row { display: flex; gap: 8px; align-items: center; }
.rename-row input[type=text] { flex: 1 1 auto; min-width: 0; }
.rename-row .btn { flex: none; }
/* Nome amigável da credencial na lista; o prefixo vira secundário. */
.cred-name { display: block; font-weight: 600; font-size: var(--fs-sm); margin-bottom: 2px; }
/* O rótulo do campo saiu daqui: deixou de ser «o rótulo do form de criar chave»
   e virou primitivo de formulário, junto do `.field-row`/`.field-stack` (busque
   por `.field-label`). O motivo é que /app/pesquisas e /app/migracao precisavam
   EXATAMENTE deste rótulo e não podiam levar a casca `.create-form` junto. */

/* Editor de PERMISSÕES de chave já criada (mesma anatomia do admin) e editor de
   LIMITE DE ENVIO da credencial/chave — mesma anatomia de propósito: são as duas
   caixas em que o cliente MEXE (as demais só levam a algum lugar). */
.scopes-editor,
.rate-editor { font-size: var(--fs-sm); min-width: 0; }
.scopes-editor > summary,
.rate-editor > summary {
  display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
  cursor: pointer; min-height: 32px;
  color: var(--brand-2); font-weight: 600; font-size: var(--fs-sm);
}
.scopes-editor > summary:hover,
.rate-editor > summary:hover { text-decoration: underline; }
.scopes-editor[open] > summary,
.rate-editor[open] > summary { margin-bottom: 8px; }
/* Quatro janelas lado a lado no desktop; quebram sozinhas no estreito. O input
   NÃO ganha largura fixa: número de 5 dígitos (teto de dia) tem que caber. */
.rate-grid {
  display: grid; gap: 8px; margin: 0 0 8px;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
}
.rate-grid label {
  display: flex; flex-direction: column; gap: 4px;
  font-size: var(--fs-xs); color: var(--muted);
}
.rate-grid input { width: 100%; min-width: 0; }
.rate-form p { margin: 0 0 8px; }
.scopes-form .scope-groups { margin-top: 0; }

/* Fileira DISCRETA de ícones de opções/permissões ativas ao lado do nome —
   informativos (title/aria-label PT), não clicáveis. Tom semântico opcional. */
.opt-icons { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; vertical-align: middle; }
.opt-ic { display: inline-flex; color: var(--muted); cursor: help; }
.opt-ic .ic { width: 14px; height: 14px; }
.opt-ic[data-tone="warn"] { color: var(--warn); }
.opt-ic[data-tone="bad"] { color: var(--bad); }

/* ── Dados de conexão sob demanda ─────────────────────────────────────────── */
.conn-details > summary { cursor: pointer; font-weight: 600; color: var(--brand-2); font-size: var(--fs-md); }
.conn-details > summary:hover { text-decoration: underline; }
.conn-details[open] > summary { margin-bottom: 6px; }

/* Ações inline no desktop (tabela densa). */
.actions-cell { white-space: nowrap; }
.actions-cell .inline-form { margin-right: 4px; }
.break { word-break: break-all; }

/* ═══════════════════════════════════════════════════════════════════════════
   DOCUMENTAÇÃO DA API (/app/api) — referência dev-first no shell do portal.
   Reusa os tokens e componentes já validados (.card, .tbl, .badge-*, os trios
   data-health --h-*). Só adiciona o que não existia: bloco de código com scroll
   horizontal próprio (o body NUNCA rola de lado) + botão de copiar, cabeçalho de
   endpoint (verbo + path + escopo) e o índice em pílulas.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Índice em pílulas — âncoras para as seções. */
.doc-toc { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.doc-toc a {
  padding: 6px 12px; border-radius: 999px; font-size: var(--fs-sm); font-weight: 600;
  border: 1px solid var(--border); background: var(--panel); color: var(--muted);
}
.doc-toc a:hover { color: var(--text); border-color: var(--border-strong); text-decoration: none; }

/* Cada seção é um .card; o id ancora do índice precisa de folga do topo. */
.doc-section { scroll-margin-top: 16px; }
.doc-section > h2 { font-size: var(--fs-2xl); margin: 0 0 4px; }
.doc-section > .card-sub { color: var(--muted); font-size: var(--fs-sm); margin: 0 0 14px; }
.doc-section p { color: var(--text); }
.doc-section p.muted, .doc-section .muted { color: var(--muted); }
.doc-section ul { margin: 8px 0; padding-left: 20px; color: var(--text); }
.doc-section ul li { margin: 4px 0; }
.doc-section code.inline {
  font-family: var(--mono); font-size: var(--fs-xs);
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 4px; padding: 1px 6px; word-break: break-word;
}

/* Bloco de código com scroll horizontal PRÓPRIO + botão copiar sobreposto. */
.code-wrap { position: relative; margin: 12px 0; }
/* Bloco de código — regra IDÊNTICA à do admin.css (tokens do painel, acabamento
   daqui). Raio 6px: o degrau de "controle/bloco" da escala de raio. */
.code-block {
  margin: 0; overflow-x: auto; -webkit-overflow-scrolling: touch;
  background: var(--surface-3); border: 1px solid var(--line); border-radius: 6px;
  padding: 14px 16px;
  font-family: var(--mono); font-size: var(--fs-sm); line-height: 1.55; color: var(--ink);
}
.code-block code { font-family: inherit; white-space: pre; }
.code-copy {
  position: absolute; top: 8px; right: 8px;
  padding: 8px 12px; font-size: var(--fs-xs); font-weight: 600; line-height: 1;
  border: 1px solid var(--border-strong); border-radius: 6px;
  background: var(--panel); color: var(--muted); cursor: pointer;
}
.code-copy:hover { color: var(--text); background: var(--panel-2); }

/* Cartão de endpoint. */
.ep {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); box-shadow: var(--shadow-sm);
  padding: 16px 18px; margin-bottom: 14px; scroll-margin-top: 16px;
}
.ep-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
.method {
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: 700; letter-spacing: .5px;
  padding: 4px 10px; border-radius: 6px;
  background: var(--h-bg); color: var(--h-ink); border: 1px solid var(--h-line);
}
.ep-path { font-family: var(--mono); font-size: var(--fs-md); font-weight: 600; color: var(--text); word-break: break-all; }
.ep-scope { font-size: var(--fs-xs); color: var(--muted); margin-left: auto; }
.ep-scope code {
  font-family: var(--mono); font-size: var(--fs-2xs);
  background: var(--panel-2); border: 1px solid var(--border);
  border-radius: 4px; padding: 1px 6px;
}
.ep-desc { color: var(--muted); font-size: var(--fs-sm); margin: 2px 0 4px; }
.ep h4 {
  margin: 14px 0 2px; font-size: var(--fs-2xs); text-transform: uppercase;
  letter-spacing: .4px; color: var(--muted); font-weight: 700;
}
@media (max-width: 480px) {
  .ep-scope { margin-left: 0; width: 100%; }
}

/* Modal da mensagem recebida (inbound Fase 2) — leitura + anexos no portal do
   cliente. O portal não tinha .eml-modal; definimos o shell aqui, theme-aware. */
/* RODADA 2 — os fallbacks cravados (`var(--surface, #fff)`) sumiram. Eles não
   eram rede de segurança: --surface/--ink/--line não existiam nesta folha, então
   o fallback é que renderizava — e ele é FIXO. Resultado: este modal saía BRANCO
   com texto escuro no tema escuro do cliente. Agora os nomes resolvem. */
.eml-modal { padding: 0; border: 1px solid var(--line-2); border-radius: var(--radius);
  width: 760px; max-width: 92vw; max-height: 86vh; background: var(--surface);
  color: var(--ink); box-shadow: var(--sh-lg); overflow: hidden; }
.eml-modal::backdrop { background: var(--scrim); }
.eml-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--surface-2); }
.eml-modal-head strong { font-size: var(--fs-md); font-weight: 700; }
/* `.mono` SOLTA — 26/08. A folha só tinha a versão ESCOPADA logo abaixo, e o
   resultado é que 27 dos 28 usos de `class="mono"` no portal renderizavam na
   fonte de texto: identificadores, códigos e tokens saindo proporcionais
   justamente onde a coluna alinhada é o ponto. Dez deles em /app/migracao, que
   foi onde o dono reparou.

   É A MESMA BRECHA DA `.field-label`, e por isso vale escrever: o gate
   `classeSemRegra` casa o NOME da classe contra a folha, não o ALCANCE do
   seletor. Com só `.eml-modal-head .mono` escrita, `class="mono"` numa tabela
   qualquer PASSA no gate e chega à tela sem pintar. Só o navegador acusa.

   Família e nada mais: tamanho e peso continuam de quem usa, e a escopada
   abaixo segue mandando no que é dela. Nenhum valor novo. */
.mono { font-family: var(--mono); }
.eml-modal-head .mono { font: 600 var(--fs-xs) var(--mono); color: var(--muted); }
.eml-modal-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
/* `min-height: 0` existe para o caso de a folha ganhar um mínimo global de
   botão (o admin.css tem: 44px). Aqui é inócuo hoje e mantém a regra IDÊNTICA
   à do painel — a divergência era só de compensação, não de intenção. */
.eml-modal-close { background: none; border: 0; color: var(--muted); font-size: var(--fs-xl);
  line-height: 1; cursor: pointer; padding: 6px 10px; border-radius: 6px; min-height: 0; }
/* Realce do X: --neutral-bg (1.13:1 contra o painel). --surface-3 mede 1.05:1 e
   o hover praticamente desaparece — o mesmo gesto foi aplicado no admin.css. */
.eml-modal-close:hover { color: var(--ink); background: var(--neutral-bg); }
.in-row { cursor: pointer; }
.in-row:hover { background: var(--surface-2); }
.in-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* DESFECHO DA MENSAGEM (105) — gêmeo do bloco em admin.css, de propósito: a
   bloqueada tem de ter a MESMA cara nas duas superfícies, senão o operador
   descreve por telefone uma tela que o cliente não está vendo.
   AVISO, nunca erro: --warn, jamais --bad. Não foi falha nossa nem do
   remetente; foi uma escolha da conta. Fundo NÃO preenchido na linha — cor é
   acento pontual; o bloco tingido fica reservado ao aviso do modal. */
.in-row-blocked > td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
.in-outcome { margin-right: 6px; vertical-align: baseline; }
/* No mobile a .table-cards vira cartão: o filete migra da 1ª célula para a
   borda esquerda do cartão inteiro, senão ele marcaria só a linha «Recebida». */
@media (max-width: 720px) {
  .in-row-blocked > td:first-child { box-shadow: none; }
  .in-row-blocked { box-shadow: inset 3px 0 0 var(--warn); }
}

.in-notice {
  margin: 0 0 14px; padding: 12px 14px; border-radius: var(--radius-sm);
  background: var(--h-bg); color: var(--h-ink);
  border: 1px solid var(--h-line); border-left-width: 3px;
}
.in-notice[hidden] { display: none; }
.in-notice p { margin: 0; }
.in-notice-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-sm); line-height: 1.45; }
/* SEM opacity/muted aqui: --warn sobre --warn-bg dá 4.99:1 (claro), que passa
   AA sem folga — atenuar a 92% derruba para 4.27 e REPROVA. A hierarquia
   dentro do aviso vem de tamanho e peso, que não custam contraste. */
.in-notice .in-notice-meta { margin-top: 6px; font-size: var(--fs-xs); line-height: 1.5; }
.in-notice .in-notice-next { margin-top: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: var(--fs-xs); line-height: 1.5; }
.in-notice-cta {
  flex: none; font-weight: 600; font-size: var(--fs-xs); color: var(--h-ink);
  border: 1px solid var(--h-line); border-radius: 6px; padding: 6px 12px;
  text-decoration: none; background: color-mix(in srgb, var(--panel) 45%, transparent);
  min-height: 32px; display: inline-flex; align-items: center;
}
.in-notice-cta:hover { background: var(--panel); }
.in-notice-cta[hidden] { display: none; }
@media (pointer: coarse) { .in-notice-cta { min-height: 44px; padding: 6px 16px; } }

.in-modal-body { margin: 0; padding: 16px; overflow: auto; max-height: calc(86vh - 50px); color: var(--ink); }
.in-modal-body h4 { margin: 16px 0 6px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.in-meta { margin: 0; display: grid; gap: 4px; }
.in-meta > div { display: grid; grid-template-columns: 120px 1fr; gap: 10px; font-size: var(--fs-sm); }
.in-meta dt { color: var(--muted); }
.in-meta dd { margin: 0; word-break: break-word; }
.in-body { margin: 0; padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2); font: var(--fs-xs)/1.55 var(--mono);
  white-space: pre-wrap; word-break: break-word; color: var(--ink); max-height: 40vh; overflow: auto; }
.in-atts { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.in-atts li { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: var(--fs-sm);
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); }
.in-att-name { font-weight: 600; word-break: break-all; }
.in-att-meta { color: var(--muted); font-size: var(--fs-xs); }
/* «Baixar» o anexo. RODADA 4 MEDIU 4,25:1 — reprova por pouco, e por 0,25 de
   um limiar que não negocia. É o ÚNICO lugar do portal que usa --accent como
   tinta de TEXTO (as outras 5 ocorrências são borda ou preenchimento), e é aí
   que está a lição: --accent foi calibrado para ser PREENCHIDO, com
   --accent-ink por cima. Como tinta ele mede 4,60:1 sobre o branco e 4,25:1
   sobre --bg — passa ou reprova conforme a superfície embaixo, o que faz dele
   uma cor de texto que ninguém consegue usar com segurança.
   --accent-hover é o degrau ESCURO da mesma rampa (e no tema escuro, o degrau
   CLARO — que é exatamente o que texto precisa de cada lado): 5,79:1 aqui,
   5,53:1 sobre --neutral-bg do próprio hover, 6,66:1 no escuro. Passa nas
   quatro superfícies. Não é token novo: é a rampa que já existia, usada no
   degrau certo. */
.in-att-dl { margin-left: auto; font-weight: 600; color: var(--accent-hover); text-decoration: none;
  padding: 4px 10px; border: 1px solid var(--line-2); border-radius: 6px; }
.in-att-dl:hover { background: var(--neutral-bg); }

/* ═══════════════════════════════════════════════════════════════════════════
   CASA ÚNICA DO DOMÍNIO — navegação de seção + prontidão da recepção
   ═══════════════════════════════════════════════════════════════════════════
   Espelho do bloco de mesmo nome no admin.css: a página do domínio carrega
   ENVIO e RECEPÇÃO, e as duas seções ganham porta com o ESTADO impresso. As
   classes são as MESMAS dos dois lados de propósito — mesma coisa, mesmo nome:
   quem for mexer numa acha a outra.
   ═══════════════════════════════════════════════════════════════════════════ */
.dom-sectnav { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 18px; }
.dom-sectnav a {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 10px 14px; min-height: 44px;          /* alvo de toque ≥44px */
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--panel); color: var(--text);
  text-decoration: none; font-size: var(--fs-sm);
}
.dom-sectnav a:hover { background: var(--panel-2); border-color: var(--border-strong); text-decoration: none; }
.dom-sectnav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.dsn-label { font-weight: 650; }

/* Âncora de seção: o cabeçalho não pode nascer colado no topo da janela quando
   o navegador salta para #envio/#recepcao/#inbound. scroll-margin, sem JS. */
.dom-sect-anchor { display: block; height: 0; scroll-margin-top: 18px; }
.dom-sect { scroll-margin-top: 18px; }

.dom-sect-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 6px; }
.dom-sect-head h2 { margin: 0; }
.dom-sub-h { margin: 22px 0 8px; font-size: var(--fs-lg); }

/* ── Prontidão da recepção: os 3 passos, e onde parou ───────────────────────
   «2 de 3 prontos» NÃO é erro. O passo pendente é NEUTRO (círculo vazado),
   nunca vermelho: vermelho ensinaria que estar no meio do caminho é falha. */
.rd-steps {
  list-style: none; margin: 14px 0 18px; padding: 0;
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}
.rd-step {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 12px 12px; border-radius: var(--radius);
  background: var(--surface-3); border: 1px solid var(--line);
  border-left: 3px solid var(--h-ink);
}
.rd-step-ic { line-height: 0; color: var(--h-ink); flex: none; margin-top: 1px; }
.rd-step-ic .ic { width: 17px; height: 17px; }
.rd-step-label { font-weight: 650; font-size: var(--fs-sm); }
.rd-step-hint { margin: 4px 0 0; font-size: var(--fs-xs); line-height: 1.45; color: var(--muted); }

/* Slot da simulação ("testar antes de ativar"): enquanto vazio não ocupa
   espaço nem deixa um título órfão na página. */
#recepcao-simular:empty { display: none; }

@media (max-width: 640px) {
  /* A ação tem de caber no polegar: os pills de seção ocupam a linha inteira,
     duas colunas lado a lado, sem rolagem horizontal. */
  .dom-sectnav { display: grid; grid-template-columns: 1fr 1fr; }
  .dom-sectnav a { justify-content: center; text-align: center; flex-direction: column; gap: 4px; padding: 8px 10px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ETAPA ④ DA NORMA — SIMULADOR DE RECEPÇÃO («testar antes de ativar»)
   ═══════════════════════════════════════════════════════════════════════════
   Bloco ESPELHADO em public/admin.css e public/portal.css — quem mexer num mexe
   no outro. Só vocabulário compartilhado (--panel/--border/--text/--muted, o
   trio [data-health], a escala --fs-*): uma regra escrita aqui resolve nos dois.

   DIREÇÃO VISUAL. O dono vetou bloco de fundo colorido como recurso de
   organização (21/07/2026, «mais sutil»). Então: superfície neutra, hierarquia
   por PESO e ESPAÇO, e a cor de estado entra em três lugares pontuais e mais
   nenhum — a marca circular do veredito, o fio de 2px da nota, o ponto do passo.
   O texto do veredito fica em --text: contraste máximo, e a cor não é o portador
   do significado (a frase já começa com «Aceita —» / «Recusado —», e a marca
   traz o glifo ✓/✕/○ para quem não distingue as duas).

   LARGURA. Desktop = duas colunas, pergunta à esquerda e resposta à direita
   (norma §8: «desktop não é o mobile centralizado com 70% de tela vazia»).
   CONTAINER query, não media query: a mesma seção vive dentro de .panel no
   painel e de .card no portal, com larguras diferentes — quem decide o layout é
   o espaço que o componente tem, não o tamanho da janela. */
#recepcao-simular { container-type: inline-size; }

.sim-wrap { display: grid; gap: 18px; align-items: start; }
/* A coluna da RESPOSTA é limitada por MEDIDA, não por «o que sobrou»: a 1400px
   um `1fr` daria linhas de ~100 caracteres, e texto explicativo a 100 caracteres
   é texto que ninguém termina. 66ch é a faixa confortável de leitura. */
@container (min-width: 700px) {
  .sim-wrap { grid-template-columns: minmax(15rem, 20rem) minmax(0, 66ch); gap: 28px; justify-content: start; }
}

/* ── A PERGUNTA ───────────────────────────────────────────────────────────── */
.sim-ask { min-width: 0; }
.sim-lbl { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 0 0 6px; }
.sim-field { display: flex; gap: 8px; flex-wrap: wrap; }
/* 44px é alvo de toque, não estética: o campo e o botão são o que se aperta no
   celular, e aqui eles são a ação inteira da seção. */
.sim-field input[type="text"] { flex: 1 1 12rem; min-width: 0; min-height: 44px; }
.sim-field .btn, .sim-field button { flex: 0 0 auto; min-height: 44px; }
.sim-opt { margin-top: 14px; }
.sim-opt > summary {
  cursor: pointer; font-size: var(--fs-sm); font-weight: 600; color: var(--brand-2);
  min-height: 32px; display: flex; align-items: center;
}
.sim-opt > summary:hover { text-decoration: underline; }
.sim-opt .sim-lbl { margin-top: 10px; }
.sim-opt input[type="text"] { width: 100%; min-height: 44px; }

/* ── A RESPOSTA ───────────────────────────────────────────────────────────── */
.sim-out { min-width: 0; position: relative; }
/* ESTADO DE TRABALHO: um fio de 2px em movimento acima do painel, e o resultado
   ANTERIOR continua legível embaixo. Spinner que apaga o conteúdo faria a tela
   piscar a cada tecla; o que se comunica aqui é «estou recalculando», não
   «esqueça o que você estava lendo». Anima só transform (GPU, sem layout). */
.sim-out[aria-busy="true"]::before {
  content: ""; position: absolute; top: -10px; left: 0; right: 0; height: 2px;
  border-radius: 999px; background: var(--border);
}
.sim-out[aria-busy="true"]::after {
  content: ""; position: absolute; top: -10px; left: 0; width: 34%; height: 2px;
  border-radius: 999px; background: var(--accent);
  animation: sim-busy 900ms ease-in-out infinite alternate;
}
@keyframes sim-busy { from { transform: translateX(0); } to { transform: translateX(194%); } }
@media (prefers-reduced-motion: reduce) {
  .sim-out[aria-busy="true"]::after { animation: none; width: 100%; opacity: .5; }
}

/* O fio de estado à esquerda do painel: sempre presente (sem salto de layout ao
   trocar de estado) e sempre discreto. A recusa é a única que sobe o tom — é o
   estado que não pode passar despercebido. */
.sim-panel {
  min-width: 0;
  border-left: 2px solid var(--h-line); padding-left: 14px;
}
.sim-panel[data-health="bad"] { border-left-color: var(--bad); }

/* A PONTE com «Adicionar endereço»: por que o painel mudou enquanto se digita
   lá em cima. */
.sim-cand { margin: 0 0 12px; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }
.sim-cand code { font-family: var(--mono); color: var(--text); word-break: break-all; }
.sim-cand[data-in="nao"] { color: var(--bad); font-weight: 600; }
.sim-cand[data-in="nao"] code { color: var(--bad); }

/* O VEREDITO — a coisa mais alta da hierarquia. Uma frase, do tamanho de uma
   frase; tudo abaixo dela é explicação. */
.sim-verdict {
  display: flex; gap: 10px; align-items: flex-start; margin: 0;
  font-size: var(--fs-2xl); font-weight: 600; line-height: 1.35;
}
.sim-mark {
  flex: none; width: 26px; height: 26px; margin-top: 1px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-md); font-weight: 700;
  color: var(--h-ink); background: var(--h-bg); border: 1px solid var(--h-line);
}
.sim-verdict-t { min-width: 0; }
.sim-why { margin: 10px 0 0; color: var(--muted); font-size: var(--fs-md); line-height: 1.55; }
.sim-fail { margin: 0; font-size: var(--fs-md); font-weight: 600; color: var(--bad); }

/* O FLUXO (norma §6: frase legível, não JSON; fluxo visual, não tabela).
   Espinha em hairline + um ponto por passo. Nenhum fundo: o único passo que
   ganha tinta é o da recusa, e ganha no ponto e na palavra. */
.sim-flow { list-style: none; margin: 18px 0 0; padding: 0; }
.sim-step { position: relative; padding-left: 22px; }
.sim-step + .sim-step { margin-top: 12px; }
.sim-step + .sim-step::before {
  content: ""; position: absolute; left: 6px; top: -12px; width: 1px; height: 14px;
  background: var(--border);
}
.sim-dot {
  position: absolute; left: 3px; top: 7px; width: 7px; height: 7px;
  border-radius: 50%; background: var(--faint);
}
.sim-step-t { font-size: var(--fs-md); line-height: 1.55; }
.sim-step[data-tipo="entrada"] .sim-dot { background: var(--muted); }
.sim-step[data-tipo="regra"] .sim-dot { background: var(--accent); }
.sim-step[data-tipo="recusa"] .sim-dot { background: var(--bad); }
.sim-step[data-tipo="recusa"] .sim-step-t { color: var(--bad); font-weight: 600; }
/* ZERO CANAIS É O CASO NORMAL — tinta neutra, jamais de alerta. */
.sim-step[data-tipo="sem-canal"] .sim-step-t { color: var(--muted); }

/* O QUE MUDA O RESULTADO — inclusive o aviso de PRECEDÊNCIA, que é o que mais
   confunde. Fio de 2px na cor do estado; o corpo do texto fica em --text para
   não perder contraste (dentro de bloco de estado, .muted reprova AA). */
.sim-notes { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 12px; }
/* A pilha de ERROS fica ACIMA do veredito: é o que a pessoa pode consertar
   agora, e o veredito fala de um e-mail hipotético. */
.sim-notes-top { margin: 0 0 14px; }
.sim-note {
  padding: 1px 0 1px 12px; border-left: 2px solid var(--h-ink);
  font-size: var(--fs-md); line-height: 1.55;
}
.sim-note-k {
  display: inline-block; margin-right: 8px; vertical-align: 1px;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--h-ink);
}

/* COMO AS REGRAS FORAM AVALIADAS + O QUE NÃO SE SABE.
   Os dois colapsam (kernel: só mostra quando precisa mostrar), e a ordem abre
   sozinha quando a precedência mordeu de verdade. Colapsado nunca é apagado. */
.sim-order, .sim-unknown { margin-top: 18px; }
.sim-order-sum {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  min-height: 32px; font-size: var(--fs-sm); font-weight: 600; color: var(--brand-2);
}
.sim-order-sum::-webkit-details-marker { display: none; }
.sim-order-sum:hover { text-decoration: underline; }
.sim-order-sum:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.sim-caret { color: var(--muted); transition: transform .14s ease; }
details[open] > .sim-order-sum .sim-caret { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .sim-caret { transition: none; } }
/* Contagem: tinta --text, não --muted — --muted sobre --surface-3 mede 4.33:1 e
   REPROVA AA. */
.sim-order-n {
  font-size: var(--fs-2xs); font-weight: 700; color: var(--text);
  background: var(--surface-3); border: 1px solid var(--border);
  border-radius: 999px; padding: 1px 8px;
}

.sim-order-body { padding: 2px 0 0 20px; }
.sim-order-sub {
  margin: 16px 0 2px; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.sim-rules { list-style: none; margin: 0; padding: 0; }
.sim-rule { padding: 10px 0; border-top: 1px solid var(--border); }
.sim-rules > .sim-rule:first-child { border-top: 0; padding-top: 4px; }
.sim-rule-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sim-rule-addr { font-family: var(--mono); font-size: var(--fs-sm); word-break: break-all; }
.sim-rule[data-state="venceu"] .sim-rule-addr { font-weight: 700; }
.sim-rule[data-state="nao"] .sim-rule-addr,
.sim-rule[data-state="off"] .sim-rule-addr { color: var(--muted); }
.sim-rule-why { margin: 4px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }
/* NÃO existe número de posição nesta lista, e a ausência é a decisão: um «1» e
   um «2» ao lado das regras seriam lidos como prioridade editável — que não
   existe (não há coluna, e a precedência exato-vence-catch-all é estrutural).
   A sequência é a ordem visual; o porquê está escrito em cada item. */
.sim-tag {
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em; white-space: nowrap;
  color: var(--h-ink); border: 1px solid var(--h-line); border-radius: 999px; padding: 2px 8px;
}

.sim-unknown-list { margin: 6px 0 0; padding-left: 20px; }
.sim-unknown-list li { font-size: var(--fs-sm); color: var(--muted); line-height: 1.55; }
.sim-unknown-list li + li { margin-top: 8px; }

/* O CAMPO DO FORMULÁRIO DE CIMA marcado quando o MOTOR recusa a regra que está
   sendo digitada (formato inválido, ou endereço/catch-all que já existe). Sem
   isto o `aria-invalid` que o JS põe seria anúncio só para leitor de tela — quem
   enxerga leria o «Corrija» no painel e não saberia qual campo arrumar. A cor
   não é o único portador: a frase do painel nomeia o problema, e o atributo
   nativo já diz «inválido» para a tecnologia assistiva. */
:where(input, select)[aria-invalid="true"] { border-color: var(--bad); }
:where(input, select)[aria-invalid="true"]:focus-visible { outline-color: var(--bad); }

/* ═══════════════════════════════════════════════════════════════════════════
   WIZARD DE CONFIGURAÇÃO DA RECEPÇÃO (.wz-*) — SÓ NO PORTAL
   ═══════════════════════════════════════════════════════════════════════════
   ESTE BLOCO NÃO É ESPELHADO EM admin.css, e a ausência é a decisão, não o
   esquecimento. A norma de UX separa os dois públicos por escrito: wizard é
   para «primeira vez, usuário não familiarizado», e o operador conhece o
   produto — o painel fica com o formulário direto, que é o atalho que a norma
   manda dar ao técnico. Espelhar estas classes lá seria CSS morto, e CSS morto
   é o começo de uma divergência silenciosa entre as duas superfícies.

   A MÁQUINA DE ETAPAS É CSS PURO, e é onde este bloco gasta a sua ousadia. Um
   radiogroup de verdade guarda a etapa corrente; `:has()` mostra o passo certo
   e os campos que aquela escolha torna relevantes. Consequência: trocar de
   etapa não recarrega, não perde o preenchido e não depende de o script ter
   carregado — e o modo avançado, que abre as quatro de uma vez, não pode perder
   nada porque não reconstrói nada: só muda o que está visível.

   DIREÇÃO VISUAL — a mesma do bloco .sim-* logo acima, e pelo mesmo veto do
   dono (21/07/2026: nada de bloco de fundo colorido para organizar).
   Superfície neutra, hierarquia por PESO e ESPAÇO, e a cor de estado em pontos
   contados: o fio de 2px do trilho na etapa corrente, a borda do card
   escolhido, o fio do bloco condicional. Em nenhum deles a cor é o único
   portador — o trilho tem numeral e posição, o card tem um checkbox nativo
   marcado, e o campo condicional só existe quando é relevante.

   LARGURA por CONTAINER query e não media query: esta seção mora dentro de um
   .card, e quem decide o layout é o espaço que o componente tem, não o tamanho
   da janela. */
.wz { container: wz / inline-size; }

/* ── A MÁQUINA ──────────────────────────────────────────────────────────────
   Os controles ficam fora de tela mas FOCÁVEIS (nunca display:none, nunca
   visibility:hidden): é por este grupo que o teclado anda entre as etapas com
   as setas, e é ele que o leitor de tela anuncia como «Etapa 2 de 4,
   selecionado» — estado nativo, que não fica velho como um aria-current escrito
   no servidor ficaria assim que o CSS trocasse o passo. */
.wz-machine { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.wz-r {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── O TRILHO DE PROGRESSO ──────────────────────────────────────────────────
   «Wizard sem indicador de progresso» é anti-padrão vetado. Ele também é o
   CONTROLE: cada item é o <label> do radio da etapa, então dá para pular para
   uma etapa já vista sem desfazer o caminho. Todas são alcançáveis de
   propósito — nada aqui é obrigatório, e espiar o resultado da etapa ④ antes de
   preencher é uso legítimo, não erro a impedir. */
.wz-prog { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0 0 16px; padding: 0; }
.wz-prog-i { flex: 1 1 0; min-width: 0; }
.wz-prog-l {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 10px 8px 10px 0; min-height: 44px;
  border-top: 2px solid var(--border); border-radius: 4px;
}
.wz-prog-n {
  flex: none; width: 20px; height: 20px; border-radius: 999px;
  display: grid; place-items: center;
  font-size: var(--fs-3xs); font-weight: 700;
  background: var(--neutral-bg); color: var(--muted); border: 1px solid var(--neutral-line);
}
.wz-prog-t {
  font-size: var(--fs-sm); font-weight: 600; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wz-prog-l:hover .wz-prog-t { color: var(--text); }
.wz:has(#wz-s1:checked) .wz-prog-i[data-step="1"] .wz-prog-l,
.wz:has(#wz-s2:checked) .wz-prog-i[data-step="2"] .wz-prog-l,
.wz:has(#wz-s3:checked) .wz-prog-i[data-step="3"] .wz-prog-l,
.wz:has(#wz-s4:checked) .wz-prog-i[data-step="4"] .wz-prog-l { border-top-color: var(--accent); }
.wz:has(#wz-s1:checked) .wz-prog-i[data-step="1"] .wz-prog-t,
.wz:has(#wz-s2:checked) .wz-prog-i[data-step="2"] .wz-prog-t,
.wz:has(#wz-s3:checked) .wz-prog-i[data-step="3"] .wz-prog-t,
.wz:has(#wz-s4:checked) .wz-prog-i[data-step="4"] .wz-prog-t { color: var(--text); }
.wz:has(#wz-s1:checked) .wz-prog-i[data-step="1"] .wz-prog-n,
.wz:has(#wz-s2:checked) .wz-prog-i[data-step="2"] .wz-prog-n,
.wz:has(#wz-s3:checked) .wz-prog-i[data-step="3"] .wz-prog-n,
.wz:has(#wz-s4:checked) .wz-prog-i[data-step="4"] .wz-prog-n {
  background: var(--accent); color: var(--on-solid); border-color: var(--accent);
}
/* O ANEL DE FOCO DO RADIO INVISÍVEL, desenhado no item do trilho. Sem isto quem
   navega por teclado moveria a seleção sem ver onde está — foco invisível é
   falha de AA, não detalhe de acabamento. */
.wz:has(#wz-s1:focus-visible) .wz-prog-i[data-step="1"] .wz-prog-l,
.wz:has(#wz-s2:focus-visible) .wz-prog-i[data-step="2"] .wz-prog-l,
.wz:has(#wz-s3:focus-visible) .wz-prog-i[data-step="3"] .wz-prog-l,
.wz:has(#wz-s4:focus-visible) .wz-prog-i[data-step="4"] .wz-prog-l {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

/* NO CELULAR OS QUATRO RÓTULOS NÃO CABEM, e truncar os quatro («Ca… Dir… Aju…
   Tes…») é a pior das saídas: ocupa o espaço todo e não informa nenhum. Então
   só o rótulo da etapa CORRENTE aparece, e os outros ficam no numeral — que
   com a posição já diz onde se está e quanto falta. O nome completo continua
   no aria-label do rádio, para quem navega por leitor de tela. */
@container wz (max-width: 559px) {
  .wz-prog-i { flex: 0 0 auto; }
  .wz-prog-t { display: none; }
  .wz:has(#wz-s1:checked) .wz-prog-i[data-step="1"],
  .wz:has(#wz-s2:checked) .wz-prog-i[data-step="2"],
  .wz:has(#wz-s3:checked) .wz-prog-i[data-step="3"],
  .wz:has(#wz-s4:checked) .wz-prog-i[data-step="4"] { flex: 1 1 auto; }
  .wz:has(#wz-s1:checked) .wz-prog-i[data-step="1"] .wz-prog-t,
  .wz:has(#wz-s2:checked) .wz-prog-i[data-step="2"] .wz-prog-t,
  .wz:has(#wz-s3:checked) .wz-prog-i[data-step="3"] .wz-prog-t,
  .wz:has(#wz-s4:checked) .wz-prog-i[data-step="4"] .wz-prog-t { display: block; }
}

/* ── MODO AVANÇADO — um clique, nunca submenu (norma §4) ───────────────────── */
.wz-advline { margin: 0 0 18px; }
.wz-advl {
  display: inline-flex; align-items: center; min-height: 44px; cursor: pointer;
  font-size: var(--fs-sm); font-weight: 600; color: var(--brand-2);
}
.wz-advl:hover { text-decoration: underline; }
.wz:has(#wz-adv:focus-visible) .wz-advl { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
.wz-adv-on { display: none; }
.wz:has(#wz-adv:checked) .wz-adv-off { display: none; }
.wz:has(#wz-adv:checked) .wz-adv-on { display: inline; }
.wz:has(#wz-adv:checked) .wz-prog { display: none; }

/* ── AS DUAS COLUNAS (norma §8) ─────────────────────────────────────────────
   «Desktop não é o mobile centralizado com 70% de tela vazia»: as perguntas à
   esquerda, o preview ao vivo à direita, atualizando enquanto se digita. A
   coluna da direita tem largura de LEITURA e não «o que sobrou» — o painel de
   resultado é texto explicativo, e texto explicativo largo demais é texto que
   ninguém termina. */
.wz-grid { display: grid; gap: 24px; align-items: start; }
@container wz (min-width: 800px) {
  .wz-grid { grid-template-columns: minmax(0, 1fr) minmax(19rem, 23rem); gap: 32px; }
}
.wz-form { min-width: 0; }
.wz-side { min-width: 0; }
/* No celular a simulação É a etapa ④ — é lá que ela é a pergunta. Deixá-la
   embaixo das quatro etapas transformaria «uma decisão por tela» numa rolagem
   com a decisão no meio. */
@container wz (max-width: 799px) {
  .wz-side { display: none; }
  .wz:has(#wz-s4:checked) .wz-side,
  .wz:has(#wz-adv:checked) .wz-side { display: block; }
}
.wz-side-h { margin: 0 0 4px; font-size: var(--fs-lg); font-weight: 700; color: var(--text); }

/* ── AS ETAPAS ──────────────────────────────────────────────────────────────
   Uma por vez; todas de uma vez no modo avançado. O <fieldset> é zerado porque
   está aqui pela SEMÂNTICA (agrupar os controles de uma pergunta sob a <legend>
   que é a pergunta), não pela moldura. */
.wz-step { border: 0; padding: 0; margin: 0; min-inline-size: 0; display: none; }
.wz:has(#wz-s1:checked) .wz-step[data-step="1"],
.wz:has(#wz-s2:checked) .wz-step[data-step="2"],
.wz:has(#wz-s3:checked) .wz-step[data-step="3"],
.wz:has(#wz-s4:checked) .wz-step[data-step="4"] { display: block; }
.wz:has(#wz-adv:checked) .wz-step { display: block; }
.wz:has(#wz-adv:checked) .wz-step + .wz-step {
  margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--border);
}

.wz-q { padding: 0; margin: 0 0 8px; font-size: var(--fs-2xl); font-weight: 700; color: var(--text); line-height: 1.3; }
/* O numeral é um marcador editorial, não um segundo distintivo: quieto, em
   mono, à frente da pergunta. Ele existe porque no modo avançado o trilho some
   e a pergunta precisa continuar ancorada na sua etapa. Em --muted (e não em
   --muted-2) porque é TEXTO, e texto responde por 4.5:1. */
.wz-q-n { font-family: var(--mono); font-weight: 600; color: var(--muted); margin-right: 8px; }
.wz-sub { margin: 0 0 4px; font-size: var(--fs-md); color: var(--muted); line-height: 1.55; }

/* ── OPÇÕES E CARDS — o mesmo componente, dois usos ─────────────────────────
   ① usa rádio (escolha exclusiva: um endereço ou todos), ② usa checkbox
   (multi-canal: o mesmo e-mail pode ir para mais de um lugar ao mesmo tempo).
   O controle NATIVO fica visível de propósito — um checkbox que parece um
   checkbox é a affordance mais barata que existe, e ela sobrevive ao alto
   contraste, ao forced-colors e a quem não distingue a borda azul. */
.wz-opts, .wz-cards, .wz-radios { display: grid; gap: 10px; margin-top: 14px; }
.wz-opt, .wz-card, .wz-check {
  display: flex; gap: 12px; align-items: flex-start; min-width: 0;
  padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel);
}
.wz-check { margin-top: 14px; }
.wz-opt, .wz-card[data-state="on"], .wz-check { cursor: pointer; }
.wz-opt:hover, .wz-card[data-state="on"]:hover, .wz-check:hover { border-color: var(--border-strong); }
/* O box-shadow inset engrossa a borda sem mudar a caixa: uma borda de 2px na
   troca de estado empurraria o conteúdo 1px e a lista inteira tremeria a cada
   clique. */
.wz-opt:has(:checked), .wz-card:has(:checked), .wz-check:has(:checked) {
  border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent);
}
.wz-opt:has(:focus-visible), .wz-card:has(:focus-visible), .wz-check:has(:focus-visible) {
  outline: 2px solid var(--focus); outline-offset: 2px;
}
.wz-opt > input, .wz-card > input, .wz-check > input {
  flex: none; width: 18px; height: 18px; margin: 2px 0 0; accent-color: var(--accent);
}
.wz-opt-b, .wz-card-b { display: grid; gap: 2px; min-width: 0; }
.wz-opt-t, .wz-card-t { font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
.wz-opt-d, .wz-card-d { font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }
.wz-opt-d code, .wz-card-d code { font-family: var(--mono); color: var(--text); word-break: break-all; }
.wz-opt-sm .wz-opt-t { font-size: var(--fs-md); }

/* ── CANAL QUE NÃO DÁ PARA ESCOLHER AGORA ───────────────────────────────────
   Dois motivos diferentes, e a tela não os confunde: `todo` é «o recurso está
   ligado, falta um passo SEU» e leva o link desse passo; `dormant` é «está
   construído e ainda não foi liberado nesta plataforma».
   O card APARECE nos dois casos — sumir consertaria a promessa falsa criando
   outra, a de que o produto não tem o recurso. O que não pode acontecer é o
   cliente escolher e descobrir depois, e por isso não existe checkbox aqui: a
   impossibilidade é ESTRUTURAL, não um `disabled` que a tela pede para
   respeitar. A borda tracejada é o sinal, e ela não depende de cor. */
.wz-card[data-state="todo"], .wz-card[data-state="dormant"] { border-style: dashed; cursor: default; }
.wz-card[data-state="todo"] .wz-card-t, .wz-card[data-state="dormant"] .wz-card-t { color: var(--muted); }
.wz-card-tag {
  display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  font-size: var(--fs-3xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  white-space: nowrap; vertical-align: middle;
  background: var(--neutral-bg); color: var(--muted); border: 1px solid var(--neutral-line);
}
.wz-card-n { font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; margin-top: 4px; }
.wz-card-a { font-size: var(--fs-sm); font-weight: 600; color: var(--brand-2); margin-top: 4px; }

/* ── CAMPO CONDICIONAL À ESCOLHA ────────────────────────────────────────────
   «Mostrar campo irrelevante à escolha» é anti-padrão vetado, e quem faz o
   condicional é o CSS: sem script, a regra continua valendo. O fio de 2px é o
   mesmo vocabulário do .sim-panel — «isto pende daquilo». */
.wz-cond { display: none; margin-top: 16px; padding-left: 14px; border-left: 2px solid var(--border); }
.wz:has(#wz-mt-exact:checked) .wz-cond[data-when="exact"],
.wz:has(#wz-mt-catchall:checked) .wz-cond[data-when="catchall"],
.wz:has(#wz-ch-webhook:checked) .wz-cond[data-when="webhook"],
.wz:has(#wz-ch-slack:checked) .wz-cond[data-when="slack"],
.wz:has(#wz-ch-telegram:checked) .wz-cond[data-when="telegram"],
.wz:has(#wz-ch-forward:checked) .wz-cond[data-when="forward"] { display: block; }
/* A frase «não há mais nada a ajustar» é o inverso: existe enquanto o webhook
   NÃO está escolhido. É o que faz a etapa ③ nunca ficar vazia sem explicação. */
.wz:has(#wz-ch-webhook:checked) .wz-nowh { display: none; }
/* O campo «URL nova» da etapa ②, quando o domínio JÁ tem canais de webhook
   (modelo 106). A direção do default é a decisão: ele nasce VISÍVEL e some
   quando um canal da biblioteca está escolhido. Esconder por padrão pareceria
   mais limpo e seria pior — num navegador sem `:has()` o campo nunca voltaria, e
   o cliente ficaria sem como criar o segundo webhook do domínio. Assim, a falta
   de suporte custa um campo a mais na tela, nunca uma capacidade a menos. E
   preencher os dois não duplica nada: `inboundWizard.parseSetup` prefere
   `channelId` quando ele é um inteiro. Sem valor de espaço/raio/fonte aqui — a
   regra é só de visibilidade, e a catraca de test/cssRampa.test.js continua em
   zero. */
.wz-form:has(#wz-url option:not([value=""]):checked) .wz-novo { display: none; }

.wz-lbl { display: block; font-size: var(--fs-sm); font-weight: 600; color: var(--text); margin: 0 0 6px; }
.wz-help { margin: 6px 0 0; font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }
.wz-help code { font-family: var(--mono); color: var(--text); }
.wz-teach { margin: 14px 0 0; font-size: var(--fs-sm); color: var(--text); line-height: 1.55; }
.wz-teach-quiet { color: var(--muted); }
.wz-input { width: 100%; max-width: 30rem; min-height: 44px; }

/* O endereço montado no próprio campo: o sufixo @dominio fica ao lado do que se
   digita, então a pessoa lê o endereço inteiro enquanto o escreve em vez de
   montá-lo de cabeça. O foco vai na CAIXA e não no input, senão o anel
   apareceria em volta de metade do controle. */
.wz-addr {
  display: flex; align-items: center; max-width: 30rem;
  border: 1px solid var(--field-border); border-radius: var(--radius-sm); background: var(--panel);
}
.wz-addr:focus-within { outline: 2px solid var(--focus); outline-offset: 1px; }
.wz-addr:has(input[aria-invalid="true"]), .wz-addr:has(input:user-invalid) { border-color: var(--bad); }
/* O input NÃO cresce até o fim da caixa: se crescesse, o sufixo iria para a
   borda direita e o cliente leria «contato ................ @dominio.com.br»,
   que é justamente a leitura que este controle existe para evitar. Com base
   fixa, o `@dominio` encosta no que se digita e a linha se lê como um endereço.
   `field-sizing: content` faz a base acompanhar o texto onde houver suporte; o
   `flex-basis` é o piso para os outros. */
.wz-addr > input {
  flex: 0 1 15rem; min-width: 0; min-height: 44px; border: 0; background: transparent;
  padding: 0 12px 0 12px; font-family: var(--mono); font-size: var(--fs-md); color: var(--text);
}
@supports (field-sizing: content) {
  .wz-addr > input { field-sizing: content; flex: 0 1 auto; min-width: 8rem; max-width: 18rem; }
}
.wz-addr > input:focus-visible { outline: 0; }
.wz-addr-suf {
  flex: 0 1 auto; min-width: 0; padding: 0 12px 0 0;
  font-family: var(--mono); font-size: var(--fs-md); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── A AÇÃO ─────────────────────────────────────────────────────────────────
   No desktop segue o fluxo; no celular ela é FIXA NO RODAPÉ (norma §8, alcance
   do polegar) e os dois botões dividem a largura — «ação fora de alcance no
   mobile» é anti-padrão vetado. 44px de alvo nos dois. */
.wz-nav { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 24px; }
/* `.btn` do portal nasce `width: 100%` («ação grande, empilhada», que é a regra
   certa para a ação única de um cartão). Aqui são DUAS ações lado a lado, e
   full-width empilharia «Voltar» e «Continuar» em duas barras da largura da
   coluna — duas ações com o mesmo peso, que é o oposto do que se quer ler. */
.wz-nav .btn { width: auto; min-height: 44px; }
.wz-nav-adv { display: none; }
@container wz (max-width: 799px) {
  /* AÇÃO FIXA NO RODAPÉ (norma §8, alcance do polegar). Elas dividem a linha, e
     a primária leva o dobro: no celular a mão decide pelo tamanho antes de ler. */
  .wz-nav {
    position: sticky; bottom: 0; z-index: 2;
    margin-top: 20px; padding: 12px 0 8px;
    background: var(--panel); border-top: 1px solid var(--border);
  }
  .wz-nav .btn { flex: 1 1 auto; }
  .wz-nav .btn-primary { flex: 2 1 auto; }
}
/* No modo avançado as quatro barras dariam quatro botões «Criar endereço» na
   mesma tela; sobra uma, no fim. Estas duas regras têm a MESMA especificidade
   das de cima (o id dentro do :has() conta), então a ORDEM no arquivo é o que
   decide — mover uma delas para antes quebra o modo avançado em silêncio. */
.wz:has(#wz-adv:checked) .wz-nav { display: none; }
.wz:has(#wz-adv:checked) .wz-nav-adv { display: flex; }

/* ── Credenciais do CLIENTE: rótulo + valor copiável ────────────────────────
   Só no portal, e de propósito: /app/credentials é a tela onde o cliente
   transcreve host, porta, usuário e senha para dentro da aplicação dele. A tela
   equivalente do operador (tenant-credentials.ejs) usa o mecanismo por SELETOR
   do admin.js e não compartilha este markup — espelhar o bloco lá seria CSS
   morto. Quando as duas telas convergirem, este bloco vai junto. */
.cred-k {
  margin: 14px 0 4px; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text);
}
.cred-copy-row { display: inline-flex; gap: 8px; align-items: center; flex-wrap: wrap; max-width: 100%; }
.cred-copy-row > code { min-width: 0; word-break: break-all; }
.cred-copy-row .btn { flex: 0 0 auto; }
/* A CÉLULA PRECISA DE CHÃO. Com `word-break: break-all` na célula (que existe
   para o usuário de 190 caracteres não estourar a tabela), o navegador calcula
   um min-content de meia dúzia de letras: «envio@lojaaurora.com.br» virava três
   linhas picadas e o botão saía do alinhamento da coluna. O piso resolve os dois
   casos — o nome curto cabe inteiro, o nome absurdo ainda quebra. */
.cred-cell { min-width: 18rem; }
@media (max-width: 640px) {
  /* No cartão a largura é a da tela; o piso viraria estouro horizontal. */
  .cred-cell { min-width: 0; }
  td .cred-copy-row { justify-content: flex-end; }
}
/* No cartão do segredo o botão é a ação da tela — alvo maior, não menor. */
.card.reveal .cred-copy-row .btn { min-height: 40px; padding-inline: 16px; }

/* ═══ REFORMA DA INTERAÇÃO DA RECEPÇÃO (08/2026) ═══════════════════════════
   Gêmeo do bloco no admin.css (o partial compartilhado veste as duas telas):
   .inb-note (flash → nota inline na seção), .cxm (modal de caixa de destino
   sobre o shell .eml-modal; pendentes reusam .c-banner) e .cj-* (o conjunto
   «Esta caixa vai para:» — caixas de marcar, salvar aplica o conjunto). */
.inb-note { margin: 10px 0; }
.eml-modal.cxm { width: min(92vw, 560px); }
.cxm-body { padding: 14px 16px; display: grid; gap: 12px; }
.cxm-form { display: grid; gap: 8px; justify-items: start; margin: 0; }
.cxm-form input[type="email"] { width: 100%; }
.cxm-h { margin: 0; font-size: var(--fs-md); }
.cxm-pend { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.cxm-pend-falta { margin: 4px 0 0; }
.cxm-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.cj-form { margin: 8px 0 0; }
.cj-set { border: 0; padding: 0; margin: 0; }
.cj-lgd { padding: 0; margin: 0 0 6px; font-weight: 700; font-size: var(--fs-sm); }
.cj-opts { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 8px; }
.cj-opt { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm); min-height: 28px; }
.cj-opt input { margin: 0; }
.cnl-card-sec { margin: 6px 0 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   FAIXA DO ACESSO DE SUPORTE (impersonação)
   ═══════════════════════════════════════════════════════════════════════════
   Aparece em TODA página do portal enquanto um operador estiver dentro do
   painel do cliente (partials/portal-head.ejs, local `imp` vindo do hook de
   escopo). Para o cliente de verdade ela não existe no HTML — não é `display:
   none`, é ausência.

   ELA MORA SÓ NESTA FOLHA, e é decisão, não esquecimento: a faixa só existe no
   portal. O painel do operador não é «impersonado» por ninguém, então uma cópia
   em admin.css seria regra morta — e regra morta é como duas folhas passam a
   divergir em silêncio (a lição do `.toast`, que era DUAS coisas com o mesmo
   nome nas duas folhas). Se um dia ela precisar aparecer no painel, este bloco
   vai inteiro, sem editar valor.

   POR QUE `--warn` E NÃO `--bad`: isto não é um erro nem um alarme — é um
   ESTADO legítimo e esperado do produto. Vermelho aqui treinaria o atendente a
   ignorar vermelho, que é a cor que ele precisa enxergar quando algo quebrar de
   verdade. Âmbar diz «preste atenção em onde você está», que é o recado.

   `position: sticky` no topo: o atendente rola uma página longa (domínios,
   mensagens) e a faixa continua na tela. Sair do painel do cliente não pode
   depender de rolar de volta. Zero cor crua e zero valor fora da rampa — tudo
   sai dos tokens desta folha (test/cssRampa.test.js). */
.imp-bar {
  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--warn-bg);
  border-bottom: 1px solid var(--warn-bd);
  color: var(--warn);
}
/* RODADA 7 — a QUINTA caixa do shell. Ela tinha ficado para trás quando
   .topbar-inner/.subnav-inner/.subnav2-inner/.wrap subiram para 1280+24, e o
   efeito era visível na foto: a marca e o h1 nasciam em x=24 e a faixa de
   operador começava em x=205, no meio da tela. As cinco caixas do shell só
   existem como shell se compartilharem a mesma coluna — uma que divirja não é
   uma faixa alinhada com quase tudo, é a única errada. */
.imp-bar-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 8px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
/* Marcador redondo — objeto GRÁFICO, não texto: o recado inteiro está escrito
   ao lado, e quem não enxerga cor não perde nada (aria-hidden na marcação). */
.imp-dot {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--warn);
}
.imp-text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-sm);
  line-height: 1.45;
}
.imp-text strong { display: block; }
/* A segunda linha é a REGRA do acesso (fica registrado, conteúdo não abre) —
   informação de leitura, não de destaque. Mesma tinta, um degrau menor. */
.imp-sub {
  display: block;
  font-size: var(--fs-xs);
  opacity: .9;
}
.imp-out { flex: 0 0 auto; margin: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   DZ — REDESIGN DA ÁREA DE DOMÍNIOS (handoff «Dominios v2», 12/08/2026).
   O handoff foi desenhado SOBRE este portal (os hex dele ≈ nossos tokens), e a
   decisão do dono foi: o shell (topbar/subnav) fica; entra a ÁREA DE CONTEÚDO —
   header com eyebrow, filtros, tabela densa, abas, cards e interações.
   Regras desta seção:
   · ZERO hex novo — tudo em token; o único tom que faltava (fundo de linha com
     pendência) sai de color-mix sobre tokens, e acompanha o tema escuro.
   · O «ok» é DISCRETO (sem fundo); cor só onde há estado — é o kernel, que a
     tabela atual já pratica e o handoff confirma.
   · Animações respeitam prefers-reduced-motion.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Cabeçalho de página do redesign ─────────────────────────────────────── */
.dz-eyebrow {
  display: block;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 4px;
}
.dz-headrow {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}
/* Mesmo degrau de .page-head h1: as duas são a linha de cabeçalho de tela, uma
   com CTA à direita e outra sem. Divergir aqui faria o h1 encolher ao ganhar um
   botão — que é exatamente o tipo de decisão que ninguém tomou. */
.dz-headrow h1 { margin: 0; font-size: var(--fs-6xl); letter-spacing: -.01em; }
/* ── RODADA 7 — O h1 DESCE NO CELULAR: 26 → 22 ──────────────────────────────
   O mockup troca `h1Fs` de 29 para 22px a 402px, em TODAS as telas; o port
   mantinha 26 nas duas larguras. A 402px de viewport com 12 de recuo sobram
   378px, e um título de 26px come duas linhas em metade dos rótulos deste
   produto («Proteção de rede», «Recepção · Entregas»). Não é encolher por
   encolher: 22px = --fs-4xl já está na escala, e é o degrau que o mockup pede.

   MORA AQUI, e não junto do `.page-head h1` lá em cima, por ORDEM: as duas
   regras têm a mesma especificidade (0,1,1) e `.dz-headrow h1` desta linha é
   a última a falar. Escrito lá em cima, o bloco de celular passava despercebido
   — media query verde, h1 em 26px no aparelho, e nada reprovando. Foi medido
   assim antes de mudar de lugar. Cobre as duas linhas de cabeçalho de uma vez:
   `.page-head` (título + subtítulo) e `.dz-headrow` (título + CTA), que são o
   mesmo papel com e sem ação à direita (MAPA §1.1). */
@media (max-width: 640px) {
  .page-head h1,
  .dz-headrow h1 { font-size: var(--fs-4xl); }
}

/* ── Adicionar domínio: <details> cujo summary é o botão primário do header.
     Sem JS abre nativo; o painel é um card ancorado à direita (dropdown). ── */
.dz-add { position: relative; }
.dz-add > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--btn-fs);
  font-weight: 600;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  user-select: none;
}
.dz-add > summary::-webkit-details-marker { display: none; }
.dz-add > summary:hover { background: var(--accent-hover); }
.dz-add[open] > summary { background: var(--brand); color: var(--on-solid); }
.dz-add-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  width: min(480px, calc(100vw - 32px));
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
  z-index: 30;
}
.dz-add-panel input[type="text"] { font-family: var(--mono); }

/* ── Filtros (pills) + monitor ao vivo ───────────────────────────────────── */
.dz-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
/* RODADA 7 — a pílula de filtro sobe de ~28px para 36px de altura. Não é
   gosto: o mockup declara `height:36px;padding:0 14px;font-size:12.5px` nas
   CINCO telas em que ela aparece, e 28px reprova o alvo de toque de 44px no
   celular por margem grande demais para uma media query resolver sozinha.
   `display:inline-flex` entra junto porque `height` num <button> com padding
   vertical não produz altura previsível — com o eixo cruzado centrado, sim.
   12,5px do mockup sobe para --fs-sm (13) pela regra dos meios-pontos. */
.dz-pill {
  appearance: none;
  display: inline-flex;
  align-items: center;
  height: 36px;
  border: 1px solid var(--border-strong);
  background: var(--panel);
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: 0 14px;
  border-radius: 999px;
  cursor: pointer;
}
.dz-pill .dz-pill-n { font-weight: 400; opacity: .8; margin-left: 4px; }
/* DIVERGÊNCIA DECLARADA (MAPA §1.6): o item ativo inverte para quase-preto, o
   MESMO idioma de .tst e .selbar (background: var(--text); color: var(--bg)) —
   e não para --brand. O motivo é que a nav de nível 2 e o filtro da barra de
   ferramentas aparecem na MESMA tela (Mensagens): «ativo» em azul num lugar e
   «ativo» em preto no outro seriam duas linguagens para um fato só.
   Os dois seletores existem porque o estado tem dois donos semânticos: o filtro
   é um botão que alterna (aria-pressed), a pílula de nav é o lugar em que você
   está (aria-current="page"). Uma regra, dois papéis — nunca duas aparências. */
.dz-pill[aria-pressed="true"],
.dz-pill[aria-current="page"] {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}
.dz-live {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.dz-live-dot {
  width: 7px; height: 7px;
  border-radius: 999px;
  background: var(--ok);
  animation: dz-pulse 2.4s ease-in-out infinite;
}
@keyframes dz-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ── Tabela densa (refina a .tbl existente sob o escopo .dz-tbl) ─────────── */
.dz-tbl thead th {
  font-size: var(--fs-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted-2);
  font-weight: 700;
  padding: 10px 16px;
}
.dz-tbl tbody td { padding: 12px 16px; }
.dz-tbl tbody tr { cursor: pointer; }
.dz-tbl tbody tr:hover { background: var(--panel-2); }
/* Linha com pendência: tom quente SUAVE (o badge é quem grita; a linha só
   inclina) — color-mix para não cunhar hex e seguir o tema. */
.dz-tbl tbody tr[data-attn="1"] {
  background: color-mix(in srgb, var(--warn-bg) 30%, var(--panel));
}
.dz-tbl tbody tr[data-attn="1"]:hover {
  background: color-mix(in srgb, var(--warn-bg) 45%, var(--panel));
}
/* `:not(.ent)`: o nome de entidade das listas (28/08) tem regra própria, e esta
   (0,2,2) vencia a dele (0,1,0) — o QA mediu mono azul sem sublinhado JUSTO na
   lista de domínios, a referência que o dono citou. */
.dz-tbl td[data-label="Domínio"] a:not(.ent) {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--accent);
  text-decoration: none;
}
/* Filtro client-side: portal.js marca as linhas fora do filtro ativo. */
.dz-tbl tbody tr[hidden] { display: none; }

/* ── Botão re-checar por linha (↻) e spinner de ação ─────────────────────── */
.dz-recheck {
  appearance: none;
  width: 30px; height: 30px;
  border: 1px solid var(--border-strong);
  background: var(--panel);
  color: var(--muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--fs-md);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.dz-recheck:hover { border-color: var(--accent); color: var(--accent); }
.dz-spin {
  display: inline-block;
  width: 12px; height: 12px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 999px;
  animation: dz-rot .7s linear infinite;
}
@keyframes dz-rot { to { transform: rotate(360deg); } }

/* ── Toasts (flutuante, criado por JS em #toasts — a norma da rodada 2:
     view NUNCA escreve class="toast"; quem monta é o portal.js) ───────────── */
#toasts {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* 360 → 420: com a fatia de ação (.tst-act) o toast passou a carregar
     «Removida da supressão.» + «Desfazer» + «×» na mesma linha. Em 360 o verbo
     quebrava para baixo do texto e o botão ficava órfão do que ele desfaz. */
  max-width: min(420px, calc(100vw - 32px));
}
.tst {
  display: flex;
  align-items: flex-start;
  /* wrap: em 402px o par ação+dispensar desce inteiro para a segunda linha em
     vez de espremer a frase a três palavras por linha. */
  flex-wrap: wrap;
  gap: 8px;
  background: var(--text);
  color: var(--bg);
  font-size: var(--fs-sm);
  line-height: 1.4;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  box-shadow: var(--sh-lg);
  animation: dz-toast-in .22s ease-out;
}
.tst-dot {
  flex: 0 0 auto;
  width: 8px; height: 8px;
  border-radius: 999px;
  margin-top: 4px;
  background: var(--info);
}
.tst[data-kind="ok"] .tst-dot { background: var(--ok); }
.tst[data-kind="warn"] .tst-dot { background: var(--warn); }
@keyframes dz-toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ── Rodapé informativo da lista ─────────────────────────────────────────── */
.dz-note {
  margin-top: 10px;
  font-size: var(--fs-xs);
  color: var(--muted-2);
}

@media (prefers-reduced-motion: reduce) {
  .dz-live-dot, .dz-spin, .tst { animation: none; }
}

/* ── DZ: detalhe do domínio — cabeçalho + abas ───────────────────────────── */
.dz-domain {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--fs-4xl);
  letter-spacing: -.01em;
}
.dz-headside {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.dz-headchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 12px;
}
.dz-headchip .dsn-label { font-size: var(--fs-xs); color: var(--muted); }

.dz-tabbar {
  display: flex;
  gap: 22px;
  border-bottom: 2px solid var(--line-2, var(--border));
  margin: 0 0 18px;
  overflow-x: auto;
}
.dz-tab {
  display: inline-block;
  padding: 8px 2px 10px;
  font-size: var(--fs-md);
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  white-space: nowrap;
}
.dz-tab:hover { color: var(--text); }
.dz-tab[aria-current="true"] {
  color: var(--text);
  font-weight: 700;
  border-bottom-color: var(--accent);
}

/* Painéis: SEM JS tudo fica impresso (progressivo). O portal.js liga
   .dz-tabs-on no body e só então os painéis fora da aba ativa somem. */
body.dz-tabs-on [data-dz-pane] { display: none; }
body.dz-tabs-on [data-dz-pane].dz-active { display: block; }

/* O cartão de recepção CONTÉM três painéis (recepção/canais/endereços) mas o
   invólucro fica fora deles — na aba Envio ele renderizava como uma CAIXA EM
   BRANCO entre o tracking e o remover (achado do dono, 13/08). Sem painel
   ativo dentro, o cartão inteiro some. */
body.dz-tabs-on #recepcao:not(:has([data-dz-pane].dz-active)) { display: none; }

/* ── Reforma das credenciais (13/08): gestos compactos + modais ─────────── */
/* A linha de ferramentas encolheu: botões lado a lado, sem as caixas grandes. */
.cred-tools-slim { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; }
.cred-tools-slim > .btn { display: inline-flex; align-items: center; gap: 6px; }
.rate-mini { font-variant-numeric: tabular-nums; }
/* Os modais reaproveitam o shell .eml-modal; aqui só o miolo. */
.cred-modal { width: min(720px, calc(100vw - 32px)); }
.cred-modal-body { padding: 12px 16px; max-height: 60vh; overflow-y: auto; }
.cred-modal-foot { padding: 10px 16px; border-top: 1px solid var(--line-2); display: flex; justify-content: flex-end; }
.rate-dlg { width: min(560px, calc(100vw - 32px)); }
.rate-dlg-body { padding: 12px 16px; }
/* Dentro do modal o details do limite já nasce aberto; o summary vira título
   redundante — some, o cabeçalho do modal já diz o que é. */
.rate-dlg .rate-editor > summary { display: none; }

/* Botão com peso de LINK (caminho secundário nos cards de criar canal — o
   POST do «conectar outro chat» precisa de form, mas o peso é de link). */
.btn-aslink {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  color: var(--brand-2); text-decoration: underline;
}
.btn-aslink:hover { color: var(--text); }

/* tr.sub com painel FECHADO (summary sr-only) não pode deixar rastro: sem o
   ajuste, cada canal/endereço ganhava uma faixa vazia com borda entre linhas. */
.tbl tr.sub td:has(> details.dgrp:not([open])) { padding: 0; border-bottom: 0; }

/* ── Switch liga/desliga (grupo «Esta caixa vai para:», 14/08) ──────────────
   Porte do componente do painel de IPs do admin, em cima de CHECKBOX (o
   mecanismo do conjunto não mudou): input invisível, trilho+knob respondem ao
   :checked. Com JS o flip salva sozinho (portal.js) e o Salvar some. */
.cj-opt.sw { display: inline-flex; align-items: center; gap: 10px; min-height: 40px; cursor: pointer; border-radius: var(--radius); padding: 2px 6px; }
.cj-opt.sw:hover { background: var(--neutral-bg); }
.sw-input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sw-track {
  position: relative; flex: none; width: 42px; height: 25px; border-radius: 999px;
  background: var(--neutral-line); transition: background .2s ease;
}
.sw-knob {
  position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%;
  /* sombra de peça pequena: --shadow-sm é a elevação canônica do tema — o knob
     não ganha rgba própria (rampa: cor cravada só com motivo, e aqui não há). */
  background: var(--sw-knob); box-shadow: var(--shadow-sm); transition: transform .2s ease;
}
.sw-input:checked ~ .sw-track { background: var(--ok); }
.sw-input:checked ~ .sw-track .sw-knob { transform: translateX(17px); }
.sw-input:focus-visible ~ .sw-track { outline: 2px solid var(--brand-2); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .sw-track, .sw-knob { transition: none; } }
/* Com autosave (JS presente), o Salvar é redundante — some. Sem JS, volta. */
:root.js-on .cj-form .cj-salvar { display: none; }

/* ── Pós-criação de endereço + fechar painel (14/08) ────────────────────── */
/* Depois de criar, o box «Criar endereço» mostra SÓ a nota de sucesso; o
   clique seguinte no summary limpa a classe e devolve o formulário original. */
#recepcao-novo-endereco.pos-criado .dgrp-body > :not(.inb-note) { display: none; }
/* O ✕ que fecha o painel de configuração — o summary é sr-only (a linha da
   tabela é o resumo), então o painel aberto precisa de uma saída visível. */
.dgrp-close { float: right; margin: 0 0 8px 12px; }

/* Corpo do conjunto de registros: mesmo respiro interno da caixinha (o .dset
   virou moldura em 14/08 — sem isto o conteúdo aberto colava na borda). */
.dset-body { padding: 0 12px 12px; }

/* ── Faixa de impersonação MAGRA (diretiva do dono, 14/08): metade da altura,
   uma linha, sair como ícone à direita. As classes -slim convivem com as
   antigas (o partial é quente; a rota some com o par velho no próximo ciclo). */
.imp-bar-inner.imp-slim { padding: 4px 20px; flex-wrap: nowrap; }
.imp-text-slim {
  flex: 1 1 auto; min-width: 0; font-size: var(--fs-xs); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.imp-sub-slim { opacity: .9; }
.imp-sair { display: inline-flex; align-items: center; gap: 6px; }
@media (max-width: 640px) { .imp-text-slim { white-space: normal; } }

/* ── Ações em ícone na tabela de canais (spec UX 14/08) ─────────────────── */
/* Engrenagem: botão só-ícone — o title/aria carregam o verbo. */
.btn-ico { padding: 6px 8px; display: inline-flex; align-items: center; }
/* Switch como BOTÃO-submit (liga/desliga do canal): mesmo trilho/knob do
   switch-checkbox; o estado vem do aria-checked. */
.sw-btn { background: none; border: 0; padding: 2px; cursor: pointer; display: inline-flex; align-items: center; }
.sw-btn[aria-checked="true"] .sw-track { background: var(--ok); }
.sw-btn[aria-checked="true"] .sw-knob { transform: translateX(17px); }
.sw-btn:focus-visible .sw-track { outline: 2px solid var(--brand-2); outline-offset: 2px; }

/* Ícone dentro de QUALQUER botão: sem esta regra o svg sem width colapsa e o
   botão vira pílula vazia (a engrenagem invisível de 14/08 — e o mail do
   «Mensagens» das credenciais estava igual). Escopo por botão, não global:
   os contextos que já dimensionam (.subnav, .tb-head…) continuam mandando. */
.btn .ic, .btn-ico .ic { width: 15px; height: 15px; flex: none; }

/* Prompt do onboarding por agente (14/08): bloco rolável, nunca estoura o box. */
.agente-prompt {
  margin: 8px 0 0; padding: 10px 12px; font-size: var(--fs-xs); line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere; max-height: 320px; overflow-y: auto;
  background: var(--neutral-bg); border: 1px solid var(--neutral-line); border-radius: var(--radius-sm);
}

/* Formulário de senha da tela de login (acesso de avaliação). Rótulo em cima,
   campo largura cheia — o `.stack` do cartão é flex e espalhava os dois. */
.pw-campo { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.pw-campo input { width: 100%; box-sizing: border-box; }
.auth-senha summary { padding: 6px 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   REDESENHO DO PORTAL · T0.5 — AS PEÇAS QUE O KIT JÁ USA E A FOLHA NÃO TINHA
   ═══════════════════════════════════════════════════════════════════════════
   O comportamento entrou primeiro (public/portal.js, T1.3) e a paleta antes
   dele (T0.3/T0.4). O que faltava era o meio: até este bloco, o painel lateral,
   a barra de ação em massa e o esqueleto renderizavam SEM ESTILO na tela do
   cliente — o mesmo modo de falha do `.badge-engaged` de 22/08, e a razão de o
   veto de `.toast` existir.

   A REGRA DE ORIGEM, e ela não é negociável aqui: quem manda no nome é o
   `docs/redesenho/MAPA-CLASSES.md`; quem manda na MARCAÇÃO é o `portal.js`.
   Onde mockup e JS divergem, o JS ganha e a divergência está anotada na regra.
   As três que apareceram:
     1. A barra de massa é em FLUXO (margin-bottom), não sobreposta ao rodapé —
        o HANDOFF-deltas §6.2 diz o contrário e o MAPA §4.4 o corrige com a
        linha do mockup que o desmente. Barra fixa come conteúdo em 402px.
     2. Ela NASCE VISÍVEL no HTML e é o JS que a esconde com [hidden]. Por isso
        `[hidden]` precisa VENCER o `display:flex` — sem a regra de duas
        especificidades abaixo, uma barra «escondida» continuaria na tela.
     3. O «é spam» tracejado (.selbar-learn) não entra: nenhuma das três telas
        em escopo tem ação de aprendizado. Classe sem consumidor não entra.

   E a regra que o scanner cobra (test/helpers/cssRampa.js): zero cor cravada
   nova nesta folha — a catraca está em 6 e trava nos DOIS sentidos. Toda
   opacidade aqui é `color-mix(… var(--x) N%, transparent)`, que o regex do
   scanner não casa porque não há literal dentro. Zero `8px`/`12px` de raio
   (`var(--radius-sm)`/`var(--radius)`), zero tamanho de fonte literal, e
   nenhum atalho `font:` — ele declara tamanho e pilha sem escrever nenhum dos
   dois nomes, e foi por ali que oito escaparam na rodada 3.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1. PAINEL LATERAL (588px) ───────────────────────────────────────────────
   <dialog class="lat" data-ovy-lat-panel>. O elemento NATIVO entrega Escape,
   trapa de foco, `inert` no fundo e ::backdrop — a «ordem de fechamento» que o
   HANDOFF §1 pede sai de graça, e o portal.js só cuida do terceiro degrau.

   `display` mora em `.lat[open]`, e isto é a armadilha do <dialog>: a regra
   `dialog:not([open]) { display: none }` é do USER-AGENT, e qualquer declaração
   de AUTOR a vence por origem de cascata, não por especificidade. Um
   `.lat { display: flex }` deixaria o painel FECHADO visível na página. */
.lat[open] { display: flex; flex-direction: column; }
.lat {
  position: fixed;
  /* colado à direita, altura cheia: `inset` não é medido pela rampa (só
     padding/margin/gap são), e `auto` na esquerda é o que ancora. */
  inset: 0 0 0 auto;
  width: min(588px, 96vw);
  max-width: 96vw;
  max-height: none;
  height: 100%;
  margin: 0 0 0 auto;
  padding: 0;
  border: 0;
  /* Só a borda esquerda arredonda — o painel encosta na direita da janela. */
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--sh-lg);
  overflow: hidden;
  animation: lat-in .24s cubic-bezier(.22, .7, .28, 1) both;
}
.lat::backdrop { background: var(--scrim); animation: dz-fade .18s both; }
@keyframes lat-in { from { transform: translateX(100%); } to { transform: none; } }
@keyframes dz-fade { from { opacity: 0; } to { opacity: 1; } }

.lat-head {
  flex: none;
  padding: 18px 22px 0;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.lat-headrow { display: flex; align-items: flex-start; gap: 12px; }
.lat-headrow > div { flex: 1; min-width: 0; }
/* Identificador mono. `font-weight: 500` de propósito: 16px em mono com 700
   vira bloco preto e rouba o olho do estado, que é a informação de cima. */
.lat-title {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-xl);
  font-weight: 500;
  letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lat-sub { font-size: var(--fs-xs); color: var(--muted); }
/* ROTULADO «Fechar», nunca «×» (MAPA §4.1): o × obriga o leitor de tela a um
   aria-label que ninguém revisa, e o alvo de 44px no dedo cabe melhor num
   rótulo do que num glifo. */
.lat-close {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 32px; padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--btn-radius);
  background: var(--panel);
  color: var(--muted);
  font-family: inherit; font-size: var(--fs-sm); font-weight: 600;
  cursor: pointer;
}
.lat-close:hover { background: var(--panel-2); color: var(--text); }
.lat-body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px 22px 32px;
  background: var(--panel-2);
}

/* AS ABAS DO PAINEL reusam .dz-tabbar/.dz-tab — o mesmo objeto do detalhe de
   domínio. Aqui elas perdem a borda própria (o .lat-head já tem a sua) e a
   margem inferior, para nascerem em cima do filete do cabeçalho. */
.lat .dz-tabbar { margin: 16px 0 0; border-bottom: 0; gap: 18px; }
.lat .dz-tab { padding: 8px 2px 10px; font-size: var(--fs-sm); }

/* O ESTADO DE CARREGAMENTO DAS ABAS. `abrePainel()` esvazia a barra e só
   `latPintaAbas()` a preenche, um round-trip depois: sem esta regra a barra
   colapsa a zero e o corpo inteiro PULA ~38px quando as abas chegam. `:empty`
   é o predicado exato do que o JS faz (textContent = ''), e o fantasma usa o
   mesmo shimmer do esqueleto — uma folha, um dicionário de espera. */
.lat .dz-tabbar:empty { min-height: 36px; }
.lat .dz-tabbar:empty::before {
  content: "";
  display: block; width: 232px; height: 12px; margin: 10px 0;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--line) 0%,
              color-mix(in srgb, var(--line) 45%, var(--surface)) 50%,
              var(--line) 100%);
  background-size: 200% 100%;
  animation: c-skel-shimmer 1.3s ease-in-out infinite;
}
/* O esqueleto do CORPO antecipa a forma, não só o texto: a primeira barra é o
   cartão de cabeçalho que todo painel abre. `esqueleto()` no portal.js emite
   linhas iguais; a diferença de altura é decisão de desenho e mora aqui. */
.lat-body .c-skel-lines > .c-skel-bar:first-child { height: 58px; }

@media (pointer: coarse) { .lat-close { min-height: 44px; } }
@media (prefers-reduced-motion: reduce) {
  .lat, .lat::backdrop { animation: none; }
}

/* ── 2. ESQUELETO ────────────────────────────────────────────────────────────
   Porte VERBATIM de admin.css:2203-2224. Mesmos nomes, mesmos tokens
   (--surface/--surface-3/--line/--radius, todos já existentes aqui), mesma
   forma — partials/comp/skeleton.ejs renderiza nas duas superfícies e não pode
   ter duas caras. Consumidores: painel lateral, e HANDOFF §5 torna obrigatório
   em TODA região que troca por AJAX. */
.c-skel-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; }
.c-skel-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.c-skel-table { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.c-skel-row { display: flex; gap: 18px; padding: 12px 14px; border-bottom: 1px solid var(--line); }
.c-skel-row:last-child { border-bottom: 0; }
.c-skel-head { background: var(--surface-3); }
.c-skel-lines { display: flex; flex-direction: column; gap: 10px; }
/* ⚠ O ÚNICO DESVIO DE VALOR DO PORTE, e ele conserta um defeito que o admin
   também tem — medido, não suposto:
       --surface-3 (#fbfcfd) sobre --panel (#ffffff) ……… 1,01:1
   A barra do esqueleto era, na prática, INVISÍVEL sobre cartão branco: o
   estado «carregando, nunca branco» do HANDOFF §5 renderizava branco. No
   escuro o delta também é mínimo (#1b2735 sobre #151f2b).
   A saída não é um valor novo: é --line, que é por DEFINIÇÃO o tom que esta
   folha já usa para ser visível contra a superfície, em qualquer tema. O
   resultado é o cinza do mockup (#EDF0F4 ≈ #e7e9ee) sem cunhar hex nenhum.
   Esta regra é copiável PALAVRA POR PALAVRA para admin.css:2211 — a
   divergência dura até alguém tocar naquele arquivo, e está no handoff. */
.c-skel-bar {
  display: block; height: 12px; border-radius: 6px;
  background: linear-gradient(90deg, var(--line) 0%,
              color-mix(in srgb, var(--line) 45%, var(--surface)) 50%,
              var(--line) 100%);
  background-size: 200% 100%;
  animation: c-skel-shimmer 1.3s ease-in-out infinite;
}
.c-skel-bar.tall { height: 24px; }
.c-skel-bar.w15 { width: 15%; } .c-skel-bar.w20 { width: 20%; } .c-skel-bar.w25 { width: 25%; }
.c-skel-bar.w30 { width: 30%; } .c-skel-bar.w40 { width: 40%; } .c-skel-bar.w50 { width: 50%; }
.c-skel-bar.w70 { width: 70%; } .c-skel-bar.w80 { width: 80%; } .c-skel-bar.w95 { width: 95%; }
.c-skel-row .c-skel-bar { flex: none; }
@keyframes c-skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* ÚNICO desvio do porte, e ele é somativo: o admin.css não neutraliza o
   shimmer. Um brilho infinito de 1,3s numa região inteira é exatamente o que
   `prefers-reduced-motion` existe para desligar — e a forma cinza continua
   dizendo «carregando» sozinha. Vale a pena portar de volta para lá. */
@media (prefers-reduced-motion: reduce) {
  .c-skel-bar, .lat .dz-tabbar:empty::before { animation: none; }
}

/* ── 3. BARRA DE AÇÃO EM MASSA ───────────────────────────────────────────────
   Em FLUXO, entre o filtro e a tabela (MAPA §4.4). O idioma escuro é o mesmo
   de .tst: `background: var(--text); color: var(--bg)` — a superfície que diz
   «isto é uma camada de controle, não conteúdo». */
.selbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  margin: 0 0 12px;
  padding: 12px 16px;
  background: var(--text);
  color: var(--bg);
  border-radius: var(--radius);
  animation: dz-toast-in .16s ease-out;
}
/* [hidden] TEM de vencer o display acima — ver a nota nº 2 no topo do bloco.
   Duas especificidades (0,2,0 contra 0,1,0) resolvem sem !important. */
.selbar[hidden] { display: none; }
/* A ORDEM É DECLARADA, não herdada do DOM: o portal.js faz appendChild do «em
   voo» no FIM da barra, depois do ×, e sem `order` a espera apareceria à
   direita do botão de desistir. Com ele a barra tem UMA leitura em qualquer
   estado — quantas · o que está acontecendo · o que fazer · desistir — e o
   arquivo de comportamento não precisa saber nada de layout. */
.selbar-n    { order: 1; font-size: var(--fs-sm); font-weight: 700; }
/* Sem JS o servidor pode render a barra com a contagem vazia (não há seleção
   antes do POST): um <strong> vazio deixaria um gap de 12px sem dono. */
.selbar-n:empty { display: none; }
.selbar-acts {
  order: 3;
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-left: auto;
}
/* O JS esconde SÓ as ações enquanto a chamada está em voo — a contagem fica,
   porque é ela que diz sobre o que a espera é. Mesmo motivo do [hidden] da
   barra: sem esta linha, `display: flex` venceria o atributo. */
.selbar-acts[hidden] { display: none; }
/* Fantasma sobre escuro — o mesmo desenho serve a .tst-act (MAPA §4.6): as
   duas são um botão de baixo peso sobre a mesma superfície, e escrever duas
   vezes seria abrir espaço para divergirem. */
.selbar-btn,
.tst-act {
  appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 32px; padding: 6px 12px;
  background: transparent;
  color: var(--bg);
  border: 1px solid color-mix(in srgb, var(--bg) 30%, transparent);
  border-radius: var(--btn-radius);
  font-family: inherit; font-size: var(--fs-sm); font-weight: 600;
  white-space: nowrap; cursor: pointer;
}
.selbar-btn:hover,
.tst-act:hover { background: color-mix(in srgb, var(--bg) 14%, transparent); }
/* O ANEL DE FOCO INVERTE SOBRE O ESCURO. O :focus-visible global é
   `outline: 3px solid var(--focus)`, e --focus é var(--accent): azul de marca
   sobre quase-preto mede ~2,2:1 e REPROVA o mínimo de 3:1 de indicador de
   foco. Sobre esta superfície o anel é a própria cor de fundo da página. */
.selbar :focus-visible,
.tst :focus-visible { outline-color: var(--bg); }
.selbar-x {
  order: 4;
  appearance: none; border: 0; background: none; cursor: pointer;
  padding: 6px 8px;
  min-height: 32px;
  color: color-mix(in srgb, var(--bg) 72%, transparent);
  font-family: inherit; font-size: var(--fs-sm); font-weight: 600;
}
.selbar-x:hover { color: var(--bg); }
/* «LIMPAR» TAMBÉM SÓ EXISTE COM JS, e pelo mesmo motivo da caixa «todas»: ele
   desmarca as caixas no cliente, gesto que sem JS não tem quem execute. A
   barra NASCE VISÍVEL (é o JS que a esconde) justamente para o caminho sem JS
   manter o botão que age sobre a seleção — mas o × não é esse botão, e deixá-lo
   lá daria um controle inerte ao lado de um que funciona. */
:root:not(.js-on) .selbar-x { display: none; }
/* «soltando 3…» — o VERBO é do servidor (data-ovy-busy no botão acionado);
   esta regra só o veste. Ocupa o lugar que as ações desocuparam (order: 2 +
   margin-left: auto), para a espera aparecer onde o olho já estava. */
.selbar-busy {
  order: 2;
  display: inline-flex; align-items: center; gap: 8px;
  margin-left: auto;
  font-size: var(--fs-sm);
  color: color-mix(in srgb, var(--bg) 80%, transparent);
}
.selbar-busy::before {
  content: "";
  width: 13px; height: 13px; flex: none;
  border: 2px solid color-mix(in srgb, var(--bg) 35%, transparent);
  border-top-color: var(--bg);
  border-radius: 999px;
  animation: dz-rot .7s linear infinite;
}
@media (pointer: coarse) {
  .selbar-btn, .tst-act, .selbar-x { min-height: 44px; }
}
@media (max-width: 640px) {
  /* Quando a barra quebra em linhas, «Limpar» cai sozinho numa linha nova e
     encostava na esquerda — a única peça da barra que trocava de lado ao
     estreitar. `margin-left: auto` vale POR LINHA num flex que quebra, então
     ele volta para a direita sem afetar o layout de uma linha só. */
  .selbar-x { margin-left: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .selbar { animation: none; }
  .selbar-busy::before { animation: none; }
}

/* CAIXA DE SELEÇÃO DA LINHA. `tr[aria-selected="true"]` é ATRIBUTO e não
   classe de propósito: é estado, e estado mora no ARIA — é o mesmo nó que o
   leitor de tela lê e que o CSS pinta, sem um segundo lugar para divergir. */
.pick { width: 20px; height: 20px; accent-color: var(--accent); cursor: pointer; margin: 0; }
th.pick-cell, td.pick-cell { width: 38px; padding-right: 0; }
/* O ALVO DE 44px SÓ EXISTE COM UM <label> EM VOLTA — uma caixa de seleção nua
   tem o tamanho que o autor lhe der, e 44×44 de caixa seria uma caixa
   grotesca. Esta regra é FORWARD-COMPATIBLE de propósito: se a view envolver o
   input num <label>, o alvo passa a ser a célula inteira sem mudar uma linha
   daqui; se não envolver, a caixa continua clicável no seu tamanho. A margem
   negativa devolve o espaço que o padding toma, para a calha não engordar. */
td.pick-cell label {
  display: flex; align-items: center; justify-content: center;
  min-height: 44px; margin: -10px -8px; padding: 10px 8px; cursor: pointer;
}
/* E a GUARDA para quando o <label> não vier: a caixa NUA (filha direta da
   célula, portanto sem rótulo em volta) cresce sozinha para os 24px do §2.5.8.
   Com rótulo ela não casa este seletor e fica nos 20px calmos lá de cima — a
   folha degrada para o mínimo conformante em vez de para o bonito. */
th.pick-cell > .pick,
td.pick-cell > .pick { width: 24px; height: 24px; }
/* «TODAS» SÓ EXISTE COM JS. Marcar as caixas da página é gesto de cliente —
   sem JS não há quem o faça, e uma caixa que não marca nada é exatamente o
   «controle que não faz nada» que fez a tela de Credenciais recusar esta mesma
   peça (routes/portal.js, nota nº 3). O caso SIMÉTRICO já mora nesta folha
   (`:root.js-on .cj-form .cj-salvar`, que esconde o Salvar quando o JS salva
   sozinho); aqui a marca é lida ao contrário. Quem põe `js-on` no <html> é o
   public/portal.js e ninguém mais. */
:root:not(.js-on) th.pick-cell > .pick { display: none; }
/* E quando ela existe, ela É o cabeçalho da coluna: «sel.» sai para os dois
   não dividirem a calha de 38px. O seletor de irmão faz a troca sem uma classe
   nova — sem caixa (nada selecionável na página), o «sel.» fica. */
:root.js-on th.pick-cell > .pick ~ .small { display: none; }
/* Trilho + tinta: o MESMO idioma de .in-row-blocked (inset 3px à esquerda da
   primeira célula), com --accent no lugar de --warn porque «selecionada» é
   escolha do usuário, não estado da mensagem. */
tr[aria-selected="true"] > td { background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
tr[aria-selected="true"] > td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
@media (pointer: coarse) {
  /* 24px é o alvo mínimo de WCAG 2.2 (2.5.8 Target Size Minimum, AA). Os 44px
     de 2.5.5 exigem um <label> envolvendo o input — decisão de MARCAÇÃO, não
     de folha; está anotada no handoff para F2. O padding da célula garante a
     linha ≥48px, que é o alvo do gesto principal (abrir). */
  .pick { width: 24px; height: 24px; }
  th.pick-cell, td.pick-cell { padding: 12px; }
}
@media (max-width: 720px) {
  /* No cartão de celular o filete E a tinta migram das células para o cartão
     inteiro. Só o filete não bastava: com a calha de 24px da caixa de seleção,
     tingir as <td> deixava uma faixa clara sob o marcador — a linha ficava
     «meio selecionada». Espelha .in-row-blocked. */
  tr[aria-selected="true"] > td:first-child { box-shadow: none; }
  tr[aria-selected="true"] > td { background: none; }
  tr[aria-selected="true"] {
    background: color-mix(in srgb, var(--accent) 7%, var(--panel));
    box-shadow: inset 3px 0 0 var(--accent);
  }
}

/* ── 4. TOAST COM FATIA DE AÇÃO ──────────────────────────────────────────────
   O desenho do .tst-act está com o .selbar-btn (mesma superfície, mesmo peso).
   Aqui mora só a POSIÇÃO: a mensagem é um nó de texto solto (o portal.js usa
   createTextNode — nada de HTML), então quem empurra para a direita é o
   primeiro elemento depois dela. Com «Desfazer» é o .tst-act; sem ele, o ×. */
.tst-act { margin-left: auto; }
.tst-x { margin-left: auto; }
.tst-act ~ .tst-x { margin-left: 0; }
.tst-x {
  appearance: none; border: 0; background: none; cursor: pointer;
  /* 24×24 no ponteiro fino também: WCAG 2.2 §2.5.8 (AA) mede 24px em TODO
     ponteiro, e a exceção de «tamanho definido pelo agente» não vale para um
     <button> que esta folha dimensiona. Os 44px do dedo estão logo abaixo. */
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px;
  padding: 0 4px;
  line-height: 1;
  color: color-mix(in srgb, var(--bg) 60%, transparent);
  font-family: inherit; font-size: var(--fs-xl);
}
.tst-x:hover { color: var(--bg); }
@media (pointer: coarse) {
  .tst-x { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; }
}

/* ── 5. SPARKLINE EM CÉLULA ──────────────────────────────────────────────────
   .spark/.spark-col/.spark-col i(.o/.w/.b) são porte de admin.css:3368-3373 —
   o componente já existe lá e o portal passa a falar o mesmo nome. O que é
   novo é o TAMANHO (.spark-sm) e a série de volume puro (i.a).
   O portal.js escreve role="img" + aria-label com a série por extenso: o
   desenho é redundante, nunca a única fonte do número. */
.spark { display: flex; align-items: flex-end; gap: 2px; height: 52px; flex: 1; min-width: 220px; max-width: 480px; }
.spark-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; background: var(--surface-3); border-radius: 0; overflow: hidden; min-width: 4px; }
.spark-col i { display: block; width: 100%; min-height: 0; }
.spark-col i.o { background: var(--ok); }
.spark-col i.w { background: var(--warn); }
.spark-col i.b { background: var(--bad); }
/* Série sem decomposição ok/warn/bad: volume puro, em acento. */
.spark-col i.a { background: var(--accent); }
/* MODIFICADOR DE CÉLULA: 26px de altura, colunas de 4px, gap de 2px. `flex:
   none` e `min-width: 0` desfazem o `flex:1; min-width:220px` da versão de
   página — dentro de um <td> aquele mínimo estouraria a coluna. */
.spark-sm {
  display: inline-flex; vertical-align: middle;
  height: 26px; flex: none; width: auto; min-width: 0; max-width: none;
}
.spark-sm .spark-col {
  flex: none; width: 4px; min-width: 4px;
  /* A BASE DA SÉRIE, e o motivo dela: um dia de zero desenha uma coluna vazia
     e um dia de «1 num pico de 900» desenha ~1px. Sem uma linha de base, os
     dois viram a mesma mancha cinza — e «zero» e «quase nada» são fatos
     diferentes (é a mesma razão do piso de 4% no portal.js). O filete usa o
     acento diluído: pertence à série, não à moldura. */
  box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--accent) 35%, var(--panel));
}

/* ── 6. NAV DE NÍVEL 2 (pílulas) + O <select> DE CELULAR ─────────────────────
   O shell já emite tudo isto (T1.1); faltava a folha. A pílula em si é
   .dz-pill — o estado ativo/EM BREVE está lá em cima, junto do filtro, para
   que os dois não possam divergir. */
.subnav2 {
  background: var(--neutral-bg);
  border-bottom: 1px solid var(--neutral-line);
}
/* MESMA caixa do .topbar-inner/.subnav-inner: largura máxima e recuo lateral
   idênticos, senão o nível 2 não alinha com o nível 1 logo acima dele. */
.subnav2-inner {
  max-width: 1280px; margin: 0 auto; padding: 0 24px;
  display: flex; align-items: center; gap: 4px;
  min-height: 38px;
  overflow-x: auto;
}
/* ── A INVERSÃO DO NÍVEL 2 (rodada 7) ───────────────────────────────────────
   O port herdava a pílula do FILTRO: fundo branco + borda no repouso, e o
   ativo invertendo para quase-preto. Sobre a faixa cinza isso lê ao contrário
   do mockup — as quatro pílulas de repouso viravam quatro botões brancos
   gritando, e a ativa virava uma mancha preta que o mockup não tem em lugar
   nenhum do nível 2.

   No mockup a faixa é cinza e é o BRANCO que marca o ativo:
     repouso : fundo transparente, borda transparente, peso 500
     ativo   : fundo #fff (var(--panel)), borda #C9D1DE, peso 600
   Ou seja: o item onde você está é o único recorte de papel sobre a faixa —
   o mesmo idioma da aba de nível 1 logo acima, que também se ENCHE.

   ISTO CORRIGE O MAPA-CLASSES §1.6, que declarou o preto-invertido como
   divergência intencional para «não falar duas línguas na mesma tela». A
   premissa estava errada, e dá para conferir no próprio mockup: o filtro de
   Domínios (`dchip`) marca o ativo com `bg:'#0F172A', fg:'#fff'` — preto —
   enquanto o nível 2 (`subnav`) marca com `bg:'#fff', bd:'#C9D1DE'`. As duas
   linguagens são DELIBERADAS e dizem respeito à superfície de baixo: no cartão
   branco quem grita é o preenchimento escuro; sobre a faixa cinza quem grita é
   o papel branco. O port não escolheu entre duas línguas — ele colapsou duas
   em uma. Por isso o `[aria-pressed]` do filtro fica exatamente como está e só
   o `[aria-current]` de dentro da faixa muda.

   ALTURA: 26px na faixa de 38 (mockup), contra os 36 da pílula de filtro que a
   mesma classe rende no conteúdo. O `min-height: 44px` de `pointer: coarse`
   continua vencendo os dois no celular — alvo de toque não cede. */
.subnav2 .dz-pill {
  display: inline-flex; align-items: center; gap: 6px;
  flex: none; white-space: nowrap; text-decoration: none;
  height: 26px; padding: 0 12px;
  background: transparent;
  border-color: transparent;
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.subnav2 .dz-pill:hover { color: var(--text); border-color: var(--border-strong); text-decoration: none; }
.subnav2 .dz-pill[aria-current="page"] {
  background: var(--panel);
  border-color: var(--border-strong);
  color: var(--text);
  font-weight: 600;
}
.subnav2 .dz-pill[aria-current="page"]:hover { color: var(--text); }
/* ── O QUE CARREGA O ESTADO, MEDIDO (e por que não é o preenchimento) ───────
   Papel branco sobre a faixa cinza é um degrau pequeno: --panel contra
   --neutral-bg mede 1,13:1, e a borda --border-strong contra a faixa, 1,46:1.
   Nenhum dos dois chega aos 3:1 de 1.4.11 — e não precisam chegar, porque
   nenhum dos dois é a única pista. Medido no navegador, nos dois temas:

     pílula em repouso : --muted sobre --neutral-bg =  4,70:1 · peso 500
     pílula ATIVA      : --text  sobre --panel      = 17,85:1 · peso 600

   O que muda de estado é o TEXTO — quase quatro vezes de contraste — mais o
   peso, mais o `aria-current="page"` que o leitor de tela anuncia. 1.4.11 pede
   3:1 para a informação visual «necessária para identificar componentes e
   estados» e abre a exceção justamente para quando ela está disponível por
   outro meio; aqui está, por três meios.

   NÃO «conserte» isto escurecendo a pílula ativa: seria voltar à mancha preta
   que a rodada 7 tirou, e ela nunca foi um problema de contraste — era um
   problema de idioma (§1.6 do MAPA). A borda do port já é 1 degrau mais forte
   que a do mockup (--border-strong #c3cad4 contra os #C9D1DE de lá). */
/* ── A DICA DE ÁREA ─────────────────────────────────────────────────────────
   Uma linha de 11,5px na ponta direita da faixa dizendo o que a área REÚNE
   («entrada e saída no mesmo lugar»). Zero ocorrências no port até aqui, sete
   no mockup — uma por área. Ela responde a pergunta que o rótulo da aba não
   responde: por que estas quatro pílulas moram juntas.
   `margin-left: auto` em vez do `<div style="flex:1">` do mockup: mesmo
   resultado, um elemento a menos no DOM de toda página.
   COR: os #7E8B9E do mockup medem 3,05:1 sobre a faixa e reprovam AA em texto
   de 12px; var(--muted) mede 4,70:1 sobre --neutral-bg. Mesma cadência,
   contraste que passa — a divergência é a de sempre (§5.6). */
.subnav2-hint {
  margin-left: auto;
  padding-left: 16px;
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
/* «EM BREVE» — AUSÊNCIA declarada, não botão desabilitado com tooltip (MAPA
   §1.4). Tracejado porque é a convenção da folha para «isto ainda não age», e
   --muted (não --faint/--muted-2, que mede 3,11:1 e reprova AA em texto). */
.subnav2 .dz-pill[aria-disabled="true"] {
  cursor: default;
  background: var(--panel-2);
  border-style: dashed;
  color: var(--muted);
}
.subnav2 .dz-pill[aria-disabled="true"] .badge { margin-left: 4px; }
/* Alvo de toque: vale para as DUAS alturas que .dz-pill rende (26 na faixa,
   36 no filtro). Antes a regra só alcançava a faixa, e o filtro de 28px
   reprovava o mínimo de 44 em cinco telas sem que nada avisasse. */
@media (pointer: coarse) { .dz-pill { min-height: 44px; } }

/* ── O SELETOR DE CONTAS (multi-conta, lote 3) ──────────────────────────────
   A faixa vive ACIMA do nível 1 e é a primeira coisa da nav — a pergunta «em
   qual conta eu estou?» vem antes de «em que área estou?», porque a resposta
   errada à primeira invalida tudo o que vier depois dela.

   `<details>` NATIVO, e não um menu montado em JS: sem script a gaveta abre e
   os <form> dentro dela postam. O portal.js só troca o POST nativo por um
   fetch; ele não é o que faz o controle existir.

   A lista é ABSOLUTA sobre a página (e a faixa NÃO tem overflow) porque uma
   gaveta que empurra o conteúdo faz a nav inteira pular no clique. `z-index`
   acima da faixa de nível 2, que é o único vizinho por baixo dela.
   Espaçamentos na rampa; raio pelos tokens. */
.contabar {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.contabar-inner {
  max-width: 1280px; margin: 0 auto; padding: 6px 24px;
  display: flex; align-items: center; gap: 8px;
}
/* O rótulo da conta ativa. É o MESMO nó nas duas formas (span sozinho e
   <summary> da gaveta), para que a linha não mude de altura nem de peso quando
   a pessoa ganha a segunda conta. */
.conta-atual {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 28px; padding: 0 10px;
  border-radius: var(--radius-sm);
  color: var(--text); font-size: var(--fs-sm); font-weight: 600;
  white-space: nowrap;
}
.conta-menu {
  position: relative;
}
/* `list-style: none` + o ::marker: o triângulo nativo do <summary> aparece de
   formas diferentes em cada navegador e nenhuma delas combina com a barra. O
   afordance fica no cursor e no hover — e o <summary> continua sendo um
   controle de teclado nativo, que é o que importa. */
.conta-menu > summary {
  cursor: pointer;
  list-style: none;
  border: 1px solid var(--border);
  background: var(--bg);
}
.conta-menu > summary::-webkit-details-marker { display: none; }
/* O AFORDANCE NÃO PODE VIVER SÓ NO CURSOR E NO HOVER: no celular não existe
   nenhum dos dois, e o que sobra é um nome com borda — indistinguível de um
   rótulo. O marcador nativo continua escondido (ele diverge por navegador);
   este o substitui por um que desenhamos.
   ELE ENTRA COMO ITEM DE FLEX, e é de propósito: `.conta-atual` é
   `inline-flex`, então o glifo acrescenta LARGURA e não altura. É o que
   preserva a promessa do bloco — a linha não muda de altura nem de peso
   quando a pessoa ganha a segunda conta. Só no <summary>: o <span> da conta
   única não abre nada, e uma seta ali prometeria um menu que não existe. */
.conta-menu > summary::after { content: '▾'; }
.conta-menu > summary:hover { border-color: var(--border-strong); }
.conta-menu[open] > summary { border-color: var(--border-strong); }
.conta-lista {
  position: absolute; z-index: 30;
  top: calc(100% + 4px); left: 0;
  min-width: 240px; max-width: 360px;
  /* TETO DE ALTURA — a gaveta é `absolute`, então o que passar da janela não
     tem como ser alcançado: não há contêiner rolável por baixo dela, e a faixa
     de contas não tem overflow (ver o cabeçalho do bloco). Item de 36px × 20
     contas = 720px pendurados do topo, e num notebook de 768px os últimos
     ficam FORA da tela, sem gesto que os traga. O `min()` mantém o teto
     proporcional na janela baixa e cravado na alta. */
  max-height: min(60vh, 420px); overflow-y: auto;
  padding: 4px;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}
/* Cada conta é um <form> (as que dão para ir) ou um <span> (a que já é a de
   agora). Os dois rendem a MESMA linha: forma igual para coisas da mesma
   natureza, e o que os distingue é a cor e o cursor, não a geometria. */
.conta-item {
  display: block;
  border-radius: var(--radius-sm);
}
.conta-item > button {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 36px; padding: 0 10px;
  border: 0; background: none;
  color: var(--text); font: inherit; font-size: var(--fs-sm); text-align: left;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.conta-item > button:hover { background: var(--neutral-bg); }
.conta-item-on {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 36px; padding: 0 10px;
  color: var(--muted); font-size: var(--fs-sm);
  background: var(--neutral-bg);
}
/* ALVO DE TOQUE (norma de UX §8) — a mesma régua que `.btn-sm`, `.navpick` e
   `.selbar-btn` já recebem nesta folha. O seletor era a exceção (28px no
   <summary>, 36px no item) e é justamente o controle que decide EM QUAL CONTA
   o próximo gesto acontece. `pointer: coarse` e não `max-width`: o que exige
   o alvo maior é o dedo, não a largura da janela — um tablet de 1024px é
   coarse e errar a conta lá custa o mesmo.
   Vale para as DUAS formas de `.conta-atual` (o <span> da conta única e o
   <summary> da gaveta) para que a faixa não mude de altura entre elas. */
@media (pointer: coarse) {
  .conta-atual, .conta-item > button, .conta-item-on { min-height: 44px; }
}

/* O <select> do nível 1 no celular. Nasce escondido e a media query o troca
   pelos links — e o <noscript> do portal-head desfaz a troca com !important,
   porque sem JS este controle não navega e um controle morto é pior que
   nenhum. A condição «não tem script» nenhuma media query sabe responder. */
.navpick { display: none; }
@media (max-width: 640px) {
  .subnav-inner { display: none; }
  .navpick {
    display: block;
    width: 100%;
    min-height: 44px;
    margin: 8px 0;
  }
  .subnav { padding: 0 16px; }
  /* A faixa de contas acompanha o recuo das outras duas no celular. Sem esta
     linha ela ficaria com 24px enquanto a nav ao lado tem 16 — um degrau de
     8px na borda esquerda que só aparece na largura onde ninguém mede. */
  .contabar-inner { padding: 6px 16px; }
  .subnav2-inner { padding: 8px 16px; }
  /* A 402px o mockup troca o nível 1 por um <select> e não desenha dica
     nenhuma: numa faixa que já rola na horizontal, a dica seria texto que só
     aparece depois de arrastar. */
  .subnav2-hint { display: none; }
}

/* ── 7. CARTÃO DE CELULAR DA LINHA DE MENSAGEM ───────────────────────────────
   NÃO é um segundo DOM: é a MESMA <table class="table-cards"> com um
   modificador que reordena as células e cala o rótulo impresso onde o cartão
   já diz o que é. Os data-label continuam todos preenchidos —
   portalMensagensCelular.test.js exige, e é isso que dá o cartão de graça em
   toda outra tabela do portal.
   A ordem é a do HANDOFF §6: desfecho e hora no topo, assunto, destinatário em
   mono, motivo por extenso, resposta SMTP como citação, ação em largura cheia. */
@media (max-width: 640px) {
  table.table-cards.msg-cards > tbody > tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 6px 10px;
    background: var(--panel);
    border-radius: var(--radius);
    padding: 12px 14px;
  }
  .msg-cards > tbody > tr > td[data-cell] {
    display: block; padding: 0; text-align: left; white-space: normal;
  }
  /* O rótulo impresso some nas células que o cartão já explica pela forma. O
     atributo FICA no HTML — quem o consome aqui é o leitor de tela, não o
     ::before. */
  .msg-cards > tbody > tr > td[data-cell]::before { display: none; }

  /* TABELA COM CAIXA DE SELEÇÃO colapsa em «24px 1fr» (HANDOFF §6), não em uma
     coluna: a caixa vira CALHA à esquerda e o cartão inteiro fica à direita
     dela. Em uma coluna só, o marcador viraria uma linha «rótulo: valor» com
     rótulo vazio no topo do cartão — um controle sem nome. Vale para QUALQUER
     .table-cards, não só a de mensagens: quem tem td.pick-cell tem calha. */
  table.table-cards > tbody > tr:has(> td.pick-cell) {
    display: grid;
    grid-template-columns: 24px minmax(0, 1fr);
    align-items: start;
    gap: 0 12px;
  }
  /* `grid-row: 1` e não `1 / -1`: sem linhas EXPLÍCITAS no template, `-1`
     resolve para a linha 1 e o intervalo degenera — o marcador escorregava
     para dentro do conteúdo e as células colidiam umas sobre as outras
     (fotografado em 402px antes desta linha). A calha fica vazia abaixo, que é
     o que «24px 1fr» quer dizer. */
  table.table-cards > tbody > tr > td.pick-cell {
    grid-column: 1; grid-row: 1;
    display: block; padding: 8px 0; text-align: left;
  }
  table.table-cards > tbody > tr > td.pick-cell::before { display: none; }
  table.table-cards > tbody > tr:has(> td.pick-cell) > td:not(.pick-cell) { grid-column: 2; }
  /* A de mensagens mantém a hora na terceira coluna; o resto atravessa. Os
     seletores repetem `table.table-cards` de propósito — a regra genérica logo
     acima tem mais tipos e venceria uma versão curta. */
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) {
    grid-template-columns: 24px minmax(0, 1fr) auto;
  }
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) > td[data-cell="when"] { grid-column: 3; }
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) > td[data-cell="subject"],
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) > td[data-cell="who"],
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) > td[data-cell="reason"],
  table.table-cards.msg-cards > tbody > tr:has(> td.pick-cell) > td[data-cell="go"] { grid-column: 2 / -1; }

  .msg-cards td[data-cell="state"] { order: 1; font-weight: 700; font-size: var(--fs-2xs); letter-spacing: .04em; }
  .msg-cards td[data-cell="when"]  { order: 2; text-align: right; font-size: var(--fs-2xs); color: var(--muted); }
  .msg-cards td[data-cell="subject"] {
    order: 3; grid-column: 1 / -1;
    font-size: var(--fs-md); font-weight: 600; line-height: 1.35;
  }
  .msg-cards td[data-cell="who"] {
    order: 4; grid-column: 1 / -1;
    font-family: var(--mono); font-size: var(--fs-2xs); color: var(--muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* O MOTIVO herda o trio --h-* da <tr data-health> — inclusive «held», que é
     preenchimento âmbar com --on-warn por cima. Bloco tingido com filete, o
     mesmo idioma de .chk-fix: a resposta SMTP entra aqui como citação. */
  .msg-cards td[data-cell="reason"] {
    order: 5; grid-column: 1 / -1;
    margin-top: 2px; padding: 8px 10px;
    background: var(--h-bg, var(--panel-2));
    color: var(--h-ink, var(--muted));
    border-left: 3px solid var(--h-ink, var(--border-strong));
    border-radius: 0 6px 6px 0;
    font-size: var(--fs-xs); line-height: 1.5;
  }
  /* A resposta SMTP é CITAÇÃO dentro do motivo, não uma segunda faixa: o
     .chk-fix traz fundo --info-bg e filete próprios, e empilhá-los dentro de um
     bloco já tingido dava duas molduras concêntricas para um fato só. Aqui ele
     perde a moldura e ganha o mono — a voz de quem falou é o servidor de lá. */
  .msg-cards td[data-cell="reason"] .chk-fix {
    margin: 6px 0 0; padding: 0; border: 0; background: none;
    color: inherit; font-family: var(--mono); font-size: var(--fs-2xs);
  }
  /* AÇÃO EM LARGURA CHEIA: no dedo, um botão de 90px no canto do cartão é o
     alvo que a pessoa erra. */
  .msg-cards td[data-cell="go"] { order: 6; grid-column: 1 / -1; margin-top: 4px; }
  .msg-cards td[data-cell="go"] .btn { width: 100%; min-height: 44px; }
  /* 128px de recuo pendurado do mockup não sobrevivem a 402px (MAPA §2.1) — o
     cartão inteiro é o recuo, e a hierarquia vem de peso e tinta. */
}

/* ── 8. OS TRÊS SABORES DE VAZIO ─────────────────────────────────────────────
   Zero classe nova: o sabor é um atributo, e ele publica o trio --h-* que o
   medalhão do ícone já consome (a caixa de 44px está lá em cima, no .c-empty-ic).
   Assim a view diz o SABOR e não precisa repetir a cor — um lugar só. */
.c-empty[data-empty="first"]  { --h-bg: var(--info-bg);    --h-ink: var(--info); }
.c-empty[data-empty="filter"] { --h-bg: var(--neutral-bg); --h-ink: var(--neutral); }
.c-empty[data-empty="good"]   { --h-bg: var(--ok-bg);      --h-ink: var(--ok); }
/* «BOA NOTÍCIA» NÃO PEDE AÇÃO E NÃO CONVIDA A CRIAR (HANDOFF §5). O rodapé
   dele é um carimbo («última verificação há 2 min»), não um CTA — a regra
   REBAIXA o peso em vez de esconder o nó: esconder o que o servidor mandou
   seria o desenho mentindo sobre o que a página contém. Se alguém puser um
   botão primário aqui, ele vai LER como nota de rodapé, e é esse o sinal. */
.c-empty[data-empty="good"] .c-empty-cta { font-size: var(--fs-xs); color: var(--muted); }

/* ── 9. LINHA DO TEMPO DA MENSAGEM ───────────────────────────────────────────
   Estende .timeline/.tl-item/.tl-dot/.tl-body, que já existem — não abre um
   segundo namespace (MAPA §1.11). Quatro colunas: nossa · trilho · deles · Δ.

   O CONECTOR É O PONTO CARO E O MOCKUP RESOLVE CERTO: o tempo decorrido vira
   DISTÂNCIA VERTICAL. A altura vai INLINE (`style="min-height:NNpx"`),
   derivada no servidor por 8 + 30·log₁₀(1+s), limitada a 8–132px — é geometria
   de DADO, não valor de design, e por isso não é medida pela rampa. E é
   `min-height` na célula, nunca `height`: com altura fixa o trilho não
   acompanha o cartão vizinho e sobram vãos de 2–4px em cada nó. */
/* A quarta coluna é o Δ. 64px e não 56: «acum. 6h 12m» encostava na borda do
   cartão em 1280px — medido. */
.tl-grid { display: grid; grid-template-columns: minmax(0, 1fr) 40px minmax(0, 1fr) 64px; }
.tl-col {
  font-size: var(--fs-3xs); letter-spacing: .1em; font-weight: 700;
  text-transform: uppercase; color: var(--muted);
}
.tl-col[data-side="ours"]   { text-align: right; padding-right: 10px; color: var(--accent); }
.tl-col[data-side="theirs"] { padding-left: 10px; color: var(--warn); }

.tl-chapter {
  grid-column: 1 / -1;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm); font-weight: 700;
}
.tl-chapter-n {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  border-radius: var(--radius-sm);
  background: var(--text); color: var(--bg);
  font-size: var(--fs-2xs); font-weight: 700;
}

.tl-gap { grid-column: 2; display: flex; align-items: stretch; justify-content: center; }
/* `align-items: stretch` no pai é quem estica — e não `height: 100%` no filho,
   que é indefinido dentro de um pai de altura automática. */
.tl-gap-line { width: 2px; background: var(--line-2); }
/* Espera longa (≥1h): o trilho vira tracejado. Atributo, não segunda classe —
   é o MESMO conector com outra qualidade de tempo. */
.tl-gap[data-long="1"] .tl-gap-line {
  background: repeating-linear-gradient(to bottom, var(--line-2) 0 4px, transparent 4px 10px);
}
/* `min-width:0` + quebra: a trilha do Δ tem 64px fixos e «acum. 2d 10h» mede
   67 — três pixels que viravam rolagem horizontal na grade inteira. Quebrar em
   duas linhas custa altura onde há espaço de sobra e devolve a largura. */
.tl-delta {
  grid-column: 4; display: flex; align-items: center; flex-wrap: wrap;
  min-width: 0; font-size: var(--fs-2xs); color: var(--muted);
}
.tl-delta[data-weight="strong"] { font-size: var(--fs-xs); font-weight: 700; color: var(--text); }

/* HALO na bolinha: escopo .tl-grid de propósito — o .timeline simples (a
   .tl-item de outras telas) não tem trilho atrás para o halo recortar. */
.tl-grid .tl-dot {
  position: relative; z-index: 1;
  width: 11px; height: 11px; margin: 0;
  background: var(--h-ink, var(--accent));
  box-shadow: 0 0 0 4px var(--panel);
}
/* Pilha de bolinhas = grupo de retentativas: uma cheia (a primeira) e as
   fantasmas por cima, para o olho contar sem ler o número. */
.tl-dots {
  grid-column: 2; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 6px 0;
}
.tl-dots::before {
  content: ""; position: absolute; top: 0; bottom: 0;
  width: 2px; background: var(--line-2);
}
.tl-dot-ghost {
  position: relative; z-index: 1;
  width: 11px; height: 11px; flex: none;
  border-radius: 50%;
  border: 2px solid var(--h-ink, var(--accent));
  background: var(--panel);
}

.tl-card {
  min-width: 0;
  padding: 10px 14px;
  background: var(--h-bg, var(--panel));
  border: 1px solid var(--h-line, var(--border));
  border-radius: var(--radius);
  font-size: var(--fs-sm); line-height: 1.4;
}
.tl-card[data-side="ours"]   { grid-column: 1; justify-self: end; text-align: right; max-width: 420px; }
.tl-card[data-side="theirs"] { grid-column: 3; max-width: 460px; }
/* ── A RESPOSTA SMTP É CITAÇÃO, NÃO METADADO ────────────────────────────────
   TIMELINE.md, textual: «é o texto mais valioso da tela inteira… deveria ser
   tratado como citação: monoespaçada, com peso, copiável — não como metadado em
   cinza claro». O port cumpria três quartos disso (mono, peso, copiável) e
   parava no quarto: um filete de topo a 8% e a tinta do estado, que é
   exatamente a cara de metadado. Faltavam o FUNDO e o FILETE LATERAL — as duas
   coisas que, num cartão, dizem «isto aqui é fala de outra pessoa».

   O filete lateral herda --h-ink: a citação de um bounce chega vermelha, a de
   uma entrega chega verde. É o mesmo trio de saúde do cartão que a contém, e
   por isso não inventa vocabulário. */
.tl-raw {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--panel-2);
  border-left: 3px solid var(--h-ink, var(--border-strong));
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: 500;
  line-height: 1.6; word-break: break-word;
  color: var(--ink);
  text-align: left;
}

/* ── O VEREDITO — a resposta em português, antes da cadeia ───────────────────
   A pergunta que traz alguém a esta tela é «por que não chegou», e a resposta
   estava distribuída em trinta e seis passos. O veredito responde primeiro; a
   cadeia vira a EVIDÊNCIA de uma frase que já foi lida.

   A frase é TRADUÇÃO do código que o provedor mandou (5.2.2 → «caixa sem
   espaço»), nunca inferência: o vocabulário é fechado e mora na lib, e código
   que não está no mapa cai no genérico honesto por classe (4xx temporário /
   5xx definitivo). É a mesma disciplina que mantém a decomposição do score
   fora desta tela — traduzir o que foi medido é legítimo, afirmar o que
   ninguém mediu não é. */
.tl-verdict { margin-bottom: 18px; }
.tl-eyebrow {
  font-size: var(--fs-3xs); letter-spacing: .1em; font-weight: 700;
  text-transform: uppercase; color: var(--muted);
  margin-bottom: 8px;
}
.tl-verdict-title { margin: 0 0 12px; font-size: var(--fs-2xl); font-weight: 700; line-height: 1.25; }
/* A citação do veredito é a MESMA peça da citação do cartão, num grau acima de
   tamanho porque aqui ela é o assunto do bloco, não o rodapé de um passo. */
.tl-quote {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px 16px;
  background: var(--panel-2);
  border-left: 3px solid var(--h-ink, var(--border-strong));
  border-radius: 0 var(--radius) var(--radius) 0;
}
.tl-quote-text {
  flex: 1; min-width: 0;
  font-family: var(--mono); font-size: var(--fs-md); font-weight: 500;
  line-height: 1.65; color: var(--ink); overflow-wrap: anywhere;
}
.tl-quote .btn { flex: none; }
.tl-verdict-plain { margin: 12px 0 0; font-size: var(--fs-sm); color: var(--tint-ink); line-height: 1.6; }
.tl-verdict-act { margin: 8px 0 0; font-size: var(--fs-xs); color: var(--muted); line-height: 1.55; }
/* «SAIU DA NOSSA MÃO» — a laje escura que corta a linha do tempo em dois
   regimes de responsabilidade. O mockup usa uma menta cravada no rótulo; ela
   cede para --bg (a catraca de cor é orçamento zero nesta folha, e menta sobre
   quase-preto precisaria de um token novo sem outro consumidor). */
.tl-handoff {
  grid-column: 1 / -1;
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  margin: 4px 0;
  padding: 12px 14px;
  background: var(--text); color: var(--bg);
  border-radius: var(--radius);
}
.tl-handoff strong {
  font-size: var(--fs-3xs); letter-spacing: .1em; font-weight: 700;
  text-transform: uppercase; white-space: nowrap;
}
.tl-handoff span {
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.5;
  color: color-mix(in srgb, var(--bg) 82%, transparent);
}

/* ── DE QUEM FOI O TEMPO — a barra precisava de dono ────────────────────────
   A barra existia solta entre dois blocos, sem título e sem legenda: duas
   faixas coloridas que o leitor tinha de adivinhar. Uma barra de proporção sem
   legenda não é gráfico, é decoração — e esta responde a pergunta nº 2 do
   brief («é problema nosso ou deles?»), que é boa demais para ficar em código
   de cor não declarado.

   Agora ela é um bloco com rótulo e legenda nomeando as duas cores. Não é um
   `.card`: a partial já é renderizada DENTRO do cartão «Linha do tempo», e
   cartão dentro de cartão duplica borda e sombra por nada. */
.tl-attrib { margin-bottom: 18px; }
.tl-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 8px 0 0; }
.tl-legend > span { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--muted); }
/* O quadrado da legenda é o MESMO objeto gráfico da fatia que ele nomeia — se
   um dia a fatia mudar de cor, as duas mudam juntas porque leem o mesmo token. */
.tl-legend i { flex: none; width: 10px; height: 10px; border-radius: 999px; }
.tl-swatch-mine   { background: var(--accent); }
.tl-swatch-theirs { background: var(--warn); }

/* DE QUEM FOI O TEMPO. As larguras vão inline (proporção é dado); os `min-width`
   garantem que uma fatia de 0,4% ainda apareça. */
.tl-split { display: flex; gap: 2px; height: 12px; border-radius: 999px; overflow: hidden; margin: 0 0 10px; }
.tl-split-mine   { background: var(--accent); min-width: 6px; }
/* --warn, e não o #E0A106 do mockup: aquele mede 2,27:1 e reprova o mínimo de
   3:1 de objeto gráfico (MAPA §5.6). */
.tl-split-theirs { background: var(--warn); min-width: 6px; }

/* DECOMPOSIÇÃO DO SCORE — «o que pesou». O sinal em coluna própria, alinhado à
   direita, para os números formarem uma régua que se lê de cima a baixo. */
.tl-score { display: flex; flex-direction: column; gap: 6px; }
.tl-factor { display: flex; align-items: baseline; gap: 10px; font-size: var(--fs-sm); line-height: 1.45; }
.tl-sign {
  flex: none; width: 30px; text-align: right;
  font-family: var(--mono); font-size: var(--fs-sm); font-weight: 600;
  color: var(--h-ink, var(--muted));
  font-variant-numeric: tabular-nums;
}
.tl-headers {
  margin-top: 12px; padding: 12px 14px;
  max-height: 260px; overflow: auto;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.7;
  color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere;
}

@media (max-width: 640px) {
  /* Quatro colunas não cabem em 402px. O trilho vai para a esquerda e os dois
     lados dividem a mesma coluna — a POSIÇÃO deixa de carregar «nossa/deles»,
     então o filete lateral do cartão passa a carregar, com as mesmas duas
     cores da barra de atribuição de tempo. Nada de scroll horizontal. */
  .tl-grid { grid-template-columns: 20px minmax(0, 1fr); }
  .tl-col { display: none; }
  .tl-gap, .tl-dots { grid-column: 1; }
  /* OS DOIS LADOS REPETEM O ATRIBUTO, e não é redundância: media query não
     acrescenta especificidade nenhuma. Um `.tl-card { grid-column: 2 }` aqui
     PERDE para o `.tl-card[data-side="ours"] { grid-column: 1 }` de cima
     (0,2,0 contra 0,1,0) — e o `theirs` continuaria na coluna 3, que nesta
     grade de duas colunas é IMPLÍCITA: ela nasce do tamanho do conteúdo,
     espreme o `minmax(0,1fr)` até zero e achata todo cartão a 0px de largura.
     O sintoma foi medido em 402px antes desta linha existir. */
  .tl-card[data-side="ours"],
  .tl-card[data-side="theirs"],
  .tl-delta { grid-column: 2; }
  /* Pela mesma aritmética de especificidade, `justify-self`, `text-align` e
     `max-width` também precisam vir pelo atributo: o cartão «nosso» é alinhado
     à direita e limitado a 420px no desktop, e um `.tl-card { stretch }` aqui
     perderia — o cartão ficaria encolhido no canto direito da coluna. */
  .tl-card[data-side="ours"],
  .tl-card[data-side="theirs"] {
    justify-self: stretch; text-align: left; max-width: none;
  }
  /* A POSIÇÃO deixou de dizer de quem é o evento — o filete passa a dizer, com
     as mesmas duas cores da barra «de quem foi o tempo». */
  .tl-card[data-side="ours"]   { border-left: 3px solid var(--accent); }
  .tl-card[data-side="theirs"] { border-left: 3px solid var(--warn); }
  .tl-delta { padding-bottom: 6px; }
  .tl-handoff, .tl-chapter { grid-column: 1 / -1; }
}

/* ── 10. AVULSAS DA LISTA FECHADA (MAPA §4.10 e §4.11) ───────────────────────
   Declaradas com consumidor nomeado; entram agora para que nenhuma tela de F2
   encontre marcação sem estilo — que é exatamente o buraco que este commit
   fecha. */
/* «Retida»: preenchimento âmbar com tinta PRÓPRIA. --on-warn é da família de
   --on-solid (tinta sobre preenchimento); usar um pelo outro dá 1,28:1. */
.badge-held { background: var(--warn-solid); color: var(--on-warn); border: 1px solid var(--warn-bd); }
/* Cartão que é MOLDURA de linhas: sem respiro interno, e recorta os cantos das
   filhas que encostam na borda. */
.card-flush { padding: 0; overflow: hidden; }
/* Bloco de código em superfície escura (/app/api). Mesmo idioma de .tst. */
.codeblock {
  margin: 0; padding: 14px 16px;
  background: var(--text); color: var(--bg);
  border-radius: var(--radius);
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.6;
  white-space: pre-wrap; overflow-wrap: anywhere;
  overflow-x: auto;
}
/* Busca com ícone à esquerda. O portal não tinha esse padrão: cinco telas em
   escopo o pedem, e sem ele cada uma inventaria o seu. O recuo do campo é o
   que reserva o lugar do ícone — o ícone não empurra, ele ocupa. */
.searchbox { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.searchbox .ic {
  position: absolute; left: 10px;
  width: 16px; height: 16px;
  color: var(--muted); pointer-events: none;
}
.searchbox input { padding-left: 32px; width: 100%; }

/* ═══════════════════════════════════════════════════════════════════════════
   PROTEÇÃO DE REDE — O CARTÃO POR CREDENCIAL (fidelidade de layout, lote A)
   ═══════════════════════════════════════════════════════════════════════════
   O PROBLEMA QUE ESTE BLOCO RESOLVE é de emparelhamento, não de enfeite. A tela
   respondia «quais faixas existem?» numa tabela e «quais IPs acessaram?» noutra,
   as duas GLOBAIS — e a pergunta que a pessoa traz é de UMA credencial: «esta
   aqui está cercada, e o que ficou de fora?». Com as duas listas separadas por
   uma tela de distância, responder exigia cruzar linhas de cabeça, credencial
   por credencial. Medido: 1211px de conteúdo contra 645px no mockup, e o
   trabalho de cruzamento por cima.

   Agora cada credencial é um cartão, e dentro dele as suas faixas ficam ao lado
   dos seus acessos. O emparelhamento deixa de ser tarefa do leitor e passa a ser
   a forma do desenho. */
.rede-cred-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
/* O interruptor da cerca encosta na borda oposta do nome: é o único CONTROLE
   do cabeçalho, e misturado aos selos (que são leitura) ele viraria mais um
   rótulo. `margin-left:auto` e não `float`/`position` porque o cabeçalho já é
   flex e quebra linha no celular — assim ele desce inteiro em vez de sobrepor. */
.rede-cerca-sw { margin-left: auto; }
.rede-cred-name { min-width: 0; font-family: var(--mono); font-size: var(--fs-sm); font-weight: 500; overflow-wrap: anywhere; }
/* As duas colunas do mockup. Abaixo de 700px viram uma só: emparelhar lado a
   lado em 402px daria duas colunas de 170px, e um CIDR não cabe em 170px sem
   quebrar no meio do endereço — que é o único lugar onde quebrar é proibido. */
.rede-cols { display: grid; gap: 18px; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 700px) { .rede-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
.rede-lab {
  font-size: var(--fs-3xs); letter-spacing: .09em; font-weight: 700;
  text-transform: uppercase; color: var(--muted); margin-bottom: 8px;
}
/* AS DUAS COLUNAS FALAM O MESMO IDIOMA DE LINHA — faixa e acesso são objetos
   do mesmo peso, e dar a um deles o formato de nuvem de etiquetas e ao outro o
   de lista faria o olho ler dois assuntos onde há um. Por isso as duas usam
   `.rede-seen-row`, e o que muda é só o que cada linha carrega à direita. */
.rede-seen-row form { display: inline-flex; margin: 0; }
/* O «×» é gesto REVERSÍVEL (recadastrar refaz a faixa), então nada de vermelho
   — a regra do produto reserva danger ao que não volta. 24px é o mínimo de alvo
   do WCAG 2.5.8; no dedo sobe para 44 como o resto da folha. */
.rede-cidr-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: inherit;
  font-family: var(--sans); font-size: var(--fs-md); line-height: 1;
  cursor: pointer;
}
.rede-cidr-x:hover { background: var(--panel-2); border-color: var(--border-strong); }
.rede-cidr-x:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
@media (pointer: coarse) { .rede-cidr-x { width: 44px; height: 44px; } }
/* CREDENCIAL SEM FAIXA — o estado que mais importa nesta tela, e o único que a
   tabela global não sabia mostrar: aberta ao mundo. Tracejado porque é ausência
   de configuração, não falha. */
.rede-open {
  padding: 10px 12px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  background: var(--panel-2);
  font-size: var(--fs-xs); color: var(--muted); line-height: 1.5;
}
.rede-seen-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 0; border-top: 1px solid var(--border);
}
.rede-seen-row:first-child { border-top: none; }
.rede-seen-ip { font-family: var(--mono); font-size: var(--fs-xs); overflow-wrap: anywhere; }
.rede-seen-when { font-size: var(--fs-2xs); color: var(--muted); }
.rede-seen-act { margin-left: auto; }
.rede-none { font-size: var(--fs-xs); color: var(--muted); line-height: 1.5; }

/* ═══════════════════════════════════════════════════════════════════════════
   ORGANIZAÇÃO (multi-conta, lote 8) — /app/org e /app/org/consolidado
   ═══════════════════════════════════════════════════════════════════════════
   Cinco classes, e nenhuma inventa um componente: a tela é feita de `.card`,
   `.tbl`/`.table-cards`, `comp/stat` e `comp/banner`, que já existem. O que
   falta a elas é só o espaçamento entre blocos e o desenho das duas coisas que
   esta tela tem e nenhuma outra: a LISTA DE PARES (pessoa × conta, com a
   origem colada) e o MOTIVO de um controle não estar ali. */

/* Título de bloco. `h2` cru dentro de uma página do portal herda margem de
   documento e cola no cartão anterior. */
.org-h {
  margin: 26px 0 10px;
  font-size: var(--fs-md); font-weight: 700; color: var(--ink);
}
.org-h:first-child { margin-top: 0; }

/* A frase que explica um número, ou a regra por trás dele. Não é `.muted` seco:
   ela precisa de respiro em cima para não ser lida como parte da tabela. */
.org-why { margin-top: 12px; line-height: 1.6; }

/* O MOTIVO DE UM CONTROLE NÃO ESTAR AQUI. Recuado e discreto de propósito — é
   explicação, não alerta: o estado normal de quem não administra a organização
   é justamente não ver os <select>. */
.org-lock { margin-top: 4px; line-height: 1.5; }

/* OS PARES (pessoa × conta). Lista de verdade — é uma enumeração, e um leitor
   de tela tem de ouvi-la como tal —, sem marcador e com uma linha por par. */
.org-chips { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.org-chip {
  padding: 4px 8px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--panel-2);
  font-size: var(--fs-xs); line-height: 1.5; overflow-wrap: anywhere;
}
/* O PAR QUE O PADRÃO NÃO GOVERNA — casa ou concessão própria. Tracejado pelo
   mesmo motivo que `.rede-open`: marca uma condição, não uma falha. É a marca
   que responde «por que essa linha não se mexeu quando eu troquei o padrão?»
   ANTES do clique, e é a metade da tela que impede o controle inerte. */
.org-chip[data-fixa] { border-style: dashed; background: transparent; }

/* Uma conta e quem a alcança. Separador entre contas dentro do mesmo cartão. */
.org-conta { padding: 14px 16px; border-top: 1px solid var(--border); }
.org-conta:first-child { border-top: none; }
.org-conta h3 { margin: 0 0 2px; font-size: var(--fs-sm); font-weight: 700; }

/* O filtro de janela do consolidado, e os três cartões de total. */
.org-filtro { margin: 0 0 14px; }
.org-stats { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); margin-bottom: 14px; }

/* ── TEMPLATES (mig. 138): o quadro do preview ────────────────────────────────
   O corpo HTML renderizado vive num <iframe sandbox srcdoc> — NUNCA no DOM da
   página (o sandbox é a segunda fechadura; a primeira é o escape do motor).
   Fundo branco cravado de propósito e nos DOIS temas: e-mail é renderizado
   sobre branco nos clientes reais, e um preview que escurecesse junto com o
   portal mentiria sobre o contraste que o destinatário vai ver. */
.tpl-preview-frame { width: 100%; min-height: 320px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; }

/* ── POLÍTICAS DE ENVIO (mig. 139/140, PLANO-POLITICAS F4) ───────────────────
   A tela é feita de peças que já existiam nesta folha (`.card`, `.tbl`,
   `.badge`, `.btn`, `.field-row`, `.tool-box`) — o que falta aqui é só o que a
   forma DELA exige. Nada de cor nova: os tokens de tema fazem o trabalho nos
   dois modos, e é isso que impede a tela nova de ser a única que erra o
   contraste quando alguém troca a paleta. */

/* A LINHA DE CONDIÇÃO — «[Assunto contém] [promoção]». O seletor manda no par:
   ele carrega a frase inteira, e o campo ao lado é só o valor. `minmax(0, …)`
   nas duas colunas porque select com opção longa estoura a grade sem isso. */
.pol-cond { display: grid; grid-template-columns: minmax(0, 260px) minmax(0, 1fr); gap: 8px; margin-bottom: 8px; }
.pol-cond-campo, .pol-cond-valor { min-width: 0; }
@media (max-width: 640px) {
  /* No celular as duas viram linhas: lado a lado, o valor fica com 90px e
     nenhum endereço cabe. */
  .pol-cond { grid-template-columns: minmax(0, 1fr); gap: 6px; }
}

/* O RESUMO DAS CONDIÇÕES na lista. Uma condição por linha, sem marcador: são um
   E, e um bullet sugeriria alternativas. */
.pol-conds { margin: 0; padding: 0; list-style: none; }
.pol-conds li { margin: 0 0 2px; color: var(--muted); }

/* POLÍTICA PAUSADA. Ela CONTINUA legível — 0.6 de opacidade apagaria o texto
   abaixo do mínimo de contraste, e pausada é o estado em que a pessoa mais
   precisa LER o que escreveu. O que muda é o fundo, não a tinta. */
.pol-off { background: var(--panel-2); }
.pol-off td { color: var(--muted); }

/* Os dois botões de ordem, um sobre o outro em telas largas e lado a lado no
   celular (onde a linha vira cartão). A largura fixa evita que a coluna dance
   quando um dos dois some na ponta da lista. */
.pol-ordem { white-space: nowrap; }
.pol-ordem .btn { min-width: 32px; padding-left: 6px; padding-right: 6px; }

/* As ações da linha: quebram em vez de estourar a célula. */
.pol-acoes { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* A legenda do que cada ação faz. Mora embaixo do formulário de criar, e não
   num tooltip: são as frases que dizem o que a ação faz HOJE (a honestidade da
   decisão P5 sobre `forcar_transacional`), e honestidade escondida atrás de um
   hover não é honestidade. */
.pol-legenda { margin: 12px 0 0; padding: 0 0 0 18px; color: var(--muted); }
.pol-legenda li { margin-bottom: 4px; }

/* O SIMULADOR. O resultado é uma região viva (role=status): destacada por
   borda, não por cor de alerta — ele não é um aviso, é uma resposta. */
.pol-sim-form { margin-bottom: 12px; }
.pol-sim-out {
  padding: 12px 14px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.pol-sim-out p { margin: 0 0 6px; }
.pol-sim-out p:last-child { margin-bottom: 0; }

/* O cartão do fuso: o controle e o relógio na mesma linha em tela larga. */
.pol-fuso select { max-width: 420px; }

/* ═══════════════════════════════════════════════════════════════════════════
   VOCABULÁRIO COMUM DAS LISTAS (28/08/2026) — pedido do dono, com referência
   visual explícita: «para a listagem de domínios, caixas, credenciais etc. eu
   gosto destas fontes — estilo Cloudflare (fonte, cor e tamanho)». Quatro
   peças, e só quatro; cada tela de lista usa ESTAS em vez de inventar a sua.

   1. `.ent` — o NOME DA ENTIDADE (domínio, endereço, usuário, canal): a fonte
      do sistema (não mono), peso 500, cor do texto (não do link), sublinhado
      fino e afastado. É o que diz «isto abre a coisa» sem gritar azul numa
      coluna inteira. Mono continua onde é valor copiável (host, chave, código).
   2. `.btn-copy` — COPIAR como gesto, não como botão: só o ícone, sem borda,
      na cor apagada; ganha cor ao passar o mouse e fica verde ao copiar
      (classe .copied, que o JS já põe quando o botão tem <svg>). Substitui o
      «⧉» — glifo que a Figtree não tem e que desenhava uma caixa vazia.
   3. `.sw-text` — o rótulo opcional AO LADO do interruptor (.sw-btn), para o
      detalhe onde o cabeçalho de coluna não está lá para dizer o que ele liga.
   4. `.facts` — a FAIXA DE FATOS: rótulo em cima, valor embaixo, várias por
      linha, sem quebra. É o «layout mais informacional, sem quebra de linha»
      que o dono pediu para o detalhe da caixa; substitui o empilhado .dgrp
      onde cada fato ocupava uma linha inteira de 1180px para dizer «Tipo».
   ═══════════════════════════════════════════════════════════════════════════ */
.ent {
  font-family: inherit; font-weight: 500; font-size: var(--fs-lg); color: var(--text);
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px;
  text-decoration-color: var(--border-strong); word-break: break-all;
}
.ent:hover, .ent:focus-visible { color: var(--brand-2); text-decoration-color: currentColor; }

.btn-copy {
  background: transparent; border: 0; padding: 4px; margin: 0 0 0 4px; cursor: pointer;
  color: var(--muted); border-radius: var(--radius-sm); display: inline-flex; align-items: center;
  vertical-align: middle; line-height: 0;
}
.btn-copy .ic { width: 16px; height: 16px; }
.btn-copy:hover, .btn-copy:focus-visible { color: var(--text); background: var(--bg); }
.btn-copy:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 1px; }
.btn-copy.copied { color: var(--ok); background: var(--ok-bg); }

.sw-text { font-size: var(--fs-sm); color: var(--muted); margin-left: 8px; vertical-align: middle; }
.sw-row { display: inline-flex; align-items: center; gap: 0; }

.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px 24px; margin: 0; padding: 0; }
.fact { min-width: 0; margin: 0; }
.fact-k { font-size: var(--fs-sm); color: var(--muted); margin: 0 0 2px; }
.fact-v { margin: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fact-v .spark { vertical-align: middle; }

/* ═══════════════════════════════════════════════════════════════════════════
   WS-TEMPLATES (28/08/2026) — o editor visual de templates
   ═══════════════════════════════════════════════════════════════════════════
   Pedido do dono: «a tela de templates precisa ficar mais bonita como editor
   visual» e «o usuário precisa saber quais variáveis ele pode usar». A tela é
   UM editor (criar e editar): cabeçalho com nome e assunto, a fonte à esquerda
   e o preview à direita, as variáveis embaixo. As peças que já existiam
   (.card, .btn, .badge, .dz-tab, .field-label, .hint, .tbl, .ent) fazem o
   resto; o que está aqui é só a FORMA que esse editor exige. Tokens de tema em
   tudo — nada de cor nova: o iframe continua o único papel branco cravado
   (.tpl-preview-frame, mais acima). Classes prefixadas `tpl-`; espaçamentos só
   na rampa da folha. */

/* O editor é uma pilha de blocos com o mesmo vão; o cabeçalho, outra. */
.tpl-editor { display: grid; gap: 18px; }
.tpl-head { display: grid; gap: 12px; }
.tpl-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0; font-size: var(--fs-md); font-weight: 700; }
.tpl-title code { font-family: var(--mono); font-size: var(--fs-md); }
.tpl-head .hint { margin: 0; }
.tpl-name input, .tpl-subject input { width: 100%; }
/* O nome é um slug curto; o assunto é uma frase — só o nome tem teto. */
.tpl-name { max-width: 420px; }

/* Os dois painéis: fonte × preview. Lado a lado em tela larga; abaixo de
   900px empilham — dois painéis de 300px não editam nem mostram nada.
   `minmax(0, …)` porque textarea e iframe não encolhem sozinhos. */
.tpl-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; align-items: stretch; }
@media (max-width: 900px) { .tpl-grid { grid-template-columns: minmax(0, 1fr); } }
.tpl-pane { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.tpl-pane-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; min-height: 32px; }
.tpl-pane-title { margin: 0; font-size: var(--fs-xs); font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; }
.tpl-pv-note { flex: 1 1 200px; }
.tpl-pv-btn { margin-left: auto; }

/* As abas HTML | Texto. `.dz-tab` dá o desenho (sublinhado azul no ativo); o
   que está aqui é o que um <button> traz de fábrica e uma aba não quer. Sem
   JS as abas somem (o portal.js liga `js-on` no <html>) e os dois corpos ficam
   impressos, um sobre o outro, cada um com o seu rótulo. */
.tpl-tabs { display: none; gap: 16px; margin-left: auto; border-bottom: 2px solid var(--border); }
:root.js-on .tpl-tabs { display: flex; }
.tpl-tabs .dz-tab { appearance: none; background: transparent; border: 0; border-bottom: 2px solid transparent; font: inherit; font-size: var(--fs-sm); line-height: 1.2; cursor: pointer; }
.tpl-tabs .dz-tab:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }
.tpl-tabpane { display: block; }
:root.js-on .tpl-tabpane:not(.tpl-on) { display: none; }
.tpl-ta-label { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 0 0 4px; }
:root.js-on .tpl-ta-label { display: none; }
/* A fonte: 320px de altura mínima, redimensionável só na vertical, a largura
   da coluna. `tab-size: 2` porque HTML de e-mail vem indentado com tab. */
.tpl-ta { display: block; width: 100%; min-height: 320px; resize: vertical; line-height: 1.5; tab-size: 2; }
.tpl-ta.mono { font-size: var(--fs-sm); }
/* O preview preenche a altura do painel-irmão (a fonte manda na altura). */
.tpl-pane .tpl-preview-frame { flex: 1 1 auto; min-height: 320px; }
/* O erro do motor — a MESMA frase do envio real — em cima do quadro, na cor
   de recusa. `hidden` some de verdade: é o JS que o mostra e esconde. */
.tpl-err { margin: 0; padding: 8px 12px; border: 1px solid var(--bad-bd); border-radius: var(--radius-sm); background: var(--bad-bg); color: var(--bad); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.tpl-err[hidden] { display: none; }

/* As variáveis: os chips detectados e, embaixo, os campos de exemplo. Um fio
   em cima separa do par fonte/preview sem abrir um segundo cartão. */
.tpl-vars { display: grid; gap: 12px; padding-top: 18px; border-top: 1px solid var(--border); }
.tpl-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.tpl-vars-empty { margin: 0; }
.tpl-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--panel-2); font-size: var(--fs-xs); line-height: 1.4; }
.tpl-chip code { font-family: var(--mono); font-size: var(--fs-xs); background: none; padding: 0; color: var(--text); }
.tpl-chip-k { color: var(--muted); }
/* Condição e lista ganham a tinta da família (info/atenção) para o olho
   separar «substitui» de «decide» e de «repete» sem ler o rótulo. */
.tpl-chip[data-kind="flag"] { border-color: var(--info-bd); background: var(--info-bg); }
.tpl-chip[data-kind="list"] { border-color: var(--warn-bd); background: var(--warn-bg); }
.tpl-fields { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px 14px; }
.tpl-fields:empty { display: none; }
.tpl-field { min-width: 0; }
.tpl-field input { width: 100%; }
.tpl-field .field-label { margin: 0 0 4px; overflow-wrap: anywhere; }

/* A referência de sintaxe, recolhida: é consulta, não leitura obrigatória. */
.tpl-syntax > summary { cursor: pointer; color: var(--muted); font-weight: 600; }
.tpl-syntax[open] > summary { margin-bottom: 8px; }
.tpl-syntax-tbl td:first-child { white-space: nowrap; }
.tpl-syntax .hint { margin-top: 8px; }

/* As ações numa linha só: salvar (o botão vive FORA do <form>, ligado por
   form=) ao lado de publicar e remover, que são formulários próprios. */
.tpl-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 16px; }
.tpl-actions form { display: inline; }
.tpl-versoes { margin: 12px 0 0; }
/* O cabeçalho da lista: título e o atalho «Criar template» na mesma linha. */
.tpl-list-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin: 0 0 12px; }
.tpl-list-head h2 { margin: 0; }

.ul-plain { list-style: none; padding: 0; margin: 0 0 12px; display: grid; gap: 6px; }
.ul-plain li { margin: 0; }

/* Botão que só faz sentido sem JS (ex.: «Trocar» do provedor da Migração): o
   portal.js marca <html class="js"> ao subir, e aí ele some. */
html.js .js-hide { display: none; }
.mg-prov-form { margin: 0 0 12px; }

/* ═══════════════════════════════════════════════════════════════════════════
   WS-API — LIGA/DESLIGA DA CHAVE DE API (mig. 148, 28/08/2026)
   O interruptor da lista de chaves é o toggle otimista (`data-ovy-toggle`):
   a linha NÃO rerrenderiza depois do clique, então o único texto que pode
   dizer o estado é o rótulo DENTRO do botão, que o contrato
   `data-ovy-sw-label` vira junto com o knob. Regra nova, nunca editando as
   do interruptor (.sw-btn/.sw-track/.sw-knob), que são compartilhadas.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   WS-EXPORT (28/08/2026) — EXPORTAR O QUE ESTÁ NA TELA (CSV · XLSX)
   Pedido do dono: «gerar CSV/XLSX de acordo com o que o usuário selecionou».
   Três peças, e só três, todas na rampa:
   1. `.msg-toolbar` — os chips de direção à esquerda e o controle «Exportar» à
      direita, na mesma linha; em tela estreita o controle desce para baixo dos
      chips (wrap). Os 14px embaixo são os que o form dos chips carregava
      inline; o `.field-row` dentro dela perde a margem porque a barra é quem
      espaça agora.
   2. `.msg-export` — rótulo apagado + dois botões, alinhados no centro.
   3. `.export-lead` / `.export-audit` — as margens que a barra clássica
      (`.export-bar`) tinha em `style=`: a legenda acima do botão e o form de
      auditoria em linha, com os campos de data 2px abaixo do rótulo.
   ═══════════════════════════════════════════════════════════════════════════ */
.msg-toolbar {
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px 16px; flex-wrap: wrap; margin: 0 0 14px;
}
.msg-toolbar .field-row { margin: 0; }
.msg-export { display: inline-flex; align-items: center; gap: 6px; }
.msg-export-label { margin-right: 2px; }
.export-lead { margin-bottom: 4px; }
.export-audit { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin: 0; }
.export-audit input[type="date"] { margin-top: 2px; }

/* No celular o botão «+ Novo X» fica à ESQUERDA do cabeçalho, e o painel que
   cai dele estava ancorado à direita — 222px saíam pela borda (QA, 28/08).
   Pré-existente em Credenciais/Domínios; propagado a Canais/Caixas no mesmo dia. */
@media (max-width: 640px) {
  .dz-add-panel { left: 0; right: auto; width: calc(100vw - 32px); }
}
