    /* ═══════════════════════════════════════════════════════════════════════════
       FONTES — auto-hospedadas em /fontes, e por que NÃO por CDN
       ═══════════════════════════════════════════════════════════════════════════

       A CSP que este painel devolve traz `font-src 'self'`: um `@import` de
       `fonts.googleapis.com` seria BLOQUEADO pela própria política e cairia no
       fallback em silêncio — o painel pareceria normal e ninguém saberia que a
       tipografia nunca carregou. Fora isso, o painel tem de ficar legível quando a
       rede está ruim, e é ele que se abre quando o resto está quebrado: texto que
       depende de terceiro para renderizar é dependência no pior momento possível.
       São 68 KB no total, com licença OFL 1.1 ao lado dos arquivos.

       `font-display: swap`, nunca `block`: o texto aparece na hora com a fonte do
       sistema e é trocado quando a fonte chega. `block` esconderia o painel por até
       3 s, e o que se procura aqui é justamente a primeira linha.

       A cadeia de fallback é OBRIGATÓRIA em toda declaração: o arquivo pode falhar
       (e há um `app.get('*')` no servidor que devolve HTML com 200 para caminho
       inexistente, então caminho errado não dá 404 — dá silêncio). Se a fonte não
       vier, o painel continua legível na fonte do sistema.

       Não declaro `unicode-range` de propósito: sem ele o fallback é por GLIFO, que
       é o comportamento seguro. Uma faixa declarada errada manda para a fonte do
       sistema todo caractere de fora dela AINDA QUE o glifo exista no arquivo — e o
       sintoma seria acento com desenho diferente do resto da palavra. */
    @font-face {
      font-family: 'Inter';
      src: url('/fontes/inter-var-latin.woff2') format('woff2');
      font-weight: 100 900;   /* arquivo VARIÁVEL: um só para todos os pesos do CSS */
      font-style: normal;
      font-display: swap;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      src: url('/fontes/plexmono-400-latin.woff2') format('woff2');
      font-weight: 400; font-style: normal; font-display: swap;
    }
    @font-face {
      font-family: 'IBM Plex Mono';
      src: url('/fontes/plexmono-600-latin.woff2') format('woff2');
      font-weight: 600; font-style: normal; font-display: swap;
    }

    /* ═══════════════════════════════════════════════════════════════════════════
       CONTRATO DE COR — três papéis, e nenhum invade o outro
       ═══════════════════════════════════════════════════════════════════════════

       O defeito de fundo da paleta antiga não era contraste: era SEMÂNTICA. Ela
       usava 6 matizes como enfeite de tile (azul, verde, vermelho, amarelo,
       laranja, roxo) e ao mesmo tempo pedia que verde e vermelho carregassem o
       estado do sistema. Roxo em "pelo snippet servido" e laranja em "erros"
       competiam com o único significado que importa nesta tela.

       Daqui em diante:

       1. TINTA (neutros) — todo DADO e toda estrutura. Número é tinta, não matiz:
          `--text` no valor, `--muted` no rótulo e na legenda. Um número não tem
          opinião; quem opina é o estado, e o estado vem do servidor.
       2. ESTADO (4 cores RESERVADAS) — `--estado-ok`, `--estado-ruim`,
          `--estado-atencao`, `--estado-naosei`. Só para estado, NUNCA como
          decoração e nunca como "série 4" de um gráfico. E sempre acompanhadas de
          ÍCONE + RÓTULO EM TEXTO: cor sozinha não é informação.
       3. AÇÃO (um acento só) — o azul. Aba ativa, link, botão primário e anel de
          foco. Azul passa a significar "você pode agir aqui", e mais nada.

       ── O que foi MEDIDO, e por isso mudou ──────────────────────────────────
       • O par antigo `--green #48bb78` ↔ `--red #fc8181` dá ΔE **4,5** em
         deuteranopia. São as duas cores que carregavam o significado inteiro do
         painel — para ~8% dos homens elas são quase a MESMA cor. Trocado.
       • `--muted #718096` sobre `--surface2 #232738` dava **3,69:1**, abaixo do
         mínimo de 4,5:1 para texto pequeno — e era a cor de TODO
         `.stat-card .label` e de todo `.sub`. Agora é `#9aa7b8` = **6,06:1**.
       • Os quatro tokens de estado passam em todos os pares entre si; o pior caso
         é ΔE **11,1** em protanopia. Contraste sobre o cartão (`--surface2`):
         ok ~11:1 · atenção ~8,5:1 · naosei 5,77:1 · ruim 5,35:1.
       • Os números acima são sobre `--surface2`. Nas `badge-*` a mesma cor fica
         sobre um véu de 15% dela mesma; não medi esse par, e por isso as badges
         usam o token só para deixar de depender de verde↔vermelho — sem eu
         afirmar número para elas.

       ── E a faixa de luminosidade do validador NÃO se aplica aqui ────────────
       O validador de paleta exige que as cores de uma série CATEGÓRICA fiquem
       numa faixa estreita de luminosidade — para que nenhuma categoria pareça
       "mais importante" que a outra. Isso é regra de categoria, não de estado.
       Em cor de ESTADO a diferença de luminosidade é exatamente o que se QUER:
       ela é o segundo canal (junto do ícone) para quem não distingue o matiz, e
       "ruim" deve pesar mais que "ok" na hierarquia visual. Não tente "corrigir"
       os quatro tokens para dentro da faixa.

       ── As `badge-*` de DADO mantêm matiz, e isso é decisão ──────────────────
       `badge-sent`, `badge-failed`, `badge-pending` e `badge-duplicate` rotulam a
       CATEGORIA de uma linha da tabela, sempre com o texto ao lado, e não o estado
       do sistema. Ali a cor é identidade de categoria: manter o matiz ajuda a
       varrer a coluna. O que elas não podem é herdar o par verde↔vermelho antigo,
       então apontam para os tokens de estado.
       ═══════════════════════════════════════════════════════════════════════════ */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    :root {
      --bg: #0f1117; --surface: #1a1d2e; --surface2: #232738;
      --border: #2d3748; --text: #e2e8f0;

      /* TINTA: rótulo, legenda, texto secundário. 6,06:1 sobre --surface2. */
      --muted: #9aa7b8;

      /* ESTADO — reservadas. Ver o bloco medido acima antes de tocar. */
      --estado-ok: #6ee7b7;
      --estado-ruim: #f87171;
      --estado-atencao: #fbbf24;
      --estado-naosei: #94a3b8;

      /* AÇÃO: um acento só. `--blue` continua com o nome antigo porque muitos
         estilos e templates o citam; `--acento` é o nome do PAPEL. */
      --blue: #4299e1; --acento: #4299e1;

      /* Anel de foco: branco. Escolhido por ser o máximo de contraste disponível
         sobre TODAS as superfícies deste tema — nenhuma delas é clara —, o que
         dispensa medir par por par e não pode ser reprovado por um fundo novo. */
      --foco: #ffffff;

      /* Matizes LEGADAS. Continuam definidas porque estilos de fundo, borda e
         badge as citam, e porque `.val-*` são gravadas por JS que testes conferem
         por nome. NÃO use nenhuma delas para afirmar estado: para isso existem os
         quatro tokens acima. */
      --green: #48bb78; --red: #fc8181; --yellow: #f6e05e;
      --orange: #ed8936; --purple: #9f7aea;

      /* Interface e monoespaçada. A cadeia depois do primeiro nome NÃO é enfeite:
         é o que sustenta o painel se o .woff2 não chegar. O genérico `monospace`
         puro cai numa fonte diferente em cada sistema, e o que mais se compara a
         olho aqui é hash, fbtrace_id e pixel_key — comparação de caractere não pode
         depender do SO de quem olha. */
      --ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
    }
    body {
      font-family: var(--ui);
      background: var(--bg); color: var(--text); min-height: 100vh;
      line-height: 1.5;
    }
    code, pre, .code-block, .err-msg, .secret-masked, textarea {
      font-family: var(--mono);
      /* Ligadura desligada no mono: ligadura que junta `==` ou `--` dentro de um
         hash é o tipo de defeito que só se descobre quando alguém compara dois IDs
         e jura que são iguais. */
      font-variant-ligatures: none;
    }

    /* Dígito de largura fixa em todo slot numérico. Este painel se redesenha a
       cada 30 s: com dígito proporcional a largura do número muda a cada refresh,
       o texto "pula" e o olho é obrigado a reescanear a linha. É legibilidade, não
       firula — e some justamente quando o painel está sendo usado para vigiar.
       `zero` é o zero cortado, que separa `0` de `O` em ID e hash; se o arquivo não
       tiver a feature, ela é ignorada e nada quebra. */
    .stat-card .value, .descarte-item .dv, td, th, .sys-item, .pagination,
    .conn-texto, .num, code, pre {
      font-variant-numeric: tabular-nums;
      font-feature-settings: 'tnum' 1, 'zero' 1;
    }

    /* Só para leitor de tela: dá nome a uma região sem acrescentar ruído visual.
       Usado nos <h2> de cada aba, que existem para a navegação por cabeçalho
       funcionar sem que a tela ganhe um título repetido em cima do outro. */
    .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;
    }

    /* ── Foco visível ────────────────────────────────────────────────────────
       Antes desta regra o arquivo tinha ZERO `:focus-visible` e um `outline: none`
       em `select`, `input` e no textarea do modal de código — ou seja, os campos
       onde se digita SEGREDO DE WEBHOOK e TOKEN DA META não davam nenhum sinal de
       onde estava o foco. Quem usa teclado ficava digitando às cegas na única tela
       do sistema que aceita segredo.
       Se você precisar tirar este anel de algum lugar, ponha OUTRO indicador no
       lugar — nunca `outline: none` sozinho. */
    a:focus-visible, button:focus-visible, summary:focus-visible,
    select:focus-visible, input:focus-visible, textarea:focus-visible,
    [tabindex]:focus-visible, .tab:focus-visible, label:focus-visible {
      outline: 3px solid var(--foco);
      outline-offset: 2px;
    }

    /* ── Layout ─────────────────────────────────────────────────────────────── */
    .app { display: flex; flex-direction: column; min-height: 100vh; }
    header {
      background: var(--surface); border-bottom: 1px solid var(--border);
      padding: 0 24px; display: flex; align-items: center; justify-content: space-between;
      height: 56px; position: sticky; top: 0; z-index: 100;
    }
    .brand { font-size: 18px; font-weight: 700; color: var(--acento); }
    .user-info { font-size: 13px; color: var(--muted); display: flex; align-items: center; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }
    .btn-logout { background: none; border: 1px solid var(--border); color: var(--muted); padding: 5px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all .2s; }
    .btn-logout:hover { border-color: var(--estado-ruim); color: var(--estado-ruim); }

    /* ── Indicador de frescor ("atualizado há Xs") ───────────────────────────
       O painel se recarrega a cada 30 s e nunca dizia isso. Quem olhava não tinha
       como distinguir "dado de agora" de "aba esquecida aberta há duas horas" — e
       aba esquecida é o modo de falha mais provável de um painel de vigia.
       É UM indicador, no cabeçalho, e não um por seção. Não pisca e não pulsa:
       a lição registrada neste projeto é que aviso permanente vira papel de
       parede, e um ponto piscando em vermelho é a versão mais rápida disso. */
    .conn { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
    .conn-ponto {
      width: 9px; height: 9px; border-radius: 50%; flex: none;
      background: var(--estado-naosei); transition: background-color .2s;
    }
    .conn-ok    .conn-ponto { background: var(--estado-ok); }
    .conn-velho .conn-ponto { background: var(--estado-atencao); }
    .conn-falha .conn-ponto { background: var(--estado-ruim); }
    .conn-falha .conn-texto { color: var(--estado-ruim); }

    nav {
      background: var(--surface); border-bottom: 1px solid var(--border);
      padding: 0 24px; display: flex; gap: 4px; flex-wrap: wrap;
    }
    /* `flex-wrap: wrap` e não rolagem horizontal: são 7 abas, e no celular a nav
       sem wrap estourava a largura da página — o dono confere isto do celular. A
       quebra de linha foi escolhida em vez da rolagem porque rolagem esconde aba,
       e aba escondida numa tela de diagnóstico é a informação que não vai ser
       procurada. Com 7 rótulos curtos ela custa no máximo duas linhas. */
    .tab {
      padding: 12px 16px; font-size: 14px; font-weight: 500; cursor: pointer;
      border-bottom: 2px solid transparent; color: var(--muted); transition: all .2s;
      background: none; border-top: none; border-left: none; border-right: none;
      min-height: 44px;   /* alvo de toque mínimo */
    }
    .tab:hover { color: var(--text); }
    .tab.active { color: var(--acento); border-bottom-color: var(--acento); }
    .tab-badge { background: var(--estado-ruim); color: #1a0505; font-size: 12px; font-weight: 700; padding: 1px 6px; border-radius: 10px; margin-left: 5px; }

    main { padding: 24px; flex: 1; max-width: 1400px; width: 100%; margin: 0 auto; }
    .tab-content { display: none; }
    .tab-content.active { display: block; }

    /* ── Cards ──────────────────────────────────────────────────────────────── */
    .card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 20px; }
    .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 20px; }
    .stat-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px; }
    .stat-card .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; }
    /* Valor SEM cor: número é tinta. A cor do tile agora sai do estado que o
       servidor calculou, e aparece no selo abaixo — nunca no número sozinho. */
    .stat-card .value { font-size: 28px; font-weight: 700; }
    .stat-card .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }

    /* `.val-*` são LEGADAS e continuam existindo porque `renderWpIntegridade()` as
       grava por JS e os testes conferem o nome exato da classe. O que mudou é para
       onde elas apontam: as que de fato carregam estado passam a usar os tokens
       reservados, e as que eram só enfeite viram tinta. Não use nenhuma delas em
       markup novo — use `pintarEstadoDoTile()`. */
    .val-blue   { color: var(--acento); }
    .val-green  { color: var(--estado-ok); }
    .val-red    { color: var(--estado-ruim); }
    .val-yellow { color: var(--estado-atencao); }
    /* `val-orange` só é usada onde o significado é "não medimos" (o `?` do retrato
       de integridade sem frescor, o tile de erros indisponível). Laranja dizia
       "atenção" para uma ausência de medição — é a confusão que este ciclo
       desfez. */
    .val-orange { color: var(--estado-naosei); }
    .val-purple { color: var(--text); }   /* era decoração de tile: virou tinta */

    /* ── Selo de estado ──────────────────────────────────────────────────────
       Estado NUNCA por cor sozinha: marcador + rótulo em texto, sempre. O
       marcador é glifo de texto e não emoji — emoji muda de desenho por sistema,
       é lido em voz alta como "marca de verificação branca grossa" e não tem
       versão monocromática garantida. Cada estado tem uma FORMA diferente, que é
       o canal que sobra para quem não distingue o matiz. */
    .estado-selo {
      display: inline-flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
      font-size: 12px; margin-top: 6px;
    }
    .estado-marca { font-weight: 700; font-size: 13px; line-height: 1; }
    .estado-rotulo { font-weight: 700; letter-spacing: .04em; }
    .estado-porque { color: var(--muted); font-weight: 400; }
    .selo-ok      { color: var(--estado-ok); }
    .selo-atencao { color: var(--estado-atencao); }
    .selo-ruim    { color: var(--estado-ruim); }
    /* `indeterminado` em cinza e itálico, com o precedente já estabelecido pela
       faixa de rastreamento: "não sei" não pode parecer bom nem parecer alarme,
       senão volta a ser lido como um dos dois. */
    .selo-naosei  { color: var(--estado-naosei); font-style: italic; }
    /* `nao_exigido` NÃO é itálico: é decisão tomada, não lacuna de medição. */
    .selo-naoexig { color: var(--muted); }

    /* ── Buttons ─────────────────────────────────────────────────────────────── */
    .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 7px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; transition: all .2s; }
    .btn-primary { background: var(--acento); color: #fff; }
    .btn-primary:hover { background: #3182ce; }
    /* O nome da classe fica (é citada no botão de resolver erro), mas a cor passa a
       ser o acento: verde é RESERVADO para estado, e um botão verde ao lado de um
       selo verde faz a pessoa ler "resolvido" onde está escrito "resolver". */
    .btn-success { background: var(--acento); color: #fff; }
    .btn-danger  { background: #742a2a; color: var(--estado-ruim); border: 1px solid #9b2c2c; }
    .btn-danger:hover { background: #9b2c2c; }
    .btn-ghost   { background: transparent; color: var(--muted); border: 1px solid var(--border); }
    .btn-ghost:hover { color: var(--text); border-color: var(--text); }
    .btn-sm      { padding: 5px 10px; font-size: 12px; }
    .btn:disabled { opacity: .5; cursor: not-allowed; }

    /* ── Table ───────────────────────────────────────────────────────────────── */
    .table-wrap { overflow-x: auto; }
    table { width: 100%; border-collapse: collapse; font-size: 13px; }
    /* 12px é o PISO de tamanho do painel, e o `th` era 11px em caixa alta com
       letter-spacing — o pior caso de leitura que existe aqui. Tabela mantém
       line-height compacto: o corpo do painel é 1.5, a tabela não. */
    th { text-align: left; padding: 10px 12px; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap; }
    td { padding: 10px 12px; border-bottom: 1px solid rgba(45,55,72,.5); vertical-align: middle; line-height: 1.35; }
    tr:last-child td { border-bottom: none; }
    tr:hover td { background: rgba(45,55,72,.3); }

    /* ── Status badges ───────────────────────────────────────────────────────── */
    .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
    .badge-sent      { background: rgba(72,187,120,.15); color: var(--estado-ok); }
    .badge-failed    { background: rgba(252,129,129,.15); color: var(--estado-ruim); }
    .badge-pending   { background: rgba(246,224,94,.15);  color: var(--estado-atencao); }
    .badge-duplicate { background: rgba(113,128,150,.15); color: var(--muted); }
    .badge-active    { background: rgba(72,187,120,.15); color: var(--estado-ok); }
    .badge-inactive  { background: rgba(113,128,150,.15); color: var(--muted); }
    .badge-admin     { background: rgba(159,122,234,.15); color: var(--purple); }
    .badge-viewer    { background: rgba(66,153,225,.15);  color: var(--acento); }
    /* Severidade de erros — fatal é mais grave que error, error mais que warn */
    .badge-fatal     { background: var(--estado-ruim); color: #1a0505; font-weight: 700; }
    .badge-error     { background: rgba(252,129,129,.18); color: var(--estado-ruim); }
    .badge-warn      { background: rgba(246,224,94,.15);  color: var(--estado-atencao); }
    .badge-resolved  { background: rgba(72,187,120,.15); color: var(--estado-ok); }

    /* [Task 3.3] Pílula da "nota de correspondência (nossa)" — NÃO é o EMQ da
       Meta, ver a nota de rodapé da tabela de Eventos. Cores emprestadas dos
       badges de status (verde/amarelo/vermelho/cinza), classes próprias para
       não prender o significado de "qualidade de sinal" ao de "status de
       envio". `sem_nota` usa a cor neutra: nunca pode ler como "baixa". */
    .badge-qual-alta  { background: rgba(72,187,120,.15); color: var(--estado-ok); }
    .badge-qual-media { background: rgba(246,224,94,.15); color: var(--estado-atencao); }
    .badge-qual-baixa { background: rgba(252,129,129,.15); color: var(--estado-ruim); }
    .badge-qual-sem   { background: rgba(113,128,150,.15); color: var(--muted); }

    /* ── Erros ───────────────────────────────────────────────────────────────── */
    tr.err-row-fatal td { box-shadow: inset 3px 0 0 var(--estado-ruim); }
    .err-msg        { max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
    .err-detail td  { background: #0a0d14; padding: 14px 16px; }
    .err-detail-label { font-size: 12px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); margin-bottom: 4px; }
    .err-detail pre { font-family: var(--mono); font-size: 12px; white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow: auto; color: var(--text); }
    .err-resolved td { opacity: .55; }
    .descarte-grid  { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
    .descarte-item  { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
    /* Descarte proposital é CONTAGEM, não estado: 200 para fora, contado por
       dentro. Era laranja, o que fazia um comportamento intencional parecer
       problema; agora é tinta, e quem interpreta é a dica de texto abaixo. */
    .descarte-item .dv { font-size: 22px; font-weight: 700; color: var(--text); }
    .descarte-item .dl { font-size: 13px; font-weight: 600; margin-top: 2px; }
    .descarte-item .dh { font-size: 12px; color: var(--muted); margin-top: 6px; line-height: 1.45; }
    .job-late       { color: var(--estado-atencao); font-weight: 600; }
    .job-fail       { color: var(--estado-ruim); font-weight: 600; }

    /* ── [B.6] Corpo cru do webhook ──────────────────────────────────────────
       `pre` com rolagem PRÓPRIA e `max-height`: um payload de Kiwify tem dezenas de
       linhas e, sem o teto, o modal empurraria os botões (inclusive o "Fechar") para
       fora da tela — o operador ficaria com PII aberta e sem saída visível. O
       `overflow-x: auto` é para a linha longa não estourar a largura do modal, e a
       fonte é a `IBM Plex Mono` auto-hospedada (var(--mono)): em corpo cru o
       alinhamento de coluna é o que faz a diferença de formato aparecer. */
    .corpo-cru {
      font-family: var(--mono); font-size: 12px; line-height: 1.5;
      background: #0a0d14; border: 1px solid var(--border); border-radius: 8px;
      padding: 12px; margin: 0; color: var(--text);
      max-height: 320px; overflow: auto; white-space: pre; tab-size: 2;
    }
    .corpo-aviso {
      border: 1px solid var(--estado-ruim); border-radius: 8px; padding: 10px 12px;
      color: var(--estado-ruim); font-size: 12px; line-height: 1.5; margin-bottom: 10px;
    }
    .corpo-extraido { width: 100%; font-size: 12px; }
    .corpo-extraido th { text-align: left; color: var(--muted); font-weight: 600; }
    .corpo-extraido td { padding: 3px 8px 3px 0; vertical-align: top; word-break: break-word; }

    /* ── Filters ─────────────────────────────────────────────────────────────── */
    .filters { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; align-items: center; }
    /* Sem `outline: none`. Era aqui e no `.form-group` abaixo que o indicador de
       foco desaparecia — inclusive no campo de token da Meta e no de segredo de
       webhook. A borda azul do `:focus` FICA, porque ela ajuda quem usa mouse; o
       que não pode é ela ser o único sinal, já que borda de 1 px muda pouco. */
    select, input[type="text"], input[type="date"], input[type="email"], input[type="password"], input[type="number"], input[type="url"] {
      background: var(--surface2); border: 1px solid var(--border); color: var(--text);
      border-radius: 7px; padding: 8px 12px; font-size: 13px;
      font-family: inherit;
    }
    select:focus, input:focus { border-color: var(--acento); }

    /* ── Modal ───────────────────────────────────────────────────────────────── */
    .modal-overlay {
      display: none; position: fixed; inset: 0; background: rgba(0,0,0,.7);
      z-index: 1000; align-items: center; justify-content: center; padding: 20px;
    }
    .modal-overlay.open { display: flex; }
    .modal {
      background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
      padding: 28px; width: 100%; max-width: 560px; max-height: 90vh; overflow-y: auto;
    }
    .modal h2 { font-size: 17px; font-weight: 600; margin-bottom: 20px; }
    .modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; flex-wrap: wrap; }
    .form-group { margin-bottom: 16px; }
    .form-group label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
    .form-group input, .form-group select, .form-group textarea {
      width: 100%; background: var(--surface2); border: 1px solid var(--border);
      color: var(--text); border-radius: 7px; padding: 9px 12px; font-size: 13px;
    }
    .form-group textarea { min-height: 80px; resize: vertical; font-family: var(--mono); font-size: 12px; }
    .form-group input:focus, .form-group select:focus { border-color: var(--acento); }

    /* ── Alert banner ────────────────────────────────────────────────────────── */
    .alert-banner { background: rgba(252,129,129,.1); border: 1px solid rgba(252,129,129,.3); color: var(--estado-ruim); border-radius: 8px; padding: 12px 16px; margin-bottom: 16px; font-size: 13px; display: none; }
    .alert-banner.show { display: block; }
    /* Marcador de severidade dos banners. Substitui o 🚨/⚠️/ℹ️ que estava no
       texto: emoji de interface é lido em voz alta pelo nome do desenho ("luz de
       carro de polícia"), muda de forma por sistema e não tem garantia
       monocromática. O glifo vai `aria-hidden` porque a frase ao lado já diz. */
    .marca { font-weight: 700; margin-right: 4px; }

    /* ── Code block ──────────────────────────────────────────────────────────── */
    .code-block { background: #0a0d14; border: 1px solid var(--border); border-radius: 8px; padding: 12px; font-size: 12px; word-break: break-all; position: relative; }
    .copy-btn { position: absolute; top: 8px; right: 8px; background: var(--surface2); border: 1px solid var(--border); color: var(--muted); border-radius: 5px; padding: 4px 8px; font-size: 12px; cursor: pointer; }
    .copy-btn:hover { color: var(--text); }

    /* ── QR Code ─────────────────────────────────────────────────────────────── */
    .qr-container { text-align: center; padding: 20px; }
    .qr-container img { max-width: 250px; border-radius: 8px; border: 8px solid #fff; }

    /* ── Misc ────────────────────────────────────────────────────────────────── */
    .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
    .section-title  { font-size: 16px; font-weight: 600; }
    .empty-state    { text-align: center; padding: 48px; color: var(--muted); }
    .empty-state p  { font-size: 15px; margin-top: 8px; }
    .flex-gap       { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
    .toggle         { cursor: pointer; font-size: 13px; }
    .system-info    { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
    .sys-item       { background: var(--surface2); border-radius: 8px; padding: 10px 14px; font-size: 12px; }
    .sys-item span  { color: var(--muted); }
    .pagination     { display: flex; gap: 8px; align-items: center; margin-top: 16px; justify-content: flex-end; flex-wrap: wrap; }
    .secret-masked  { letter-spacing: 2px; }
    .success-once   { background: rgba(72,187,120,.1); border: 1px solid rgba(72,187,120,.3); color: var(--estado-ok); border-radius: 8px; padding: 12px 16px; font-size: 13px; margin-bottom: 16px; }
    /* Prosa de explicação ("o que isto NÃO cobre", motivo de cada requisito): é
       texto para ler, não dado para varrer, e por isso mantém o 1.5 do corpo. */
    .prosa          { line-height: 1.5; }
    /* Grade de dois campos que vira um no celular. Estava como `style=` inline no
       formulário de SMTP, e estilo inline não é alcançável por @media — é por isso
       que virou classe. */
    .grid-2         { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

    /* [B.2] [B.4] Integrações de pagamento e prontidão de Purchase */
    /* `.integ-bloco` (a moldura do bloco de integrações DENTRO do cartão) foi removida
       junto com o bloco: o que era uma quinta seção do cartão passou a ser uma seção
       do diálogo de detalhe. */
    .integ-linha    { background: var(--surface2); border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; font-size: 12px; }
    .integ-titulo   { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
    .badge-nunca    { background: rgba(246,224,94,.15);  color: var(--estado-atencao); }
    .badge-erro     { background: var(--estado-ruim); color: #1a0505; font-weight: 700; }
    .badge-ok       { background: rgba(72,187,120,.15); color: var(--estado-ok); }
    .pront-linha    { display: flex; gap: 8px; align-items: baseline; font-size: 12px; padding: 3px 0; }
    .pront-ok       { color: var(--estado-ok); }
    .pront-falta    { color: var(--estado-atencao); }
    .pront-card     { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; margin-bottom: 10px; }
    .afirm-ok       { color: var(--estado-ok); }
    .afirm-nok      { color: var(--estado-ruim); font-weight: 600; }

    /* [D.5] Prontidão do rastreamento. Uma cor por estado, e VERDE SÓ PARA `ok`:
       `indeterminado` é cinza de propósito — "não sei" não pode parecer bom nem
       parecer alarme, senão volta a ser lido como um dos dois.
       Os nomes de classe ficam: `prontidaoRastreamento.test.js` confere `rast-indet`
       e a AUSÊNCIA de `rast-falta` por nome. O que mudou é o token por trás. */
    .rast-ok        { color: var(--estado-ok); }
    .rast-atencao   { color: var(--estado-atencao); }
    .rast-falta     { color: var(--estado-ruim); }
    .rast-indet     { color: var(--estado-naosei); font-style: italic; }
    .rast-naoexig   { color: var(--muted); }
    .rast-mede      { color: var(--muted); font-size: 12px; }
    .rast-nota      { color: var(--muted); font-size: 12px; font-style: italic; }

    /* ── [Task 3.2] Funil de venda ────────────────────────────────────────────
       Três barras (PageView → InitiateCheckout → Purchase), cada uma partida em
       navegador × webhook. É série CATEGÓRICA (canal de coleta), não ESTADO: os
       dois tokens abaixo são as matizes LEGADAS (`--blue`/`--orange`), nunca os
       `--estado-*` reservados — não existe "canal certo" e "canal errado". */
    .funil-resumo         { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px; font-size: 13px; color: var(--muted); }
    .funil-resumo strong  { color: var(--text); }
    .funil-etapa          { margin-bottom: 4px; }
    .funil-etapa-cabecalho{ display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
    .funil-etapa-total    { font-size: 12px; color: var(--muted); }
    .funil-bar-track      { display: flex; height: 28px; background: var(--surface2); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
    .funil-bar-seg        { height: 100%; transition: width .3s; }
    .funil-seg-navegador  { background: var(--blue); }
    .funil-seg-webhook    { background: var(--orange); }
    .funil-legenda        { display: flex; gap: 16px; font-size: 12px; color: var(--muted); margin-top: 6px; }
    .funil-legenda-item   { display: inline-flex; align-items: center; gap: 5px; }
    .funil-dot            { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
    .funil-dot-navegador  { background: var(--blue); }
    .funil-dot-webhook    { background: var(--orange); }
    /* Rótulo sem ambiguidade (regra 1 da Task 3.2): "pessoas distintas" nunca
       fica perto de "evento" sem dizer qual é qual — é a mesma lição do "nota de
       correspondência (nossa)" da Task 3.3. */
    .funil-pessoas        { font-size: 12px; color: var(--muted); margin-top: 4px; }
    .funil-seta           { display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--muted); margin: 10px 0 16px 4px; }
    .funil-seta strong    { color: var(--text); }
    /* Nota de descontinuidade (regra 2): NÃO é `--estado-*` — não é estado
       computado pelo servidor, é um fato histórico fixo (o deploy de
       24/08/2026). Usa só `--muted`/`--border`/`--surface2`, que já existem. */
    .funil-nota           { display: flex; gap: 8px; align-items: flex-start; background: var(--surface2);
                             border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
                             font-size: 12px; color: var(--muted); line-height: 1.5; margin: 8px 0 16px; }
    .funil-nota strong    { color: var(--text); }

    /* ── [Task 3.4] Desempenho por anúncio ────────────────────────────────────
       `.desemp-num` só alinha à direita e herda `tabular-nums` de `.num`. O
       PERCENTUAL de cobertura (`ⓘ 47%...`) fica em tinta neutra, dentro da
       mesma caixa de nota que o Funil de venda já usa (`.funil-nota`,
       reaproveitada) — é dado agregado, não veredito de saúde do sistema.
       Já a linha "(sem origem identificada)" É um estado computado pelo
       servidor (venda sem campanha conhecida), então usa o token reservado —
       mesma linguagem de `tr.err-row-fatal`, mas em --estado-atencao: é
       lacuna de atribuição, não falha do sistema. */
    .desemp-num            { text-align: right; }
    th.desemp-ordenavel    { cursor: pointer; user-select: none; }
    th.desemp-ordenavel:hover { color: var(--text); }
    tr.desemp-sem-origem td { box-shadow: inset 3px 0 0 var(--estado-atencao); font-style: italic; color: var(--muted); }

    /* ── [ORG] O cartão do pixel ──────────────────────────────────────────────
       O cartão acumulava, um embaixo do outro: dados do pixel, a faixa de Purchase
       com 5 requisitos, a faixa de rastreamento com 9, o bloco de integrações, DUAS
       URLs de webhook por plataforma (quatro no total), os botões e três parágrafos
       de aviso. Quem lê esta tela é o dono do negócio, e ele abre a aba para
       responder três perguntas: "este cliente está rastreando?", "falta o quê para o
       Purchase entrar?" e "tem algo esperando minha aprovação?".
       Agora o cartão tem só isso — uma linha por pergunta —, e todo o detalhe vive
       atrás de botão, num diálogo onde há espaço para explicar. Nenhuma cor nova:
       os selos usam os mesmos `--estado-*` do resto do painel, sempre com marcador
       de texto e rótulo em palavra ao lado. */
    /* A ORDEM DE LEITURA é a ordem do DOM — identificação, resumo, ações —, e no
       desktop a grade joga os botões de ação para o alto à direita sem mexer nela.
       Visto no Chromium a 375 px: com os botões ANTES do resumo na marcação, o
       celular mostrava duas fileiras de botões cinza entre o nome do cliente e as
       três linhas que respondem as perguntas — a resposta ficava abaixo do menu de
       ações, que é o inverso do que se procura. Aqui a coluna única do celular herda
       a ordem certa de graça. */
    .px-card        { display: grid; grid-template-columns: 1fr auto; gap: 12px;
                      grid-template-areas: "ident acoes" "resumo resumo" "conta conta"; align-items: start; }
    .px-ident       { grid-area: ident; }
    .px-acoes       { grid-area: acoes; justify-self: end; }
    .px-nome        { font-size: 16px; font-weight: 600; }
    .px-meta        { font-size: 12px; color: var(--muted); margin-top: 4px; }
    /* Grade de uma coluna: as três linhas do resumo, na ordem das três perguntas. */
    .px-resumo      { grid-area: resumo; display: grid; gap: 8px; }
    /* [Task 2.5] Vínculo de conta de anúncio: mesma largura cheia do resumo, linha
       própria — não é uma quarta pergunta do resumo, é outro assunto (Marketing
       API, não CAPI de eventos). */
    .px-contaanuncio { grid-area: conta; border: 1px solid var(--border); border-radius: 8px;
                        padding: 10px 12px; font-size: 13px; }
    .px-contaanuncio .px-meta { margin-top: 2px; }
    .px-linha       { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: 13px; }
    /* O marcador (✓ / ✕ / –) NÃO se separa da frase que ele qualifica: sem isto, a
       375 px o texto longo quebrava para a linha seguinte inteira e o ✕ ficava
       sozinho na linha de cima, parecendo um símbolo solto sem significado.
       `flex: 1 1 0` e não `1 1 auto`: a quebra de linha do flex é decidida pelo
       tamanho-base do item ANTES de encolher, e com base `auto` (= largura do
       conteúdo) a frase longa continuava sendo empurrada para a linha seguinte, com
       `min-width: 0` e tudo. Medido no Chromium a 375 px, nas duas tentativas. */
    .px-linha .px-marca { flex: 0 0 auto; }
    .px-linha .px-texto { flex: 1 1 0; min-width: 0; }
    .px-rotulo      { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
    .px-frase       { color: var(--muted); font-size: 12px; }
    /* A linha de "chamadas em espera" é PERMANENTE, com fila vazia inclusive: o lugar
       de aprovar tem de existir antes de haver o que aprovar, senão quem procura não
       acha e a venda em espera vence junto com a retenção do corpo. */
    .px-espera      { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }
    /* Nenhuma cor nova: o `rgba` é o valor de `--estado-atencao` (#fbbf24) a 8%, que é
       a mesma convenção dos `.badge-*` deste arquivo. O selo e o marcador de texto
       continuam sendo o que AFIRMA o estado — o fundo só destaca a caixa. */
    .px-espera-cheia { border-color: var(--estado-atencao); background: rgba(251,191,36,.08); }
    .px-botoes      { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
    /* Grupo do diálogo de detalhe: cada seção do popup começa com um título curto. */
    .px-secao       { margin-bottom: 14px; }

    /* ═══════════════════════════════════════════════════════════════════════════
       RESPONSIVO — e este trecho é sobre o uso real
       ═══════════════════════════════════════════════════════════════════════════
       Até hoje o arquivo não tinha UMA `@media`. O caso concreto: a nav de 7 abas
       em `display:flex` sem wrap, com `padding: 0 24px`, estourava a largura no
       celular — e é do celular que o dono confere se a coleta está de pé. */
    @media (max-width: 760px) {
      header { padding: 0 12px; height: auto; min-height: 56px; flex-wrap: wrap; gap: 8px; padding-top: 8px; padding-bottom: 8px; }
      nav { padding: 0 8px; }
      main { padding: 14px 12px; }
      .card { padding: 14px; }
      .stat-card { padding: 12px 14px; }
      .stat-card .value { font-size: 24px; }
      .card-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
      .grid-2 { grid-template-columns: 1fr; }
      .modal { padding: 18px; max-height: 94vh; }
      .modal-overlay { padding: 8px; align-items: flex-start; }
      /* O modal precisa começar no topo em tela baixa: centralizado com teclado
         aberto, o botão de salvar fica fora do alcance da rolagem. */
      .filters > * { flex: 1 1 auto; }
      /* O cabeçalho de seção era `space-between` sem wrap: a 375 px o título
         "Pixels configurados" quebrava em duas linhas contra os botões, e os dois
         disputavam a mesma faixa. */
      .section-header { flex-wrap: wrap; gap: 10px; }
      /* Uma coluna: os botões de ação passam para DEPOIS do resumo, que é a ordem
         do DOM — a resposta antes do menu. */
      .px-card { grid-template-columns: 1fr; grid-template-areas: "ident" "resumo" "acoes" "conta"; }
      .px-acoes { justify-self: start; }
      .err-msg { max-width: 100%; white-space: normal; }
      /* `user-scalable=no` não existe aqui e não pode passar a existir: o dono
         amplia a tela para ler o número, e o navegador é dele. */
    }

    /* Alvo de toque de 44 px SÓ onde não há mouse. Aplicar em tudo engordaria o
       painel no desktop, onde a densidade é o que permite ver os 7 pixels juntos. */
    @media (hover: none) and (pointer: coarse) {
      .btn, .btn-sm, .btn-logout, .copy-btn, .tab {
        min-height: 44px; padding-top: 10px; padding-bottom: 10px;
      }
      .copy-btn { min-width: 44px; }
      select, input, textarea { min-height: 44px; font-size: 16px; }
      /* 16px no campo evita o zoom automático do iOS ao focar, que desloca o
         layout inteiro e faz o resto do formulário sair da tela. */
    }

    /* Quem pediu menos movimento não recebe nenhum. As transições de .2s do painel
       são enfeite de hover; o indicador de frescor muda de COR, e cor não precisa
       de animação para ser percebida. */
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        transition-duration: .01ms !important;
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
      }
    }
