/* OVEYON Mail — painel do operador. Folha própria, self-hosted, sem recurso
   externo (CSP-ready). A partir da FASE 0: tema CLARO e ESCURO.

   ── MAPA DE TOKENS SEMÂNTICOS ──────────────────────────────────────────────
   Superfícies  --surface (cartão) · --surface-2 (fundo da página) ·
                --surface-3 (realce sutil / hover de linha)
   Texto        --ink (principal) · --muted (secundário) · --faint (terciário)
   Linhas       --line (borda padrão) · --line-2 (divisor mais forte)
   Marca        --brand (navy, texto-marca) · --brand-2 (link / texto-ação)
   Ação         --accent (preenchimento primário) + --accent-ink + --accent-hover
   Navegação    --nav-bg (superfície navy da sidebar/login — ESCURA nos dois temas)
   Saúde (par de texto+fundo, mais a linha):
                --ok      / --ok-bg      / --ok-line
                --warn    / --warn-bg    / --warn-line
                --bad     / --bad-bg     / --bad-line
                --neutral / --neutral-bg / --neutral-line
                --info    / --info-bg    / --info-line
   Apoio        --tint-ink (texto secundário sobre tints) · --dot-ink (traço do
                marcador da timeline) · --focus (anel de foco)
   Sólido       --on-solid (tinta sobre preenchimento colorido: badge, aba ativa)
   Navy         --nav-ink / --nav-ink-2 / --nav-ink-3 (rampa de TINTA) ·
                --nav-ink-ws (acento do grupo primário) ·
                --nav-hover-soft / --nav-hover / --nav-line / --nav-active /
                --nav-outline / --nav-well (rampa de LAVAGEM, um degrau por papel) ·
                --nav-focus (foco) · --nav-ok / --nav-bad (saúde sobre navy) ·
                --nav-context (trilha "estou dentro de um cliente")
   Elevação     --sh-sm / --sh-md / --sh-pop / --sh-lg (rampa) · --scrim (véu)

   INVARIANTES DE TEMA (definidos só no :root, NUNCA em bloco de tema):
   --badge-danger-bg, --on-solid e toda a família --nav-*. Eles vivem sobre
   superfícies que não mudam com o tema (navy, preenchimento sólido); se algum
   deles for redefinido no bloco escuro, o contraste desaba. Ver o bloco abaixo.

   Compatibilidade: os nomes originais (--bg/--panel/--text/--border) seguem
   sendo a FONTE-DA-VERDADE referenciada pelo CSS; --surface/--surface-2/--ink/
   --line são apelidos semânticos que acompanham via var(). O modo escuro
   sobrescreve só a fonte; os apelidos herdam. Contraste AA em claro E escuro. */
/* ── A FONTE DO REDESENHO (22/08/2026) — O PAINEL ACOMPANHA ─────────────────
   O motivo, os pesos, os subsets e a decisão de auto-hospedar estão escritos
   por extenso no portal.css, junto do bloco gêmeo deste. Aqui fica só a
   pergunta que é DESTE arquivo: o painel do operador acompanha o portal na
   troca de família?

   SIM, e não por gosto. `--sans` e `--mono` são tokens de VOCABULÁRIO
   COMPARTILHADO: as duas folhas os definem com o mesmo valor desde a rodada 2,
   e test/portalContraste.test.js tem catraca de divergência que só admite as
   SETE divergências declaradas (identidade e densidade) — família de fonte não
   é uma delas, e abrir a oitava para um redesenho que não é do painel seria
   exatamente a «divergência silenciosa» que aquela catraca existe para pegar.

   O que sustenta na prática: o painel e o portal compartilham COMPONENTES
   inteiros (o modal de mensagem, os selos de saúde, .btn-sm, a barra de
   simulação). Um componente escrito numa família e lido noutra deixa de ser o
   mesmo componente — foi assim que as quatro pilhas mono derivadas escaparam
   na rodada 5. Uma família, os dois lados, um vocabulário.

   Os quatro arquivos são os MESMOS bytes servidos ao portal (public/fonts/),
   então o operador que abre as duas superfícies no mesmo navegador baixa a
   fonte UMA vez. */
@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;
  /* knob de switch: PEÇA FÍSICA branca nos DOIS temas (mesma doutrina do
     .switch-knob do painel de IPs) — a cor mora no token, uma vez. */
  --sw-knob: #fff;

  /* superfícies e texto — fonte-da-verdade (esquerda) + apelido semântico.
     RODADA 2: os apelidos do PORTAL (--panel-2/--border-strong/--muted-2) entram
     como sinônimos para que as duas folhas falem o MESMO vocabulário. Uma regra
     copiada de um lado para o outro passa a resolver, em vez de cair num
     fallback cravado que ignora o tema. */
  --bg: #f4f5f8;            --surface-2: var(--bg);
  --panel: #ffffff;         --surface: var(--panel);
  --surface-3: #fbfcfd;     --panel-2: var(--surface-3);
  --text: #0f172a;          --ink: var(--text);
  /* Tinta SECUNDÁRIA — valor vindo do portal (#5b6b7d), escolhido por MEDIDA e
     não por qual folha é dona: sobre --panel o antigo #66788c dava 4.54:1 (passa
     raspando) e sobre --bg dava 4.19:1 (REPROVA AA); o novo dá 5.46:1 e 5.05:1.
     Três regras do painel saíam do AA só por causa disso (.jn-rcpt, .eml-tab,
     .money-input .money-sym). */
  --muted: #5f6c80;
  /* Tinta TERCIÁRIA — hoje usada SÓ em ícone decorativo (.c-stat-ic,
     .c-empty-ic). Valor do portal (#8494a5): 3.11:1 sobre --panel, cruza o
     limiar 3:1 de objeto gráfico; o antigo #8a99a9 media 2.91:1 e reprovava.
     NÃO use em texto — nenhum dos dois chega aos 4.5:1 de corpo. */
  /* --muted-2 é o NOME QUE O PORTAL USA para esta mesma tinta. Fica aqui sem
     consumidor de propósito: alias de vocabulário existe exatamente para a regra
     copiada de lá resolver aqui. É categoria diferente de token de VALOR sem
     consumidor (--shadow, --btn-gap), que foram removidos nesta rodada. */
  --faint: #8494a5;         --muted-2: var(--faint);
  --border: #e7e9ee;        --line: var(--border);
  --line-2: #c3cad4;        --border-strong: var(--line-2);
  /* ── A BORDA DO CAMPO — token novo, e a mudança de maior alcance da rodada 5.
     RODADA 4 MEDIU: a borda de input/select/textarea era `var(--border)`, a
     MESMA linha dos cartões e divisores. Sobre --panel isso dá **1,29:1** no
     claro e 1,33:1 no escuro. WCAG 1.4.11 (contraste de elemento NÃO-textual)
     pede 3:1, e aqui se aplica com força total: a borda é a ÚNICA fronteira do
     campo — o preenchimento do input é --panel e o cartão onde ele vive também.
     Sem borda visível o campo não tem forma, e a pessoa descobre onde clicar
     por tentativa. 49 telas, 2140 ocorrências: o defeito de maior alcance do
     produto, e o motivo de esta rodada existir.

     POR QUE UM TOKEN NOVO, E NÃO «escurecer --border». --border desenha divisor
     de cartão, linha de tabela, separador — decoração, que 1.4.11 NÃO cobre
     («elemento de puro enfeite» é exceção escrita na norma). Escurecer aquele
     token engrossaria a folha inteira e trocaria um defeito de acessibilidade
     por um de hierarquia: se toda linha grita, nenhuma informa. O que muda de
     categoria é só o CONTROLE — por isso o nome diz CAMPO, não «borda forte».

     O VALOR SAIU DE MEDIDA. O campo aparece sobre --panel (#fff), sobre
     --surface-3 (#f8fafc) e sobre --bg (#f4f6f9), e a norma pede 3:1 contra o
     que houver de CADA lado da linha. #79879a mede 3,66 · 3,49 · 3,38:1 nas
     três — folga nas três. Reusar --faint (#8494a5) teria sido mais barato e
     estaria errado: dá 3,11 sobre o branco mas 2,97 sobre --surface-3, ou seja,
     passaria na tela de login e reprovaria no formulário dentro do cartão. */
  --field-border: #79879a;

  /* marca / ação */
  --brand: #16405f;
  --brand-2: #1f6699;
  /* ── DIVERGÊNCIA DECLARADA 1 de 2: IDENTIDADE ─────────────────────────────
     RODADA 6 (22/08/2026). Estas três (--accent, --accent-hover, --focus) e as
     quatro de DENSIDADE (--btn-pad-y/-x, --btn-fs, --radius) são as ÚNICAS sete
     que divergem do portal.css. Medido: 72 nomes em comum, 65 no MESMO valor.
     O navy é a identidade do OPERADOR; o portal adotou o azul do redesenho
     (#2563eb). Duas identidades, por decisão — e só aqui. Tudo o que é
     SIGNIFICADO (tinta, superfície, ok/warn/bad/info, borda de campo) tem o
     mesmo valor nas duas folhas, porque um --ok que é um verde numa tela e
     outro na outra é divergência SILENCIOSA: ninguém decidiu, ninguém revisa.
     Com veto desde a rodada 6: test/portalContraste.test.js reprova
     divergência NOVA e exige que a declarada continue declarada. */
  --accent: #1f6699;         /* DIVERGENTE (portal: #2563eb) */
  --accent-ink: #ffffff;
  --accent-hover: #16405f;   /* DIVERGENTE (portal: #1d4fd7) */
  --nav-bg: #16405f;

  /* ── SAÚDE ─────────────────────────────────────────────────────────────────
     RODADA 2 — cada par texto/fundo é o MELHOR das quatro combinações que
     existiam entre as duas folhas, medido (WCAG, contrast.js), não escolhido:
       ok    #1b7440 (painel) sobre #e6f4ec (portal) = 5.12:1  ← era 5.04 e 4.49
       warn  #8a5a00 (portal) sobre #fff3d1 (painel) = 5.36:1  ← era 4.99 e 5.32
       bad   #b32731 (igual)  sobre #fbe6e8 (portal) = 5.41:1  ← era 5.33 e 5.41
       info  #1f5aa8 (igual)  sobre #e6eefb (portal) = 5.83:1  ← era 5.74 e 5.83
     O caso `ok` é o que importa: o cliente via 4.49:1 (REPROVA) em TODO sinal de
     sucesso do portal — flash, toast, badge, marca de checklist.

     RODADA 6 — os quatro pares adotam a paleta do redesenho do portal, e por
     isso mudam AQUI TAMBÉM, no mesmo commit. Não é redesenhar o painel: é troca
     de valor, sem tocar em nenhuma regra de layout. Medido contra o tint em que
     cada um é usado, e não sobre branco:
       ok    #0b7a47 sobre #e7f7ef = 4,87:1     ← era 5,12
       warn  #8a6304 sobre #fdf3de = 4,93:1     ← era 5,36
       bad   #b4442f sobre #fdedea = 4,85:1     ← era 5,41
       info  #1d4fd7 sobre #eef4ff = 6,03:1     ← era 5,83
     Três folgas encolheram e a quarta cresceu; nenhuma cruzou o 4,5. E os
     consumidores do painel foram medidos contra os valores NOVOS antes de o
     commit sair: --tint-ink sobre os quatro tints não desce de 7,89:1, o navy
     não usa nenhum destes tokens (rampa própria, invariante), e o --accent do
     painel não mudou. */
  --ok: #0b7a47;      --ok-bg: #e7f7ef;    --ok-line: #bfe7d2;
  --warn: #8a6304;    --warn-bg: #fdf3de;  --warn-line: #f0e2b4;
  --bad: #b4442f;     --bad-bg: #fdedea;   --bad-line: #f0cfc8;
  /* Preenchimento SÓLIDO de badge de contagem (com texto branco): vermelho fundo
     FIXO nos dois temas — no escuro o --bad é #f08a92 (cor de TEXTO) e branco
     sobre ele reprova AA. Só definido aqui no :root ⇒ não é sobrescrito no dark.
     RODADA 6 — acompanha o --bad novo (5,51:1 com branco, era 6,46). Ele é uma
     CÓPIA do vermelho claro, congelada contra o tema; deixá-la para trás faria o
     selo de contagem ser um vermelho e o estado ser outro, na mesma tela. */
  --badge-danger-bg: #b4442f;
  /* Mesma categoria, outro sinal: ENGAJAMENTO (Lido/Clicado) é sucesso ALÉM da
     entrega, e o teal preenchido salta do verde de "Entregue" sem gritar. Também
     é preenchimento SÓLIDO com --on-solid por cima ⇒ fixo nos dois temas. */
  --engaged-bg: #0d7d78;
  --neutral: #5f6c80; --neutral-bg: #eef1f6; --neutral-line: #d8dee6;
  /* INFO É DA FAMÍLIA DA MARCA (28/08) — o mesmo retinto do portal, no mesmo
     commit: os tokens de significado resolvem IGUAL nas duas folhas (catraca
     de portalContraste). Era #1d4fd7 sobre #eef4ff, um azul solto na paleta. */
  --info: #1f4b70;    --info-bg: #eef2f6;  --info-line: #cfdae6;
  /* Apelidos `-bd` do portal para a MESMA borda de estado. Existem para que uma
     regra escrita numa folha resolva na outra (era `var(--bad-bd)` no portal e
     `var(--bad-line)` aqui, para a mesma linha). */
  --ok-bd: var(--ok-line); --warn-bd: var(--warn-line);
  --bad-bd: var(--bad-line); --info-bd: var(--info-line);
  /* ── ÂMBAR SÓLIDO — chega do redesenho do portal, e chega AQUI TAMBÉM ──────
     RODADA 6. «Retida» é um estado ATIVO que pede decisão humana, e por isso é
     PREENCHIMENTO (~55% de saturação) e não tint de 8–10% como os outros
     quatro. Preenchimento exige tinta própria: branco sobre #f7e2a8 mede 1,28:1
     e o próprio --warn mede 4,24:1 — os dois reprovam. #6b5410 mede 5,65:1.
     --on-warn é da família de --on-solid (tinta sobre PREENCHIMENTO), não de
     --warn (tinta sobre TINT). Nunca use um pelo outro.
     Por que o painel ganha um token que talvez ele ainda não pinte: porque a
     regra é copiada entre as duas folhas o tempo todo, e o modo de falha
     conhecido desta base é `var(--x)` que não resolve — a rodada 2 existe por
     causa dele (`var(--surface, #fff)` renderizando branco no tema escuro).
     Um nome que existe de um lado só é a próxima ocorrência daquele bug. */
  --warn-solid: #f7e2a8;
  --on-warn: #6b5410;

  /* apoio */
  --tint-ink: #3c4a60;
  --dot-ink: #ffffff;
  --focus: var(--accent);

  /* ── TINTA SOBRE SUPERFÍCIE SÓLIDA ─────────────────────────────────────────
     --on-solid é a tinta de texto sobre um PREENCHIMENTO colorido sólido
     (badge de contagem, aba ativa, .badge-engaged). NÃO use --accent-ink aqui:
     ele é #0b1420 no escuro (a tinta certa sobre o accent CLARO do tema escuro),
     e num badge vermelho/teal viraria texto quase preto. Fixo nos dois temas,
     como --badge-danger-bg logo acima e pelo mesmo motivo. */
  --on-solid: #ffffff;

  /* ── SUPERFÍCIE NAVY (sidebar, mtopbar, switcher de cliente) ───────────────
     --nav-bg é ESCURA NOS DOIS TEMAS, então nada que vive sobre ela pode usar
     os tokens de tema (--text/--panel/--border/--accent): no modo escuro eles
     invertem e o contraste desaba. Era por isso que estas cores estavam
     cravadas à mão — o CSS já anotava "cores fixas como os demais controles da
     sidebar". Estes tokens são exatamente aqueles valores, agora nomeados: o
     caminho fácil volta a ser o caminho certo. Fixos nos dois temas DE PROPÓSITO
     — não os redefina em bloco de tema. */
  --nav-ink: #ffffff;              /* tinta primária sobre navy (título, item ativo) */
  --nav-ink-2: #c8d9e8;            /* tinta secundária (item de nav, cabeçalho de subgrupo) */
  --nav-ink-3: #9fb8cc;            /* tinta terciária (legenda, link de plataforma, desabilitado) */
  /* RODADA 3 — o ÚNICO acento de COR da navy: o rótulo do grupo PRIMÁRIO (o
     workspace do cliente). Era #9fdcff cravado, com `opacity:.9` por cima;
     agora é um nome, e a opacidade saiu (ver A OPACIDADE QUE SAIU, abaixo). */
  --nav-ink-ws: #9fdcff;
  /* ── RAMPA DE LAVAGEM SOBRE A NAVY ─────────────────────────────────────────
     RODADA 3. Eram SETE alfas de branco escritos à mão (.05 .09 .10 .12 .14
     .16 .30) para CINCO papéis — dois pares de gêmeos separados por 1 e 2
     pontos de alfa, que ninguém distingue e ninguém defende numa revisão.
     Agora são cinco degraus, um por PAPEL declarado: quem for pintar algo novo
     sobre a navy escolhe o PAPEL, não o alfa.
     As duas fusões (.09→.10 e .14→.16) mudam pixel DE VERDADE, pouco: 2 e 4 de
     255 por canal. Estão medidas e na galeria — não são "arredondamento". */
  --nav-hover-soft: rgba(255,255,255,.05); /* hover de ITEM de nav — fraco DE PROPÓSITO:
                                      era .07 contra .14 do ativo, perto demais, e num
                                      relance parecia haver dois itens selecionados */
  --nav-hover: rgba(255,255,255,.1); /* realce de CONTROLE (botão, sino, item de menu)
                                      e bloco em REPOUSO sobre a navy */
  --nav-line: rgba(255,255,255,.12); /* FIO: divisor de seção e contorno de bloco */
  --nav-active: rgba(255,255,255,.16); /* SELECIONADO: item ativo, controle aberto,
                                      borda do bloco aberto */
  --nav-outline: rgba(255,255,255,.3); /* contorno de controle FANTASMA (sem preenchimento) */
  --nav-well: rgba(0,0,0,.22);     /* POÇO: bloco RECUADO dentro da navy (menu do
                                      switcher). Único degrau escuro — recuo, não elevação. */
  --nav-focus: #6db3e8;            /* anel de foco / marca de seleção sobre navy
                                      (--accent claro é #1f6699 e some na navy) */
  /* Saúde SOBRE A NAVY. Os tokens de saúde do tema não servem aqui: --ok claro
     é #1b7440 e some na navy; --bad escuro é #f08a92, que é cor de TEXTO. Estes
     dois são os valores que já estavam cravados em `.tsw-dot`, agora nomeados. */
  --nav-ok: #55c08a;
  --nav-bad: #e2707a;
  /* Trilha de "estou dentro do workspace de um cliente". Âmbar porque não
     compete com nenhum azul da interface e não carrega semântica de erro — lê
     como CONTEXTO, que é o que ela é. Fica aqui, e não em --warn, por isso. */
  --nav-context: #f0a640;

  /* ── A OPACIDADE QUE SAIU ──────────────────────────────────────────────────
     RODADA 3. `opacity` num rótulo é um multiplicador que NENHUM medidor de
     contraste enxerga: ele lê a cor declarada, não o pixel que sai. `.nav-group`
     trazia `opacity: .65` e `.pf-label` uma tinta cravada; o par rendia 2,75:1
     no tema claro (3,75:1 no escuro) — REPROVA AA — em QUATRO rótulos
     ("Operação", "Entrega", "Clientes", "Sistema") de TODA página do painel.
     E o instrumento dizia "0 reprovações", porque a regra não declara fundo e a
     tinta não era token: o defeito estava escondido nas duas pontas.
     A regra que fica: sobre a navy a hierarquia se faz com a RAMPA DE TINTA
     (--nav-ink / -2 / -3), NUNCA com `opacity`. Dimming que o medidor não vê é
     dimming que ninguém audita. */


  /* ── BOTÃO — a FORMA vira token; o PAPEL continua sendo de cada superfície ──
     Eram 10 variantes (.btn .btn-primary .btn-secondary .btn-ghost .btn-danger
     .btn-link .btn-sm .btn-inline .btn-copy-inline .btn-archive-view), cada uma
     com padding, raio e tipo escritos à mão — o sintoma clássico de componente
     sem token: onze jeitos de dizer "botão".
     O que legitimamente MUDA entre as duas superfícies é o papel do
     preenchimento e o tamanho do alvo primário: no painel o botão padrão é
     CHEIO e denso (o operador clica dezenas por dia); no portal ele é
     CONTORNADO e grande (a tela de entrada empilha três opções de mesmo peso).
     O que NÃO muda — e por isso tem o mesmo valor nos dois arquivos — é a forma
     do botão pequeno e o raio: `--btn-*-sm`, `--btn-radius` e `--btn-min-h`
     são idênticos aqui e no outro arquivo. É isso que finalmente faz `.btn-sm`
     ser a MESMA coisa nas duas telas. */
  /* ── DIVERGÊNCIA DECLARADA 2 de 2: DENSIDADE ──────────────────────────────
     Quatro linhas: --btn-pad-y, --btn-pad-x, --btn-fs e --radius. O painel é
     denso porque o operador clica dezenas de vezes por dia; o portal é folgado
     porque a tela do cliente empilha três ações de mesmo peso. Decisão de
     produto, não dívida — e por isso cada uma carrega a marca na própria
     linha: quem for «unificar» tem de ler o motivo antes. */
  --btn-pad-y: 8px;    --btn-pad-x: 14px;      /* DIVERGENTE (portal: 12px/16px) */
  --btn-pad-y-sm: 6px; --btn-pad-x-sm: 12px;   /* IGUAL no portal.css */
  --btn-radius: 6px;                            /* IGUAL no portal.css */
  --btn-fs: var(--fs-md);                       /* 14px — DIVERGENTE (portal: 15px) */
  --btn-fs-sm: var(--fs-sm);                    /* 13px — IGUAL no portal.css */
  /* (sem --btn-gap aqui: o botão do painel não usa flex/gap. Token de VALOR
     sem consumidor é dívida — a regra que apliquei em --shadow vale para mim.) */
  --btn-min-h: 44px;                            /* alvo de toque do primário */

  --radius: 8px;                                /* DIVERGENTE (portal: 12px) */
  /* ── SOMBRA ────────────────────────────────────────────────────────────────
     RODADA 2 — `--sh-lg` era referenciada por .eml-modal e NUNCA foi definida.
     var() sem fallback e sem token é inválido em tempo de computação: a
     propriedade cai para o valor inicial, então aquele modal simplesmente não
     tinha sombra. O valor é o mesmo do modal do portal. */
  /* RODADA 3 — a rampa de ELEVAÇÃO fechou. Eram seis sombras escritas à mão,
     cada uma com seu alfa e seu desfoque, e nenhuma com nome; a rampa é a
     resposta à pergunta "a quantos milímetros do papel esta peça flutua?".
     Quatro degraus, do mais colado ao mais solto — todos INVARIANTES de tema
     (a sombra do painel é preta nos dois; quem varia com o tema é --shadow, e
     ele mora no portal.css, ver a nota lá embaixo). */
  --sh-sm: 0 1px 6px rgba(0,0,0,.06);   /* colado: realce de cartão em hover */
  --sh-md: 0 6px 20px rgba(0,0,0,.16);  /* solto: peça efêmera (toast) */
  --sh-pop: 0 8px 28px rgba(0,0,0,.22); /* ancorado: popover preso a um botão */
  --sh-lg: 0 20px 60px rgba(0,0,0,.25); /* flutuante: modal e cartão de login.
     RODADA 2 — este token era referenciado por .eml-modal e NUNCA foi definido.
     var() sem fallback e sem token é inválido em tempo de computação: a
     propriedade cai para o valor inicial, então aquele modal simplesmente não
     tinha sombra. O valor é o mesmo do modal do portal. */
  /* VÉU atrás de peça flutuante (backdrop de modal, overlay da gaveta). Uma cor,
     três consumidores aqui e um no portal — era a mesma rgba escrita quatro
     vezes, e o dia em que uma delas mudasse sozinha ninguém notaria. */
  --scrim: rgba(0,0,0,.5);
  /* NÃO existe --shadow/--shadow-sm aqui de propósito. Eles chegaram a ser
     definidos para que o `.card` do painel pudesse ganhar a elevação do portal;
     com aquela unificação revertida (é densidade, não acabamento — ver o
     comentário do `.card`), ficariam como token sem consumidor, que é dívida
     nova disfarçada de vocabulário. O painel só eleva o que flutua.
     E NÃO existe --sh-drawer/--sh-botnav/--sh-knob: as três sombras que sobram
     cravadas são DIRECIONAIS ou de PEÇA (ver as declarações no fim do arquivo).
     Token de um consumidor só é um apelido, não vocabulário. */
  /* 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 (rodada 2) ─────────────────────────────────────────
     Fechada: 14 degraus, os MESMOS nos dois arquivos. Antes eram 25 valores em
     que 12/12.5 e 13/13.5 conviviam sem querer dizer coisas diferentes — meia
     dúzia de meio-pixel que ninguém consegue defender numa revisão.
     Não é progressão geométrica de propósito: um painel de dados legítimo tem
     11/12/13/14 como densidades distintas. O que a escala garante é que o
     conjunto é FECHADO — nenhum tamanho fora desta lista existe no arquivo, e
     escrever um obriga a nomeá-lo aqui primeiro. */
  --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
     Sem token por valor de propósito: espaço aparece em atalho de 2 a 4 valores
     (`padding: 2px 14px 12px`) e `var(--sp-3) var(--sp-7)` seria ilegível. A
     regra que dá para cobrar é "nenhum valor fora da rampa" — e ela é medida
     por `node measure.js`, não confiada à memória de quem edita. */

  /* ── ESCALA DE RAIO ────────────────────────────────────────────────────────
     4px (marca/amostra) · 6px (controle: botão, campo, bloco) ·
     var(--radius) (cartão, painel, modal) · 999px (pílula) · 50% (círculo). */
}

/* ── MODO ESCURO ────────────────────────────────────────────────────────────
   Espelha portal.css. A media query = default pela preferência do SO; os blocos
   [data-theme] deixam o TOGGLE do usuário vencer nos dois sentidos (o
   data-theme=light re-afirma o claro por especificidade maior que a media).
   Só redefinimos a fonte-da-verdade; os apelidos acompanham. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0d141d;
    --panel: #151f2b;
    --surface-3: #1b2735;
    --text: #e7eef6;
    --muted: #9fb0c2;
    --faint: #74869a;
    --border: #263548;
    --line-2: #34465c;
    /* 4,03:1 sobre --panel e 3,67:1 sobre --surface-3 — a mesma folga do claro,
       do outro lado. O escuro tinha o defeito gêmeo: 1,33:1. */
    --field-border: #6b7f95;
    --brand: #cfe0f0;
    --brand-2: #7fb2e6;
    --accent: #5b8df5;
    --accent-ink: #0b1420;
    --accent-hover: #7aa3f7;
    --nav-bg: #0f1a24;
    --ok: #4cc07d;      --ok-bg: #12271b;    --ok-line: #234c33;
    --warn: #e6b855;    --warn-bg: #2a2210;  --warn-line: #4d3f18;
    --bad: #f08a92;     --bad-bg: #2a1315;   --bad-line: #522328;
    --neutral: #9fb0c2; --neutral-bg: #1b2735; --neutral-line: #33465c;
    --info: #8fb6ee;    --info-bg: #131f31;  --info-line: #26364f;
    /* ÂMBAR SÓLIDO no escuro: o par INVERTE — fundo da página sobre o âmbar
       de texto, 10,00:1. #6b5410 aqui seria tinta escura sobre tinta escura. */
    --warn-solid: var(--warn); --on-warn: var(--bg);
    --tint-ink: #c3d3e6;
    --dot-ink: #0d141d;
  }
}
/* Toggle força escuro (vence a preferência do SO). */
:root[data-theme="dark"] {
  --bg: #0d141d;
  --panel: #151f2b;
  --surface-3: #1b2735;
  --text: #e7eef6;
  --muted: #9fb0c2;
  --faint: #74869a;
  --border: #263548;
  --line-2: #34465c;
  --field-border: #6b7f95;
  --brand: #cfe0f0;
  --brand-2: #7fb2e6;
  --accent: #5b8df5;
  --accent-ink: #0b1420;
  --accent-hover: #7aa3f7;
  --nav-bg: #0f1a24;
  --ok: #4cc07d;      --ok-bg: #12271b;    --ok-line: #234c33;
  --warn: #e6b855;    --warn-bg: #2a2210;  --warn-line: #4d3f18;
  --bad: #f08a92;     --bad-bg: #2a1315;   --bad-line: #522328;
  --neutral: #9fb0c2; --neutral-bg: #1b2735; --neutral-line: #33465c;
  --info: #8fb6ee;    --info-bg: #131f31;  --info-line: #26364f;
  --warn-solid: var(--warn); --on-warn: var(--bg);   /* par INVERTIDO — ver a media acima */
  --tint-ink: #c3d3e6;
  --dot-ink: #0d141d;
}
/* Toggle força claro (re-afirma os valores base, vencendo a media escura). */
:root[data-theme="light"] {
  --bg: #f4f5f8;
  --panel: #ffffff;
  --surface-3: #fbfcfd;
  --text: #0f172a;
  --muted: #5f6c80;
  --faint: #8494a5;
  --border: #e7e9ee;
  --line-2: #c3cad4;
  --field-border: #79879a;
  --brand: #16405f;
  --brand-2: #1f6699;
  --accent: #1f6699;
  --accent-ink: #ffffff;
  --accent-hover: #16405f;
  --nav-bg: #16405f;
  --ok: #0b7a47;      --ok-bg: #e7f7ef;    --ok-line: #bfe7d2;
  --warn: #8a6304;    --warn-bg: #fdf3de;  --warn-line: #f0e2b4;
  --bad: #b4442f;     --bad-bg: #fdedea;   --bad-line: #f0cfc8;
  --neutral: #5f6c80; --neutral-bg: #eef1f6; --neutral-line: #d8dee6;
  /* INFO É DA FAMÍLIA DA MARCA (28/08) — o mesmo retinto do portal, no mesmo
     commit: os tokens de significado resolvem IGUAL nas duas folhas (catraca
     de portalContraste). Era #1d4fd7 sobre #eef4ff, um azul solto na paleta. */
  --info: #1f4b70;    --info-bg: #eef2f6;  --info-line: #cfdae6;
  --warn-solid: #f7e2a8; --on-warn: #6b5410;
  --tint-ink: #3c4a60;
  --dot-ink: #ffffff;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font: var(--fs-lg)/1.5 var(--sans);
  min-height: 100vh;
}
a { color: var(--brand-2); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ── ACESSIBILIDADE BASE ─────────────────────────────────────────────────────
   Anel de foco visível e consistente em qualquer interativo (:where() mantém
   especificidade 0, então não atropela o estilo dos componentes). */
:where(a, button, .btn, input, select, textarea, summary, [tabindex], [role="menuitem"]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---- shell: sidebar + conteúdo ---- */
.shell { display: flex; min-height: 100vh; }
/* Chrome mobile (top bar, drawer overlay, bottom nav): inerte no desktop.
   O media query ≤900px liga cada um; ≥901px nada disso existe visualmente. */
.mtopbar, .botnav, .drawer-overlay { display: none; }
.sidebar {
  width: 220px;
  flex: 0 0 220px;
  background: var(--nav-bg);
  /* RODADA 3: era #dce8f2 cravado. É a tinta HERDADA por quem não declara a
     sua dentro da barra; passa a ser o mesmo degrau do item de nav, que é o
     que ela sempre quis ser. (8,73:1 → 7,53:1 sobre a navy; segue AA folgado.) */
  color: var(--nav-ink-2);
  display: flex;
  flex-direction: column;
  /* No desktop a sidebar precede o conteúdo no DOM; sem lift de empilhamento o
     dropdown do sino (que transborda a sidebar) pintaria ATRÁS do conteúdo. Esse
     z-index sobe a sidebar inteira acima do .content (só o popover transborda —
     a barra navy nunca cobre o conteúdo). No mobile a regra da drawer sobrescreve
     (position:fixed; z-index:60), então a ordem drawer/overlay/botnav é preservada. */
  position: relative;
  z-index: 30;
}
.sidebar .logo {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  padding: 18px 16px;
  font-weight: 700;
  font-size: var(--fs-xl);
  color: var(--nav-ink);
  letter-spacing: .5px;
  border-bottom: 1px solid var(--nav-line);
}
.sidebar .logo small { display: block; font-weight: 400; font-size: var(--fs-2xs); color: var(--nav-ink-3); letter-spacing: 1px; }
.sidebar nav { flex: 1; padding: 10px 0; }
/* Item de nav como PÍLULA recuada, não faixa de ponta a ponta. A faixa cheia
   colava nos rótulos de seção ("CLIENTE", "OPERAÇÃO") — sem respiro, o rótulo
   parecia parte do item — e deixava o realce do hover quase indistinguível do
   item ativo (duas barras iguais na largura toda). Recuo + raio resolvem os
   dois: o realce fica contido e o rótulo volta a flutuar sozinho. */
.sidebar nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 10px 12px;
  margin: 1px 8px;
  border-radius: var(--radius);
  color: var(--nav-ink-2);
  font-size: var(--fs-md);
}
/* Ícone de nav (sprite <use>): escala com a fonte, herda a cor do link
   (currentColor) e nunca encolhe/estica quando o rótulo quebra. Decorativo. */
.sidebar nav a .ic { width: 1.1em; height: 1.1em; flex-shrink: 0; }
/* Hover BEM mais fraco que o ativo: antes eram .07 e .14 — perto demais, e num
   relance parecia haver dois itens selecionados. */
.sidebar nav a:hover { background: var(--nav-hover-soft); text-decoration: none; }
/* SEM traço azul no item: a sidebar JÁ tem uma trilha azul de altura total
   (.sidebar.in-tenant) dizendo "você está dentro do workspace de um cliente".
   Um segundo traço da mesma cor, a 8px dela, competia com o primeiro e virava
   ruído — o olho via duas barras azuis e nenhuma das duas dizia nada sozinha.
   A pílula (fundo + texto branco + peso) já marca "você está aqui". */
.sidebar nav a.active {
  background: var(--nav-active);
  color: var(--nav-ink);
  font-weight: 600;
}
.sidebar .whoami {
  padding: 12px 16px;
  border-top: 1px solid var(--nav-line);
  font-size: var(--fs-xs);
  color: var(--nav-ink-3);
  word-break: break-all;
}
.sidebar .whoami button {
  margin-top: 8px;
  width: 100%;
  background: transparent;
  border: 1px solid var(--nav-outline);
  color: var(--nav-ink-2);
  border-radius: 6px;
  padding: 6px;
  cursor: pointer;
  font-size: var(--fs-xs);
}
/* «Trocar senha» — o link do rodapé da sidebar. RODADA 4 MEDIU **1,77:1**, em
   52 telas: era o pior contraste de texto do produto.
   A CAUSA é a mesma do .tsw-sub logo abaixo e está escrita no bloco de tokens:
   a NAVY é a única superfície ESCURA NOS DOIS TEMAS, então nada sobre ela pode
   usar token de tema. Este link não pedia cor nenhuma — só herdava a regra
   global `a { color: var(--brand-2) }`, que no claro é #1f6699. Azul escuro
   sobre azul escuro. (No escuro o --brand-2 vira #7fb2e6 e mediria 7,89:1: o
   defeito só existia no tema CLARO, que é o default de quem nunca mexeu em
   nada. Herança silenciosa é pior que cor errada — ninguém escreveu esta cor,
   e por isso ninguém foi conferi-la.)
   --nav-ink-2 sobre a navy: 7,53:1 no claro, 12,19:1 no escuro. O sublinhado
   entra porque, sem o azul, cor sozinha deixaria de dizer «isto é um link» no
   meio de um bloco que já tem texto e dois botões (1.4.1 — cor não pode ser o
   único portador). */
.sidebar .whoami a { color: var(--nav-ink-2); text-decoration: underline; }
/* Você já está nesta página: a marca é PESO + tinta cheia, o mesmo vocabulário
   do item ativo da nav logo acima. */
.sidebar .whoami a.active { color: var(--nav-ink); font-weight: 600; }
.sidebar .whoami button:hover { background: var(--nav-hover); }

/* Toggle de tema — vive no navy da sidebar (idêntico nos dois temas), então usa
   cores fixas como os demais controles do whoami. O ícone reflete o tema
   EFETIVO: lua no claro (→ escuro), sol no escuro (→ claro). */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; margin-top: 8px; min-height: 44px;
  background: transparent; border: 1px solid var(--nav-outline);
  color: var(--nav-ink-2); border-radius: 6px; padding: 6px 10px;
  cursor: pointer; font: inherit; font-size: var(--fs-xs);
}
.theme-toggle:hover { background: var(--nav-hover); }
.theme-toggle svg { width: 16px; height: 16px; flex: 0 0 auto; }
.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; }

.content { flex: 1; padding: 24px 28px; min-width: 0; }
.content h1 { margin: 0 0 18px; font-size: var(--fs-4xl); }
.content h2 { margin: 26px 0 10px; font-size: var(--fs-xl); }
.content h3 { margin: 18px 0 8px; font-size: var(--fs-lg); }

/* ── `.page-head` — o cabeçalho de tela, na DENSIDADE do painel ──────────────
   A dívida de 22/08: sete telas do painel (/inbound, /inbound/config,
   /inbound/destinations, /logs, /logs/:id, /queue e as abas de entregabilidade)
   abriam com <div class="page-head"> e a regra só existia em portal.css. O que
   isso custava, medido no DOM e não deduzido: o <p class="sub"> caía no
   `margin: 1em 0` do navegador e na tinta de CORPO — o subtítulo lia com o
   mesmo peso do texto da página, e o par título/subtítulo abria 18px no meio
   (a margem do h1 colapsando com a do p) em vez dos 4px de uma linha só de
   cabeçalho.

   NÃO é cópia da regra do portal, e a diferença é deliberada: lá o h1 é
   --fs-6xl (26px), aqui o painel já declara `.content h1` em --fs-4xl (22px).
   Copiar o degrau promoveria SETE telas acima de todas as outras do painel —
   uma decisão de densidade que ninguém tomou, entrando de carona num conserto.
   É a mesma razão pela qual --btn-fs e --radius divergem entre as folhas.

   Os 18px do invólucro são os mesmos 18px que `.content h1` já deixava embaixo:
   uma tela COM subtítulo passa a terminar o cabeçalho na mesma altura de uma
   tela sem. `> h1, > h2` porque /queue usa o mesmo invólucro para abrir a seção
   «Resgate» com um <h2> (e ali o 26px de topo de `.content h2` tem de sair do
   caminho do margin-top que a própria tela pede). */
.page-head { margin-bottom: 18px; }
.page-head > h1, .page-head > h2 { margin: 0 0 4px; }
/* --muted sobre --bg é o par «subtítulo de tela» que portalContraste mede a
   cada commit; nunca --faint, que reprova AA em texto pequeno. */
.page-head p { margin: 0; color: var(--muted); }

/* ═══ Sino de alertas de saúde operacional ═══════════════════════════════════
   Substitui a antiga faixa .banner-danger full-width (que empurrava a página).
   O sino vive no topo da barra navy — desktop no topo da sidebar (.alertbell-d),
   mobile na mtopbar (.alertbell-m) — e abre um popover com os MESMOS alertas.
   Só renderiza quando há alerta (o partial não emite markup vazio). Abre/fecha
   por JS (classe .is-open no wrapper), Esc e clique-fora. */
.alertbell { position: relative; display: inline-flex; flex: none; }

/* Botão-sino: sobre a barra navy (idêntico nos dois temas) — cores fixas como
   os demais controles da sidebar/mtopbar. Alvo de toque ≥44px. */
.alertbell-btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; padding: 8px;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--nav-ink-2); cursor: pointer;
}
.alertbell-btn:hover { background: var(--nav-hover); }
.alertbell.is-open .alertbell-btn { background: var(--nav-active); color: var(--nav-ink); }
.alertbell-btn:focus-visible { outline: 2px solid var(--nav-focus); outline-offset: 2px; }
.alertbell-ic { width: 22px; height: 22px; }

/* Badge de contagem: vermelho urgente (mesma linguagem de .nav-badge), o sinal
   que o operador PRECISA notar sem a faixa gigante. */
.alertbell-badge {
  position: absolute; top: 4px; right: 4px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: var(--radius); font-size: var(--fs-3xs); font-weight: 700; line-height: 16px;
  text-align: center; background: var(--badge-danger-bg); color: var(--on-solid);
  border: 1px solid var(--nav-bg);
}

/* Popover: superfície clara/escura (tokens), ancorado no sino. Nunca rola a
   página lateral (max-width limitado à viewport) nem quebra a barra. */
.alertbell-panel {
  position: absolute; z-index: 60;
  width: 340px; max-width: calc(100vw - 32px);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--sh-pop);
  padding: 8px;
}
.alertbell-head {
  padding: 6px 8px 8px; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .4px; text-transform: uppercase; color: var(--muted);
}
.alertbell-item {
  padding: 10px; border-radius: 6px;
  background: var(--bad-bg); border: 1px solid var(--bad-line);
  border-left: 3px solid var(--bad);
}
.alertbell-item + .alertbell-item { margin-top: 8px; }
.alertbell-item-title { font-size: var(--fs-sm); font-weight: 700; color: var(--bad); margin-bottom: 4px; }
.alertbell-item-body { font-size: var(--fs-sm); line-height: 1.45; color: var(--ink); word-break: break-word; }
.alertbell-item-body a { color: var(--brand-2); text-decoration: underline; }

/* Desktop: sino no topo da sidebar; popover cai logo abaixo, transbordando p/ o
   conteúdo (a sidebar tem z-index:30, então pinta acima do .content). */
.alertbell-d .alertbell-panel { top: calc(100% + 6px); left: 8px; }
/* O sino de desktop não faz sentido dentro do drawer mobile — some ≤900px. */
@media (max-width: 900px) { .alertbell-d { display: none; } }
/* O sino mobile vive na mtopbar (já display:none no desktop); trava por garantia. */
@media (min-width: 901px) { .alertbell-m { display: none; } }
@media (max-width: 900px) {
  /* Mobile: popover fixo abaixo da barra, largura da tela, com rolagem própria
     se a lista crescer — nunca empurra nem rola a página lateralmente. */
  .alertbell-m .alertbell-panel {
    position: fixed; top: calc(52px + env(safe-area-inset-top));
    left: 8px; right: 8px; width: auto; max-width: none;
    max-height: calc(100vh - 72px); overflow-y: auto;
  }
}

/* ---- 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 { padding: 12px 14px; border-radius: var(--radius); margin-bottom: 16px; font-size: var(--fs-md); border: 1px solid transparent; }
.flash-ok { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-bd); }
.flash-error { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-bd); }
.flash-info { background: var(--info-bg); color: var(--info); border-color: var(--info-bd); }
/* A ação DEU CERTO, mas o resultado tem ressalva (ex.: credencial reabilitada
   que volta sob contenção). Verde diria "pronto, siga"; vermelho diria "falhou",
   e não falhou — este é o terceiro caso, e ele precisa parar o olho. */
.flash-warn { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-bd); }

/* ---- segredo exibido uma única vez ---- */
.reveal-box {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  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-line);
  padding: 4px 8px;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: var(--fs-md);
  word-break: break-all;
  margin: 6px 0;
}

/* ---- cards ---- */
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; margin-bottom: 22px; }
/* ── DENSIDADE É DECISÃO DE ARQUITETURA DE INFORMAÇÃO, NÃO ACABAMENTO ───────
   Correção aplicada depois da primeira leva da rodada 2. A regra da rodada
   ("portal manda em acabamento") tinha sido esticada até aqui e o `.card` do
   painel virou 20px + sombra + 18px de respiro, como o do portal.
   Isso não é acabamento. ACABAMENTO é cor, raio, borda, contraste — muda como o
   componente PARECE. DENSIDADE é padding e respiro — muda QUANTO CABE na tela,
   e isso é legitimamente diferente por superfície: o operador varre muitas
   linhas em muitas telas; o cliente lê uma coisa por vez.
   Medido antes de reverter, a 1400px: o cartão passava de 107px para 119px e
   ganhava 18px de margem — **+30px por cartão**. Contando `.card` por TOKEN de
   classe (a primeira contagem usou substring e inflou o número — `ops-card-link`
   não é `.card`), o pior caso é /tenants/:id com 5 cartões: **+150px, 7,5% da
   altura da página**; /domains e /inbound, +120px (6%). Numa página de 2010px
   isso é uma linha e meia de tabela que deixa de caber, em toda visita.
   Mesma categoria de `--radius` (8px aqui, 12px no portal): divergência
   DECLARADA, com motivo. Não "conserte" isto para igualar ao portal.
   ELEVAÇÃO segue o mesmo raciocínio e a mesma decisão: no painel, sombra é
   reservada a superfície FLUTUANTE (modal, dropdown). Cartão que não flutua não
   tem sombra — é o que está em produção e é o que a tela densa pede. */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px;
}
.card .label { font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.card .value { font-size: var(--fs-6xl); font-weight: 700; margin-top: 2px; }
.card .sub { font-size: var(--fs-xs); color: var(--muted); }
.card.card-ok .value { color: var(--ok); }
.card.card-warn .value { color: var(--warn); }
.card.card-bad .value { color: var(--bad); }

/* ---- painel genérico ---- */
.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 20px;
}
.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }

/* Painel de EVIDÊNCIA ruim (hoje: link malicioso na mensagem). Mesma caixa do
   .panel, com a moldura de alerta — este bloco existe justamente para NÃO ser
   lido como "mais um painel" na tela. Cor pelas variáveis (--bad-*), então o
   tema escuro vem de graça. */
.panel-bad { background: var(--bad-bg); border-color: var(--bad-line); }
.panel-bad > h2:first-child { color: var(--bad); }

/* ---- painel PRIVILEGIADO (só admin): colapsável, começa fechado ----
   <details> nativo. Acento âmbar SUTIL (fio à esquerda + fundo levemente
   tingido) = "cuidado, controles sensíveis" — sem pintar tudo. O <summary>
   é o cabeçalho clicável (cadeado + título + tag). */
details.panel-locked {
  border-left: 3px solid var(--warn);
}
.panel-lock-summary {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  cursor: pointer; list-style: none; min-height: 44px;
}
.panel-lock-summary::-webkit-details-marker { display: none; }
.panel-lock-summary h2 {
  margin: 0; display: inline-flex; align-items: center; gap: 10px; min-width: 0;
}
.panel-lock-summary h2 .ic { width: 18px; height: 18px; flex: none; color: var(--warn); }
.panel-lock-summary .lock-tag {
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
  color: var(--warn); background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  padding: 1px 10px; border-radius: 999px;
}
.panel-lock-summary::after {
  content: "▾"; margin-left: auto; color: var(--warn);
  font-size: var(--fs-sm); line-height: 1; transition: rotate .15s ease;
}
details.panel-locked[open] .panel-lock-summary::after { rotate: 180deg; }
.panel-lock-body { margin-top: 14px; }

/* ---- tabelas ---- */
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: var(--fs-md); }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .4px; color: var(--muted); font-weight: 600; }
tr:hover td { background: var(--surface-3); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td code, .mono { font-family: var(--mono); font-size: var(--fs-sm); }

/* ---- badges ---- */
/* ── 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. */
.badge {
  display: inline-block; font-size: var(--fs-2xs); font-weight: 600;
  padding: 2px 8px; border-radius: 999px; 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-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); }
/* Engajamento (Lido/Clicado): sucesso ALÉM da entrega — teal preenchido, salta do
   verde de "Entregue" sem gritar. Independe de var de tema (funciona claro/escuro). */
.badge-engaged { background: var(--engaged-bg); color: var(--on-solid); }
/* Retida: estado ATIVO que não é erro — pede decisão humana, então puxa o olho
   sem virar o vermelho de falha. Gêmea de portal.css:3627, e existe aqui para
   o SSOT de `statusPresentation` poder apontar `frozen` para ela: enquanto a
   classe vivia em uma folha só, virar o mapa canônico deixaria o selo SEM
   ESTILO na tela do operador. É a mesma família de defeito que custou quatro
   consertos em 22/08 — a trilha de Retidas reconheceu o padrão e parou em vez
   de forçar, e esta linha é o que destrava a virada. */
.badge-held { background: var(--warn-solid); color: var(--on-warn); border: 1px solid var(--warn-bd); }

/* ---- formulários ---- */
form.inline { display: inline; }
label { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 10px 0 4px; }
label .hint { font-weight: 400; color: var(--muted); }
/* `search` entrou na lista na rodada 5. As views usam `type="search"` em quatro
   lugares (filtro de IP, busca de mensagem) e ele NÃO casava com nenhum seletor
   daqui: aqueles quatro campos vinham crus do agente de usuário, com outro
   raio, outro respiro e outra borda que a do produto. Campo de busca que não
   parece campo do produto é o mesmo defeito de affordance por outro caminho. */
input[type=text], input[type=email], input[type=password], input[type=number], input[type=date], input[type=url], input[type=search], select, textarea {
  width: 100%;
  max-width: 480px;
  padding: 8px 10px;
  border: 1px solid var(--field-border);
  border-radius: 6px;
  font: inherit;
  background: var(--panel);
  color: var(--text);
}
input:focus, select:focus, textarea:focus { border-color: var(--brand-2); }
.form-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-end; }
.form-row > div { flex: 0 1 auto; }
.checkbox-row { display: flex; align-items: center; gap: 8px; margin: 10px 0; }
.checkbox-row label { margin: 0; }

button, .btn {
  display: inline-block;
  background: var(--accent);
  color: var(--accent-ink);
  border: none;
  border-radius: var(--btn-radius);
  padding: var(--btn-pad-y) var(--btn-pad-x);
  font: inherit;
  font-size: var(--btn-fs);
  cursor: pointer;
  min-height: var(--btn-min-h);
}
button:hover, .btn:hover { background: var(--accent-hover); text-decoration: none; }
button.secondary, .btn.secondary { background: var(--surface); color: var(--brand-2); border: 1px solid var(--brand-2); }
button.secondary:hover { background: var(--info-bg); }
/* ── VERMELHO É PARA O IRREVERSÍVEL — a regra do dono, 07/08/2026 ───────────
   ESTA VARIANTE TEM DONO E TEM PORTA. Antes desta data o vermelho era o
   acabamento de "ação séria", e isso é uma categoria que só cresce: 26 botões
   vermelhos nas duas superfícies, dos quais 13 desfaziam-se com um clique no
   botão vizinho. O gatilho foi /nodes — com os badges calados pelo kernel dos
   registros, os ONZE «Pausar outbound» viraram a coisa mais chamativa da tela,
   e pausar é `outboundPaused = 1`: o nó sai do sorteio, a mensagem defere, a
   fila segura. Nada se perde.

   É O MESMO ARGUMENTO DA CONFIRMAÇÃO, em cor. O documento de fluxo do dono diz
   que «confirmação para tudo treina a pessoa a clicar sim sem ler»; vermelho
   para tudo treina o olho a não ver vermelho. O custo não se paga na tela que
   ficou espalhafatosa — paga-se no dia do ato que destrói mesmo, quando o olho
   já não distingue.

   A PERGUNTA, ÚNICA, ANTES DE ESCREVER `danger` NUM BOTÃO:
       depois de clicar, dá para voltar ao estado anterior SEM PERDA?
   Operacionalmente ela quase sempre já está respondida no SQL da rota:
     · UPDATE simétrico de flag (pausar/retomar, ligar/desligar, suspender/
       reativar, congelar/liberar) → volta → SECUNDÁRIO, e a confirmação FICA.
       Consequência operacional séria não muda a resposta: derrubar o envio de
       um cliente inteiro é grave e é reversível.
     · DELETE de linha, UPDATE de status sem rota de volta, sobrescrita de
       segredo, descarte de chave → não volta → VERMELHO.
   Duvidoso não se resolve na folha de estilo: fica como está e vai para o dono.

   A COR NUNCA É O ÚNICO PORTADOR. O rótulo já diz o verbo («Remover domínio»,
   «Revogar»), a confirmação diz a consequência, e as duas irreversíveis mais
   caras (remover domínio no portal, cancelar conta) ainda exigem digitar o
   nome exato. O vermelho é o quarto sinal, não o primeiro — é por isso que
   tirá-lo de onde não cabe não tira informação de ninguém.

   ONDE ISTO É VETADO: test/vermelhoIrreversivel.test.js congela o inventário
   dos botões vermelhos das duas superfícies. Botão vermelho novo reprova até
   entrar no inventário com a justificativa escrita. Documento não veta. */
button.danger { background: var(--bad); }
/* #000 aqui NÃO é cor de paleta: é o operando de ESCURECIMENTO do color-mix
   ("78% do vermelho, 22% de preto"). Trocá-lo por um token seria fingir que a
   operação matemática é uma escolha de marca. Declarado. */
button.danger:hover { background: color-mix(in srgb, var(--bad) 78%, #000); }
/* `button.small` é o botão de CÉLULA DE AÇÃO — o mais repetido do painel: 30
   ocorrências em /settings, 22 em /domains/:id, 18 em /messages. Medido, os
   tokens de `.btn-sm` o levavam de 26px para 32px de altura: +6px × 30 numa
   tela é densidade de tabela, não acabamento. Volta ao tamanho de produção;
   4px e 10px estão na rampa de espaço e o tipo continua vindo da escala.
   (Dívida conhecida e declarada: o painel tem DOIS tamanhos de botão pequeno,
   `button.small` e `.btn-sm`. Unificá-los é trabalho do redesenho de tabela,
   não desta rodada — ver o relatório.) */
button.small { padding: 4px 10px; font-size: var(--fs-xs); }
button[data-copy] { background: var(--surface); color: var(--brand-2); border: 1px solid var(--border); }

/* Alvos de toque densos do painel ficam compactos (min-height 44 é para os
   controles primários e para a nav; tabelas/inline preservam a densidade). */
button.small, .btn.small, button[data-copy], .btn-link, .btn-inline,
.modal-x, .pw-eye, .tier-form button, .inline-quota button,
.actions-cell button, .actions-cell .btn { min-height: 0; }
.btn-link, .btn-inline { display: inline; }

/* ── ESTADO DESABILITADO — reposição de affordance ──────────────────────────
   O NAVEGADOR DESENHA ISTO DE GRAÇA. Um <button disabled> vem cinza, um <input
   disabled> vem acinzentado, e ambos vêm sem o cursor de ação. O produto tinha
   o estado antes de existir folha de estilo.
   O QUE O DESIGN SYSTEM FEZ: declarou `background` e `color` em `button` e em
   `input/select/textarea`. Declaração de autor vence folha do agente de usuário
   — e o desenho de desabilitado do navegador foi cancelado junto, sem nada no
   lugar. RODADA 4 MEDIU a consequência com a sonda de perceptibilidade
   (compara getComputedStyle com e sem o estado): **893 de 1117 controles
   desabilitados não mudavam UMA propriedade visível**, em 55 telas. O controle
   ficava idêntico ao ativo e só revelava o estado quando o clique não fazia
   nada. Isto não é escolha de cor: é affordance que o produto tinha e perdeu.

   O CRITÉRIO NÃO É UM NÚMERO. WCAG 1.4.3/1.4.11 ISENTAM componente inativo de
   contraste, de propósito — texto apagado é justamente como se comunica «não
   dá para usar». Então a régua aqui é «dá para ver que está desabilitado SEM
   clicar», e a prova é a sonda, não o medidor de contraste.

   ESPECIFICIDADE DELIBERADA. Medido na folha, o seletor mais específico que
   pinta um botão é `.ipfilter button.chip:hover` — (0,3,1). Um `button:disabled`
   natural é (0,1,1) e PERDERIA para ele: o chip de filtro desabilitado
   continuaria repintando no hover. `:root:root` leva o conjunto a (0,4,0), que
   vence (0,3,1) com margem de um degrau. O número está escrito aqui para poder
   ser conferido quando alguém acrescentar uma variante mais específica.

   POR QUE `filter` E NÃO `background`+`color` nos botões: são 10 variantes
   (primário, secundário, danger, small, chip, switch, ghost, link, copy, eye) e
   enumerá-las é uma lista que envelhece — a variante nova de amanhã nasceria
   sem estado. `filter: grayscale()` age sobre o resultado JÁ PINTADO, seja ele
   qual for, inclusive o repintado por `:hover`. Uma regra cobre as dez de hoje
   e as de amanhã. Custo declarado: `filter` cria contexto de empilhamento no
   botão — inócuo aqui, porque nenhum botão desta folha ancora popover ou
   elemento posicionado. */
:root:root :is(button, .btn, [role="button"]):is(:disabled, [disabled]) {
  /* Os dois portadores num só valor: DESSATURA (o primário deixa de ser a
     chamada da tela — é o que o olho lê em menos de 1s) e APAGA. */
  filter: grayscale(1) opacity(.5);
  box-shadow: none;
  /* O único sinal que chega ANTES do clique. */
  cursor: not-allowed;
}
/* O CAMPO tem tratamento próprio: cinza-escala não faz nada num campo branco
   sobre cartão branco. Aqui os três portadores são superfície, tinta e borda —
   e a borda é a parte que fala: o campo ATIVO ganhou a borda forte da rodada 5
   (--field-border, 3,66:1); o desabilitado a DEVOLVE e volta à linha decorativa
   (--border, 1,29:1). A hierarquia fica literal — só o que se pode usar tem
   contorno de controle. */
:root:root :is(input, select, textarea):is(:disabled, [disabled]) {
  background: var(--neutral-bg);
  color: var(--muted);
  border-color: var(--border);
  cursor: not-allowed;
}
/* Rótulo do campo desabilitado acompanha; senão o formulário fica com metade
   dos rótulos em peso de ativo sobre campos inertes. `:has()` resolve sem JS. */
:root:root label:has(+ :is(input, select, textarea):disabled),
:root:root .checkbox-row:has(input:disabled) { color: var(--muted); }

/* ---- filtros ---- */
.filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; margin-bottom: 16px; }
.filters > div { min-width: 130px; }
.filters label { margin-top: 0; }
.filters input, .filters select { max-width: 220px; }

/* ---- gráfico SVG ---- */
.chart-box { overflow-x: auto; }
.chart-box svg { display: block; min-width: 720px; width: 100%; height: auto; }
.legend { display: flex; gap: 18px; flex-wrap: wrap; font-size: var(--fs-sm); color: var(--muted); margin-top: 8px; }
.legend span::before {
  content: '';
  display: inline-block;
  width: 11px; height: 11px;
  border-radius: 4px;
  margin-right: 6px;
  vertical-align: -1px;
  /* RODADA 3: o fallback era #ccc. Ele NUNCA renderiza — `--swatch` é sempre
     escrito inline pela view (ops-home.ejs) — mas um fallback cravado é um
     valor que passa despercebido no dia em que a view esquecer de escrever, e
     aí o quadradinho sairia cinza-claro sobre o tema escuro. Agora degrada
     para a linha neutra do tema. */
  background: var(--swatch, var(--neutral-line));
}

/* ---- paginação ---- */
.pagination { display: flex; gap: 8px; align-items: center; margin-top: 14px; font-size: var(--fs-md); }

/* ---- login ---- */
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: var(--nav-bg); }
.login-box { background: var(--surface); border-radius: var(--radius); padding: 32px 32px; width: 360px; box-shadow: var(--sh-lg); }
.login-box h1 { margin: 0 0 4px; font-size: var(--fs-3xl); color: var(--brand); }
.login-box .sub { color: var(--muted); font-size: var(--fs-sm); margin-bottom: 14px; }
.login-box button { width: 100%; margin-top: 16px; }

/* ---- misc ---- */
.muted { color: var(--muted); }
/* `.small` estava definida DUAS vezes neste arquivo, com valores diferentes
   (12.5px aqui, 12px lá embaixo). A de baixo vencia pela ordem — ou seja, esta
   linha era decoração. Agora as duas dizem a mesma coisa, e a escala é uma só. */
.small { font-size: var(--fs-xs); }
hr.sep { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 20px; }
/* Lista rótulo/valor das telas de detalhe (mensagem, inbound, abuse, sistema).
   No DESKTOP fica LADO A LADO (grid de 2 colunas): empilhado, cada item ocupava
   duas linhas e a leitura virava uma coluna alta de rolagem. No mobile continua
   empilhado — lá não há largura para duas colunas. */
.kv { font-size: var(--fs-md); }
.kv dt { font-weight: 600; color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .4px; margin-top: 10px; }
.kv dd { margin: 1px 0 0; }

@media (min-width: 641px) {
  .kv {
    display: grid;
    grid-template-columns: minmax(120px, max-content) 1fr;
    column-gap: 18px;
    align-items: baseline;
  }
  .kv dt { margin-top: 0; padding: 6px 0; }
  /* min-width:0 deixa o conteúdo longo (resposta SMTP crua, URL) quebrar dentro
     da coluna em vez de esticar o grid e estourar o painel. */
  .kv dd { margin: 0; padding: 6px 0; min-width: 0; overflow-wrap: anywhere; }
  /* Filete separando os pares — sem ele, duas colunas viram um bloco só. */
  .kv dt:not(:first-of-type), .kv dd:not(:first-of-type) { border-top: 1px solid var(--line); }
}

/* Trajeto da mensagem numa célula só: remetente em cima (esmaecido, é o
   contexto), destinatário embaixo com a seta (é o que se procura na varredura).
   A seta é decorativa — o <th> já diz "De → Para" em texto. */
.cell-route { line-height: 1.35; }
.cell-route .cr-from,
.cell-route .cr-line { display: block; overflow-wrap: anywhere; }
.cell-route .cr-from { color: var(--muted); }
.cell-route .cr-to { overflow-wrap: anywhere; }
/* '↳' (e não '→'): em lista EMPILHADA o gancho descendente diz "daqui desce
   para ali", enquanto a seta reta sugere continuação lateral e se perde no meio
   dos e-mails. Fica FORA do <a> (elemento próprio, não ::before do link) para
   não virar área clicável nem sublinhar junto no hover. */
.cell-route .cr-arrow { color: var(--muted); margin-right: 4px; }

/* Linha secundária de célula no padrão do De→Para (↳ + contexto esmaecido):
   usada em Rota (faixa dedicada) e Origem (IP de partida) do /messages.
   Classe própria, e não .cr-* reaproveitada, porque .cell-route carrega regras
   de largura da coluna própria — herdar dali quebraria o layout das outras. */
.sub-line { display: block; overflow-wrap: anywhere; }
.sub-line .sub-arrow { color: var(--muted); margin-right: 4px; }

/* Célula que não pode quebrar em duas linhas (data+hora, IP, versão). A data
   quebrando entre "27/07/2026," e "16:24" fazia cada linha da lista ter altura
   dupla e o olho perder a coluna. */
.cell-nowrap { white-space: nowrap; }

/* Cabeçalho de painel com ação à direita (ex.: "Dados" + copiar). */
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.panel-head h2 { margin: 0; }
.actions-cell { white-space: nowrap; }
.actions-cell form { display: inline-block; margin-right: 4px; }

/* BLOQUEAR CREDENCIAL (mig. 128) — gaveta na célula de ações.
   O ato do operador exige DUAS decisões antes do clique (o motivo, que o
   cliente lê na tela dele, e o grau: se ele pode desbloquear sozinho). Um
   botão solto não tem onde perguntar isso, e um modal seria a única coisa
   modal desta tela. Fica num <details> fechado: a linha continua magra, e
   quem vai bloquear encontra as duas perguntas juntas do ato.
   `white-space: normal` desfaz o nowrap da célula SÓ aqui dentro — o motivo
   é um campo de texto, e herdar o nowrap o esticaria pela tabela afora. */
.actions-cell .cred-block { display: inline-block; white-space: normal; vertical-align: top; }
.actions-cell .cred-block > summary { cursor: pointer; user-select: none; }
.actions-cell .cred-block[open] { display: block; max-width: 32rem; margin-top: 6px; }
.actions-cell .cred-block form { display: block; margin: 6px 0 0; }
.actions-cell .cred-block input[type="text"] { width: 100%; margin: 2px 0 6px; }
.actions-cell .cred-block label { display: block; margin-bottom: 4px; }

/* ═══════════════════════════════════════════════════════════════════════════
   SHELL MOBILE (≤900px) — o painel no celular: top bar + drawer + bottom nav.
   Breakpoint ÚNICO em 900px (o ≥901px é desktop, intocado). No desktop nada
   disto renderiza (o chrome mobile fica display:none). O abre/fecha do drawer
   é dirigido por JS (classe .drawer-open no <html>); o CSS só reage ao estado.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  /* A sidebar sai do fluxo (vira drawer). Sobra top bar (sticky) + conteúdo. */
  .shell { display: block; }

  /* ---- Top bar fina e fixa: hambúrguer + marca + tema ---- */
  .mtopbar {
    display: flex; align-items: center; gap: 8px;
    position: sticky; top: 0; z-index: 40;
    height: 52px; padding: 0 6px;
    padding-top: env(safe-area-inset-top);
    background: var(--nav-bg); color: var(--nav-ink);
    border-bottom: 1px solid var(--nav-line);
  }
  .mtopbar-burger {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 8px;
    background: none; border: 0; color: var(--nav-ink); cursor: pointer; border-radius: 6px;
  }
  .mtopbar-burger:hover { background: var(--nav-hover); }
  .mtopbar-burger .ic { width: 24px; height: 24px; }
  .mtopbar-brand { flex: 1; font-weight: 700; font-size: var(--fs-xl); color: var(--nav-ink); letter-spacing: .3px; }
  /* Reaproveita .theme-toggle (cores navy fixas), mas compacto e sem rótulo. */
  .mtopbar-toggle {
    width: auto; margin: 0; gap: 0;
    min-width: 44px; min-height: 44px; padding: 8px; border: 0;
  }
  .mtopbar-toggle .tt-label { display: none; }

  /* ---- Sidebar → drawer off-canvas (desliza da esquerda) ---- */
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0;
    width: 280px; max-width: 84vw;
    flex: none; z-index: 60;
    transform: translateX(-100%);
    transition: transform .24s ease;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    /* SOMBRA DIRECIONAL, declarada: a gaveta empurra para a DIREITA (2px 0),
       não para baixo. Geometria é a informação aqui, então ela não cabe na rampa
       de elevação — --sh-* é vertical por definição. Fica cravada de propósito. */
    box-shadow: 2px 0 16px rgba(0,0,0,.35);
  }
  .drawer-open .sidebar { transform: translateX(0); }

  /* Overlay escurecido atrás do drawer (clicável para fechar). */
  .drawer-overlay {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: var(--scrim);
    opacity: 0; visibility: hidden; transition: opacity .24s ease;
  }
  .drawer-open .drawer-overlay { opacity: 1; visibility: visible; }

  /* Trava o scroll do fundo enquanto o drawer está aberto. */
  .drawer-open, .drawer-open body { overflow: hidden; }

  /* ---- Bottom tab bar (thumb-reach + safe-area) ---- */
  .botnav {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    background: var(--panel); border-top: 1px solid var(--border);
    padding-bottom: env(safe-area-inset-bottom);
    /* SOMBRA DIRECIONAL, declarada: a barra inferior empurra para CIMA
       (0 -1px). Mesmo motivo da gaveta — não entra na rampa. */
    box-shadow: 0 -1px 8px rgba(0,0,0,.08);
  }
  .botnav-item {
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 4px; min-height: 52px; padding: 6px 2px;
    background: none; border: 0; cursor: pointer;
    color: var(--muted); font: inherit; font-size: var(--fs-2xs); text-decoration: none;
  }
  .botnav-item .ic { width: 22px; height: 22px; }
  .botnav-item.on { color: var(--accent); }
  .botnav-item:hover { text-decoration: none; }

  /* Conteúdo: padding menor + espaço para não ficar atrás da bottom bar. */
  .content { padding: 16px; padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
  .content h1 { font-size: var(--fs-3xl); }
  .grid-2 { grid-template-columns: 1fr; }

  /* Tabelas largas rolam dentro do próprio container em vez de estourar a página. */
  .table-wrap { overflow-x: auto; }
  table.table { min-width: 520px; }
  /* …exceto quando ela virou CARTÃO: aí cada linha é um bloco de largura total e
     o min-width só forçaria a PÁGINA a rolar de lado (visível em /logins e no
     bloco DKIM de /domains/:id, que usam .table sem .table-wrap). */
  table.table.table-cards { min-width: 0; }

  /* Toasts sobem acima da bottom bar (senão o bottom:20px fica atrás dela). */
  #toasts { bottom: calc(72px + env(safe-area-inset-bottom)); }
}


/* ===================================================================
   Componentes acrescentados (anti-spam, logs, Cloudflare, modal).
   Usam as variáveis do tema — nada de cor chumbada. O primeiro corte
   destes foi escrito para tema escuro e virou texto escuro sobre fundo
   escuro: o modal ficava literalmente ilegível.
   =================================================================== */

/* ---- Auditoria anti-spam ---- */
.verdict { padding: 12px 16px; border-radius: var(--radius); margin-bottom: 18px;
           border: 1px solid; line-height: 1.5; }
.verdict-good { background: var(--ok-bg);   border-color: var(--ok-line); color: var(--ok); }
.verdict-warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.verdict-bad  { background: var(--bad-bg);  border-color: var(--bad-line); color: var(--bad); }

.score-row { display: flex; align-items: center; gap: 18px; margin-bottom: 20px; flex-wrap: wrap; }
.score-badge { display: flex; align-items: baseline; justify-content: center; min-width: 100px;
               padding: 12px 16px; border-radius: var(--radius); border: 1px solid; }
.score-good { background: var(--ok-bg);   border-color: var(--ok-line); color: var(--ok); }
.score-warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
.score-bad  { background: var(--bad-bg);  border-color: var(--bad-line); color: var(--bad); }
.score-num { font-size: var(--fs-7xl); font-weight: 700; line-height: 1; }
.score-max { font-size: var(--fs-md); opacity: .7; margin-left: 2px; }
.score-meta { flex: 1; min-width: 240px; line-height: 1.6; }

.checklist { list-style: none; margin: 12px 0 0; padding: 0; display: grid; gap: 2px; }
.checklist li { padding: 12px 0; border-bottom: 1px solid var(--border); }
.checklist li:last-child { border-bottom: 0; }
/* flex-wrap: sem ele o carimbo de tempo («quebrado desde … · checado …»), que é
   empurrado para a direita por margin-left:auto, esmagava o NOME do registro
   para duas letras no celular — o rótulo mais importante da linha virava o mais
   ilegível. Com wrap, o carimbo desce para a própria linha quando não cabe. */
.chk-head { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* Coluna do ícone de estado (✅ ⚠️ ❌ ⏭️ — em antispam-domain.ejs e no <li> que
   o admin.js monta para /antispam/ip). Largura FIXA porque `.chk-detail` já
   recua 26px, e 26 = 16 desta caixa + os 10 do gap acima. Sem a largura, o
   recuo é uma suposição sobre quanto um emoji mede — e emoji mede coisas
   diferentes em cada plataforma, então o detalhe alinhava com o título no Mac
   e escorregava no Windows. `flex: none` para a caixa não encolher quando o
   título é longo e a linha entra em disputa. */
.chk-icon { flex: none; width: 16px; text-align: center; font-size: var(--fs-md); line-height: 1.3; }
.chk-title { font-weight: 600; }
.chk-detail { margin: 4px 0 0 26px; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }
.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; }
.chk-fail .chk-title { color: var(--bad); }
.chk-warn .chk-title { color: var(--warn); }
.chk-skip { opacity: .6; }

/* Bloco de código — regra IDÊNTICA à do portal.css. O acabamento é o de lá
   (padding 14/16, entrelinha 1.55, rolagem horizontal com inércia no toque); os
   tokens são os daqui (--surface-3/--line). */
.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);
}
.mono { font-family: var(--mono); }
.small { font-size: var(--fs-xs); }
.inline-form { margin-left: auto; }

/* ---- Cloudflare / Domain Connect ---- */
.cf-badge { padding: 10px 12px; border-radius: 6px; background: var(--info-bg);
            border: 1px solid var(--info-line); color: var(--info); display: inline-block; }
.cf-plan { margin-top: 16px; padding: 12px; border: 1px solid var(--border);
           border-radius: 6px; background: var(--surface-3); }
.cf-plan h3 { margin: 0 0 10px; font-size: var(--fs-md); }
.row-actions { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0; align-items: center; }
.row-actions form { margin: 0; }
.btn-link { color: var(--brand-2); text-decoration: none; }
.btn-link:hover { text-decoration: underline; }
.btn-inline { text-decoration: none; }
.warn-inline { background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn);
               padding: 10px 12px; border-radius: 6px; font-size: var(--fs-xs); line-height: 1.5; margin: 8px 0; }

/* ---- Modal ---- */
.modal { border: 1px solid var(--border); border-radius: var(--radius);
         background: var(--panel); color: var(--text);
         max-width: 760px; width: 92%; padding: 20px; }
.modal::backdrop { background: var(--scrim); }
.modal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.modal-head h3 { margin: 0; }
.modal-x { background: none; border: 0; color: var(--muted); font-size: var(--fs-6xl);
           cursor: pointer; line-height: 1; padding: 0 4px; }
.modal-x:hover { color: var(--text); }
.modal-foot { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px;
              padding-top: 14px; border-top: 1px solid var(--border); }
.cf-rec { border: 1px solid var(--border); border-radius: 6px; padding: 12px;
          margin-bottom: 12px; background: var(--surface-3); }
.cf-rec-head { margin-bottom: 8px; }
.cf-rec label { display: block; font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .5px;
                color: var(--muted); margin: 8px 0 4px; }
.copy-row { display: flex; gap: 8px; align-items: flex-start; }
.copy-row code { flex: 1; background: var(--panel); border: 1px solid var(--border);
                 border-radius: 4px; padding: 8px 10px; font-size: var(--fs-xs);
                 font-family: var(--mono); color: var(--text); overflow-x: auto; }
.copy-row code.wrap { word-break: break-all; white-space: pre-wrap; overflow-x: visible; }
.copy-row button { white-space: nowrap; }

/* ---- Detalhe do domínio (/domains/:id) — health-first, espelha o /ips ------
   Resumo reativo (.c-stats) → checklist de registros por status (SSOT dnsState)
   → automação em destaque. Só tokens do design system; dark/light herdados. */
.dom-summary { margin-bottom: 18px; }

/* Card de automação: superfície semântica (info) com a ação de publicar 1-clique
   em destaque — nunca um botão solto e grosseiro. */
.dom-automation {
  border: 1px solid var(--h-line); border-left: 4px solid var(--h-ink);
  border-radius: var(--radius); background: var(--h-bg);
  padding: 16px 16px; margin: 0 0 18px;
}
.dom-auto-head { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 12px; }
.dom-auto-ic { color: var(--h-ink); line-height: 0; flex: none; margin-top: 1px; }
.dom-auto-ic .ic { width: 20px; height: 20px; }
.dom-auto-head strong { color: var(--h-ink); }
.dom-automation-wrap { margin: 0 0 18px; }
.dom-auto-summary { cursor: pointer; font-weight: 600; list-style-position: inside; padding: 6px 0; }
.dom-auto-summary:hover { color: var(--brand-2); }
.dom-auto-summary .ic { width: 15px; height: 15px; vertical-align: -2px; }

/* Checklist de registros: um item por registro, status semântico à esquerda. */
.dom-records .checklist li { padding: 14px 0; }
.dom-rec .chk-detail { margin-left: 26px; }
.dom-rec-body {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 8px 18px; margin: 10px 0 0 26px;
}
.dom-rec-field { min-width: 0; }
.dom-k {
  display: block; font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); font-weight: 600; margin-bottom: 4px;
}
/* Valor publicado: verde quando bate, vermelho quando diverge do esperado.
   Selector qualificado por .copy-row code p/ ganhar a borda base sem !important. */
.copy-row code.dom-obs-ok  { color: var(--ok);  border-color: var(--ok-line); }
.copy-row code.dom-obs-bad { color: var(--bad); border-color: var(--bad-line); }

/* Botão copiar por ícone — feedback por classe (.copied), sem apagar o ícone. */
.dom-copy {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  padding: 6px; background: var(--surface); color: var(--brand-2);
  border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
}
.dom-copy .ic { width: 15px; height: 15px; }
.dom-copy:hover { background: var(--info-bg); }
.dom-copy.copied { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
.dom-copy.copy-failed { color: var(--bad); border-color: var(--bad-line); }

.dom-records h2 .ic, .panel h2 .ic { width: 17px; height: 17px; vertical-align: -3px; margin-right: 6px; color: var(--muted); }

/* ── O CONJUNTO DE DECISÕES (.dset) ──────────────────────────────────────────
   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 linhas dizendo «ok».
   Linha que não pede nada não disputa espaço com linha que pede: tudo
   verificado ⇒ UMA linha («Registros DNS · 6 de 6 verificados · checado …»);
   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, de propósito. É 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: caixa dentro de caixa lê como dois cartões concorrentes.

   Cor nunca é o único portador de significado: o glifo em .dset-mark + a
   PALAVRA em .dset-state. Gêmeo em portal.css — quem mexer aqui mexe lá. */
.dset { display: block; }
/* 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 0; 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(--focus); 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-line); }
.dset[data-health="warn"] .dset-mark { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.dset[data-health="bad"]  .dset-mark { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }

/* DOIS nomes de classe, não um: `.content h2` (0,1,1) é mais específico que
   `.dset-name` (0,1,0) e estava vencendo — o <h2> vinha com 17px e 10px de
   margem inferior, que num item de flex vira espaço morto e desalinha o glifo.
   Medido no navegador, não deduzido. */
.dset-sum .dset-name { margin: 0; font-size: var(--fs-xl); line-height: 1.3; font-weight: 700; letter-spacing: -.01em; }
/* --muted, nunca --faint: o terciário 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 — medido, são 314px de 320 disponíveis, então 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 painel. */
.dset[open] > .dset-sum { border-bottom: 1px solid var(--line); 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; }

/* ── GRUPO DE DECISÃO (.dgrp) ────────────────────────────────────────────────
   O KERNEL do UX (UX-FLUXO-CONFIG): «só mostra quando precisa mostrar».
   Seis registros verificados não são seis blocos — 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, com teclado e estado expandido anunciado de graça.

   Cor NUNCA é o único portador de significado (daltonismo): cada estado traz o
   FILETE lateral + um GLIFO em .dgrp-mark + a PALAVRA em .dgrp-state.
   Sem fundo colorido para organizar — acento pontual (fio e ícone) apenas; o
   fundo cheio fica reservado ao banner de AÇÃO no topo da página.

   Gêmeo em portal.css (mesma família, mesmos nomes): o cliente não pode ver
   diferente do operador. Quem mexer aqui mexe lá. */
.dgrp {
  border: 1px solid var(--line); border-left: 3px solid var(--neutral-line);
  border-radius: var(--radius); background: var(--surface); 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) var(--radius) 0;
}
.dgrp-sum::-webkit-details-marker { display: none; }
.dgrp-sum::marker { content: ""; }
/* Hover NÃO tinge o fundo de propósito: --muted sobre --surface-3 (#f8fafc) cai
   para 4.33:1 e REPROVA AA em texto normal. A affordance vem da borda e do
   sublinhado — nenhum dos dois mexe no contraste do texto. */
.dgrp:hover { border-color: var(--line-2); }
.dgrp-sum:hover .dgrp-more { text-decoration: underline; }
.dgrp-sum:focus-visible { outline: 2px solid var(--focus); 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-line); }
.dgrp[data-health="warn"] .dgrp-mark { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.dgrp[data-health="bad"]  .dgrp-mark { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.dgrp[data-health="info"] .dgrp-mark { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }

/* 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, nunca --faint: #8a99a9 sobre branco dá 2.9:1 e reprova AA. */
.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); }
.dgrp-body .checklist li:first-child { padding-top: 10px; }
.dgrp-body .checklist li:last-child { border-bottom: 0; padding-bottom: 4px; }
/* Nota do grupo (o que a decisão significa) — uma frase, não um parágrafo. */
.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 cards do assistente no portal), nunca
   o encolhimento. Borda tracejada = «ainda não dá para criar» — sinal que não
   depende de cor. Grid auto-fit: três colunas onde cabe, uma no celular, sem
   breakpoint. Bloco IDÊNTICO em conceito no portal.css (raio e rótulo são o
   dialeto de cada folha). */
.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);
  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 label { 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; }
/* As ações da pendente (Reenviar/Revogar) vivem NA LINHA do endereço, dentro do
   <li> — só o painel as renderiza (fwGestao). O .inline-form genérico desta
   folha é um empurrador de flex (margin-left:auto); aqui ele precisa correr
   inline atrás do texto, como o irmão do portal (.actions-cell .inline-form). */
.cnl-pend .inline-form { display: inline-block; margin: 0 0 0 6px; }

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

/* ---- Logs ---- */
.stat-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.stat { flex: 1; min-width: 120px; padding: 12px; border: 1px solid var(--border);
        border-radius: 6px; background: var(--surface-3); }
.stat-warn { border-color: var(--warn-line); background: var(--warn-bg); }
.stat-n { display: block; font-size: var(--fs-5xl); font-weight: 700; line-height: 1.2; }
.stat-l { display: block; font-size: var(--fs-xs); color: var(--muted); margin-top: 4px; }
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0; }
.filters input { flex: 1; min-width: 200px; }
.pager { display: flex; align-items: center; gap: 14px; margin-top: 16px; }

/* ---- Tier ---- */
.tier-form { display: inline-flex; gap: 6px; align-items: center; margin-right: 10px; }
.tier-label { font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .5px; color: var(--muted); }

/* Cabeçalho de card com controle à direita (ex.: seletor de IP no anti-spam). */
.card-head-row { display: flex; align-items: center; gap: 12px; }
.card-head-row h2 { margin: 0; }
.card-head-row .inline-form { margin-left: auto; margin-bottom: 0; }
#ipChecksMeta { margin: 4px 0 8px; font-size: var(--fs-sm); }
#ipSelector { max-width: 420px; }

/* Rotas: callout de aviso, linhas de radio, botão secundário. */
.callout { padding: 12px 14px; border-radius: var(--radius); margin: 12px 0; font-size: var(--fs-md); border: 1px solid var(--border); }
.callout-warn { background: var(--warn-bg); border-color: var(--warn); }
.radio-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; cursor: pointer; }
.radio-row input { margin-top: 4px; }
.radio-row span { flex: 1; }
.btn-secondary { background: transparent; color: var(--brand); border: 1px solid var(--border); border-radius: var(--btn-radius); }
.btn-secondary:hover { background: var(--info-bg); }

/* Hub Entregabilidade: barra de abas compartilhada por /deliverability,
   /bounces, /reputation e /antispam (partials/deliv-tabs.ejs). Aba = navegação
   entre páginas irmãs do hub (underline); .chip continua sendo o pill de
   FILTRO dentro de uma página — não misturar os dois papéis. */
.tab-bar { display: flex; gap: 2px; margin: 10px 0 6px; border-bottom: 1px solid var(--border); overflow-x: auto; }
.tab {
  padding: 10px 14px; margin-bottom: -1px; border-bottom: 2px solid transparent;
  color: var(--text); text-decoration: none; font-size: var(--fs-md); white-space: nowrap;
}
.tab:hover { background: var(--info-bg); }
.tab-on { font-weight: 600; border-bottom-color: var(--accent); }

/* Entregabilidade por provedor: barra de filtro, chips de janela, taxas. */
.filter-bar { display: flex; align-items: center; gap: 8px; margin: 12px 0 16px; }
.chip { display: inline-block; padding: 4px 12px; border-radius: 999px; border: 1px solid var(--border); color: var(--text); text-decoration: none; font-size: var(--fs-sm); }
.chip:hover { background: var(--info-bg); }
.chip-on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.rate-good { color: var(--ok); font-weight: 600; }
.rate-warn { color: var(--warn); font-weight: 600; }
.rate-bad  { color: var(--bad); font-weight: 600; }
.reason-link { color: var(--text); text-decoration: none; border-bottom: 1px dotted var(--border); }
.reason-link:hover { color: var(--brand); }
.deliverability-table td.num, .deliverability-table th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Jornada da mensagem: timeline vertical em DUAS camadas legíveis —
   ciclo de vida (sistema, marcadores sólidos + ícone) e engajamento
   (destinatário, marcador em anel + card destacado). */

/* Legenda das duas camadas — orienta o operador em 1s. */
.jn-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 0 0 12px; font-size: var(--fs-xs); color: var(--text); }
.jn-legend .lg-mark { display: inline-block; width: 12px; height: 12px; border-radius: 50%; vertical-align: -1px; margin-right: 6px; }
.jn-legend .lg-sys { background: var(--muted); }
.jn-legend .lg-eng { background: var(--panel); box-shadow: inset 0 0 0 2px var(--brand-2); }

.journey { list-style: none; margin: 8px 0 0; padding: 0; }
.jn { position: relative; padding: 0 0 20px 32px; border-left: 2px solid var(--border); }
.jn:last-child { border-left-color: transparent; padding-bottom: 0; }

/* Marcador. Marcos (jn-milestone) = disco 24px com ícone; passos intermediários
   (jn-minor) = ponto pequeno neutro, pra não competir com os marcos. O traço do
   ícone (--dot-ink) inverte no escuro para manter contraste sobre o disco. */
.jn-dot { position: absolute; box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  left: -12px; top: 0; width: 24px; height: 24px; border-radius: 50%;
  background: var(--muted); border: 3px solid var(--panel); }
.jn-dot svg { width: 12px; height: 12px; fill: none; stroke: var(--dot-ink); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.jn-minor .jn-dot { left: -6px; top: 4px; width: 13px; height: 13px; border-width: 2px; }
.jn-minor .jn-dot svg { display: none; }

/* Cores dos marcos do ciclo de vida (disco sólido). */
.jn-accepted .jn-dot { background: var(--brand-2); }
.jn-delivered .jn-dot, .jn-fallback_delivered .jn-dot { background: var(--ok); }
.jn-bounced .jn-dot, .jn-fallback_bounced .jn-dot, .jn-failed .jn-dot,
.jn-suppressed .jn-dot, .jn-expired .jn-dot, .jn-unsubscribed .jn-dot { background: var(--bad); }
/* link_flagged é ÂMBAR, não vermelho: é uma acusação que ainda espera decisão
   humana (no modo padrão a mensagem fica congelada, esperando release ou
   descarte). Vermelho aqui é para desfecho negativo CONSUMADO (bounce,
   suppressed, expirada) — usá-lo faria um alerta acionável parecer caso
   encerrado. */
.jn-deferred .jn-dot, .jn-frozen .jn-dot, .jn-link_flagged .jn-dot { background: var(--warn); }
.jn-provider_relay .jn-dot, .jn-failed_over .jn-dot, .jn-rescued .jn-dot { background: var(--brand); }

.jn-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.jn-summary { font-weight: 700; }
.jn-milestone .jn-summary { font-size: var(--fs-md); }
.jn-minor .jn-summary { font-weight: 600; color: var(--muted); }
.jn-delta { font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
/* Destinatário do evento. Fonte monoespaçada porque é um endereço — o olho
   compara caractere a caractere quando há vários parecidos na mesma história
   (vendas@ e marketing@ no mesmo domínio, por exemplo). */
.jn-rcpt {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  background: var(--surface-2);
  padding: 1px 6px;
  border-radius: 4px;
  word-break: break-all;
}
.jn-meta { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; }

/* Engajamento (open/click): marcador em ANEL (fundo painel + aro da marca) —
   sinaliza "ação humana", distinto dos discos sólidos do sistema. O corpo vira
   um card levemente tingido, agrupando a camada de engajamento. */
.jn-opened .jn-dot, .jn-clicked .jn-dot { background: var(--panel); box-shadow: inset 0 0 0 2.5px var(--brand-2); }
.jn-opened .jn-dot svg, .jn-clicked .jn-dot svg { stroke: var(--brand-2); }
.jn-track .jn-body { background: var(--info-bg); border: 1px solid var(--info-line); border-radius: var(--radius); padding: 8px 12px; }
.jn-track .jn-summary { color: var(--info); }
/* Texto secundário DENTRO do card tingido usa --tint-ink (mais escuro que
   --muted no claro; claro o suficiente no escuro) pra manter AA sobre o tint. */
.jn-track .jn-meta, .jn-track .jn-delta, .jn-track .jn-line { color: var(--tint-ink); }
.jn-track .jn-meta .muted { color: var(--tint-ink); }

.jn-line { font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; min-width: 0; }
.jn-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.jn-tag { font-size: var(--fs-2xs); line-height: 1.7; padding: 2px 10px; border-radius: 999px; background: var(--surface); border: 1px solid var(--info-line); color: var(--tint-ink); white-space: nowrap; }
.jn-tag-client { color: var(--brand); font-weight: 600; }

/* Chip de "tempo de leitura" — inequivocamente uma ESTIMATIVA (≈, borda
   tracejada e selo). E-mail não mede tempo real de leitura. */
.jn-read { display: inline-flex; align-items: center; gap: 6px; margin-top: 8px;
  font-size: var(--fs-2xs); padding: 4px 10px; border: 1px dashed var(--info-line); border-radius: 999px;
  background: var(--surface); color: var(--tint-ink); cursor: help; }
.jn-read strong { color: var(--text); font-variant-numeric: tabular-nums; }
.jn-read .jn-est { text-transform: uppercase; letter-spacing: .04em; font-size: var(--fs-3xs); font-weight: 700; color: var(--brand-2); }

/* Estado vazio de engajamento — entregue mas sem abertura/clique. Deixa claro
   que é intencional (não bug). */
.jn-empty { display: flex; gap: 10px; align-items: flex-start; margin-top: 14px; padding: 12px 14px;
  border: 1px dashed var(--border); border-radius: var(--radius); background: var(--bg); }
.jn-empty svg { flex: 0 0 auto; width: 18px; height: 18px; margin-top: 1px; fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.jn-empty strong { display: block; font-size: var(--fs-sm); }
.jn-empty span { font-size: var(--fs-xs); }
.jn-caveat { margin-top: 12px; }

.break { word-break: break-all; }

/* Cabeçalho de painel com ação à direita (ex.: Headers + copiar). */
.panel-head-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.panel-head-row h2 { margin: 0; }

/* Botão "copiar" compacto ao lado de um valor. */
.btn-copy-inline { vertical-align: middle; }
.kv-copy { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.kv-copy code { min-width: 0; word-break: break-all; }

/* Headers crus: colapsável (muitos cabeçalhos) e com scroll horizontal CONTIDO
   — linhas longas rolam dentro da caixa em vez de estourar o layout. */
.headers-box > summary { cursor: pointer; font-size: var(--fs-sm); font-weight: 600; color: var(--brand-2); padding: 4px 0; list-style-position: inside; }
.headers-box > summary:hover { text-decoration: underline; }
.headers-box[open] > summary { margin-bottom: 8px; }
.headers-raw { white-space: pre; overflow: auto; max-height: 420px; overscroll-behavior: contain;
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.5; background: var(--bg);
  border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin: 0; }

/* Rótulos de grupo na sidebar — organiza o menu longo em seções. */
/* Rótulo de seção: respiro maior EMBAIXO (8px) para não encostar na pílula do
   item logo abaixo, e recuo alinhado com ela. */
/* RODADA 3 — duas correções numa linha só:
   (1) `color: var(--muted)` era um token de TEMA sobre a superfície NAVY — a
       coisa que o bloco de tokens proíbe por escrito. No claro resolvia para
       #5b6b7d, que sobre a navy é 1,56:1: ilegível. Só não aparecia porque as
       cinco ocorrências da view sempre trazem .ws-label ou .pf-label junto,
       sobrescrevendo a cor. Era uma mina, não um estilo.
   (2) `opacity: .65` multiplicava a tinta e nenhum medidor via — ver A
       OPACIDADE QUE SAIU, no bloco de tokens. Com ela fora, .pf-label sobe de
       2,75:1 para 5,28:1 no claro (3,75 → 8,55 no escuro). É a mudança de
       aparência mais visível da rodada, e é a que conserta um defeito. */
.nav-group { font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--nav-ink-3); margin: 18px 0 8px; padding: 0 20px; font-weight: 600; }
.nav-group:first-child { margin-top: 0; }

/* Estado de carregamento (regiões que puxam auditoria por AJAX). */
.loading-box { padding: 16px; color: var(--muted); background: var(--panel); border: 1px dashed var(--border); border-radius: var(--radius); text-align: center; }
li.loading-box { list-style: none; }

/* Callout "pior provedor" na visão geral — link clicável. */
a.worst-provider { display: block; text-decoration: none; margin-bottom: 20px; }
a.worst-provider:hover { text-decoration: none; filter: brightness(0.98); }

/* Sinais do spam score no detalhe da mensagem. */
.spam-signals ul { list-style: none; margin: 4px 0 0; padding: 0; font-size: var(--fs-sm); }
.spam-signals li { padding: 2px 0; }
.sig-pts { display: inline-block; min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; margin-right: 6px; }
.sig-bad { color: var(--bad); }
.sig-good { color: var(--ok); }

/* Rampa de warmup: stats + barra de progresso. */
.warmup-stats { display: flex; flex-wrap: wrap; gap: 22px; margin: 8px 0; font-size: var(--fs-md); }
.warmup-bar { height: 8px; background: var(--border); border-radius: 999px; overflow: hidden; margin: 6px 0 2px; }
.warmup-fill { height: 100%; background: var(--brand-2); border-radius: 999px; transition: width .3s; }
.warmup-form { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }

/* Página de IPs: dá mais largura à coluna de blocklists (menos linhas quebradas)
   e estreita a de ações. Só no desktop — no mobile o .grid-2 já empilha. */
@media (min-width: 901px) {
  .grid-ip { grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr); }
}

/* Dados de conexão SMTP (box de reveal + junto das credenciais). */
.conn-info { margin: 12px 0; padding: 12px 14px; background: var(--info-bg); border-radius: var(--radius); font-size: var(--fs-md); }
.reveal-box .conn-info { background: color-mix(in srgb, var(--surface) 70%, transparent); margin-top: 10px; }
.conn-table { width: auto; margin: 8px 0 4px; }
.conn-table td { padding: 4px 14px 4px 0; border: 0; }
.conn-table td:first-child { color: var(--muted); white-space: nowrap; }

/* WS-1: editor de profile de envio (por credencial/API key). Funcional, compacto. */
/* ── Perfil de envio — editor ESTRUTURADO (grid multi-coluna no desktop,
   empilha no mobile). Vive numa linha full-width (tr.cred-detail) da tabela
   de credenciais; abre sob demanda (progressive disclosure). ─────────────── */
.profile-editor { font-size: var(--fs-sm); min-width: 0; flex: 1; }
.profile-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);
}
.profile-editor > summary:hover { text-decoration: underline; }
.profile-editor[open] > summary { margin-bottom: 10px; }
.profile-form { margin: 0; }
.profile-grid {
  display: grid; gap: 12px; align-items: stretch;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
.pf-group {
  margin: 0; min-width: 0;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-3); padding: 8px 12px 12px;
}
.pf-group legend {
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--muted); padding: 0 4px;
}
.pf-group legend .hint { text-transform: none; letter-spacing: 0; font-weight: 400; }
.pf-opt {
  display: flex; gap: 8px; align-items: flex-start;
  font-weight: 400; font-size: var(--fs-sm); margin: 0; padding: 6px 0;
}
.pf-opt input { flex: none; margin-top: 4px; }
.pf-rates { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pf-rates label { display: flex; flex-direction: column; gap: 4px; margin: 0; font-size: var(--fs-xs); font-weight: 600; }
.pf-rates input { width: 100%; }
.pf-group > label { margin: 10px 0 0; }
.pf-group > label:first-of-type { margin-top: 4px; }
.pf-risk { border-color: var(--warn-line); }
.pf-risk legend { color: var(--warn); }
.pf-actions { margin-top: 12px; }
.pf-frozen {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 12px; padding: 8px 12px;
  background: var(--warn-bg); color: var(--warn);
  border: 1px solid var(--warn-line); border-radius: var(--radius);
  font-size: var(--fs-sm); font-weight: 600;
}

/* ── Linha de detalhe por credencial: caixas-ferramenta QUIETAS lado a lado
   (MENSAGENS → PERFIL → PERMISSÕES). Superfície neutra + hairline; hierarquia
   por peso/tamanho/respiro, não por fundo colorido. O SSOT data-health segue
   no markup, mas aqui vira ACENTO pontual: azul só no ícone/seta do link de
   mensagens (é navegação), fio âmbar de 2px só nas permissões (mexer ali é
   sensível). Editor aberto expande pra linha inteira via :has(). ─────────── */
tr.cred-detail td { padding: 4px 10px 14px; }
tr.cred-detail:hover td { background: transparent; }
tr:has(+ tr.cred-detail) td { border-bottom: 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);
  background: var(--surface-3); border: 1px solid var(--line);
}
/* Único resto de cor de estado no bloco: fio de atenção nas Permissões —
   mesmo gesto do .c-stat, sem pintar o fundo. */
.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; background: var(--surface-2); }
a.tool-box:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.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)); }
/* tint-ink, não muted: 12px sobre surface-3 precisa de mais tinta pra AA
   (muted dá 4.33:1 no claro; tint-ink dá 7.0:1 claro / 9.9:1 escuro). */
.tb-sub { font-size: var(--fs-xs); color: var(--tint-ink); }
.tb-arrow { color: var(--h-ink); }
/* Editores dentro das caixas: summary neutro (o peso é a hierarquia); um
   chevron cinza diz "abre" — cor fica pros badges de estado. */
.tool-box .profile-editor > summary, .tool-box .scopes-editor > summary { color: var(--text); }
.tool-box .profile-editor > summary::after, .tool-box .scopes-editor > summary::after {
  content: "▾"; color: var(--muted); font-size: var(--fs-2xs); line-height: 1;
  transition: rotate .15s ease;
}
.tool-box .profile-editor[open] > summary::after,
.tool-box .scopes-editor[open] > summary::after { rotate: 180deg; }
.tool-box:has(.profile-editor[open]), .tool-box:has(.scopes-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 .small { flex: none; }
.cred-name { display: block; font-weight: 600; font-size: var(--fs-sm); margin-bottom: 2px; }
.create-form .field-label { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 2px 0 4px; }
.create-form .field-label .muted { font-weight: 400; }

/* Editor de PERMISSÕES de chave já criada — espelha o profile-editor. */
.scopes-editor { font-size: var(--fs-sm); min-width: 0; }
.scopes-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 { text-decoration: underline; }
.scopes-editor[open] > summary { margin-bottom: 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); }

/* ── 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;
  /* Sem sombra pelo mesmo motivo do `.card` logo acima: no painel a elevação é
     de superfície flutuante, não de cartão. */
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius);
}
.create-item[open] { grid-column: 1 / -1; border-color: var(--brand-2); }
.create-item > summary {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; min-height: 44px; cursor: pointer; list-style: none;
}
.create-item > summary::-webkit-details-marker { display: none; }
.create-item > summary:hover { background: var(--surface-3); border-radius: var(--radius); }
.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(--line); }
.create-form > label:first-of-type { margin-top: 0; }
.create-form input[type=text] { max-width: 340px; }
.create-form button[type=submit] { margin-top: 4px; }

/* ── 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(--line); border-radius: var(--radius);
  background: var(--surface-3); 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(--surface); border: 1px solid var(--line);
  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-line);
}

/* Self-service (visão do operador dos tenants do portal) */
.text-bad { color: var(--bad); font-weight: 600; }
.row-burning { background: var(--bad-bg); }
.inline-quota { display: inline-flex; gap: 4px; align-items: center; }
.inline-quota input { width: 84px; }

/* ===================================================================
   NAV TENANT-FIRST — o workspace do cliente é a superfície primária; a
   plataforma (infra global) é a camada secundária, discreta mas acessível.
   Um seletor de cliente fica sempre à mão no topo da barra.
   =================================================================== */

/* Faixa de contexto quando o operador está DENTRO de um cliente: um filete
   de accent no topo da barra sinaliza "você está no mundo de um cliente". */
/* Trilha de "estou dentro do workspace de um cliente". Era #6db3e8 (azul suave)
   sobre a navy #16405f — contraste baixo demais, quase não aparecia. Âmbar não
   compete com nenhum azul da interface e não carrega semântica de erro (o
   vermelho é de falha; o verde é de saúde), então lê como CONTEXTO, que é o que
   ela é. Mais larga (6px) porque é um marcador de modo, não um detalhe. */
.sidebar.in-tenant { box-shadow: inset 6px 0 0 var(--nav-context); }

/* ---- Seletor de cliente (switcher) — <details> nativo, sem JS ---- */
.tsw { margin: 10px 12px 4px; }
.tsw > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-radius: var(--radius);
  background: var(--nav-hover);
  border: 1px solid var(--nav-active);
}
.tsw > summary::-webkit-details-marker { display: none; }
.tsw > summary:hover { background: var(--nav-active); }
.tsw:focus-within > summary { outline: 2px solid var(--focus); outline-offset: 1px; }
.tsw-cur { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.tsw-name { color: var(--nav-ink); font-weight: 700; font-size: var(--fs-md); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* «ativo · trocar» — a legenda sob o nome do cliente. RODADA 4 MEDIU 3,95:1 em
   repouso e **3,31:1 sob o cursor**, em 53 telas.
   AQUI O TOKEN NÃO ESTAVA ERRADO — o SUPORTE é que é outro. --nav-ink-3 foi
   dimensionado contra a navy CHEIA (--nav-bg), onde mede 5,28:1 e passa; mas o
   switcher é um bloco ERGUIDO sobre ela (--nav-hover = branco a 10%, que
   compõe #2d536f) e no hover sobe mais um degrau (--nav-active, #3b5f79). Cada
   degrau de elevação clareia o fundo e come contraste da tinta: 5,28 → 3,95 →
   3,31. O estado de hover, que existe para AJUDAR a mirar, era o que mais
   escurecia a informação.
   Sobe para --nav-ink-2: 5,64:1 em repouso e 4,69:1 no hover (9,17 e 7,45 no
   escuro) — passa nos quatro. --nav-ink-3 continua certo onde nasceu, sobre a
   navy cheia; o que estava errado era usá-lo sobre a navy ERGUIDA. */
.tsw-sub { color: var(--nav-ink-2); font-size: var(--fs-2xs); letter-spacing: .02em; }
.tsw-caret { color: var(--nav-ink-3); font-size: var(--fs-xs); transition: transform .15s ease; }
.tsw[open] > summary .tsw-caret { transform: rotate(180deg); }
.tsw-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.tsw-dot.ok { background: var(--nav-ok); } .tsw-dot.bad { background: var(--nav-bad); }
.tsw-menu {
  margin-top: 6px; padding: 6px; border-radius: var(--radius);
  background: var(--nav-well); border: 1px solid var(--nav-line);
  max-height: 320px; overflow-y: auto;
}
.tsw-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 6px; color: var(--nav-ink-2); font-size: var(--fs-sm); }
.tsw-item:hover { background: var(--nav-hover); text-decoration: none; }
/* RODADA 3: era rgba(109,179,232,.18) — que é EXATAMENTE --nav-focus a 18%.
   Escrito assim, a marca de "este é o cliente atual" passa a SEGUIR o token de
   foco em vez de repetir o hex dele à mão. Em sRGB o color-mix com transparent
   dá o mesmo rgba, ao pixel. */
.tsw-item.on { background: color-mix(in srgb, var(--nav-focus) 18%, transparent); color: var(--nav-ink); }
.tsw-item-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tsw-check { color: var(--nav-focus); }
.tsw-all { display: block; margin-top: 4px; padding: 8px 10px; font-size: var(--fs-xs); color: var(--nav-ink-3); border-top: 1px solid var(--nav-line); }
.tsw-all:hover { color: var(--nav-ink); text-decoration: none; }
.tsw-empty { padding: 8px 10px; font-size: var(--fs-xs); color: var(--nav-ink-3); }

/* ---- Grupo WORKSPACE (primário) vs PLATAFORMA (secundário) ---- */
.nav-workspace { padding-bottom: 6px; }
.ws-label { color: var(--nav-ink-ws); }
.nav-workspace a { font-size: var(--fs-md); }
.nav-disabled { display: block; padding: 8px 16px; font-size: var(--fs-xs); color: var(--nav-ink-3); line-height: 1.4; }

.nav-platform { margin-top: 6px; border-top: 1px solid var(--nav-line); padding-top: 8px; }
.pf-label { color: var(--nav-ink-3); }
/* Links de plataforma um tom mais discretos que os do workspace. */
.nav-platform > a { color: var(--nav-ink-3); font-size: var(--fs-sm); }
.nav-platform > a:hover { color: var(--nav-ink); }

/* ===================================================================
   CHROME DO WORKSPACE (área de conteúdo do cliente)
   =================================================================== */
.ws-bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
.ws-bar-main { min-width: 0; }
/* O lado DIREITO da barra de workspace — nove telas de tenant-* e a do domínio.
   Nunca teve regra, e o conteúdo dele não é um item só: é badge + link de
   trilha (domain-detail), badge + form de acesso de suporte (tenant-detail),
   dois botões (tenant-acessos). Sem `display: flex` eles eram inline, separados
   só pelo espaço em branco do HTML — quebravam colados um no outro quando a
   linha apertava. `flex: none` porque este lado é o que NÃO cede: quem encolhe
   é o `.ws-bar-main`, que já declara `min-width: 0` para isso. */
.ws-bar-side { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; flex: none; }
.ws-eyebrow { display: block; font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--brand-2); margin-bottom: 2px; }
.ws-bar h1 { margin: 0; }
.ws-id { color: var(--muted); font-weight: 400; font-size: var(--fs-lg); }
.ws-cards { margin-bottom: 16px; }

/* Sub-nav de âncoras dentro da visão geral do cliente. */
.ws-anchors { display: flex; flex-wrap: wrap; gap: 6px 8px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px; }
.ws-anchors a { font-size: var(--fs-sm); padding: 4px 12px; border-radius: 999px; border: 1px solid var(--border); color: var(--text); background: var(--panel); }
.ws-anchors a:hover { background: var(--info-bg); border-color: var(--brand-2); text-decoration: none; }
:target { scroll-margin-top: 12px; }
/* No mobile o topbar sticky (52px) cobriria o alvo da âncora (ex.: /queue#resgate).
   Compensa a altura. */
@media (max-width: 900px) { :target { scroll-margin-top: calc(52px + env(safe-area-inset-top) + 12px); } }

/* Estado vazio genérico (onboarding / abas ainda sem dado). */
.empty-state { text-align: center; padding: 28px 20px; }
.empty-state .empty-ico { font-size: var(--fs-8xl); line-height: 1; margin-bottom: 10px; }
.empty-state h2, .empty-state h3 { margin: 0 0 8px; }
.empty-state p { max-width: 56ch; margin: 0 auto 14px; }

/* Nota de spec — "isto ainda não tem backend; aqui está o que ligar". */
.spec-note {
  margin: 20px 0; padding: 12px 14px; font-size: var(--fs-sm); line-height: 1.55;
  background: var(--info-bg); border-left: 3px solid var(--brand-2);
  border-radius: 0 6px 6px 0; color: var(--tint-ink);
}
.spec-note code { background: color-mix(in srgb, var(--surface) 70%, transparent); padding: 1px 6px; border-radius: 4px; font-family: var(--mono); font-size: var(--fs-xs); }
.spec-note pre.code-block { margin: 8px 0 0; }

/* ===================================================================
   NÓS & SERVIDORES (inventário de infra)
   =================================================================== */
.node-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); gap: 16px; margin: 10px 0 22px; }
.node-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; }
.node-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.node-host { font-weight: 700; font-size: var(--fs-md); word-break: break-all; }
.node-ip { color: var(--muted); font-size: var(--fs-sm); }
.node-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 14px; margin: 0 0 12px; }
.node-meta > div { min-width: 0; }
.node-meta dt { font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: .4px; color: var(--muted); font-weight: 600; }
.node-meta dd { margin: 1px 0 0; font-size: var(--fs-sm); }
.node-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
.node-actions form { margin: 0; display: flex; align-items: flex-end; gap: 6px; }
.node-foot { margin-top: 10px; font-size: var(--fs-xs); }

/* ── As seis classes de estrutura de nodes.ejs que nunca tiveram regra ───────
   Elas não são marcação morta: cada uma nomeia um slot que o cartão de nó
   precisa, e sem regra o slot herdava o fluxo de bloco — que é o layout errado
   para todas as seis. */
/* Selo de estado, no canto direito do cabeçalho do cartão (`.node-head` é
   flex + space-between): não cede espaço para o hostname, que é o que pode
   quebrar em várias linhas. */
.node-flags { flex: none; display: flex; align-items: center; gap: 6px; }
/* Leitura ao vivo do nó (badge + frase de contexto), abaixo do `.node-meta`.
   Badge e texto na MESMA linha, quebrando junto quando não cabem. */
.node-live { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
/* Controle do nó (restart / promover) — ações RCE-adjacent atrás de <details>.
   O bloco fica separado do resto do cartão por um fio: é a fronteira entre
   LER o nó e AGIR sobre ele. */
.node-control { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.node-control > summary { cursor: pointer; font-size: var(--fs-sm); font-weight: 600; }
.node-control-body { padding-top: 8px; }
/* Duas formas empilhadas (reiniciar, promover) sem nada entre elas liam como
   um formulário só de quatro campos — e são DUAS ações destrutivas diferentes,
   cada uma pedindo o hostname de novo. */
.node-control-form + .node-control-form { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border); }
/* A linha campo+botão de cada confirmação. `align-items: flex-end` alinha o
   botão pela base do input, não pelo topo. */
.node-control-row { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 8px; }

/* Caixas do sistema — chips de endereço de caixa. */
.mbox { display: inline-block; background: var(--info-bg); color: var(--info); border-radius: 6px; padding: 1px 8px; font-family: var(--mono); font-size: var(--fs-xs); }

/* ---- Ajustes mobile de conteúdo (≤900px). A nav vertical + grupos vêm dos
   estilos-base da sidebar (o drawer é a sidebar na forma natural), então aqui
   só sobra espaçamento do seletor de cliente e layouts que empilham. ---- */
@media (max-width: 900px) {
  .tsw { margin: 10px 12px; }
  .ws-bar { flex-direction: column; gap: 8px; }
  .node-grid { grid-template-columns: 1fr; }
}

/* Duas colunas de painel lado a lado que empilham sozinhas (abuse.ejs: «Por IP
   de saída» e «Por cliente»). A regra morava no atributo `style` da própria
   div — a classe prometia o layout e não entregava nada, e o layout ficava
   fora do alcance de qualquer media query ou tema. */
.cols { display: flex; gap: 16px; flex-wrap: wrap; }

/* Revisão de abuse: destaque de linha acima do limiar de reclamação. */
tr.row-bad td { background: var(--bad-bg); }
tr.row-bad:hover td { background: color-mix(in srgb, var(--bad-bg) 75%, var(--bad-line)); }

/* Troca de senha: campo com botão "olho" (mostrar/ocultar) e erro inline. */
.pw-field { position: relative; }
.pw-field input { width: 100%; padding-right: 40px; }
.pw-eye {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 4px; cursor: pointer;
  color: var(--muted); opacity: .8;
}
.pw-eye:hover { opacity: 1; color: inherit; }
.pw-eye:focus-visible { outline: 3px solid var(--focus); border-radius: 4px; }
.pw-eye .ic { width: 1.1em; height: 1.1em; }
.pw-eye .ic-eye-off { display: none; }
.pw-eye.showing .ic-eye { display: none; }
.pw-eye.showing .ic-eye-off { display: inline; }
.pw-error { color: var(--bad); font-size: var(--fs-sm); margin: 8px 0 0; font-weight: 600; }

/* ── MOVIMENTO REDUZIDO ──────────────────────────────────────────────────────
   Respeita quem pediu menos animação: neutraliza transições/animações. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   COMPONENTES (FASE 1) — biblioteca reutilizável
   Um jeito SÓ de mostrar cada coisa. Cada bloco casa com um partial em
   src/views/partials/comp/*.ejs. Só tokens semânticos da Fase 0 (zero cor crua).

   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.
   ═══════════════════════════════════════════════════════════════════════════ */

/* SSOT saúde→token. Qualquer elemento com data-health ganha o trio de vars
   locais --h-ink/--h-bg/--h-line; todo componente abaixo consome essas vars —
   então a cor de cada estado é definida em UM 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. Uma
   linha, e todo consumidor do trio --h-* já o fala. Único cujo --h-ink NÃO é o
   token de texto do próprio estado: --warn sobre --warn-solid dá 4,24:1. */
[data-health="held"]    { --h-ink: var(--on-warn); --h-bg: var(--warn-solid); --h-line: var(--warn-bd); }

/* Utilitário: só para leitor de tela (ex.: rótulo do skeleton, texto do toast). */
.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 (unifica .card e .stat) ─────────────────── */
/* rodada 2: minmax 170px → 160px, o valor do portal (portal manda em acabamento). */
.c-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }
.stat-row > .c-stat { flex: 1 1 140px; min-width: 140px; }
.c-stat {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);   /* health rail (neutra por padrão) */
  border-radius: var(--radius);
  padding: 12px 16px;
}
.c-stat[data-health] { border-left-color: var(--h-ink); }
.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; }
.c-stat-value { font-size: var(--fs-6xl); 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; }

/* ── comp/badge — pílula de status (dot + rótulo, tint-bg + ink AA) ────────── */
.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/ célula de tabela (sem fundo, compacto) ─── */
.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/formrow — linha de formulário (label + controle + ajuda + erro) ──── */
.c-formrow { margin: 0 0 14px; max-width: 520px; }
.c-formrow-label { display: block; font-size: var(--fs-sm); font-weight: 600; margin: 0 0 4px; }
.c-formrow-help { font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; }
.c-formrow-error { font-size: var(--fs-xs); color: var(--bad); font-weight: 600; margin-top: 6px; }
.c-formrow.has-error .c-formrow-control :where(input, select, textarea) { border-color: var(--bad); }

/* ── comp/empty — estado vazio ────────────────────────────────────────────── */
.c-empty { text-align: center; padding: 32px 20px; }
.c-empty-ic { display: inline-flex; color: var(--faint); margin-bottom: 12px; line-height: 0; }
.c-empty-ic .ic { width: 40px; height: 40px; stroke-width: 1.6; }
.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 (generaliza .banner-danger) ─── */
.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(--surface) 45%, transparent);
}
.c-banner-action:hover { text-decoration: none; background: var(--surface); }

/* ── comp/dangerzone — painel isolado p/ ação irreversível ─────────────────── */
.c-dangerzone {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 16px 18px; margin: 22px 0; border-radius: var(--radius);
  background: var(--h-bg); border: 1px solid var(--h-line);
}
.c-dangerzone-body { flex: 1; min-width: 240px; }
.c-dangerzone-title { margin: 0 0 4px; font-size: var(--fs-lg); color: var(--h-ink); }
.c-dangerzone-desc { margin: 0; font-size: var(--fs-sm); color: var(--tint-ink); }
.c-dangerzone-action { flex: none; }

/* ── comp/skeleton — placeholder de forma (blocos, não spinner) ────────────── */
.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; }
.c-skel-bar {
  display: block; height: 12px; border-radius: 6px;
  /* `--surface-3` sobre `--surface` mede 1,01:1 — a barra fantasma era BRANCA
     sobre branco, e «carregando, nunca branco» renderizava branco. Medido em
     22/08 ao portar o esqueleto para o portal; a folha de lá já saiu certa e
     esta acompanha. Latente aqui (nenhuma view do painel consome o partial
     ainda), e é por isso que ninguém tinha visto. */
  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; } }

/* ── toast — confirmação client-side de ação assíncrona (ver admin.js) ──────
   Complementa o flash server-side (POST+reload). Entra de baixo; o bloco global
   de prefers-reduced-motion acima já neutraliza a transição. */
#toasts {
  position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 8px; z-index: 1000;
  width: min(92vw, 420px); pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; border-radius: var(--radius); font-size: var(--fs-md);
  background: var(--h-bg, var(--neutral-bg)); color: var(--h-ink, var(--ink));
  border: 1px solid var(--h-line, var(--line)); border-left-width: 4px;
  box-shadow: var(--sh-md);
  opacity: 0; transform: translateY(16px);
  transition: opacity .22s ease, transform .22s ease;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }

/* ── PADRÃO: tabela responsiva .table-cards ─────────────────────────────────
   Aplique a classe na <table> e ponha data-label="Rótulo" em cada <td>. No
   mobile (≤640px) cada <tr> vira um cartão empilhado "rótulo: valor". Use SÓ em
   tabelas de leitura (poucas colunas); tabelas densas/forenses continuam com
   .table-wrap (overflow-x).

   ESCOPO POR FILHO (> tbody > tr > td), e não descendente: sem isto o padrão
   DESCE para tabelas ANINHADAS dentro de uma célula. Foi o que quebrou o card de
   IP no mobile — o card é injetado numa <td> da tabela-índice (que é
   .table-cards), então as tabelas DELE viravam lista empilhada sem rótulo, com
   o thead escondido, mesmo sem pedir. Tabela aninhada só entra no modo cartão
   se ela mesma tiver a classe. */
@media (max-width: 640px) {
  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(--line); border-radius: var(--radius);
    background: var(--surface); margin-bottom: 10px; padding: 4px 12px;
  }
  table.table-cards > tbody > tr:hover > td { background: transparent; }
  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 («há 26h») + saída («resolver →»)
       estourava o cartão e o link era CORTADO na borda — ação ilegível é ação
       inexistente. Regra idêntica no portal.css (classe compartilhada). */
    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: ""; }
  table.table-cards > tbody > tr > td.num, table.table-cards > thead th.num { text-align: right; }

  /* Texto LONGO (detalhe de blocklist, resposta SMTP): rótulo em cima e valor
     embaixo, alinhado à esquerda — o par lado a lado espremeria o valor numa
     coluna de 3 palavras. Irmão de .actions-cell. */
  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; }

  /* Estado vazio ("nenhum registro"): é um <td colspan> sem rótulo — sem isto o
     texto fica jogado à direita pelo text-align do cartão. */
  table.table-cards > tbody > tr > td[colspan] { display: block; text-align: left; }

  /* Separador de grupo de colunas some no cartão (não há colunas para separar). */
  table.table-cards > tbody > tr > td.col-sep { border-left: 0; }

  /* Célula de AÇÕES no modo card: rótulo em cima, botões fluindo com quebra e
     alvo de toque ≥44px (no desktop denso os .small continuam compactos). */
  table.table-cards > tbody > tr > td.actions-cell { display: block; text-align: left; white-space: normal; }
  table.table-cards > tbody > tr > td.actions-cell::before { display: block; margin-bottom: 6px; }
  table.table-cards > tbody > tr > td.actions-cell form { margin: 2px 6px 2px 0; }
  table.table-cards > tbody > tr > td.actions-cell button,
  table.table-cards > tbody > tr > td.actions-cell .btn { min-height: 44px; padding: 10px 14px; }

  /* Linha de detalhe (perfil/conversa) 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; }
  .profile-editor > summary, .scopes-editor > summary { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ── Fase 3 — Central de Operações (raiz health-first) ────────────────────────
   Só tokens + os componentes das fases anteriores (comp/stat, comp/banner,
   comp/empty). Aqui mora só o "cimento" de layout: cartão de rollup clicável,
   seção de atenção e os badges de contagem (sidebar + botnav). */

/* Cartão de rollup: o comp/stat vira um link inteiro para a tela do sinal, sem
   sublinhar (herda a estética do cartão, não a de um link de texto). */
.ops-card-link { display: block; text-decoration: none; color: inherit; border-radius: var(--radius); }
.ops-card-link:hover { text-decoration: none; }
.ops-card-link:hover .c-stat { border-color: var(--line-2); box-shadow: var(--sh-sm); }
.ops-card-link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Seção "Precisa de você" / feed de alertas: só empilha os banners. */
.ops-attention { margin-bottom: 22px; }
.ops-h2 { font-size: var(--fs-lg); margin: 22px 0 12px; }
.ops-h2:first-child { margin-top: 0; }

/* Contagem ao lado do título "Precisa de você" — pílula semântica via data-health. */
.ops-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 8px; margin-left: 6px;
  border-radius: 999px; font-size: var(--fs-xs); font-weight: 700; vertical-align: middle;
  background: var(--h-bg); color: var(--h-ink); border: 1px solid var(--h-line);
}

/* Badge de alertas na sidebar (nav escura): empurrado à direita do link. */
.nav-with-badge { }
.nav-badge {
  margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 18px; padding: 0 6px; border-radius: 999px;
  font-size: var(--fs-2xs); font-weight: 700; line-height: 1;
  background: var(--badge-danger-bg); color: var(--on-solid);
}

/* Badge de alertas na bottom-nav: bolha sobreposta ao ícone. */
.botnav-ic-wrap { position: relative; display: inline-flex; }
.botnav-badge {
  position: absolute; top: -6px; left: 12px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--radius);
  font-size: var(--fs-3xs); font-weight: 700; line-height: 1;
  background: var(--badge-danger-bg); color: var(--on-solid); border: 1px solid var(--panel);
}

/* ── Fase 4 — tela de Configurações table-driven (settingsSchema) ─────────────
   Só layout: a busca no topo, o grupo em <section.panel>, a linha de setting e o
   checkbox inline do formrow. Os controles reusam .c-formrow / .pw-field / .badge. */
.set-toolbar { position: sticky; top: 0; z-index: 5; background: var(--bg); padding: 10px 0; margin-bottom: 10px; }
/* idem: `input.` para empatar com input[type=search] e vencer pela ordem —
   senão o max-width 480px da regra base substituiria os 440px desta tela. */
input.set-search { width: 100%; max-width: 440px; }
.set-search-empty { color: var(--muted); font-size: var(--fs-sm); margin: 6px 2px 0; }
.set-group.set-hidden, .set-row.set-hidden { display: none; }
.set-row { }
.c-check { display: inline-flex; align-items: center; gap: 8px; font-weight: 400; cursor: pointer; }
.set-csv { font-family: var(--mono); width: 100%; max-width: 520px; font-size: var(--fs-sm); }

/* ── Fase 4 — tail de arquivo de log (/logs/files) ───────────────────────────
   Monospace, rolagem horizontal isolada (nunca empurra o body), conteúdo é
   escapado na view (log = entrada não-confiável). */
.logtail { max-height: 70vh; overflow: auto; background: var(--surface-3); border: 1px solid var(--line); border-radius: var(--radius); }
.logtail-pre {
  margin: 0; padding: 12px 14px; white-space: pre;
  font-family: var(--mono); font-size: var(--fs-xs); line-height: 1.5;
  color: var(--ink);
}

/* --- FASE 4: dialog de envio de teste + mini-timeline --------------------- */
.testsend-modal { max-width: 480px; width: 92vw; }
.testsend-mode { border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 12px; margin: 12px 0 4px; }
.testsend-mode legend { padding: 0 6px; font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.testsend-timeline { list-style: none; margin: 4px 0 0; padding: 0; }
.testsend-ev { display: flex; align-items: flex-start; gap: 10px; padding: 6px 0; font-size: var(--fs-sm); }
.testsend-ev-dot { flex: 0 0 auto; width: 9px; height: 9px; margin-top: 6px; border-radius: 50%; background: var(--muted); }
.testsend-ev-dot[data-ev="delivered"] { background: var(--ok); }
.testsend-ev-dot[data-ev="bounced"], .testsend-ev-dot[data-ev="failed"], .testsend-ev-dot[data-ev="suppressed"] { background: var(--bad); }
.testsend-ev-dot[data-ev="deferred"], .testsend-ev-dot[data-ev="frozen"] { background: var(--warn); }
.testsend-ev-dot[data-ev="opened"], .testsend-ev-dot[data-ev="clicked"] { background: var(--accent); }

/* Modal do "Ver" da cópia arquivada — mostra o .eml cru na própria página. */
.eml-modal {
  padding: 0; border: 1px solid var(--line-2); border-radius: var(--radius);
  width: 900px; max-width: 92vw; max-height: 86vh;   /* rodada 2: era 84vh; portal manda em acabamento */
  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; }
.eml-modal-head .mono { font: 600 var(--fs-xs) var(--mono); color: var(--muted); }
.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 media 1.05:1 —
   um hover que praticamente não existia. Mesmo gesto no portal.css. */
.eml-modal-close:hover { color: var(--ink); background: var(--neutral-bg); }
.eml-modal-body {
  margin: 0; padding: 16px; overflow: auto; max-height: calc(86vh - 50px);
  font: var(--fs-xs)/1.55 var(--mono);
  white-space: pre-wrap; word-break: break-word; color: var(--ink); background: var(--surface);
}

/* Ações no cabeçalho do modal de cópia + o botão "olho" na lista de mensagens. */
.eml-modal-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.btn-archive-view { display: inline-flex; align-items: center; padding: 1px 4px; vertical-align: middle; color: var(--muted); background: none; border: 0; cursor: pointer; }
.btn-archive-view:hover { color: var(--accent); }
.btn-archive-view .ic { width: 15px; height: 15px; }

/* Modal da cópia: abas Renderizado/Fonte + iframe sandboxed do HTML. */
.eml-modal-frame { width: 100%; height: calc(86vh - 50px); border: 0;
  /* PAPEL DE E-MAIL, declarado: este <iframe> renderiza HTML de TERCEIRO, e
     corpo de e-mail é escrito assumindo fundo branco. Aqui #fff não é uma
     superfície do produto — é o papel. Trocar por --surface pintaria o fundo de
     escuro sob um texto que o remetente escreveu preto. Fixo nos dois temas. */
  background: #fff; display: block; }
.eml-modal-frame[hidden] { display: none; }
.eml-modal-tabs { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.eml-tab { background: var(--surface-2); border: 0; padding: 6px 12px; font-size: var(--fs-xs); font-weight: 600; color: var(--muted); cursor: pointer; min-height: 0; }
.eml-tab + .eml-tab { border-left: 1px solid var(--line); }
/* Preenchimento de --accent pede --accent-ink, não --on-solid: --on-solid é
   branco FIXO (certo sobre vermelho/teal), e no tema escuro o --accent é claro
   (#5b8df5) — branco sobre ele mede 3.19:1 e REPROVA AA. --accent-ink é a tinta
   pareada com o próprio --accent: 6.14:1 no claro, 5.79:1 no escuro. Achado da
   rodada 2; era a única regra do painel abaixo de 4.5:1 depois da consolidação. */
.eml-tab.on { background: var(--accent); color: var(--accent-ink); }
/* ── A ABA INATIVA SOB O CURSOR ─────────────────────────────────────────────
   RODADA 4 MEDIU 1,99:1 no claro e **1,13:1 no escuro**.
   MESMA CAUSA RAIZ do chip logo acima, numa variação mais traiçoeira: aqui
   quem repinta não é uma regra da aba, é a regra GENÉRICA `button:hover
   { background: var(--accent-hover) }` (0,1,1). `.eml-tab` (0,1,0) declarou a
   própria tinta (--muted) e o próprio fundo (--surface-2) mas, sendo um
   <button>, herda o hover de TODOS os botões — e aquele hover foi escrito para
   botão PREENCHIDO de accent, onde a tinta é branca. Resultado: --muted sobre
   --accent-hover, duas cores que nunca foram pensadas juntas.
   Lição que fica para a folha: **variante que declara a própria tinta precisa
   declarar o próprio hover**, senão o hover genérico do elemento a atropela.
   O hover de uma aba não é «ficar da cor da ação» — é vir para a frente: a aba
   sobe para a superfície do painel e a tinta fecha em --text. */
.eml-tab:hover { background: var(--panel); color: var(--text); }
/* E a aba ATIVA não muda de identidade ao ser apontada — só escurece um degrau,
   o mesmo vocabulário do chip selecionado. */
.eml-tab.on:hover { background: var(--accent-hover); color: var(--accent-ink); }

/* ═══════════════════════════════════════════════════════════════════════════
   SAÚDE DOS IPs & RAMPA — redesign (append). Só tokens semânticos da Fase 0 e
   os componentes existentes; nada de cor crua. Mobile-first (body nunca rola
   lateral; tabelas rolam no .table-wrap); alvos de toque ≥44px. Light + dark
   herdam via var(). Cor = SIGNIFICADO (estado), nunca decoração.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Resumo reativo — herda .c-stats/.c-stat; só o subtítulo "de N" quando filtra. */
.ip-summary .c-stat-sub[hidden] { display: none; }

/* ── Barra de filtros combináveis ─────────────────────────────────────────── */
.ipfilter {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 18px;
  padding: 12px 14px; margin-bottom: 16px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
}
.ipfilter-group { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.ipfilter-label {
  font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: .5px;
  color: var(--muted); font-weight: 600;
}
/* Grupos de chips (reusa .chip/.chip-on do admin.css). O segmented conecta os
   pills num trilho; o chipset só agrupa com respiro. */
.segmented, .chipset { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.segmented {
  gap: 0; padding: 4px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--line);
}
/* Chips do filtro são <button> — zera o fundo accent herdado do reset de button;
   o estado "ligado" (chip-on / aria-pressed) reafirma o accent. */
.ipfilter button.chip { cursor: pointer; font: inherit; font-size: var(--fs-sm); line-height: 1.2;
  min-height: 32px; display: inline-flex; align-items: center; background: transparent; color: var(--text); }
.ipfilter button.chip:hover { background: var(--info-bg); }
.segmented .chip { border: 0; border-radius: 999px; }
.segmented .chip.chip-on { background: var(--accent); color: var(--accent-ink); }
.chipset .chip[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }

/* ── O CHIP SELECIONADO SOB O CURSOR ────────────────────────────────────────
   RODADA 4 MEDIU **1,17:1** no claro e 1,12:1 no escuro, em 8 telas: apontar o
   filtro que está LIGADO fazia o rótulo dele sumir.
   O MECANISMO é o de sempre nesta folha: uma regra de ESTADO repinta o FUNDO e
   não mexe na TINTA. `.chip:hover` (0,2,0) troca o preenchimento por --info-bg
   e vence `.chip-on` (0,1,0), que é quem tinha posto o accent; mas o
   `color: var(--accent-ink)` do .chip-on continua valendo, porque nada o
   sobrescreveu. Sobra branco sobre azul-clarinho. O hover não «desligou» o
   chip: desmontou metade dele.
   O CONSERTO É DE PAR, não de cor: quem mexe no fundo mexe na tinta junto. O
   selecionado escurece um degrau no hover (--accent-hover), que é o feedback
   que o chip normal já dá, e a tinta segue sendo a pareada com o accent —
   10,87:1 no claro, 7,41:1 no escuro.
   ESPECIFICIDADE: precisa vencer `.ipfilter button.chip:hover`, que é (0,3,1);
   este conjunto é (0,4,1). Sem isso o chip do filtro de IP — que é justamente
   uma das 8 telas medidas — continuaria quebrado. */
:is(.chip, .segmented .chip, .ipfilter button.chip):is(.chip-on, [aria-pressed="true"]):hover {
  background: var(--accent-hover);
  color: var(--accent-ink);
  border-color: var(--accent-hover);
}
.ipfilter-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin-left: auto; }
/* Atualizar / Limpar como botões-ícone quadrados, alinhados ao rodapé da barra. */
.ipfilter-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; min-height: 36px; padding: 0;
  background: transparent; color: var(--muted); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;
}
.ipfilter-iconbtn:hover { background: var(--surface-3); color: var(--brand-2); }
.ipfilter-iconbtn:disabled { opacity: .5; cursor: default; }
.ipfilter-icon { font-size: var(--fs-xl); line-height: 1; }
#ipRefreshAll:disabled .ipfilter-icon,
#wuRefreshAll:disabled .ipfilter-icon { animation: ip-spin .8s linear infinite; display: inline-block; }
/* "Limpar" é um <button class=btn-link> — neutraliza o fundo accent do reset. */
.ipfilter-actions button.btn-link { background: transparent; border: 0; min-height: 0; padding: 4px 8px; cursor: pointer; }
.ipfilter-actions button.btn-link:hover { text-decoration: underline; background: transparent; }
/* `input.` e não só `.ipfilter-search`: quando esta rodada acrescentou
   `input[type=search]` à lista de campos base, aquele seletor passou a valer
   (0,1,1) e venceria este, que era (0,1,0) — a caixa de filtro esticaria para
   100% no meio da barra de ferramentas. Especificidade vence ordem de arquivo,
   então estar mais abaixo não bastava. Com `input.` os dois empatam em (0,1,1)
   e aí sim a ordem decide, a favor do específico.
   A BORDA acompanha o resto do produto: --field-border, 3,66:1 em vez de 1,29. */
input.ipfilter-search {
  width: 200px; max-width: 46vw; min-height: 36px; padding: 6px 10px;
  border: 1px solid var(--field-border); border-radius: 6px; font: inherit; font-size: var(--fs-sm);
  background: var(--panel); color: var(--text);
}
.ipfilter-empty { margin: 0 2px 16px; }

/* ── Grade de cards de IP ─────────────────────────────────────────────────── */
.ip-cards { display: grid; grid-template-columns: 1fr; gap: 16px; margin-bottom: 22px; }
/* Aproveita o desktop largo: dois cards lado a lado quando há espaço real. */
@media (min-width: 1500px) { .ip-cards { grid-template-columns: 1fr 1fr; } }

.ip-card { margin-bottom: 0; scroll-margin-top: 16px; }
/* Rail de atenção: o crítico (listado/pausado/fcrdns) salta à vista. */
.ip-card { border-left: 3px solid var(--line); }
.ip-card[data-health="bad"] { border-left-color: var(--bad); }
.ip-card[data-health="warn"] { border-left-color: var(--warn); }

/* Cabeçalho: IP+host à esquerda, badges no meio, refresh à direita. */
.ip-card-head { display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap; }
.ip-card-id { display: flex; align-items: baseline; gap: 10px; min-width: 0; flex-wrap: wrap; }
.ip-card-ip { font-family: var(--mono); font-size: var(--fs-xl); font-weight: 700; color: var(--text); }
.ip-card-host { color: var(--muted); font-size: var(--fs-sm); word-break: break-all; }
/* Botão-ÍCONE copiar-IP ao lado do endereço (handler global [data-copy]; o
   feedback é por classe .copied — não apaga o ícone). */
.ip-copy {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  color: var(--muted); background: var(--panel-2);
  border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
.ip-copy .ic { width: 15px; height: 15px; }
.ip-copy:hover { background: var(--panel); border-color: var(--border-strong); color: var(--text); }
.ip-copy.copied { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
.ip-copy.copy-failed { color: var(--bad); border-color: var(--bad-line); }
/* Link externo (ex.: Spamhaus) ao lado de um check LISTADO. Cor = significado (bad). */
.check-ext { display: inline-flex; align-items: center; margin-left: 6px; color: var(--bad); vertical-align: middle; }
.check-ext:hover { color: var(--bad); text-decoration: none; }
.check-ext .ic { width: 15px; height: 15px; }

/* Link "abrir a mensagem de origem" na lista de respostas 4xx/5xx do card do IP. */
.msg-link { display: inline-flex; align-items: center; color: var(--muted); vertical-align: middle; }
.msg-link:hover { color: var(--accent); text-decoration: none; }
.msg-link .ic { width: 16px; height: 16px; }
.ip-card-badges { display: flex; flex-wrap: wrap; gap: 6px; }
.ip-refresh {
  margin-left: auto; flex: none;
  min-width: 36px; min-height: 36px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--muted); border: 1px solid var(--line); border-radius: var(--radius);
  cursor: pointer; padding: 0;
}
.ip-refresh:hover { background: var(--surface-3); color: var(--brand-2); }
.ip-refresh-ic { font-size: var(--fs-xl); line-height: 1; }

/* "X" para fechar o card expandido — mesma caixa do ↻, sem o margin-left:auto
   (ele fica logo ao lado do refresh, agrupado à direita do cabeçalho). */
.ip-close {
  flex: none;
  min-width: 36px; min-height: 36px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--muted); border: 1px solid var(--line); border-radius: var(--radius);
  cursor: pointer; padding: 0;
}
.ip-close:hover { background: var(--surface-3); color: var(--bad); }
.ip-close-ic { font-size: var(--fs-3xl); line-height: 1; }
@keyframes ip-spin { to { transform: rotate(360deg); } }

/* ── Faixa de operação (switches + tier + teto) ───────────────────────────── */
.ip-glance {
  display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center;
  margin: 12px 0 4px; padding: 12px 0; border-top: 1px solid var(--line);
}
.switch-form { margin: 0; }
/* Switch estilo iPhone: um <button> (submit) — funciona sem JS (POST→302);
   com JS o clique é interceptado e o card é trocado por fragmento. */
.switch {
  display: inline-flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 4px 6px; background: transparent; border: 0; cursor: pointer;
  color: var(--text); font: inherit; border-radius: var(--radius);
}
.switch:hover { background: var(--surface-3); }
.switch-track {
  position: relative; flex: none; width: 42px; height: 25px; border-radius: 999px;
  background: var(--neutral-line); transition: background .2s ease;
}
.switch-knob {
  position: absolute; top: 3px; left: 3px; width: 19px; height: 19px; border-radius: 50%;
  /* PEÇA FÍSICA, declarada: o botão do interruptor é branco e tem sombra
     PRÓPRIA nos dois temas — é um objeto sobre o trilho, não uma superfície do
     tema. Um knob que muda de cor com o tema deixa de parecer um objeto. */
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.35); transition: transform .2s ease;
}
.switch[aria-checked="true"] .switch-track { background: var(--ok); }
.switch[aria-checked="true"] .switch-knob { transform: translateX(17px); }
/* Outbound: ON = ativo (bom, cor de marca); OFF = pausado (âmbar de atenção). */
.switch-danger[aria-checked="true"] .switch-track { background: var(--accent); }
.switch-danger[aria-checked="false"] .switch-track { background: var(--warn); }
.switch-text { display: flex; flex-direction: column; line-height: 1.15; text-align: left; }
.switch-name { font-size: var(--fs-xs); color: var(--muted); }
.switch-state { font-size: var(--fs-sm); font-weight: 600; }
.switch.is-busy { opacity: .55; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .switch-track, .switch-knob { transition: none; } }

.ip-glance-meta { display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: center; margin-left: auto; }
.ip-glance-k { font-size: var(--fs-xs); color: var(--muted); }
.ip-glance-tier { display: inline-flex; align-items: center; gap: 8px; }
.ip-glance-cap { display: flex; flex-direction: column; gap: 4px; min-width: 150px; }
.ip-glance-cap-val { font-size: var(--fs-sm); font-weight: 600; font-variant-numeric: tabular-nums; }
.ip-glance-bar { margin: 2px 0 0; }

/* ── Seções colapsáveis do card (métricas / checks / smtp) ────────────────── */
.ip-sec { margin-top: 12px; border-top: 1px solid var(--line); }
.ip-sec > summary {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 10px 0 8px; list-style: none; min-height: 20px;
}
.ip-sec > summary::-webkit-details-marker { display: none; }
.ip-sec > summary::after {
  content: "▾"; color: var(--muted); font-size: var(--fs-2xs); margin-left: 4px; transition: rotate .15s ease;
}
.ip-sec[open] > summary::after { rotate: 180deg; }
.ip-sec > summary h3 { margin: 0; font-size: var(--fs-md); }
.ip-sec-hint { margin-left: auto; font-size: var(--fs-xs); color: var(--muted); }
.ip-sec-count { font-weight: 400; color: var(--muted); }
/* Sub-cabeçalho DENTRO de uma seção do cartão de IP («Última checagem»,
   «Histórico de listagem»). É um <h4>, e o painel só declara h1/h2/h3 — o
   quarto nível caía no default do navegador (1em bold, margem de 1.33em), que
   num cartão denso lê como se fosse outro h2 e abre um buraco no meio da
   seção. Mesmo vocabulário do sub-cabeçalho que `.in-modal-body h4` já usa. */
.ip-sub { margin: 14px 0 6px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.ip-sec .table-wrap { margin-bottom: 4px; }

/* ── Ações agrupadas ──────────────────────────────────────────────────────── */
.ip-actions {
  display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
}
.ip-actions form { margin: 0; }
.ip-actions .secondary.small, .ip-actions .ip-action-tier button, .ip-actions .ip-action-cap button { min-height: 36px; }
.ip-action-tier, .ip-action-cap { display: inline-flex; align-items: center; gap: 6px; }
.ip-action-tier .tier-label, .ip-action-cap .tier-label { margin: 0 2px 0 0; white-space: nowrap; }
.ip-action-tier input, .ip-action-cap input { min-height: 36px; margin: 0; }
.ip-ext { margin: 12px 0 0; }


/* ── Tabela-índice de IPs (master) + linha de detalhe (card completo) ──────── */
.ip-index-wrap { margin-bottom: 8px; }
.ip-index { font-size: var(--fs-sm); }
/* Por FILHO: o card injetado tem tabelas ANINHADAS aqui dentro — sem o `>`, o
   sticky do índice passava a valer no thead delas também. */
.ip-index > thead th { white-space: nowrap; position: sticky; top: 0; background: var(--surface); z-index: 1; }
.ip-index > tbody > tr.ip-row { cursor: pointer; }
.ip-index > tbody > tr.ip-row > td { vertical-align: middle; }
.ip-index > tbody > tr.ip-row:hover > td { background: var(--surface-3); }
.ip-index tbody tr.ip-row.is-open > td { background: var(--surface-3); }
.ip-index tbody tr.ip-row:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }
.ip-row-idcell code { font-family: var(--mono); font-weight: 700; font-size: var(--fs-md); color: var(--text); }
.ip-row-link { text-decoration: none; }
.ip-row-link:hover { text-decoration: none; }
.ip-row-host { display: block; color: var(--muted); font-size: var(--fs-xs); margin-top: 1px; word-break: break-all; }
.ip-row-state { line-height: 2; }
.ip-row-state .badge { margin-right: 2px; }
.ip-row-exp { width: 34px; text-align: center; color: var(--muted); }
.ip-exp-ic { display: inline-block; transition: rotate .15s ease; }
.ip-row.is-open .ip-exp-ic { rotate: 90deg; }
@media (prefers-reduced-motion: reduce) { .ip-exp-ic { transition: none; } }

/* Linha de detalhe: o card ocupa a largura toda da tabela, sem hover de linha. */
.ip-detail-row > td { padding: 0; background: var(--surface-2); }
.ip-detail-row:hover > td { background: var(--surface-2); }
.ip-detail { padding: 4px 0 16px; }
.ip-detail .ip-card { margin: 0; border-radius: var(--radius); }
.ip-detail .loading-box { margin: 12px; }

/* Página de 1 IP (/ips/:id) — o card sem grade. */
.ip-cards-single { grid-template-columns: 1fr; }

.legacy-rollup > summary { cursor: pointer; list-style-position: inside; }

/* Explicador único (pool × rotação × pausa × tier) — mata a repetição por card. */
.ip-help > summary { cursor: pointer; font-size: var(--fs-md); font-weight: 600; list-style-position: inside; }
.ip-help > summary:hover { color: var(--brand-2); }
.ip-help ul { margin: 12px 0 0; padding-left: 18px; }
.ip-help li { margin: 6px 0; color: var(--muted); line-height: 1.5; }
.ip-help li strong { color: var(--text); }

/* ── Mobile (≤900px): empilha tudo, nada rola lateral no body ─────────────── */
@media (max-width: 900px) {
  .ip-cards { grid-template-columns: 1fr; }
  .ipfilter-actions { margin-left: 0; width: 100%; }
  input.ipfilter-search { flex: 1; width: auto; max-width: none; }
  .ip-glance-meta { margin-left: 0; width: 100%; }
  .ip-glance-cap { min-width: 0; flex: 1; }
  /* Switches ocupam a largura toda, um por linha, alvo de toque cheio. */
  .switch-form { flex: 1 1 100%; }
  .switch { width: 100%; min-height: 48px; justify-content: flex-start; }
  .ip-glance { gap: 6px 0; }
  /* Alvo de toque ≥44px no touch (no desktop denso seguem compactos, como o
     padrão .actions-cell). Botões-ícone, refresh e ações do card. */
  .ipfilter-iconbtn { width: 44px; height: 44px; min-height: 44px; }
  .ip-refresh, .ip-close { min-width: 44px; min-height: 44px; }
  .ip-actions .secondary.small,
  .ip-actions .ip-action-tier button,
  .ip-actions .ip-action-cap button,
  .ip-actions .ip-action-tier input,
  .ip-actions .ip-action-cap input { min-height: 44px; }
}

/* Separador entre GRUPOS de colunas (ex.: "Últimas 24h" | "Últimos 7 dias").
   Classe em vez de style inline: inline venceria o `border:0` do modo cartão e
   deixaria um risco vertical solto dentro do cartão no mobile. */
.col-sep { border-left: 1px solid var(--border); }

@media (max-width: 640px) {
  /* Métricas por provedor: o nome do provedor vira TÍTULO do cartão (não é um
     par rótulo/valor como as métricas). */
  .ip-metrics-t > tbody > tr > td.metric-provider {
    display: block; text-align: left; font-weight: 700;
    padding: 4px 0 8px; border-bottom: 1px solid var(--line); margin-bottom: 4px;
  }
}

/* ≤640px: a tabela-índice vira cartões (.table-cards). Ajusta detalhe + chevron. */
@media (max-width: 640px) {
  /* Tudo escopado por FILHO: estas regras miram as linhas DA TABELA-ÍNDICE. Sem
     o `>`, o `td::before { content: "" }` abaixo descia para as tabelas do card
     injetado e apagava TODO rótulo de cartão dele no mobile. */
  .ip-index > tbody > tr.ip-row > td.ip-row-exp { display: none; }
  .ip-index > tbody > tr.ip-row { position: relative; }
  .ip-index > tbody > tr.ip-row::after { content: "toque para abrir"; display: block; text-align: right;
    font-size: var(--fs-2xs); color: var(--brand-2); padding: 2px 0 0; }
  .ip-index > tbody > tr.ip-detail-row { border: 0; margin: 0 0 10px; padding: 0; background: transparent; }
  .ip-index > tbody > tr.ip-detail-row > td { display: block; padding: 0; text-align: left; }
  .ip-index > tbody > tr.ip-detail-row > td::before { content: ""; }
  .ip-detail { padding: 0 0 4px; }
}

/* --- /relay: painel da válvula de escoamento por provedor ----------------- */
.relay-danger { border-left: 3px solid var(--warn); }

/* Denominador da cota no resumo (.c-stat-value é 26px/700; o "/ teto" recua). */
.relay-stat-den { font-size: var(--fs-lg); font-weight: 600; color: var(--muted); }

.relay-toggle {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 12px; margin-top: 8px;
  border: 1px solid var(--border); border-left: 3px solid var(--border);
  border-radius: var(--radius);
  transition: border-color .15s ease;
}
.relay-toggle:first-of-type { margin-top: 0; }
/* Ligado = estado que desvia tráfego: rail de atenção + nome âmbar comunicam
   sem JS. Sem tint de fundo (mantém o texto muted em contraste AA na surface). */
.relay-toggle:has(input:checked) { border-left-color: var(--warn); }
.relay-toggle:has(input:checked) .relay-toggle-name { color: var(--warn); }
.relay-toggle input[type="checkbox"] {
  margin-top: 2px; flex: none; width: 16px; height: 16px; accent-color: var(--warn);
}
.relay-toggle-text { display: flex; flex-direction: column; gap: 2px; }
.relay-toggle-name { font-weight: 600; }
.relay-provider { display: flex; flex-direction: column; gap: 8px; }
.relay-provider-head { display: flex; align-items: center; gap: 8px; }
.relay-provider-host { font-family: var(--mono); }
.relay-provider-scope code { margin-right: 4px; }
.relay-bar {
  height: 8px; border-radius: 4px; overflow: hidden;
  background: var(--surface-3); border: 1px solid var(--border);
}
.relay-bar-fill { display: block; height: 100%; background: var(--ok); }
.relay-bar-fill[data-health="warn"] { background: var(--warn); }
.relay-bar-fill[data-health="bad"] { background: var(--bad); }
.relay-bar-legend { display: flex; justify-content: space-between; gap: 12px; }
.relay-legacy { border-left: 3px solid var(--info); }
.relay-legacy code { margin-right: 4px; }

/* Gatilho + seletor de provedor lado a lado (empilha em telas estreitas). */
.relay-toggle-row { display: flex; align-items: stretch; gap: 12px; margin-top: 8px; flex-wrap: wrap; }
.relay-toggle-row:first-of-type { margin-top: 0; }
.relay-toggle-row .relay-toggle { flex: 1 1 320px; margin-top: 0; }
.relay-toggle-provider { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-width: 190px; }
.relay-toggle-provider select { width: 100%; }
/* Ação ligar/desligar no rodapé do card de provedor. */
.relay-provider-toggle { display: flex; align-items: center; gap: 12px; margin-top: 2px; }

/* ── Página /plans — matriz de planos (features/limites/preços) ───────────── */
.plan-card { margin-bottom: 12px; }
.plan-summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; cursor: pointer; }
.plan-name { display: flex; align-items: baseline; gap: 8px; }
.plan-tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.plan-form { margin-top: 14px; display: flex; flex-direction: column; gap: 16px; }
.plan-group { border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; }
.plan-group legend { padding: 0 6px; font-weight: 600; font-size: var(--fs-sm); color: var(--muted); }
.plan-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px 16px; }
.plan-field { display: flex; flex-direction: column; gap: 4px; }
.plan-field label { font-size: var(--fs-xs); color: var(--muted); }
.plan-field input { width: 100%; }
.plan-check { display: flex; align-items: center; gap: 8px; padding: 6px 0; }
.plan-actions { display: flex; justify-content: flex-end; }
.plan-summary .hint, .plan-field .hint { font-weight: 400; color: var(--muted); font-size: var(--fs-2xs); }

/* /plans — input de moeda com prefixo + overage calculado + botão normal. */
/* O wrapper É o campo (o <input> lá dentro tem `border: 0`), então a borda de
   CAMPO é dele — mesma razão, mesmo token. */
.money-input { display: flex; align-items: stretch; border: 1px solid var(--field-border); border-radius: var(--radius); overflow: hidden; }
.money-input .money-sym { display: inline-flex; align-items: center; padding: 0 8px; background: var(--surface-3); color: var(--muted); font-size: var(--fs-xs); border-right: 1px solid var(--line); }
.money-input input { border: 0; border-radius: 0; flex: 1; min-width: 0; text-align: right; }
.money-input:focus-within { border-color: var(--brand-2); }
.plan-overage { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 10px 2px 0; }
.plan-overage .ic { width: 14px; height: 14px; }
/* Botão salvar: tamanho normal (o SVG solto no botão estava esticando). */
.plan-actions .plan-save { width: auto; min-width: 0; align-self: flex-end; padding: 8px 16px; }
.plan-actions .plan-save .ic { width: 15px; height: 15px; }

/* Tamanho-BASE do ícone .ic: sem isto, um .ic em contexto sem regra própria
   (ex.: dentro de <button class="btn-primary">) renderiza no tamanho intrínseco
   do SVG = gigante, e o botão vira um quadrado enorme. 1em acompanha o texto; as
   regras contextuais (.dom-auto-ic .ic {20px} etc.) vencem por specificity. */
.ic { width: 1em; height: 1em; flex: none; vertical-align: -0.15em; }
button .ic, .btn-primary .ic, .secondary .ic { width: 1.05em; height: 1.05em; margin-right: 6px; }

/* ── Botão pequeno / minimalista (ações contextuais: verificar, re-checar…) ── */
/* Botão pequeno — regra IDÊNTICA à do portal.css, palavra por palavra. Era a
   divergência mais gritante das 12: 5px/11px/12.5px/raio 7 aqui, 8px/13px/13.5px
   lá, para a mesma classe. Vence o acabamento do portal (mais folga), agora
   escrito em token. `width: auto` é inócuo aqui e necessário lá (o .btn do
   portal é 100%): a regra ser a mesma vale mais que a declaração ser mínima. */
.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;
}
.btn-sm .ic { width: 14px; height: 14px; margin-right: 4px; }
/* ALVO DE TOQUE (norma de UX §8). No ponteiro fino a densidade do painel é uma
   feature; no toque, um botão de 27px é um erro de mira. `pointer: coarse`
   separa os dois casos sem inventar breakpoint — e é fotografado no harness
   (--blink-settings=primaryPointerType=2 nas capturas de 390px). */
@media (pointer: coarse) {
  .btn-sm { min-height: 44px; }
  button.small, .btn.small, button[data-copy] { min-height: 44px; }
}

/* Resultado transiente de "Verificar no DNS" — uma LINHA de chips, não um bloco
   despejado no topo. Só as falhas ganham detalhe abaixo. */
.dom-verify-line { display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin: 12px 0 0; padding: 10px 12px; border-radius: var(--radius);
  background: var(--ok-bg); border: 1px solid var(--ok-line); }
.dom-verify-line[data-health="bad"] { background: var(--bad-bg); border-color: var(--bad-line); }
.dom-verify-tag { font-weight: 650; font-size: var(--fs-xs); color: var(--h-ink); margin-right: 4px; }
.dom-verify-fails { margin: 8px 0 0; padding-left: 20px; color: var(--tint-ink); }
.dom-verify-fails li { margin: 2px 0; }

/* Verificar a partir da linha divergente — recuado, discreto. */
.dom-rec-verify { margin: 10px 0 0 26px; }

/* Badge de SSL do tracking — linha de cadeado, tom OK. */
.dom-ssl-ok { margin-top: 8px; background: var(--ok-bg); border-color: var(--ok-line); color: var(--tint-ink); }
.dom-ssl-ok code { background: transparent; }

/* Zona de perigo: superfície neutra + filete de acento à esquerda (não bloco
   vermelho cheio). O vermelho concentra-se no botão, que agora é btn-sm. */
.c-dangerzone { border-left: 3px solid var(--bad); }
.c-dangerzone-desc { margin: 0; }

/* Modal da mensagem recebida (inbound Fase 2) — leitura + anexos. Reusa o shell
   .eml-modal (dialog/head/close); o corpo é próprio (estruturado, não monoespaço
   puro). Linha clicável na lista de recebidas. */
.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) — a bloqueada é distinguível À PRIMEIRA OLHADA.
   AVISO, nunca erro: filete e badge em --warn, jamais em --bad. Não foi falha
   nossa nem do remetente; foi uma escolha da conta, e a tela não acusa ninguém
   de um botão funcionando. O fundo NÃO é preenchido — cor é acento pontual;
   um bloco tingido inteiro roubaria a hierarquia da linha que o cliente
   realmente quer ler. */
.in-row-blocked > td:first-child { box-shadow: inset 3px 0 0 var(--warn); }
.in-outcome { margin-right: 6px; vertical-align: baseline; }

/* O aviso dentro do modal. Vem ANTES dos metadados: ele muda como se lê tudo o
   que está abaixo. Aqui o fundo tingido SE JUSTIFICA — é um bloco de aviso
   dedicado, não decoração de linha de tabela. */
.in-notice {
  margin: 0 0 14px; padding: 12px 14px; border-radius: var(--radius);
  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(--surface) 45%, transparent);
  /* ≥44px de alvo no toque sem inflar a caixa visual (norma §8). */
  min-height: 32px; display: inline-flex; align-items: center;
}
.in-notice-cta:hover { background: var(--surface); }
.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); 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); 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); }
.in-att-dl { margin-left: auto; font-weight: 600; color: var(--accent); 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 + seção de recepção
   ═══════════════════════════════════════════════════════════════════════════
   A página do domínio passou a carregar ENVIO e RECEPÇÃO (a antiga /inbound/:id
   virou #recepcao aqui). Duas seções longas exigem porta, e a porta carrega o
   ESTADO: sem isso o operador rolaria a página inteira só para descobrir se a
   recepção está ligada. Âncora, não aba — as duas seções continuam impressas,
   buscáveis (Ctrl+F) e linkáveis de fora.
   ═══════════════════════════════════════════════════════════════════════════ */
.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(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink);
  text-decoration: none; font-size: var(--fs-sm);
}
.dom-sectnav a:hover { background: var(--surface-2); border-color: var(--line-2); 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. scroll-margin resolve 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); }
.dom-sect-foot { margin: 18px 0 0; padding-top: 12px; border-top: 1px solid var(--border); }

/* ── Prontidão da recepção: os 3 passos, e onde parou ───────────────────────
   «2 de 3 prontos» NÃO é erro. Por isso o passo pendente é NEUTRO (círculo
   vazado, cinza), nunca vermelho: vermelho aqui ensinaria que estar no meio do
   caminho é falha — exatamente o que a norma de UX proíbe. */
.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); }

/* A frase de CONSEQUÊNCIA do registro ("sem ele, não chegam") — alinhada com o
   .chk-detail, mas com peso de texto normal: é leitura, não rótulo. */
.chk-consequence { margin: 10px 0 0 26px; font-size: var(--fs-sm); line-height: 1.5; color: var(--tint-ink); }
.dom-rec-eta { margin: 6px 0 0 26px; }

/* 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 e
     as duas colunas ficam 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; }
  .chk-consequence, .dom-rec-eta, .dom-rec-verify { margin-left: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   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); }

/* ═══ REFORMA DA INTERAÇÃO DA RECEPÇÃO (08/2026) ═══════════════════════════
   Três peças, zero vocabulário visual novo além delas:
   · .inb-note — o flash extraído da resposta do PRG vira nota inline, colada
     no formulário que agiu (o banner de topo pós-reload morreu);
   · .cxm — o modal «Cadastrar caixa de destino», sobre o shell .eml-modal que
     já existe nas duas folhas (as pendentes internas reusam .c-banner);
   · .cj-* — o conjunto «Esta caixa vai para:» (caixas de marcar compactas,
     rotuladas com o nome de cada canal; 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; }

/* ═══════════════════════════════════════════════════════════════════════════
   DZ — REDESIGN replicado do portal (handoff «Dominios v2», 12/08/2026).
   Mesmos nomes de componente das duas folhas (vocabulário único da rodada 2);
   diferenças ficam nos TOKENS (o --radius daqui já é o 8px denso do painel).
   Zero hex novo; «ok» discreto; cor só onde há estado.
   ═══════════════════════════════════════════════════════════════════════════ */
.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;
}
.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, var(--on-solid));
  font-size: var(--fs-md);
  font-weight: 600;
  padding: 8px 16px;
  border-radius: var(--radius);
  user-select: none;
}
.dz-add > summary::-webkit-details-marker { display: none; }
.dz-add > summary:hover { filter: brightness(1.08); }
.dz-add-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  width: min(560px, calc(100vw - 32px));
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* `var(--shadow)` era do PORTAL e não existe nesta folha: a declaração era
     descartada e o painel flutuava sem sombra sobre a tabela. A rampa daqui tem
     o degrau com o nome exato do caso — «ancorado: popover preso a um botão».
     Mesmo modo de falha do badge-engaged consertado em 22/08: regra copiada
     entre folhas cujo token não atravessa junto. */
  box-shadow: var(--sh-pop);
  padding: 16px;
  z-index: 30;
}
.dz-tbl thead th {
  font-size: var(--fs-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted-2, var(--muted));
  font-weight: 700;
}
.dz-tbl tbody tr[data-dz-row] { cursor: pointer; }
.dz-tbl tbody tr[data-dz-row]:hover { background: var(--panel-2); }
.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));
}
/* Título de detalhe em mono (o domínio É o título — mesma cara da lista). */
.dz-domain { font-family: var(--mono); letter-spacing: -.01em; }

/* ── Switch em cima de CHECKBOX (grupo «Esta caixa vai para:» do partial
   compartilhado inbound-avisos, 14/08). O componente .switch acima é botão-
   submit; este é o mesmo desenho respondendo a :checked — o admin não tem o
   autosave do portal, então o Salvar do grupo continua visível aqui. */
.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(--surface-3); }
.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%;
  /* mesma peça física do .switch-knob acima (branco nos dois temas) — a cor
     mora LÁ, com o motivo; aqui só a elevação canônica do tema. */
  background: var(--sw-knob); box-shadow: var(--shadow-sm, none); 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(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .sw-track, .sw-knob { transition: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   A BIBLIOTECA DE CANAIS NO PAINEL — o outro lado de partials/inbound-canais
   ═══════════════════════════════════════════════════════════════════════════
   `partials/inbound-canais.ejs` renderiza UMA vez para as duas superfícies:
   `portal-domain-detail.ejs` o inclui direto, e `domain-detail.ejs` o alcança
   em dois saltos (por `partials/domain-inbound.ejs`). Seis nomes de classe do
   partial só tinham regra em portal.css. Este bloco é o par deles aqui — e
   NÃO é uma cópia: onde o painel já tem vocabulário próprio (a tabela, o anel
   de foco), ele manda.
   ═══════════════════════════════════════════════════════════════════════════ */

/* `.tbl` — a tabela do partial, no vocabulário do painel.
   ATENÇÃO a quem for «completar» esta regra com o resto do bloco de portal.css:
   NÃO traga `.tbl th, .tbl td { padding: 10px 12px; border-top: … }`. O painel
   estiliza `table`, `th` e `td` como ELEMENTO (linha ~810), com border-BOTTOM;
   somar o border-top do portal daria DUAS linhas entre cada par de células —
   um defeito novo entrando por baixo de um conserto. As três declarações
   abaixo repetem de propósito o que o seletor de elemento já dá: elas são o
   contrato de `.tbl` (é uma tabela de largura cheia, colapsada, no corpo
   denso), e sem elas a classe seria um nome sem regra outra vez. */
.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
/* O ÚNICO comportamento que o painel não tinha: a linha do painel de
   configuração de cada canal. O <summary> dela é sr-only (a linha da tabela JÁ
   é o resumo), então com o <details> FECHADO sobrava uma faixa vazia — padding
   de célula mais a borda de baixo — entre um canal e o seguinte. Uma listra
   fantasma por canal, que ninguém reporta porque não parece erro. */
.tbl tr.sub td:has(> details.dgrp:not([open])) { padding: 0; border-bottom: 0; }

/* Linha de campos/ações lado a lado (a célula «Ações» de cada canal). */
.field-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Botão só-ícone (a engrenagem «Configurar»): o title/aria carregam o verbo.
   Vem DEPOIS de `.btn` (linha ~883) de propósito — as duas são (0,1,0), então
   quem decide o padding é a ordem, e o modificador tem de ser o último.

   O QUE NÃO ENTRA AQUI, e por quê: portal.css acompanha esta classe de
   `.btn .ic, .btn-ico .ic { width: 15px; height: 15px }` — a regra que
   consertou a «engrenagem invisível» de 14/08. Ela NÃO vem para cá. O painel
   já resolve o mesmo problema por outro caminho: `.ic { width: 1em }` (linha
   ~2955) é o tamanho-base de TODO ícone desta folha, e `button .ic,
   .btn-primary .ic, .secondary .ic` sobe para 1.05em. Trazer os 15px fixos
   daria (0,1,0) empatando com o `.ic` solto e vencendo por ordem — todo ícone
   de todo botão do painel, inclusive os `.btn-sm`, trocaria uma medida
   RELATIVA ao texto por uma fixa. Seria uma mudança de tamanho em toda a
   superfície entrando de carona num conserto de outra coisa. */
.btn-ico { padding: 6px 8px; display: inline-flex; align-items: center; }

/* Botão com peso de LINK: o «Conectar outro chat do Telegram» precisa de
   <form> (é POST), mas é caminho secundário — sem esta regra ele sai com a
   moldura de <button> nativo do sistema operacional. */
.btn-aslink {
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
  color: var(--brand-2); text-decoration: underline;
  /* Estas duas NÃO estão na versão do portal, e precisam estar aqui: o painel
     tem uma regra de ELEMENTO — `button, .btn` (linha ~877) — que o portal não
     tem. Ela dá a todo <button> o raio e os 44px de alvo de toque do botão
     primário. Sem desfazê-los, o «Conectar outro chat» sairia com 44px de
     altura e um retângulo arredondado invisível em volta do texto: um link que
     ocupa espaço de botão. Copiar a regra do portal e parar aí teria deixado
     exatamente isso, e ninguém veria — a cor e o sublinhado já estariam certos. */
  border-radius: 0; min-height: 0;
}
.btn-aslink:hover { color: var(--text); }

/* O ✕ que fecha o painel de configuração do canal. Com o summary sr-only, o
   painel aberto não tem outra saída visível. */
.dgrp-close { float: right; margin: 0 0 8px 12px; }

/* Switch como BOTÃO-submit (liga/desliga o canal inteiro): mesmo trilho e
   mesmo knob do switch-checkbox acima, mas o estado vem do `aria-checked` em
   vez do `:checked`. O foco usa --accent, e não o --brand-2 do portal: é o
   anel do resto desta folha (ver `.sw-input:focus-visible` acima) — divergir
   aqui faria o painel ter dois anéis de foco no mesmo componente. */
.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(--accent); outline-offset: 2px; }

/* ── /system: host map da frota + panorama 24h (19/08/2026) ──────────────────
   Estilo host-map: um tile por máquina/grupo, estado no dot, números em
   tabular-nums e o pulso de 24h em barras empilhadas (ok/warn/bad). Tudo
   sobre os tokens de tema existentes — nada de cor fora da paleta. */
.stat-row{display:flex;flex-wrap:wrap;gap:18px;align-items:flex-end;margin:2px 0 16px}
.stat{display:flex;flex-direction:column;min-width:84px}
.stat-n{font-size:var(--fs-4xl);font-weight:600;font-variant-numeric:tabular-nums;line-height:1.15}
.stat-n.ok{color:var(--ok)}
.stat-n.warn{color:var(--warn)}
.stat-n.bad{color:var(--bad)}
.stat-l{font-size:var(--fs-2xs);color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:1px}
.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)}
.frota-tab td{vertical-align:top}
.frota-grupo td{background:var(--surface-3);font-size:var(--fs-lg);padding-top:10px;padding-bottom:10px}
.frota-grupo .dot{display:inline-block;vertical-align:baseline;margin-right:2px}
.dot{width:10px;height:10px;border-radius:50%;flex:none}
.dot-ok{background:var(--ok)}
.dot-warn{background:var(--warn)}
.dot-bad{background:var(--bad);box-shadow:0 0 0 3px var(--bad-bg)}
.chip{font-size:var(--fs-3xs);padding:1px 8px;border-radius:999px;border:1px solid var(--border);color:var(--muted);white-space:nowrap;line-height:1.6}
.chip-warn{border-color:var(--warn-line);color:var(--warn);background:var(--warn-bg)}
.chip-emx{border-color:var(--accent);color:var(--accent)}
/* As marcas de envio na frota do /system. `chip-ovy` era referenciada pela view
   e NÃO tinha regra (herdava o chip neutro por acidente); `chip-mxm` é a faixa
   europeia maxmail.net, que chegou em 28/08 e vinha sendo pintada de «oveyon». */
.chip-ovy{border-color:var(--border-strong);color:var(--muted)}
.chip-mxm{border-color:var(--ok);color:var(--ok)}

/* ── /borda: bloqueios na borda (nftables), 01/09/2026 ──────────────────────
   A tela escreve na tabela edgeBlocks; o timer do SRE aplica. Tudo sobre os
   componentes que já existem (.panel, .kv, .form-row, .badge, .flash.inb-note,
   button.small.danger). O que há de próprio é geometria: os fatos da sync em
   linha, a largura do campo de CIDR/motivo e o form de remover em linha na
   célula (era `style="display:inline"` nas telas irmãs — aqui é classe). */
.borda-kv { display: flex; flex-wrap: wrap; gap: 0 40px; margin: 0 0 12px; }
.borda-kv dt { margin-top: 0; }
.borda-cidr { min-width: 240px; }
.form-row > .borda-motivo { flex: 1 1 320px; }
.borda-motivo input { width: 100%; }
.borda-inline { display: inline; }
.borda-hist > summary { cursor: pointer; font-weight: 600; }
.borda-hist > summary .badge { vertical-align: middle; }
