/* ══════════════════════════════════════════════════════════════════════════
   OpsHub — design system

   Transposição do bundle `Decisões de design do portal BI`, entregue pelo Claude
   Design: `_ds/modernist-.../styles.css` mais o bloco <style> das telas.

   Duas mudanças conscientes em relação ao protótipo:

   1. **Seletores posicionais viraram classes.** O protótipo usa coisas como
      `aside>div:first-child>div:nth-child(2)` porque não controlava o markup.
      Aqui controlamos, e um seletor por posição quebra em silêncio ao inserir um
      elemento. Os valores computados são os mesmos.
   2. **A fonte não é carregada de fonts.googleapis.com.** O container não deve
      depender de rede externa para renderizar, e o endereço de quem acessa o
      portal não deve vazar para terceiro a cada página. Archivo é usada quando
      está instalada; a pilha de reserva assume quando não. Hospedar os arquivos
      localmente é passo pendente, registrado no README.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Informa ao navegador o esquema em vigor, para que scrollbar, <select> e
     controles nativos acompanhem. Sem isto o tema escuro fica com widgets
     nativos claros no meio da página. */
  color-scheme: light;

  --color-bg: #f3f2f2;
  --color-surface: #eae9e9;
  --color-text: #201e1d;
  /* Azul e laranja do logotipo do OpsHub, que também são as cores dominantes
     de Fortal, Real e Novati. O accent usado em texto é o passo 700, não o
     azul exato da marca (#3078b4): aquele rende 4,15:1 sobre o fundo claro e
     reprova o mínimo de 4,5:1 da WCAG para texto normal — era o mesmo defeito
     do vermelho anterior, que dava 3,76:1 em todo link do portal. */
  --color-accent: #276191;
  --color-accent-2: #cc6819;
  /* Atenção e erro NÃO usam o accent. Enquanto o accent era vermelho os dois
     papéis coincidiam por acidente; com a marca azul, manter a ligação pintaria
     de azul todo aviso e todo estado de erro. Cor de alerta é semântica, não
     identidade — e aqui ela é o laranja do próprio logotipo. */
  --color-atencao: #a35314;

  /* Falha de sistema, separada de restrição de perfil. Issue #523.

     **A cor é o canal de reforço, não o primário — e no tema escuro ela não
     carrega a distinção de forma alguma.** Medido: contra `--color-atencao`
     este carmim separa 1,92:1 no claro e apenas 1,11:1 no escuro. Não é falta
     de tentativa. Buscando as três restrições juntas — ≥4,5:1 contra `bg` e
     contra `surface`, mais ≥2:1 de separação do âmbar em visão normal,
     protanopia e deuteranopia — o tema escuro devolve **zero** candidatas:
     texto legível sobre fundo escuro precisa ser claro, o âmbar já é claro, e
     todo vermelho legível fica colado nele em luminância. Matiz não resolve —
     rosé a 350° dá a mesma separação de 1,1:1 que o tijolo a 5°.

     Quem carrega a distinção é o **glifo** em `.estado-titulo::before` e o
     título por extenso, como já fazem `.estado-processando` e
     `.estado-desativado`. Isto aqui ajuda onde pode e não finge o resto.

     Carmim (344°) e não tijolo (5°) porque ele fica do lado **oposto** do
     laranja em vez do vizinho: 1,92:1 contra 1,62:1 de separação no claro, com
     9,48:1 sobre `bg` e 8,74:1 sobre `surface`.

     O `--crit` do desenho da 1.0 foi medido e recusado: 1,20:1 contra o âmbar
     atual, e o próprio par `--crit`×`--warn` daquele sistema dá 1,15:1 — 1,06:1
     sob deuteranopia. Adotá-lo não corrigiria nada.

     **Como refazer a medição.** Contraste é WCAG 2.x sobre sRGB, e sai dos
     hexadecimais deste bloco e do bloco escuro — recalculável sem ferramenta
     nenhuma. A simulação de protanopia e deuteranopia é
     **Viénot-Brettel-Mollon**, a aproximação linear no espaço LMS: sem nomear
     a transformação, os números simulados seriam os únicos desta entrega que
     ninguém poderia conferir, e foi o revisor de tela quem apanhou a
     assimetria. */
  --color-erro: #7d1128;
  --color-divider: color-mix(in srgb, #201e1d 40%, transparent);

  /* Superfícies escuras nos DOIS temas: barra lateral, faixa de carregamento e
     skip link. Elas não podem sair da rampa neutra, porque a rampa inverte de um
     tema para o outro — e era exatamente isso que, no tema escuro, punha
     `--color-neutral-100` (escuro) sobre `--color-neutral-900` (mais escuro
     ainda) e derrubava o contraste para ~1,3:1. O skip link é o único atalho de
     teclado do portal e a faixa de carregamento é a região `aria-live`: os dois
     ficavam ilegíveis justamente para quem mais depende deles. */
  --color-superficie-escura: #2d2b2b;
  --color-texto-sobre-escura: #f8f4f4;
  --color-texto-sobre-escura-fraco: #bab6b6;

  /* Rampas tonais geradas em OKLCH sobre uma única escala de luminosidade, para
     que o mesmo passo de qualquer papel case em valor visual com os demais. */
  --color-neutral-100: #f8f4f4;
  --color-neutral-200: #eae7e7;
  --color-neutral-300: #d7d3d3;
  --color-neutral-400: #bab6b6;
  --color-neutral-500: #9b9797;
  --color-neutral-600: #7d7979;
  --color-neutral-700: #605d5d;
  --color-neutral-800: #444141;
  --color-neutral-900: #2d2b2b;

  /* ── Escala de série de gráfico — Issue #524 ──────────────────────────────
     **Ordem fixa, nunca ciclada.** Ciclar faria a sexta série repetir a cor da
     primeira e mentir sobre identidade; quem passa do quinto degrau cai em
     `--color-serie-resto`, que declara ser "o resto" em vez de fingir ser uma
     categoria nova.

     **Os quatro primeiros vêm do mockup 1.0** (`docs/design/README.md`), onde
     se chamam `--s1..--s4`. O nome aqui segue a convenção desta folha, que
     prefixa por família (`--color-*`); o de-para está no README do design.

     **A escala substituiu uma rampa de opacidade, e a medição foi o que a
     matou.** Até aqui o renderizador do servidor separava as fatias por
     opacidade sobre `--color-neutral-500`: 0,72 → 0,54 → 0,36, com piso 0,20.
     Medido em 19/09/2026 contra o piso de 3:1 da WCAG 1.4.11 para elemento
     gráfico:

       fatia 1 (0,72)  1,82:1 claro · 2,33:1 escuro   reprova
       fatia 2 (0,54)  1,54:1        · 1,86:1         reprova
       fatia 3 (0,36)  1,33:1        · 1,48:1         reprova
       fatia 4+ (0,20) 1,16:1        · 1,23:1         reprova

     E o segundo modo de falha é pior que o primeiro: **entre fatias adjacentes
     a separação ficava em 1,14–1,26:1**. A legenda da rosca liga cada nome à
     fatia **pela cor** — a docstring de `rosca_com_legenda` diz que item de
     legenda com cor própria apontaria para a fatia errada —, então a legenda
     existia e não ligava.

     **A separação entre séries não se mede por contraste de luminância.** Duas
     cores de luminância parecida podem ser triviais de distinguir por matiz;
     medir isso por contraste subestima e reprovaria paleta boa. A medida é ΔE
     no CIELAB, depois de simular cada tipo de daltonismo por
     Viénot-Brettel-Mollon — a mesma transformação que a #523 nomeou.

     **O quinto degrau é medido, e não herdado.** O mockup declara quatro, e a
     rosca de efetivo por filial desenha até cinco filiais nomeadas
     (`LIMITE_DE_FILIAIS_NOMEADAS`) mais a agrupada. Varrido o espaço HSL com
     duas restrições — piso de 3:1 contra a superfície **deste** portal, que não
     é a do mockup, e luminância dentro da faixa dos outros quatro, para a série
     nova não virar a mais proeminente do gráfico sendo a menor fatia —, o
     melhor par é rosa em H=332: **19,5 ΔE de separação mínima no claro e 18,1
     no escuro**.

     **Isso é melhor que o piso da própria escala do mockup, e o achado
     corrige a prancha.** Ela declara que as quatro passam "com rótulo direto em
     todas, porque o par âmbar/verde-azulado fica no piso em tritanopia".
     Medido: âmbar × verde-azulado em tritanopia dá 84,1 ΔE no claro e 66,5 no
     escuro — é o **melhor** par da paleta. O pior é **azul × roxo sob
     protanopia: 2,7 ΔE no escuro**, praticamente indistinguível, e azul ×
     verde-azulado em tritanopia com 3,6. A prancha nomeia o par errado.

     **Colisão aceita e declarada:** o rosa fica a 5,3 ΔE de `--crit` no claro e
     a 5,0 de `--ok` no escuro, sob simulação. Série e estado não disputam o
     mesmo canal de leitura — desde a #523 o estado carrega forma e palavra —,
     e a alternativa era um matiz com separação pior dentro da própria escala,
     que é o que a rosca precisa distinguir. */
  --color-serie-1: #1f5fd0;
  /* **Escurecido do `#0e9384` do mockup, e o motivo é o dígito** — Issue #524.
     A amostra da legenda carrega o número da fatia, e número é texto: o piso é
     4,5:1 da WCAG 1.4.3, não os 3:1 de elemento gráfico. Sobre o verde-azulado
     original, o branco dava **3,80:1** — o único degrau da escala a reprovar.
     Escurecido no mesmo matiz até 4,53:1, a separação mínima contra as outras
     cinco fica em 5,0 ΔE contra os 5,1 de antes: o ajuste não custa distinção.
     A cor do tema escuro não muda, porque lá a tinta é escura e todas passam. */
  --color-serie-2: #0d8577;
  --color-serie-3: #b54708;
  --color-serie-4: #6941c6;
  --color-serie-5: #c61065;
  /* A fatia agrupada não é uma categoria: ela é o que sobrou depois do corte, e
     o mockup reserva a neutra para isso. Dar-lhe um degrau da escala afirmaria
     que ela é comparável às outras. */
  --color-serie-resto: #6b7685;
  /* A tinta do número que a amostra e o arco carregam. Branco no claro e quase
     preto no escuro, escolhidos por medição: são as duas únicas que passam
     4,5:1 contra **todos** os seis degraus do respectivo tema. `--color-bg` não
     serve no claro — ele é `#f3f2f2`, e o par com o verde-azulado cai a 3,40:1. */
  --color-serie-tinta: #ffffff;

  --color-accent-100: #f1f4f6;
  --color-accent-200: #d7e3ec;
  --color-accent-300: #aacce8;
  --color-accent-400: #76adda;
  --color-accent-500: #4891ce;
  --color-accent-600: #3079b5;
  --color-accent-700: #276191;
  --color-accent-800: #1d486d;
  --color-accent-900: #14334c;

  --color-accent-2-100: #f7f3f0;
  --color-accent-2-200: #f0e0d3;
  --color-accent-2-300: #f3c4a0;
  --color-accent-2-400: #eca065;
  --color-accent-2-500: #e68030;
  --color-accent-2-600: #cc6819;
  --color-accent-2-700: #a35314;
  --color-accent-2-800: #7b3e0f;
  --color-accent-2-900: #562c0b;

  --font-heading: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-heading-weight: 800;
  --font-body: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Escala de tipo. Os cinco degraus são os que os templates já usavam inline,
     convertidos de `px` para `rem` — a 100% nenhum tamanho renderizado muda.

     **A unidade é a entrega, não o nome.** A escala de letra do portal é
     `html { font-size: 125% / 150% }`, e ela só alcança o que está em `rem`:
     as 25 declarações inline estavam todas em `px`, em 13 arquivos, então
     esses textos ficavam parados enquanto o entorno crescia 50%. Em
     `macros.html` isso alcançava toda tela que usa a macro. Issue #521.

     `--font-size-corpo` já era **pedido** em `.table tbody th[scope="row"]` e
     nunca fora declarado: o que renderizava era o fallback. Mesma família da
     #301 e da #473 — um nome que o markup pede e a folha não tem, sem HTML
     inválido e sem erro de console.

     Os nomes são os do desenho da 1.0 onde o valor coincide (`secao` = 14px
     lá e aqui), para que adotar aquela escala um dia seja troca de valor e
     não redesenho. `corpo` é a exceção deliberada: a 1.0 propõe 12,5px e aqui
     ele fica em 12px, que é o que a tela mostra hoje — mudar isso é decisão
     de aparência, e esta entrega não muda aparência.

     `destaque` tem **cinco** casas onde o resto do arquivo usa quatro, e é de
     propósito: `0.7812rem` vale 12,4992px, e a afirmação acima é absoluta.
     Meio milésimo de pixel não se vê, mas prosa que não se sustenta ao pé da
     letra é o que este repositório cobra em §13.28 e §13.38 — então o valor
     cede, não a frase. Os cinco `0.7812rem` literais que já existiam no
     arquivo ficam como estão: eles nunca prometeram reproduzir um `px`.

     Divergência conhecida, herdada e **não** modelo a copiar: o `<h3>` de
     "Como ler…" usa `secao` em oito telas e `subtitulo` só no `/diagnostico`.
     Já era 15px contra 14px antes de a escala existir; nomear os degraus faz
     o acidente parecer intenção, e não é. */
  --font-size-titulo: 1.0625rem;
  --font-size-subtitulo: 0.9375rem;
  --font-size-secao: 0.875rem;
  --font-size-destaque: 0.78125rem;
  --font-size-corpo: 0.75rem;
  /* Sexto degrau, acrescentado em 19/09/2026 (#529). O mockup declara
     `--t-nota` em 11,5px e a escala não tinha degrau abaixo de `corpo`;
     sem ele a chave técnica do catálogo sairia do mesmo tamanho do texto
     que ela acompanha. Convertido, não copiado: 11,5px = 0.71875rem. */
  --font-size-nota: 0.71875rem;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;

  /* Movimento. Os quatro valores são os que já estavam espalhados pelo
     arquivo: centralizar não muda tempo nenhum, só tira a decisão de 15
     lugares e põe em um. Issue #88.

     `var(--easing-saida)` em tudo é deliberado: é a curva que desacelera na chegada, e
     num portal de consulta o que importa é a resposta parecer ter chegado,
     não sair com estilo. */
  --duracao-micro: 120ms;
  --duracao-curta: 160ms;
  --duracao-media: 180ms;
  --duracao-longa: 220ms;
  --easing-saida: ease-out;
  /* --space-5 era usado em tres regras e nunca declarado. Variavel ausente
     invalida a declaracao inteira: o padding do cabecalho e do rodape do modal
     ia a zero, encolhendo a area de toque dos botoes. */
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  /* Raio e elevação do mockup 1.0 (doc `docs/design/`), **convertidos e não
     copiados**: a prancha `tokens` dá 5/7/10/12px e é assim que entram, porque
     raio não participa da escala de letra — diferente do tipo, que a #521
     converteu para `rem`.

     **Eram 0 em todos os níveis, e o visual chapado não vinha daí.** Medido em
     19/09/2026: 354 regras na folha, **10 com `border-radius` e 5 com
     `box-shadow`**, e os componentes de conteúdo — `bloco`, `kpi`, `cartao`,
     `ranking`, `tira`, `moldura` — com **zero dos dois**. Trocar o valor
     sozinho arredondava seis regras; quem arredonda é a regra que declara a
     propriedade, e elas não existiam. */
  --radius-sm: 5px;
  --radius-md: 7px;
  --radius-lg: 10px;
  --radius-xl: 12px;

  /* Elevação discreta, para separar cartão de fundo sem sombra de catálogo. */
  --elev-1: 0 1px 2px color-mix(in srgb, #201e1d 6%, transparent);
  --elev-2: 0 2px 6px color-mix(in srgb, #201e1d 8%, transparent);

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);
}

/* Tema escuro. Aplicado em [data-tema] no elemento raiz da aplicação, para que a
   troca não dependa de JavaScript ter rodado. */
[data-tema="escuro"] {
  color-scheme: dark;

  --color-bg: #171615;
  --color-surface: #221f1e;
  --color-text: #f3f2f2;
  --color-divider: color-mix(in srgb, #f3f2f2 26%, transparent);
  --color-accent: #76adda;
  --color-accent-2: #eca065;
  --color-atencao: #eca065;
  /* 7,59:1 sobre `bg` e 6,87:1 sobre `surface`, e **1,11:1 contra o âmbar
     acima** — ver a nota do tema claro: neste tema a cor não separa erro de
     aviso, e nenhuma cor separa. O glifo separa. */
  --color-erro: #eb8d9b;

  --color-superficie-escura: #0e0d0d;
  --color-texto-sobre-escura: #f3f2f2;
  --color-texto-sobre-escura-fraco: #bab6b6;

  /* As três rampas invertem por inteiro. Até 13/08/2026 só 15 dos 33 tokens de
     cor eram sobrescritos aqui: os outros 18 permaneciam com o valor claro, o
     que dava combinações claro-sobre-claro em qualquer componente que usasse um
     passo não coberto. Rampa pela metade é pior que rampa nenhuma, porque falha
     só em algumas telas. */
  --color-neutral-100: #2d2b2b;
  --color-neutral-200: #3a3736;
  --color-neutral-300: #4a4645;
  --color-neutral-400: #5c5857;
  --color-neutral-500: #726e6d;
  --color-neutral-600: #8d8988;
  --color-neutral-700: #b0acac;
  --color-neutral-800: #d7d3d3;
  /* 900 é a exceção deliberada: continua o passo mais escuro, porque serve de
     véu da gaveta (`.gaveta-detalhe::backdrop`), que precisa escurecer em ambos
     os temas. Quem precisa de "superfície escura com texto legível" usa
     `--color-superficie-escura`, acima. */
  --color-neutral-900: #0e0d0d;

  /* Escala de série no tema escuro — passos próprios, nunca inversão
     automática, pela mesma razão da prancha do mockup: a cor é escolhida
     contra a superfície escura. As cinco passam o piso de 3:1 contra
     `--color-surface` deste tema (4,15 a 4,81:1), medido em 19/09/2026.
     A dívida do par azul × roxo é do tema escuro e está acima: 2,7 ΔE sob
     protanopia, que é o motivo de a legenda precisar de um segundo canal. */
  --color-serie-1: #4f83d8;
  --color-serie-2: #189c8c;
  --color-serie-3: #c07836;
  --color-serie-4: #8d6fd0;
  --color-serie-5: #ee2b86;
  --color-serie-resto: #7d8b9b;
  --color-serie-tinta: #171615;

  --color-accent-100: #14334c;
  --color-accent-200: #1d486d;
  --color-accent-300: #276191;
  --color-accent-400: #3079b5;
  --color-accent-500: #4891ce;
  --color-accent-600: #76adda;
  --color-accent-700: #aacce8;
  --color-accent-800: #d7e3ec;
  --color-accent-900: #f1f4f6;

  --color-accent-2-100: #562c0b;
  --color-accent-2-200: #7b3e0f;
  --color-accent-2-300: #a35314;
  --color-accent-2-400: #cc6819;
  --color-accent-2-500: #e68030;
  --color-accent-2-600: #eca065;
  --color-accent-2-700: #f3c4a0;
  --color-accent-2-800: #f0e0d3;
  --color-accent-2-900: #f7f3f0;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.55);
  --shadow-md: 0 3px 10px rgba(0, 0, 0, 0.6);
}

/* ── escala de letra ─────────────────────────────────────────────────────── */

/* `data-fonte` no <html>, gravado pelo servidor a partir do cookie de
   preferência (utils/preferencias.py). Só funciona porque tipografia e
   espaçamento passaram a `rem`: até 13/08/2026 eram 58 das 59 declarações de
   `font-size` em px, e nem o zoom de texto do navegador movia o portal — apenas
   o zoom de página inteira.

   Os degraus param em 150% porque é onde a grade de dois blocos ainda cabe sem
   virar rolagem horizontal. */
html { font-size: 100%; }
html[data-fonte="grande"] { font-size: 125%; }
html[data-fonte="maior"] { font-size: 150%; }

/* ── base ───────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  line-height: 1.55;
  font-weight: 400;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}
h1 { font-size: 2.625rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5625rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
h1, h2, h3 { letter-spacing: -0.02em; }

p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
a:hover { color: var(--color-accent-600); }
img { display: block; max-width: 100%; }
figure { margin: 0; }

/* 68% é o piso: abaixo disso o texto secundário reprova o mínimo de 4,5:1 da
   WCAG AA em pelo menos um dos quatro pares (bg e surface, nos dois temas).
   Estava em 50% e 55%, o que dava 3,18:1 e 3,66:1 — e essas classes carregam
   justamente a explicação do número, o rótulo do campo e a origem do dado. */
.text-muted { color: color-mix(in srgb, var(--color-text) 68%, transparent); }

:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

/* Texto só para leitor de tela. Usado nas descrições dos gráficos, que são
   imagens para quem enxerga e precisam de alternativa para quem não. */
.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;
}

.hr { height: 2px; border: 0; margin: var(--space-4) 0; background: var(--color-divider); }

/* ── botões ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; text-decoration: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 0.75rem; line-height: 1.2; color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  padding: 5px 11px;
  border-radius: var(--radius-md);
  transition: background-color var(--duracao-media) var(--easing-saida), border-color var(--duracao-media) var(--easing-saida),
              color var(--duracao-media) var(--easing-saida), transform var(--duracao-micro) var(--easing-saida);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { background: var(--color-accent); color: var(--color-bg); }
.btn-primary:hover { background: var(--color-accent-600); }
.btn-primary:active { background: var(--color-accent-700); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-block { width: 100%; justify-content: flex-start; text-align: left; }

/* ── formulários ────────────────────────────────────────────────────────── */

.field > label {
  display: flex; align-items: flex-end; min-height: 25px;
  font-size: 0.6562rem; margin-bottom: 3px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.field .ajuda {
  font-size: 0.6875rem; margin-top: 4px;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.input {
  width: 100%; min-height: 29px; padding: 4px 8px; font: inherit;
  font-size: 0.7812rem; color: var(--color-text); caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 68%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }

.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; font-size: 0.75rem; cursor: pointer;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.seg-opt:has(input:checked) { background: var(--color-accent); color: var(--color-bg); }
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* ── etiquetas ──────────────────────────────────────────────────────────── */

.tag {
  display: inline-flex; align-items: center; font-size: 0.625rem;
  letter-spacing: 0.02em; padding: 2px 7px;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

/* Etiquetas de estado do diagnóstico técnico. Saem dos tokens que já existem,
   então tema claro e escuro continuam de graça.

   **`.tag-erro` acompanha `.estado-erro`, e é a mesma decisão da #523.** As
   duas rotulam falha de sistema — "falhou" na tabela do `/diagnostico`, caixa
   de erro nas telas —, e o sistema não pode falar duas línguas para um
   significado só. Até esta entrega as duas apontavam para `--color-atencao`,
   e o comentário que estava aqui dizia isso como virtude; a frase sobreviveu
   nove linhas à mudança do token e virou falsa, que é o mesmo fóssil de prosa
   que esta entrega foi caçar em `rosca()` e em `graficos.ts`.

   **`.tag-aviso` fica no âmbar de propósito:** ela rotula "concluído sem
   linhas", que não é falha — é resultado legítimo e vazio. Antes as duas eram
   a mesma cor e só a borda contra o fundo as separava; agora a cor também
   separa.

   A cor **acompanha** o rótulo, nunca o substitui: cada etiqueta escreve o
   estado por extenso ("falhou", "em execução"), e a tabela é ordenada com as
   falhas no topo. Diagnóstico que depende de distinguir dois cinzas é
   diagnóstico que não serve para daltônico nem para captura de tela em preto e
   branco colada num chamado. */
.tag-erro { border: 1px solid var(--color-erro); color: var(--color-erro); }
.tag-processando { border: 1px dashed var(--color-accent); color: var(--color-accent); }
.tag-aviso { background: var(--color-neutral-100); color: var(--color-atencao); }
.tag-ok { background: var(--color-neutral-100); color: var(--color-neutral-800); }

/* ── tabelas ────────────────────────────────────────────────────────────── */

.table { width: 100%; border-collapse: collapse; font-size: 0.75rem; }
.table th {
  text-align: left; font-size: 0.5938rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
  padding: 5px 8px; border-bottom: 2px solid var(--color-divider);
  white-space: nowrap;
}
.table td { padding: 4px 8px; border-bottom: 1px solid var(--color-divider); }

/* Cabeçalho de LINHA não é cabeçalho de coluna, e a regra acima não distinguia.
   `.table th` vale para qualquer `th`, inclusive o `th scope="row"` do corpo —
   e `white-space: nowrap` é herdado pelos `<span>` filhos dele. Numa célula que
   concatena nome do agregado, as telas e o aviso de cache, isso força rolagem
   horizontal em tela estreita: o oposto do que a #301 corrigiu no cabeçalho.
   O maiúsculo também sai, porque ali o texto é nome e não rótulo de coluna.
   Achado da revisão de tela da #473. */
.table tbody th[scope="row"] {
  white-space: normal;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--font-size-corpo);
  color: var(--color-text);
}

/* Os dois papéis de `.nota` na célula do agregado, no /diagnostico.

   Antes da #473 a `.nota` só aparecia ali quando havia algo digno de atenção —
   o valor servido do cache. A coluna de telas a pôs em **todas** as 28 linhas,
   para um dado rotineiro, e isso dilui o aviso justamente onde ele importa.
   Dois papéis, dois tratamentos, e nenhum depende só da posição. */
.agregado-telas { display: block; font-style: italic; }
.agregado-defasado { display: block; color: var(--color-atencao); font-weight: 600; }
.table tbody tr { transition: background-color var(--duracao-curta) var(--easing-saida), box-shadow var(--duracao-curta) var(--easing-saida); }
.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
.table .destaque { color: var(--color-accent); font-weight: 600; }
/* Linha selecionada, cujo detalhe está aberto na janela interna. */
.table tr[data-selecionada="true"] {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--color-accent);
}
.moldura-tabela {
  border-top: 2px solid var(--color-divider);
  border-bottom: 2px solid var(--color-divider);
  overflow-x: auto;
}

/* ══════════════════════════════════════════════════════════════════════════
   Estrutura da aplicação
   ══════════════════════════════════════════════════════════════════════════ */

.aplicacao {
  display: grid;
  /* Valor **literal**, não `var(--largura-da-barra)`: a transição de
     `grid-template-columns` não interpola quando o valor vem de uma
     propriedade personalizada — o Chromium prende a coluna no valor antigo
     e a barra simplesmente não encolhe. A variável ao lado existe para o
     botão se posicionar na borda; as duas ficam sempre no mesmo bloco, para
     que divergir exija ignorar a linha vizinha. */
  /* `auto` na primeira coluna: quem define a largura é a própria barra,
     via `--largura-da-barra`. Antes a coluna carregava o valor e era ela
     que animava — e uma transição de `grid-template-columns` fica **presa**
     no valor anterior: medido neste projeto, a classe aplicava, a regra
     vencia, a variável resolvia 3,5rem e o valor computado continuava
     176px. Largura de elemento transiciona sem esse defeito. */
  grid-template-columns: auto minmax(0, 1fr);
  /* **`min-height`, nunca `height`.** Esta caixa é o bloco de contenção do
     `position: sticky` da barra lateral e do cabeçalho, e sticky só percorre
     dentro do bloco de contenção. Com `height: 100dvh` a caixa parava na altura
     da janela mesmo em página mais alta, e o curso de rolagem sticky ia a zero:
     em tela larga a barra tem a altura da janela e subia junto com o documento
     na primeira rolagem; abaixo de 1000px, onde ela vira faixa de ~48px, o
     curso era de uma tela e a faixa desaparecia depois disso. Um sintoma só,
     duas escalas. `min-height` deixa a caixa crescer com o conteúdo, que é o
     que dá curso ao sticky dos dois. */
  min-height: 100vh; min-height: 100dvh;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  /* Sem transição em `grid-template-columns`. Medido neste projeto: com ela,
     a coluna fica **presa** no valor anterior — a classe aplica, a regra
     está na folha com o valor literal, e o valor computado não muda. Sem
     ela, a coluna vai a 3,5rem imediatamente. Trocar o recolher que
     funciona por uma animação que o quebra não é troca. O movimento que
     restou está nos filhos da barra, que transicionam normalmente. */
}

/* Recolher **encolhe** para uma trilha de ícones, como no DocForge; não some
   com a barra. Sumir tira a orientação de onde a pessoa está: o item ativo
   desaparece junto, e voltar exige lembrar onde estava. */


/* **O véu não existe fora do regime de gaveta, e `display: none` é a única
   declaração que garante isso** — Issue #530.

   Ele é filho de `.aplicacao`, que é grid de duas colunas. Um `<button>` tem
   `display: inline-block` por padrão, então sem esta linha ele **ocupa uma
   célula**: medido em 1440px logo depois de acrescentá-lo, o grid resolveu
   `1425px 0px`, o véu tomou a primeira trilha, `.principal` a segunda e a
   barra foi parar em `x: 1425` — fora da janela, com a página em branco e
   rolagem horizontal.

   `opacity: 0` e `pointer-events: none` não bastavam, e é o ponto: os dois
   tratam pintura e clique, e nenhum tira o elemento do **layout**. Nada
   reprovava — o HTML é válido, o console fica limpo, e o único sintoma é o
   portal desenhado errado. */
.veu-da-barra { display: none; }

/* ── barra lateral ──────────────────────────────────────────────────────── */

.barra-lateral {
  background: var(--color-superficie-escura);
  color: var(--color-texto-sobre-escura);
  display: flex; flex-direction: column; gap: 11px;
  padding: 10px 0 12px;
  width: var(--largura-da-barra); flex: none;
  position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: auto;
  /* Sem transição aqui. `width` vem de `var(--largura-da-barra)`, e uma
     propriedade animada cujo valor sai de uma variável fica **presa** no
     valor anterior neste motor: medido três vezes neste projeto — a classe
     aplica, a regra vence, a variável resolve 3,5rem e o valor computado
     permanece 176px. Só cancelar a transição destrava. Recolher instantâneo
     que funciona vale mais que animação que impede recolher. */
}
/* Na trilha, o que some é o texto — a barra continua no lugar, visível e
   navegável por teclado. Os rótulos saem por CSS, não do HTML: leitor de tela e
   busca da página seguem encontrando "Funcionários", "Férias" e os demais. */
.barra-recolhida .barra-lateral { overflow-x: hidden; }
.barra-recolhida .item-nav {
  justify-content: center;
  padding-inline: 0;
  gap: 0;
}
/* O rótulo é **clipado**, não `display: none`. A diferença importa: `none`
   tira o elemento da árvore de acessibilidade, e o nome do link passaria a
   depender do `title` — que alguns leitores anunciam como descrição, não como
   nome. Clipado, o texto continua sendo o nome acessível do link, e some só
   para quem enxerga. */
.barra-recolhida .item-nav-rotulo {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.barra-recolhida .item-nav-marca,
.barra-recolhida .item-nav-externo,
.barra-recolhida .titulo-grupo,
.barra-recolhida .marca-empresas,
.barra-recolhida .sessao-usuario,
.barra-recolhida .sessao-perfil,
.barra-recolhida .origem-texto,
.barra-recolhida .versao-portal,
.barra-recolhida .rodape-sessao form,
.barra-recolhida .link-aviso { display: none; }
.barra-recolhida .marca,
.barra-recolhida .rodape-sessao { padding-inline: 0; text-align: center; }
.barra-recolhida .marca-risco { margin-inline: auto; }
.marca { padding: 0 14px; }
/* Logotipo completo na barra expandida; símbolo isolado na trilha. Só um dos
   dois é exibido, e o que sai vai junto com o `display: none` — o nome "OpsHub"
   nunca é anunciado duas vezes. `height` fixa e `width: auto` mantêm a
   proporção 994×251 sem depender do CSS de imagem herdado. */
.marca-logotipo { height: 1.75rem; width: auto; max-width: 100%; }
.marca-simbolo { display: none; height: 1.75rem; width: 1.75rem; }
.barra-recolhida .marca-logotipo { display: none; }
.barra-recolhida .marca-simbolo { display: block; margin-inline: auto; }
.marca-empresas {
  font-size: 0.5312rem; letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap; color: var(--color-texto-sobre-escura-fraco); margin-top: 1px;
}
.marca-risco { height: 2px; background: var(--color-accent); margin-top: 8px; width: 32px; }

.navegacao { display: flex; flex-direction: column; gap: 9px; }
.grupo-nav { display: flex; flex-direction: column; gap: 1px; }
/* Rodapé do menu (Issue #581): Portal e Ecossistema continuam no mesmo `<nav>`
   — mesmo boost, mesma ordem de teclado —, mas saem do corpo das três camadas
   de decisão. O primeiro grupo de rodapé abre a separação; os seguintes a
   herdam. Tamanhos em `rem`, porque a escala de letra só alcança `rem` (#521). */
.grupo-nav:not(.grupo-rodape) + .grupo-rodape {
  margin-top: 0.75rem; padding-top: 0.75rem;
  border-top: 1px solid var(--color-texto-sobre-escura-fraco);
}
.grupo-rodape .item-nav { font-size: 0.75rem; }
.titulo-grupo {
  font-size: 0.5312rem; letter-spacing: 0.16em; line-height: 1.3; text-transform: uppercase;
  color: var(--color-texto-sobre-escura-fraco); padding: 0 14px 1px;
}
.item-nav {
  display: flex; align-items: center; gap: 8px;
  /* `relative` existe pelo rótulo clipado do estado recolhido (:437), e não por
     desenho. Aquele rótulo é `position: absolute`, e `overflow` só clipa
     descendente absoluto quando o bloco contentor está **dentro** do elemento
     que clipa. `.navegacao` é `static`, então não é bloco contentor de nada: o
     contentor era `.barra-lateral`, que é `sticky`. Na faixa horizontal
     recolhida, onde a barra declara `overflow: visible`, o rótulo escapava dos
     dois e esticava o documento — medido em 390px, `scrollWidth` 742 contra
     `clientWidth` 390, independente do conteúdo da página. Com `relative` aqui
     o contentor passa a ser o próprio item, que fica dentro do
     `overflow-x: auto` da navegação. */
  position: relative;
  padding: 2px 14px; font-size: 0.7812rem; line-height: 1.6;
  text-decoration: none; color: var(--color-texto-sobre-escura-fraco);
  border-left: 3px solid transparent;
  transition: color var(--duracao-curta) var(--easing-saida), background-color var(--duracao-curta) var(--easing-saida),
              border-color var(--duracao-curta) var(--easing-saida);
}
.item-nav:hover { color: var(--color-texto-sobre-escura); }
.item-nav[aria-current="page"] {
  color: var(--color-texto-sobre-escura);
  border-left-color: var(--color-accent);
  background: color-mix(in srgb, var(--color-texto-sobre-escura) 9%, transparent);
}
.item-nav-icone { width: 1.125rem; height: 1.125rem; flex: none; }
.item-nav-rotulo { flex: 1; }
.item-nav-marca {
  font-size: 0.5625rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--color-accent); flex: none;
}
.item-nav-externo {
  font-size: 0.5625rem; flex: none; white-space: nowrap; color: var(--color-texto-sobre-escura-fraco);
}

.rodape-sessao {
  margin-top: auto; padding: var(--space-3) 14px 0;
  border-top: 1px solid color-mix(in srgb, var(--color-texto-sobre-escura) 22%, transparent);
  display: flex; flex-direction: column; gap: 3px;
}
.sessao-usuario {
  font-size: 0.75rem; color: var(--color-texto-sobre-escura); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis;
}
.sessao-perfil { font-size: 0.6562rem; color: var(--color-texto-sobre-escura-fraco); }
.rodape-sessao .btn-secondary {
  color: var(--color-texto-sobre-escura); border-color: var(--color-texto-sobre-escura-fraco);
  justify-content: flex-start; margin-top: 6px; width: 100%;
}
.origem-dados { display: flex; align-items: center; gap: 7px; margin-top: 8px; }
.origem-dados img {
  max-width: 62px; max-height: 14px; object-fit: contain;
  filter: brightness(0) invert(1); opacity: 0.7;
}
.origem-marca { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; flex: none; }
.versao-portal {
  color: var(--color-texto-sobre-escura-fraco); font-size: 0.625rem; line-height: 1.1;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.origem-texto { font-size: 0.5625rem; line-height: 1.3; color: var(--color-texto-sobre-escura-fraco); }
/* Link permanente para o Aviso de Privacidade (Issue #242). Alvo de toque de 44
   px como os demais controles da barra, e sublinhado só no foco/hover: ele é
   procedência, não ação — compete com o botão Sair logo acima. */
.link-aviso {
  display: flex; align-items: center; min-height: 44px; margin-top: 2px;
  font-size: 0.625rem; line-height: 1.2; text-decoration: none;
  color: var(--color-texto-sobre-escura-fraco);
}
.link-aviso:hover, .link-aviso:focus-visible { text-decoration: underline; }

/* ── cabeçalho ──────────────────────────────────────────────────────────── */

.principal { display: flex; flex-direction: column; min-width: 0; }

/* **`flex-wrap` entrou com a etiqueta de defasagem** (#529), e a medição é a
   razão. A 150% de escala de letra, a etiqueta tirou largura da linha e quem
   cedeu foi o selo de ambiente: ele declara `overflow-wrap: anywhere`, então
   `DESENVOLVIMENTO` saía partido em `DESENVOLVIM`/`ENTO` — 53px de altura
   contra 29px sem a etiqueta. Espremer o vizinho até ele partir palavra é o
   modo de falha do `nowrap`, e ele não avisa: o HTML é válido e o console fica
   limpo.

   Com `wrap`, sob pressão o último item desce uma linha e cada um fica
   inteiro. A 100% nada muda — a linha continua cabendo. */
.cabecalho {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: 7px var(--space-6);
  border-bottom: 2px solid var(--color-divider);
  background: var(--color-bg);
  position: sticky; top: 0; z-index: 5;
}
/* Botão de recolher, ancorado na **borda** da barra — é o que ele controla.
   `position: fixed` porque a barra tem `overflow: auto`: dentro dela, o botão
   rolaria junto com o menu e sairia da tela. A posição acompanha a largura da
   barra pela mesma variável que o grid usa, então os dois nunca se desencontram. */
.aplicacao { --largura-da-barra: 206px; }
.aplicacao.barra-recolhida { --largura-da-barra: 3.5rem; }

.botao-menu {
  position: fixed; top: 12px; z-index: 10;
  /* `left: 0` mais `translateX`, e não `left` animado: `left` é propriedade
     de layout, então animá-la faz o navegador refazer layout a cada quadro da
     transição -- com a barra lateral inteira ao lado. `transform` roda em
     composição e não toca o layout. Issue #88. */
  left: 0;
  transform: translateX(calc(var(--largura-da-barra) - 0.875rem));
  width: 1.75rem; height: 1.75rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--color-texto-sobre-escura);
  background: var(--color-superficie-escura);
  border: 1px solid color-mix(in srgb, var(--color-texto-sobre-escura) 26%, transparent);
  border-radius: 999px;
  cursor: pointer; font: inherit;
  transition: transform var(--duracao-longa) var(--easing-saida),
              background-color var(--duracao-curta) var(--easing-saida),
              border-color var(--duracao-curta) var(--easing-saida);
}
/* O alvo de toque cobre os 44px da WCAG 2.5.8 sem inflar o desenho. */
.botao-menu::after {
  content: ""; position: absolute; inset: -0.5rem;
}
.botao-menu[hidden] { display: none; }
.botao-menu:hover { border-color: var(--color-accent); color: var(--color-accent-700); }
.botao-menu-icone {
  width: 1.125rem; height: 1.125rem; flex: none;
  transition: transform var(--duracao-longa) var(--easing-saida);
}
/* Aponta para dentro quando a barra está aberta e para fora quando recolhida:
   o próprio ícone diz o que o clique vai fazer. */
.barra-recolhida .botao-menu-icone { transform: rotate(180deg); }
.cabecalho-titulo {
  font-family: var(--font-heading); font-weight: 800; font-size: 0.875rem;
  margin: 0 auto 0 0;
}
.logos { display: flex; align-items: center; gap: 12px; flex: none; }
.logos img { height: 13px; width: auto; object-fit: contain; flex: none; transition: opacity 0.15s; opacity: 0.85; }
.logos:hover img { opacity: 1; }
[data-tema="escuro"] .logos img { filter: grayscale(1) brightness(0) invert(1); }

.selo-ambiente {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.625rem; letter-spacing: 0.04em; text-transform: uppercase;
  overflow-wrap: anywhere; hyphens: auto;
  padding: 3px 8px; font-weight: 600;
}
.selo-ambiente::before { content: ""; width: 6px; height: 6px; background: currentColor; display: block; }
.selo-producao { background: var(--color-accent); color: var(--color-bg); }
.selo-homologacao { background: var(--color-accent-200); color: var(--color-accent-800); }
.selo-desenvolvimento { background: var(--color-neutral-200); color: var(--color-neutral-800); }
.somente-leitura { font-size: 0.6875rem; color: color-mix(in srgb, var(--color-text) 68%, transparent); }

/* ── conteúdo ───────────────────────────────────────────────────────────── */

.conteudo {
  width: 100%; max-width: 1560px;
  /* Sem isto o conteudo encosta a esquerda e sobra fundo vazio a direita: em
     2560px eram ~794px, em 3440px ~1674px. */
  margin-inline: auto;
  padding: var(--space-6);
  display: flex; flex-direction: column; gap: var(--space-6);
}
.opshub-ativo .conteudo { animation: opshub-entrada-conteudo var(--duracao-longa) var(--easing-saida) both; }

.rotulo-campo {
  font-size: 0.625rem; letter-spacing: 0.04em; text-transform: uppercase;
  overflow-wrap: anywhere; hyphens: auto;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.nota {
  margin: 0; font-size: 0.6875rem; text-wrap: pretty;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
/* Ênfase dentro de prosa esmaecida (Issue #299). A nota é impressa a 68% do
   texto, e negrito sobre 68% lê como "um pouco menos apagado". Devolver a cor
   cheia dá à ênfase dois canais — peso e contraste — em vez de um. Os dois
   valores saem de tokens do tema, então a regra vale no claro e no escuro; a
   grade de granularidade não precisa dela porque `.detalhe-valor` já herda a
   cor cheia. */
.nota strong { color: var(--color-text); font-weight: 600; }
/* Issue #583: o aviso é o estado da leitura (parcial, piso da série,
   supressão desta leitura) e fica fora do "Como ler". Cor cheia, e não os 68%
   da metodologia: é a diferença que o separa dela sem depender de posição. */
.nota-aviso { color: var(--color-text); }
.como-ler { font-size: 0.6875rem; }
.como-ler > .nota { margin-top: var(--space-1); }
.legenda-dos-estados { display: flex; flex-direction: column; gap: var(--space-1); }
/* **`.intro` precisa da mesma correção, e só agora ela é alcançável.** Issue
   #526, achado do `revisor-opshub-ui`. A regra acima existe porque negrito
   sobre texto a 68% lê como "um pouco menos apagado" — e `.intro` esmaece a
   72%, que é o mesmo caso. Ela nunca precisou disto porque as outras três
   ocorrências de `class="intro"` imprimem prosa estática; o resumo da versão é
   o primeiro a receber markup gerado, e é onde estão 9 dos 37 pares.
   `.lista-mudancas li` fica de fora de propósito: não reduz opacidade, então
   ali o peso do navegador já é o segundo canal. */
.intro strong { color: var(--color-text); font-weight: 600; }
.intro {
  margin: 0; max-width: 76ch; font-size: 0.7812rem; text-wrap: pretty;
  color: color-mix(in srgb, var(--color-text) 72%, transparent);
}

/* Faixa de detalhe técnico: fonte, granularidade, limite, cache. Só aparece em
   modo técnico — é o que distingue "o número está errado" de "a view mudou". */
.faixa-tecnica {
  display: flex; flex-wrap: wrap; gap: var(--space-6);
  border-top: 1px solid var(--color-divider);
  border-bottom: 1px solid var(--color-divider);
  padding: var(--space-2) 0;
}
.faixa-tecnica dd { margin: 2px 0 0; font-size: 0.8125rem; }
.faixa-tecnica div { min-width: 0; }
code { font-size: 0.94em; overflow-wrap: anywhere; }

/* Cartão de filtros. */
.cartao-filtros { border: 1px solid var(--color-divider); background: var(--color-surface); }
.cartao-filtros-topo {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
}
.cartao-filtros-legenda {
  font-size: 0.625rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--color-accent); font-weight: 600;
}
.cartao-filtros-grade {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--space-4); padding: var(--space-4); align-items: start;
}
.cartao-filtros-acoes {
  display: flex; align-items: center; gap: var(--space-4);
  padding: 0 var(--space-4) var(--space-4); flex-wrap: wrap;
}

/* Barra de recorte do painel: uma linha, sempre visível. */
.recorte {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  border: 1px solid var(--color-divider); background: var(--color-surface);
  padding: var(--space-3) var(--space-4);
}
/* O recorte global (Issue #585): a mesma barra nas nove telas de Decidir e
   Entender. O `fieldset` da janela zera a moldura do navegador para herdar a
   do segmentado; o botão vai para a direita como ia no formulário de cada
   tela, e a frase de alcance ocupa a linha inteira embaixo. */
.recorte-global fieldset.seg { margin: 0; padding: 0; min-width: 0; }
.recorte-global > .btn { margin-left: auto; }
/* O rótulo quebra para cima do `select` quando não cabe, e o `select` nunca
   passa da largura disponível: ele assume a largura da opção mais longa ("Mesmo
   mês do ano anterior"), e com letra de 150% a 375px a página rolava 49px.
   Medido pelas guardas de navegador da #514 e da #582, na tela inicial. */
.campo-do-recorte {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  font-size: 0.8125rem; min-width: 0; max-width: 100%;
}
.campo-do-recorte select { min-height: 44px; max-width: 100%; min-width: 0; }
.recorte-alcance { flex-basis: 100%; }

/* Tira de indicadores. O fundo é o divisor e cada célula é um retângulo de
   fundo da página — é o que produz as linhas de 2px entre os números.

   **Era `grid` com `repeat(auto-fit, minmax(132px, 1fr))`, e a troca por `flex`
   é o que permite o cartão ser tão largo quanto o seu próprio número** —
   Issue #514.

   O `auto-fit` **proíbe** trilha de tamanho intrínseco: `minmax(min-content, 1fr)`
   é inválido dentro de `repeat(auto-fit, …)`, então a única largura mínima que
   aquela forma aceitava era uma constante. E constante em `px` governando
   trilha cujo conteúdo é dimensionado em `rem` é o defeito de origem: a célula
   não acompanhava a escala de letra, ela **encolhia** quando a escala subia,
   porque o `padding` de `.kpi` cresce em `rem` e a coluna não. Medido em
   18/09/2026: 155px de célula na escala normal a 375px, 139px na escala máxima.

   Em `flex`, o `min-width: auto` de item flexível já resolve para `min-content`,
   e com o numeral em `nowrap` (ver `.kpi-valor`) isso **é** a largura do número.
   O cartão nunca fica mais estreito que o valor que ele publica, e a tira quebra
   em mais linhas quando precisa. As duas metades são uma correção só: `nowrap`
   sem isto troca número partido por número transbordando a coluna.

   `8.25rem` são os mesmos 132px na escala normal, agora em `rem` para que a base
   acompanhe a preferência de letra — regra do projeto, e aqui ela é a causa.

   **A coluna mais estreita não é a do telefone**, e por isso a guarda desta tira
   varre os seis viewports: `auto-fit` produzia suas colunas mais estreitas logo
   acima de um múltiplo de 132px, o que dava 103px a 975px com sete cartões
   contra 155px a 375px. */
.tira-kpi {
  display: flex; flex-wrap: wrap;
  gap: 2px; background: var(--color-divider);
  border-top: 2px solid var(--color-divider);
  border-bottom: 2px solid var(--color-divider);
  padding: 2px 0;
}
.kpi {
  background: var(--color-bg); padding: var(--space-3) var(--space-4);
  display: flex; flex-direction: column; gap: 3px;
  flex: 1 1 8.25rem;
}
/* **O numeral não quebra em lugar nenhum** — Issue #514.

   Esta regra tinha `overflow-wrap: anywhere` desde o commit inicial do portal
   (`8b5e93b`), e `anywhere` autoriza a quebra entre **dois dígitos**:
   `1.690.679` saía como `1.690.6` e `79`, e quem parava na primeira linha lia
   `1.690,6` — três ordens de grandeza abaixo. Nenhuma guarda pegava, e nenhuma
   podia: o HTML é válido, o console fica limpo e o número exibido é o número
   que o serviço mandou. O defeito mora entre o texto e a caixa.

   **Ela é reescopada, não removida.** Levantamento de todo conteúdo de
   `.kpi-valor` nos oito templates em 18/09/2026: `inteiro`, `dinheiro`,
   `percentual`, `ou_travessao` e os quatro estados da macro `valor_do_kpi` —
   **nenhum texto livre**. A única palavra que a célula desenha é `desativado`,
   e ela tem corpo próprio de `0.8125rem`. Mas quebra agressiva é inofensiva
   justamente nos estados textuais, e é lá que ela fica: tirá-la de vez deixaria
   a próxima palavra que alguém puser num estado sem rede nenhuma. */
.kpi-valor {
  font-family: var(--font-heading); font-weight: 800;
  font-size: 1.375rem; line-height: 1.05; white-space: nowrap;
}
.kpi-valor .kpi-desativado,
.kpi-valor .kpi-preparando,
.kpi-valor .kpi-vazio { white-space: normal; overflow-wrap: anywhere; }
.kpi-valor.em-atencao { color: var(--color-atencao); }
/* Indicador cujo agregado não voltou dentro do orçamento da tela. É o terceiro
   estado do KPI, e ele existe desde a Issue #237: antes, "ainda vindo" era
   representado escondendo a faixa inteira, o que apagava junto os indicadores
   que já tinham chegado. Discreto de propósito — não é erro nem alerta, e
   compete com números de verdade ao lado. */
.kpi-preparando {
  color: color-mix(in srgb, var(--color-text) 38%, transparent);
  letter-spacing: 0.08em;
}
/* Indicador cuja fonte o portal está instruído a não consultar. É o quarto
   estado, e ele existe desde a Issue #330: antes, "fonte desligada" era
   desenhada como ausência de número, que é o mesmo desenho de "o bloco não veio
   agora" — e a legenda da faixa afirmava o segundo. Permanente lido como
   transitório convida a tomar ausência de dado por ausência de problema.

   **Palavra, e não número em falta**, e por isso não herda o corpo de 1.375rem
   do valor: um "desativado" do tamanho de um KPI competiria com os números de
   verdade ao lado, e este cartão é justamente o que não tem o que informar. */
.kpi-desativado {
  font-family: var(--font-base); font-weight: 600;
  font-size: 0.8125rem; line-height: 1.6;
  letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--color-text) 52%, transparent);
}
/* Agregado que terminou sem número — o bloco não veio nesta abertura. O
   travessão é a marca que o resto do portal já usa para ausência de valor, e
   está aqui porque antes o cartão saía **em branco**: rótulo, unidade e nada
   entre eles, indistinguível de um defeito de renderização. */
.kpi-vazio { color: color-mix(in srgb, var(--color-text) 38%, transparent); }
.kpi-nota { font-size: 0.6875rem; color: color-mix(in srgb, var(--color-text) 68%, transparent); }
/* Sparkline do cartão (Issue #95). Sem eixo nem rótulo, mas a geometria
   reproduz a série: quem a homologou homologou os valores. O mês em andamento
   sai tracejado e com marcador vazado (#302), e a cor não é o único canal —
   a frase acessível e a amplitude visível dizem o mesmo. */
.sparkline { display: block; width: 100%; max-width: 7.5rem; height: 1.75rem; margin-top: var(--space-1); }
.sparkline-linha, .sparkline-parcial { stroke: var(--color-accent); stroke-width: 1.5; fill: none; }
.sparkline-parcial { stroke-dasharray: 3 2; }
.sparkline-ponto { fill: var(--color-accent); }
.sparkline-ponto-parcial { fill: var(--color-surface); stroke: var(--color-accent); stroke-width: 1.5; }

/* Blocos analíticos. */
.grade-blocos {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: var(--space-3); align-items: start;
}
.grade-blocos-largos { grid-template-columns: repeat(auto-fit, minmax(680px, 1fr)); }
/* Issue #582: bloco que é tabela de várias colunas. `min(100%, …)` impede a
   coluna de exigir mais que a largura disponível na tela estreita. */
.grade-blocos-medios { grid-template-columns: repeat(auto-fit, minmax(min(100%, 26.25rem), 1fr)); }
.bloco {
  display: flex; flex-direction: column; gap: var(--space-3);
  border: 1px solid var(--color-divider); padding: var(--space-4);
  background: var(--color-bg); min-width: 0;
}
.bloco-topo {
  display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap;
  border-bottom: 2px solid var(--color-divider); padding-bottom: var(--space-2);
}
.bloco-topo h3 { margin: 0; font-size: 0.875rem; }

/* Variação contra a competência anterior, no topo do cartão (Issue #331).

   **Cor neutra de propósito.** Cor semântica é julgamento, e julgar exige o
   limiar que esta entrega não introduz: alta de efetivo e alta de afastamento
   não são a mesma notícia, e decidir qual é boa é regra de negócio, que mora na
   view. Quem carrega o sentido é a palavra ("alta de", "queda de", "sem
   variação"); o glifo é `aria-hidden` e entra como segundo canal, que é o
   precedente da #305.

   Mais forte que `.nota` (82% contra 68%) porque é dado, não legenda — e mais
   fraco que o texto cheio porque o número principal do cartão é o gráfico. */
.delta-serie {
  margin: 0; font-size: 0.75rem; text-wrap: pretty;
  color: color-mix(in srgb, var(--color-text) 82%, transparent);
}
.delta-glifo { margin-right: 4px; font-size: 0.85em; }
.bloco-fonte {
  font-size: 0.5938rem; overflow-wrap: anywhere; margin-left: auto;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}

/* Histórico técnico de versões. Mantém a linguagem retangular e os tokens
   existentes; a linha vertical comunica ordem sem depender apenas de cor. */
.cabecalho-versoes {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-6); border-bottom: 2px solid var(--color-divider);
  padding-bottom: var(--space-5);
}
.cabecalho-versoes h2 { margin: var(--space-1) 0 var(--space-2); font-size: 1.25rem; }
.versao-destaque {
  min-width: 150px; border: 1px solid var(--color-divider);
  padding: var(--space-3) var(--space-4); background: var(--color-surface);
  display: grid; gap: 2px;
}
.versao-destaque strong {
  font-family: var(--font-heading); font-size: 1.375rem; font-variant-numeric: tabular-nums;
}
.versao-destaque > span:last-child {
  color: var(--color-accent); font-size: 0.625rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase;
}
.historico-versoes {
  list-style: none; margin: 0; padding: 0 0 0 var(--space-6);
  border-left: 2px solid var(--color-divider);
  display: flex; flex-direction: column; gap: var(--space-4);
}
.registro-versao { position: relative; }
.registro-versao::before {
  content: ""; position: absolute; width: 10px; height: 10px;
  left: calc(-1 * var(--space-6) - 6px); top: var(--space-5);
  background: var(--color-bg); border: 2px solid var(--color-neutral-500);
}
.registro-versao-atual::before { border-color: var(--color-accent); background: var(--color-accent); }
.registro-versao article {
  border: 1px solid var(--color-divider); background: var(--color-bg);
  padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3);
}
.registro-versao-cabecalho h3 { margin: var(--space-2) 0 0; font-size: 0.9375rem; }
.registro-versao-meta { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
.registro-versao-meta time { font-size: 0.6562rem; color: color-mix(in srgb, var(--color-text) 68%, transparent); }
.tag-atual { background: var(--color-accent); color: var(--color-bg); border-color: var(--color-accent); }
.lista-mudancas { margin: 0; padding-left: 18px; display: grid; gap: var(--space-2); }
.lista-mudancas li { padding-left: 3px; font-size: 0.75rem; line-height: 1.45; text-wrap: pretty; }
.lista-mudancas li::marker { color: var(--color-accent); }
/* Bloco que o perfil não alcança. Substitui o conteúdo, não o esconde. */
.bloco-restrito {
  flex: 1; display: flex; flex-direction: column; justify-content: center;
  gap: var(--space-3); border: 1px dashed var(--color-divider);
  padding: var(--space-6); min-height: 190px;
}

/* Barras horizontais de ranking, feitas com div — o protótipo faz assim. */
.ranking { display: flex; flex-direction: column; gap: var(--space-3); }
.ranking-item { display: flex; flex-direction: column; gap: 4px; }
.ranking-linha {
  display: flex; justify-content: space-between; gap: var(--space-3); font-size: 0.75rem;
}
.ranking-nome { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ranking-valor { font-variant-numeric: tabular-nums; flex: none; }
.ranking-trilho { height: 8px; background: var(--color-neutral-200); }
.ranking-barra { height: 8px; background: var(--color-neutral-500); }
.ranking-barra.em-atencao { background: var(--color-atencao); }
/* Marca não-cromática do estado "em atenção" (Issue #305, WCAG 1.4.1): a cor da
   barra não pode ser o único canal. O glifo ▲/▼ é forma, e um `.sr-only`
   acompanha para o leitor de tela. */
.ranking-atencao { margin-left: 6px; color: var(--color-atencao); font-size: 0.75em; }

/* Barra empilhada de composição, e sua legenda. */
.empilhada { display: flex; height: 34px; border: 1px solid var(--color-divider); }
/* **O segmento carrega o índice da fatia** — Issue #575, pela mesma razão que
   pôs o número na amostra da legenda na #524: a escala de série separa fatias
   vizinhas por **matiz**, não por luminância — medido, o pior par fica a 1,03:1
   no tema escuro —, e quem casa a fatia com a linha da legenda é o número.

   `overflow: hidden` é a rede de `LARGURA_MINIMA_PARA_NUMERO`, que é uma fração
   da barra calculada sobre o cartão mais estreito da grade e sobre a largura do
   dígito na escala de letra de 150%. No dia em que uma pilha de fontes mais
   larga desminta a conta, o algarismo é **cortado** — nunca derramado sobre a
   fatia ao lado, onde apontaria para a linha errada da legenda. */
/* **`min-width: 0` aqui é o padrão, e o piso vem de fora** — `graficos.py`
   escreve `min-width: 3px` no estilo da fatia **que tem valor**, e só nela.
   Medido: 13 postos em 23.601 dão 1,31px numa barra de 1.317px, e 0,25px no
   cartão mais estreito da grade — abaixo da borda de 1px da própria barra. O
   piso não pode morar aqui porque a folha não sabe o valor da fatia, e dá-lo à
   fatia em zero faria o gráfico afirmar existência onde não há. */
.empilhada-fatia {
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; min-width: 0;
  color: var(--color-serie-tinta);
  font-size: var(--font-size-nota); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.legenda { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
.legenda-item { display: flex; align-items: center; gap: 8px; font-size: 0.75rem; }
/* **A amostra carrega o número da fatia** — Issue #524. A cor sozinha não liga
   legenda e arco: medido, o par azul × roxo da escala fica a 2,7 ΔE sob
   protanopia no tema escuro, e antes disso a rampa de opacidade que ela
   substituiu deixava fatias adjacentes a 1,2:1. O número é texto, e texto não
   depende de o leitor separar dois matizes.

   Ela cresceu de 10px para 15px porque precisa comportar um dígito legível, e
   o `--color-bg` no texto vale nos dois temas: as cinco cores da escala passam
   o piso de 3:1 contra a superfície, então nenhuma delas é clara o bastante
   para o fundo da página sumir em cima. */
.legenda-cor {
  width: 15px; height: 15px; flex: none; border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--font-size-nota); font-weight: 700; line-height: 1;
  color: var(--color-serie-tinta); font-variant-numeric: tabular-nums;
}
.legenda-nome { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legenda-numeros { flex: none; display: flex; gap: 8px; font-variant-numeric: tabular-nums; }
.legenda-percentual { color: color-mix(in srgb, var(--color-text) 68%, transparent); width: 44px; text-align: right; }

/* Navegação por páginas da grade — Issue #334. Em `rem` e `var(--space-*)`,
   porque a escala de letra do portal precisa alcançar os controles: `px` novo em
   `font-size` quebra o recurso em silêncio. */
.navegacao-de-paginas {
  margin-top: var(--space-4); display: flex; flex-wrap: wrap;
  align-items: center; justify-content: space-between; gap: var(--space-3);
}
/* A medida da página substituiu a frase de posição na #550: "Mostrando 1–500
   de 1.284" subiu para a barra de resultado, e o que sobra aqui é o tamanho da
   página e o teto por pesquisa. */
.navegacao-de-paginas-medida {
  margin: 0; font-size: var(--font-size-nota);
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}
.navegacao-de-paginas-controles {
  margin: 0; display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-1);
}
/* Alvo de 28px de lado para o número, que é o menor controle desta barra.
   `min-width` e não `width`: com a escala de letra em 150% o dígito cresce, e
   largura fixa cortaria o segundo algarismo a partir da página 10. */
.navegacao-de-paginas-numero { min-width: 1.75rem; justify-content: center; }
.navegacao-de-paginas-atual {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.75rem; padding: 5px 11px;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 0.75rem; line-height: 1.2;
  background: var(--color-accent); color: var(--color-bg);
  border: 1px solid var(--color-accent); border-radius: var(--radius-sm);
}

/* Gráficos SVG gerados em utils/graficos.py. A geometria vive lá; aqui fica só
   o que é constante, para que a CSP não precise liberar estilo embutido por
   causa de uma largura máxima. */
/* ── O gráfico de eixo, depois que o rótulo saiu do SVG ────────────────────
   Issue #536. O texto vivia em unidades de `viewBox`, e por isso a escala de
   letra não o alcançava: medido a 100%, 125% e 150%, o rótulo ficava em 14px
   nas três enquanto o texto da página ia de 12 para 18px. Na outra ponta, num
   bloco de 360px, ele renderizava a 7px.

   Agora `.grafico` é a **caixa**: uma grade de duas colunas e duas linhas, com
   os rótulos em HTML e só a geometria dentro do SVG.

   **O teto de 700px saiu junto.** Ele existia para o texto do SVG não crescer
   demais em tela larga — a 1500px sobrava metade do espaço sem uso. Com o texto
   fora, a largura deixou de ter esse custo. */
.grafico {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  column-gap: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--font-size-nota);
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}

/* **A altura é `aspect-ratio` entre dois limites em `rem`.** Sem limites, um
   gráfico de 1200px de largura teria 282px de altura e outro de 340px teria 80,
   e a mesma série pareceria duas coisas diferentes. Os limites em `rem` fazem o
   gráfico acompanhar a escala de letra também na altura — o que importa porque
   o rótulo ao lado cresce, e um eixo Y mais alto que o gráfico desalinha. */
.grafico-tela { grid-area: 1 / 2; position: relative; min-width: 0; }
.grafico-area {
  display: block;
  width: 100%;
  /* **528/127 é a área de plotagem, e 664/156 era o retângulo antigo.** O
     número velho sobreviveu ao `viewBox` que o justificava e descrevia 0,1%
     a mais de largura relativa — inofensivo na tela e enganoso na leitura,
     que é a forma como este projeto perde uma hora. */
  aspect-ratio: 528 / 127;
  min-height: 8rem;
  max-height: 16rem;
}

/* **O ponto da série é HTML, e a razão é a mesma do rótulo: ele não pode
   esticar.** `preserveAspectRatio="none"` distorce toda forma de proporção
   fixa, e medido em 19/09/2026 o `<circle r="2.59">` saía em 2,63 × 7,83px num
   bloco de 340px com a letra em 150% — três vezes mais alto que largo. Em
   `rem` ele é redondo em qualquer largura e ainda cresce com a escala de
   letra. */
.grafico-ponto {
  position: absolute;
  width: 0.44rem;
  height: 0.44rem;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
.grafico-ponto-destacado { width: 0.7rem; height: 0.7rem; }

/* ── Painéis empilhados, no lugar do eixo duplo ────────────────────────────
   Issue #539. Duas grandezas em escalas independentes dentro do mesmo
   retângulo fazem a posição de uma em relação à outra não significar nada:
   medido em 19/09/2026, acrescentar **um** mês à série de turnover, sem tocar
   em nenhum dos onze valores anteriores, muda o teto de 5% para 10% e desce a
   linha inteira **36,9 pontos percentuais da altura**.

   **A coluna do eixo Y é uma só, e a razão não é a que parece.** A primeira
   versão deste comentário dizia que duas caixas independentes teriam colunas de
   larguras diferentes, porque `24k` e `6%` não medem o mesmo. Medido em
   navegador: **falso** — a marca do eixo Y é `position: absolute` desde a #536,
   está fora do fluxo e não dimensiona a coluna, que mede `2.5rem` sempre, com
   qualquer texto (40px a 100%, 60px a 150%). Duas grades separadas ficariam
   alinhadas hoje.

   O que a grade única compra é **coincidir não é garantir**: a largura é uma
   constante da folha, e no dia em que deixar de ser, duas grades divergem em
   silêncio enquanto o eixo de mês continua afirmando a correspondência que
   perdeu. Numa grade só, as duas áreas são a mesma coluna.

   Fronteira medida junto: o rótulo mais largo que o portal produz hoje tem
   cinco dígitos e ocupa 31px contra os 40 da coluna; `1.234.567` ocuparia
   48,4px e vazaria 8,4px à esquerda. Não acontece porque nenhuma série de eixo
   chega lá. */
.grafico-empilhado {
  /* A base declara `grid-area` fixo em cada filho e duas linhas; aqui as linhas
     nascem do conteúdo, e cada filho diz só a coluna. */
  grid-template-rows: none;
  row-gap: var(--space-1);
}
.grafico-empilhado > .grafico-legenda-serie { grid-area: auto; grid-column: 1 / -1; }
.grafico-empilhado > .grafico-eixo-y { grid-area: auto; grid-column: 1; }
.grafico-empilhado > .grafico-tela { grid-area: auto; grid-column: 2; }
.grafico-empilhado > .grafico-eixo-x { grid-area: auto; grid-column: 2; }

/* **A grandeza principal fica maior, e as duas somadas ficam no tamanho do
   gráfico único** — o bloco não pode crescer por ter deixado de mentir. Medido
   em 20/09/2026 num bloco de 700px, com a área em 618px: 98,3px e 51,5px, que
   somam **149,8** contra os **148,6** que um gráfico único teria ali. A
   diferença é de 1,2px, e a revisão de tela chegou a esse número antes de mim —
   a primeira redação dizia "somando menos", o que era conta de cabeça e não
   medição. A proporção de 1,9 entre os dois vem da prancha `03/04` do mockup
   1.0, e é **editorial**: nenhuma das duas escalas se lê contra a outra.

   **A palavra "editorial" fica, e a razão é de prosa.** O painel de cima é
   sempre o das barras, nas quatro telas — é convenção de leitura, não medição
   por bloco. Justificar a proporção pela "importância" da métrica trocaria uma
   convenção declarada por uma afirmação não medida sobre qual das duas
   grandezas importa mais, e é a classe de defeito que esta Issue inteira
   existe para remover. Achado da revisão de tela da #539. */
.grafico-empilhado .grafico-painel-1 .grafico-area {
  aspect-ratio: 528 / 84;
  min-height: 5.5rem;
  max-height: 11rem;
}
.grafico-empilhado .grafico-painel-2 .grafico-area {
  aspect-ratio: 528 / 44;
  min-height: 3rem;
  max-height: 6rem;
}

/* A legenda de cada painel: sem eixo duplo, é ela que diz o que aquele desenho
   é e em que unidade. **A amostra tem forma além de cor** — quadrado para
   barra, traço para linha —, que é a regra da #523 num lugar onde ela sai de
   graça, porque as duas séries já têm formas diferentes no desenho. */
.grafico-legenda-serie {
  display: flex; align-items: center; gap: var(--space-2);
  margin: 0; font-size: var(--font-size-nota); color: var(--color-text);
}
.grafico-amostra { flex: none; display: inline-block; }
.grafico-amostra-barra { width: 0.65rem; height: 0.65rem; border-radius: 0.15rem; }
.grafico-amostra-linha { width: 1rem; height: 0.2rem; border-radius: 0.1rem; }
.grafico-unidade { color: color-mix(in srgb, var(--color-text) 55%, transparent); }

/* Os dois eixos são caixas de posicionamento: cada marca sabe a própria
   posição em porcentagem, calculada pelo servidor a partir da mesma constante
   que o SVG usa para desenhar a linha. Duas contas para a mesma coordenada
   divergiriam, e o sintoma seria rótulo apontando para a linha errada. */
.grafico-eixo-y {
  grid-area: 1 / 1;
  position: relative;
  min-width: 2.5rem;
}
.grafico-marca-y {
  position: absolute; right: 0; transform: translateY(-50%);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
/* **O padrão lateral é o que deixa todas as marcas centradas.** A última fica
   a 95,8% da área e, centrada sem folga, metade dela sairia da caixa — foi por
   isso que uma versão desta entrega a ancorou à direita com
   `translateX(-100%)`. Medido em navegador: ancorada assim ela colide com a
   penúltima, porque a distância que a cadência garante é entre **centros**, e
   uma marca alinhada à direita ocupa todo o espaço à esquerda do seu ponto.
   `ago/26` e `set/26*` saíam sobrepostos a 700px.

   Com folga de meio rótulo de cada lado, todas seguem a mesma regra e a
   verificação de colisão do servidor volta a descrever o que a tela faz. */
.grafico-eixo-x {
  grid-area: 2 / 2;
  position: relative;
  height: 1.6em;
  /* **Nem `padding` nem `overflow: hidden`, e as duas exclusões foram
     medidas.** Uma versão desta entrega deu folga lateral com
     `padding-inline`, e isso **desalinha o rótulo da barra**: para elemento
     posicionado o bloco de contenção é o *padding box*, então `left: 95,8%`
     deixa de ser 95,8% da área que o SVG desenha. O rótulo passa a apontar
     para a barra errada, que é pior que vazar.

     Sem padding, a primeira e a última marca avançam meio rótulo sobre a
     vizinhança — à esquerda sobre a coluna do eixo Y, à direita sobre o
     respiro do cartão. É espaço que já existe, e `overflow: visible` deixa o
     texto inteiro aparecer em vez de cortá-lo pela metade. */
  overflow: visible;
}
.grafico-marca-x {
  position: absolute; top: 0; transform: translateX(-50%);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* **A última competência nunca some**, e a regra é da #354: forçar o índice
   final o põe a uma faixa do anterior, e quem cede o lugar são os vizinhos —
   nunca ele, porque é a informação mais recente da série. A classe existe para
   a container query abaixo saber quem não pode esconder — e **não** para
   ancorá-la de forma diferente: ela é centrada como todas, e quem lhe dá espaço
   é o padrão lateral do eixo. */

/* **Quem decide o que cabe é a largura real, não o servidor.** Ele escolhe a
   cadência sem saber a largura do bloco nem a escala de letra do leitor; a
   container query sabe as duas. Medido em 19/09/2026, o rótulo mais largo que o
   portal produz é `set/26*` — competência parcial — com 36,5px a 100% e
   **54,7px a 150%**: doze deles pedem ~504px numa escala e ~756px na outra, e
   nenhuma largura fixa atende os dois. */
.grafico { container-type: inline-size; container-name: grafico; }
/* **34rem, e o número veio da medição, não do arredondamento.** A 30rem o
   corte só agia abaixo de 480px, e a 520px os sete rótulos restantes ficavam a
   **0,1px** um do outro — sem colisão pela régua e frágil por qualquer coisa:
   fonte do sistema diferente, um rótulo de quatro dígitos de ano, meio pixel de
   arredondamento. Folga que depende de sorte não é folga. */
@container grafico (max-width: 34rem) {
  /* **O vizinho da última também sai, e isto é a regra da #354 em CSS.**
     Esconder alternados preserva a cadência, mas a última marca é forçada fora
     dela — ela fica a **meia faixa** da penúltima mostrada, não a uma faixa.
     Medido em navegador: a 340px sobravam os índices 0,2,4,6,8,10 mais o 11, e
     o par final colidia em **−12px**.

     A #354 resolveu isso no servidor com a frase "quem colidiria com o último
     cede o lugar"; a container query a desfazia, porque ela seleciona por
     posição de filho e não sabe quem ficou ao lado de quem. `nth-last-child(2)`
     é exatamente esse vizinho. */
  .grafico-marca-x:nth-child(2n):not(.grafico-marca-x-ultima),
  .grafico-marca-x:nth-last-child(2) { display: none; }
}
@container grafico (max-width: 16rem) {
  .grafico-marca-x:not(:first-child):not(.grafico-marca-x-ultima) { display: none; }
}
/* `bloco-vazio` divide a regra com `grafico-vazio` porque o papel é o mesmo —
   o vazio declarado dentro de um bloco — e o que muda é só a origem: lá um
   gráfico sem série, aqui um grupo de exceção com zero contratos (Issue #460).
   Duas regras iguais divergiriam no primeiro ajuste; e classe sem regra é o
   padrão do navegador aplicado em silêncio, que foi o achado da #301. */
.grafico-vazio,
.bloco-vazio {
  margin: 0; padding: var(--space-6) 0; text-align: center; font-size: 0.75rem;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
/* **A rosca tinha `width="92" height="92"` cravados no markup** — Issue #536.
   Ela não crescia com a largura do bloco nem com a escala de letra: medida a
   100%, 125% e 150%, ficava em 92px nas três, enquanto o texto da página ia de
   12 para 18px. Quem aumenta a letra aumenta tudo menos o gráfico.

   `clamp` resolve os dois eixos com uma declaração, e cada ponta tem motivo:

   - o **piso em `rem`** é o que faz a rosca acompanhar a escala de letra, e
     também o que protege o número da fatia (ver abaixo);
   - o **meio em `%`** é o que a faz usar o bloco: numa tela larga ela cresce
     em vez de deixar espaço morto ao lado da legenda;
   - o **teto em `rem`** impede que ela roube a largura da legenda, que é quem
     nomeia cada fatia — e desde a #524 também carrega o número que liga a
     legenda ao arco.

   `aspect-ratio` substitui o `height` cravado: a rosca é circular por
   definição, e altura derivada da largura nunca desencontra das duas. */
/* **A rosca cresceu e comeu o nome da filial** — medido a 340px de bloco com a
   escala de letra em 150%, logo depois de torná-la responsiva: os nomes sumiram
   da legenda e sobraram só o número e o percentual, porque `.legenda-nome` tem
   `min-width: 0` e corta com reticências até o nada.

   **É a #202 voltando pela porta dos fundos.** Aquela Issue existe porque a
   rosca sozinha não diz de quem é a concentração — o nome de cada filial vivia
   só no `<title>`, fora do alcance de toque e teclado. Deixar a rosca engolir a
   legenda devolve o mesmo sintoma com outra causa.

   Duas correções, e as duas são do conjunto, não do círculo: a caixa **quebra
   em duas linhas** quando não cabe, e a legenda declara uma **largura mínima**
   abaixo da qual ela força essa quebra em vez de encolher o nome. */
.grafico-rosca-caixa {
  display: flex; gap: var(--space-4); align-items: center; flex-wrap: wrap;
}
/* **`min(11rem, 100%)`, e o `100%` não é excesso de zelo.** O piso existe para
   a legenda quebrar de linha em vez de encolher o nome até sumir; sem o `min`,
   ele vira um piso rígido maior que o espaço disponível. Medido: a 150% de
   escala de letra `11rem` são 264px, e o cartão mais estreito que a
   `.grade-blocos` admite tem 250px — cerca de 202px internos. A legenda
   estouraria a lateral do cartão, que é um defeito diferente do que o piso
   veio corrigir, e pior, porque `.bloco` não declara `overflow`. */
.grafico-rosca-caixa .legenda { flex: 1 1 11rem; min-width: min(11rem, 100%); }

.grafico-rosca {
  flex: none;
  /* **O piso é `5.75rem` porque ele é o tamanho de antes desta entrega**, e
     baixá-lo tem um custo que não é de estética. O número da fatia (#524) vive
     em unidades de `viewBox` — `font-size="9"` sobre `viewBox="0 0 140 140"` —,
     então o tamanho físico dele escala com a largura renderizada da rosca:

       rosca  92px (5.75rem) -> número 5,9px   o que o portal já tinha
       rosca  72px (4.5rem)  -> número 4,6px   **menor que antes**

     Uma versão desta entrega shippou `4.5rem`, e a revisão de tela pegou: o
     segundo canal que liga legenda e fatia ficaria menor justamente no cartão
     estreito. Escala não pode piorar o que ela veio melhorar.

     O número do arco tem o mesmo defeito estrutural dos rótulos de eixo — texto
     dentro do `viewBox` —, e ele se resolve na outra metade da #536. Aqui o
     compromisso é **não regredir**. */
  width: clamp(5.75rem, 22%, 10rem);
  aspect-ratio: 1;
  /* A primeira fatia começa no topo. Estava no `style` do elemento, junto do
     tamanho; veio para cá com ele, porque é a mesma decisão de desenho. */
  transform: rotate(-90deg);
}

.grafico-progressivo { min-width: 0; }
/* Altura reservada assim que o gráfico entra na fila de inicialização sob
   demanda. O SVG do servidor é mais baixo que o interativo — mesma largura,
   proporção do viewBox contra os 290px de baixo —, então a troca saltava o
   layout. O atributo é escrito pelo JavaScript: sem script não há troca, e
   reservar espaço para um gráfico que não vem seria buraco em branco. */
.grafico-progressivo[data-opshub-observado] { min-height: 290px; }
.grafico-interativo { width: 100%; min-height: 290px; }
.grafico-dados { margin-top: var(--space-3); font-size: 0.75rem; }
.grafico-dados summary,
.como-ler summary {
  min-height: 44px; display: flex; align-items: center; width: fit-content;
  cursor: pointer; color: var(--color-accent); font-weight: 600;
}
.grafico-dados .moldura-tabela { max-height: 320px; }
.grafico-dados th, .grafico-dados td { font-variant-numeric: tabular-nums; }
.grafico-dados tbody th { text-align: left; font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .grafico-interativo * { animation-duration: 0.01ms !important; }
}

/* Heatmap de calendário: semana por linha, dia por coluna. */
.calendario { display: grid; grid-template-columns: 34px repeat(7, minmax(0, 1fr)); gap: 3px; align-items: center; }
.calendario-cabecalho, .calendario-semana {
  font-size: 0.5625rem; color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.calendario-cabecalho { text-align: center; }
.calendario-celula { height: 17px; background: var(--color-accent); }
.calendario-celula.sem-escala { background: var(--color-neutral-200); opacity: 0.5; }
.escala-intensidade {
  display: flex; align-items: center; gap: 6px; font-size: 0.625rem;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.escala-intensidade span[data-nivel] { width: 16px; height: 9px; background: var(--color-accent); }

/* Aviso em faixa, com a régua vertical à esquerda. */
.aviso {
  display: flex; gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-divider); background: transparent;
}
.aviso::before { content: ""; width: 3px; background: var(--color-divider); flex: none; }
.aviso p { margin: 0; font-size: 0.75rem; text-wrap: pretty; color: color-mix(in srgb, var(--color-text) 68%, transparent); }
.aviso-atencao { border-color: var(--color-atencao); }
.aviso-atencao::before { background: var(--color-atencao); }

/* Detalhe do registro selecionado: modal nativo com grade de rótulo/valor. */
.grade-detalhe {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 2px; background: var(--color-divider); padding: 2px;
}
.detalhe-campo { background: var(--color-bg); padding: var(--space-3) var(--space-4); }
.detalhe-valor { font-size: 0.7812rem; margin-top: 3px; }
/* ── gaveta de detalhe (Issue #89) ────────────────────────────────────────
   O elemento continua sendo o `<dialog>` que `showModal()` promove — foco
   preso, fundo inerte e `Escape` são da top layer nativa, não desta folha. O
   que muda aqui é só a **forma**: em vez de caixa centralizada, uma gaveta
   encostada na borda de leitura, que preserva a grade visível ao lado.

   O estado base é o de **fora da tela**, e o `[open]` é que traz a gaveta para
   dentro. Escrito ao contrário — base dentro, classe para fora — a saída
   exigiria JavaScript segurando o `close()` até a animação terminar, e sob
   `prefers-reduced-motion` (que desliga `transition-property` mais abaixo)
   o `transitionend` nunca chegaria: a gaveta ficaria **presa aberta**. */
.gaveta-detalhe {
  width: min(560px, 100vw); max-height: none;
  padding: 0; overflow: hidden;
  color: var(--color-text); background: var(--color-bg);
  border: 1px solid var(--color-divider); border-radius: var(--radius-lg);
  box-shadow: 0 24px 64px color-mix(in srgb, #000 40%, transparent);
}
/* Sem JavaScript o `<dialog open>` é não modal, e os dois "Fechar" são links de
   verdade. É por isso que a gaveta em si vive sob `.opshub-ativo` — desenhar
   `position: fixed` sem script daria um painel grudado na borda que nada fecha
   por véu nem por `Escape`.

   `position: static` porque a folha do navegador dá `absolute` a `<dialog>`
   não modal, e absoluto sai do fluxo: medido em 02/09/2026 no Chromium, a
   seção seguinte (a leitura analítica) começava no mesmo `top` do cartão e
   ficava **debaixo** dele. Vale para o caminho sem script, que é onde a página
   inteira chega de uma vez. */
html:not(.opshub-ativo) .gaveta-detalhe {
  position: static; margin: var(--space-4) auto;
}
.opshub-ativo .gaveta-detalhe {
  position: fixed; inset-block: 0; inset-inline-end: 0; inset-inline-start: auto;
  margin: 0; height: 100%; max-height: 100dvh;
  border-radius: 0; border-block: 0; border-inline-end: 0;
  /* `translate` e não `transform`: o eixo fica isolado numa propriedade só,
     então nada aqui disputa com um `transform` de outra regra. */
  translate: 100% 0; opacity: 0;
  transition: translate var(--duracao-longa) var(--easing-saida),
              opacity var(--duracao-curta) var(--easing-saida),
              display var(--duracao-longa) var(--easing-saida) allow-discrete,
              overlay var(--duracao-longa) var(--easing-saida) allow-discrete;
}
.opshub-ativo .gaveta-detalhe[open] { translate: 0 0; opacity: 1; }
/* Sem `@starting-style` a abertura não teria de onde partir: o elemento nasce
   no DOM já com `[open]`, e transição precisa de valor anterior. Onde
   `@starting-style` e `allow-discrete` não existem, a gaveta aparece e some
   num quadro — que é exatamente o que o modal fazia antes desta Issue. */
@starting-style {
  .opshub-ativo .gaveta-detalhe[open] { translate: 100% 0; opacity: 0; }
}
.gaveta-detalhe::backdrop {
  background: color-mix(in srgb, var(--color-neutral-900) 68%, transparent);
  opacity: 0;
  transition: opacity var(--duracao-curta) var(--easing-saida),
              display var(--duracao-curta) var(--easing-saida) allow-discrete,
              overlay var(--duracao-curta) var(--easing-saida) allow-discrete;
}
.gaveta-detalhe[open]::backdrop { opacity: 1; }
@starting-style { .gaveta-detalhe[open]::backdrop { opacity: 0; } }
.gaveta-detalhe-painel { display: flex; flex-direction: column; max-height: inherit; }
.opshub-ativo .gaveta-detalhe-painel { height: 100%; }
.gaveta-detalhe-cabecalho,
.gaveta-detalhe-rodape {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); padding: var(--space-4) var(--space-5);
  background: var(--color-surface);
}
.gaveta-detalhe-cabecalho { border-bottom: 1px solid var(--color-divider); }
.gaveta-detalhe-cabecalho h2 { margin: 3px 0 2px; font-size: 1.1875rem; }
.gaveta-detalhe-corpo { overflow: auto; padding: var(--space-4); }
.opshub-ativo .gaveta-detalhe-corpo { flex: 1 1 auto; }
.gaveta-detalhe-nota { margin-top: var(--space-3); }
.gaveta-detalhe-rodape { border-top: 1px solid var(--color-divider); }

/* ── paleta de comandos (Issue #94) ───────────────────────────────────────
   `Ctrl+K` para ir a uma página que o perfil já alcança pelo menu. O índice é
   o HTML que o servidor manda, então esta folha desenha uma lista que já está
   pronta — não há estado de "carregando" nem de "sem resultado do servidor".

   **A diferença em relação à gaveta é o `display: none` de origem.** A gaveta
   chega com `[open]` no HTML, para funcionar sem script, e por isso as regras
   dela ficam sob `.opshub-ativo`. A paleta nunca chega aberta: sem o bundle
   ela é um `<dialog>` fechado, que a folha do navegador já esconde, e por isso
   aqui não há par de regras com e sem script. */
.botao-paleta {
  display: inline-flex; align-items: center; gap: var(--space-2);
  flex: none; font-size: 0.6875rem;
}
/* Sem isto o `hidden` do HTML perde para o `display: inline-flex` acima e o
   botão aparece **antes** de o bundle o ativar — clicável e inerte. Mesma
   armadilha de `.botao-menu[hidden]`, trinta linhas acima. */
.botao-paleta[hidden] { display: none; }
.botao-paleta-tecla {
  font: inherit; font-size: 0.5625rem; letter-spacing: 0.04em;
  padding: 1px 4px; border: 1px solid var(--color-divider);
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.paleta-de-comandos {
  /* Ancorada perto do topo, e não centrada: a lista cresce para baixo à medida
     que o filtro afrouxa, e caixa centrada faria o painel inteiro escorregar
     na tela a cada tecla. */
  inset: 0; margin: 10vh auto auto;
  width: min(560px, calc(100vw - 2rem)); max-height: min(70vh, 34rem);
  padding: 0; overflow: hidden;
  color: var(--color-text); background: var(--color-bg);
  border: 1px solid var(--color-divider); border-radius: var(--radius-lg);
  box-shadow: 0 24px 64px color-mix(in srgb, #000 40%, transparent);
  /* Estado fechado. Ele existe para a abertura por ponteiro ter de onde
     partir; na abertura por teclado nada transiciona, então o `[open]` abaixo
     aplica os dois valores finais no mesmo quadro. */
  translate: 0 -6px; opacity: 0;
}
.paleta-de-comandos[open] { translate: 0 0; opacity: 1; }
/* **A transição é só do ponteiro, e é o `data-abertura` que separa.** Pedido da
   Issue #94, e a razão é de uso: quem chama por `Ctrl+K` repete o gesto muitas
   vezes por sessão e qualquer atraso vira atrito; quem clica está olhando para
   o botão e ganha a continuidade do movimento. O atributo é escrito pelo
   bundle no momento de abrir (`ativarPaletaDeComandos`), antes do
   `showModal()` — depois já seria tarde para o `@starting-style`.

   `allow-discrete` em `display` e `overlay` é o que faz a **saída** existir:
   sem ele o elemento sai da top layer no primeiro quadro e a transição não tem
   onde acontecer. Nada disso depende de `transitionend`, e é por isso que o
   bloco global de `prefers-reduced-motion` — que desliga
   `transition-property` — deixa a paleta funcionando por inteiro, apenas sem
   movimento. */
.paleta-de-comandos[data-abertura="ponteiro"] {
  transition: translate var(--duracao-curta) var(--easing-saida),
              opacity var(--duracao-curta) var(--easing-saida),
              display var(--duracao-curta) var(--easing-saida) allow-discrete,
              overlay var(--duracao-curta) var(--easing-saida) allow-discrete;
}
@starting-style {
  .paleta-de-comandos[data-abertura="ponteiro"][open] { translate: 0 -6px; opacity: 0; }
}
.paleta-de-comandos::backdrop {
  background: color-mix(in srgb, var(--color-neutral-900) 68%, transparent);
}
.paleta-painel {
  display: flex; flex-direction: column; gap: var(--space-3);
  max-height: inherit; padding: var(--space-4);
}
.paleta-topo {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
}
.paleta-titulo {
  margin: 0; font-family: var(--font-heading); font-weight: 800;
  font-size: 1.0625rem;
}
.paleta-lista {
  list-style: none; margin: 0; padding: 0;
  flex: 1 1 auto; overflow: auto;
  border-top: 1px solid var(--color-divider);
}
/* O filtro esconde `<li>`, e sem esta regra um `display` de autor futuro
   venceria o `[hidden]` da folha do navegador — o item filtrado voltaria a
   aparecer sem que nada no bundle mudasse. */
.paleta-item[hidden] { display: none; }
.paleta-acao {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); width: 100%;
  /* 44px é o alvo de toque da WCAG 2.5.8, e aqui ele também é o ritmo da
     lista: item mais baixo obriga a mira fina numa lista longa. */
  min-height: 44px; padding: var(--space-2) var(--space-3);
  text-align: left; text-decoration: none;
  color: inherit; background: transparent;
  border: 0; border-bottom: 1px solid var(--color-divider);
  font: inherit; cursor: pointer;
}
.paleta-acao:hover, .paleta-acao:focus-visible { background: var(--color-surface); }
.paleta-rotulo { font-weight: 600; font-size: 0.8125rem; }
.paleta-grupo {
  flex: none; font-size: 0.6875rem;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}

/* Lista numerada de leitura ("Como ler este painel"). */
.orientacoes {
  display: grid; grid-template-columns: minmax(130px, 170px) minmax(0, 1fr);
  gap: var(--space-6); border-top: 2px solid var(--color-divider); padding-top: var(--space-6);
}
.orientacoes ul {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-4) var(--space-8);
}
.orientacoes li { display: flex; gap: var(--space-3); font-size: 0.7812rem; }
.orientacoes li span:first-child {
  color: var(--color-accent); font-family: var(--font-heading); font-weight: 800;
}
.orientacoes li span:last-child { text-wrap: pretty; }

/* Estados: aguardando, vazio, erro, sem autorização. */
.estado {
  display: flex; flex-direction: column; gap: var(--space-2);
  border: 1px solid var(--color-divider); background: var(--color-surface);
  padding: var(--space-4);
}
.estado-titulo { font-family: var(--font-heading); font-weight: 800; font-size: 0.8125rem; }
.estado p { margin: 0; font-size: 0.75rem; text-wrap: pretty; color: color-mix(in srgb, var(--color-text) 72%, transparent); }
/* Falha de sistema. Ver `.estado-aviso`, que é a outra metade do par — as duas
   moram longe uma da outra neste arquivo por acidente de histórico, e o glifo é
   o que as separa de verdade. Issue #523. */
.estado-erro { border-color: var(--color-erro); }
.estado-erro .estado-titulo::before {
  content: ""; display: inline-block; width: 8px; height: 8px;
  margin-right: var(--space-2); background: var(--color-erro);
}
/* Bloco cuja fonte está desligada no portal — Issue #474.

   **Ele precisa se distinguir do `vazio` sem depender de leitura**, e os dois
   usavam a mesma classe sem modificador: quem varre a tela não tinha sinal
   nenhum para separar "esta fonte nunca vai trazer o número" de "esta consulta
   voltou zero". Achado do `revisor-opshub-ui`.

   A marca é a **barra lateral** e o fundo recuado, e não cor de alerta: fonte
   desligada é decisão de produto, não incidente — `estado-erro` diria a coisa
   errada em vermelho. O tom é o mesmo do `kpi-desativado`, que resolve o mesmo
   estado no cartão irmão desde a #330, e **a cor não é o único canal**: o
   título diz "Bloco desativado" por extenso, e é ele que o leitor de tela
   recebe. */
.estado-desativado {
  border-left-width: 3px;
  border-left-color: color-mix(in srgb, var(--color-text) 40%, transparent);
  background: color-mix(in srgb, var(--color-text) 3%, var(--color-surface));
}
.estado-processando { border-style: dashed; }
.estado-processando .estado-titulo::before {
  content: ""; display: inline-block; width: 8px; height: 8px;
  margin-right: var(--space-2); border-radius: 50%; background: var(--color-accent);
  /* Sem pulso. Um ponto piscando sem parar num painel que fica aberto o dia
     inteiro cobra atenção continuamente e não informa nada além do que a cor e
     a borda tracejada já dizem. Issue #88. */
}
.bloco-estado { min-height: 190px; }

/* Feedback progressivo enquanto a próxima resposta renderizada é preparada. */
.carregamento-global {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  min-height: 34px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--color-superficie-escura) 94%, transparent);
  color: var(--color-texto-sobre-escura); font-size: 0.75rem; font-weight: 600;
  box-shadow: 0 2px 8px color-mix(in srgb, #000 25%, transparent);
}
.carregamento-global[hidden] { display: none; }

/* ── Esqueleto da região de resultado ──────────────────────────────────────
   O htmx põe `htmx-request` em tudo que o `hx-indicator` do formulário aponta,
   e tira quando o ciclo fecha. Aqui isso alterna esqueleto e conteúdo: quem
   consulta vê a forma da tabela que está chegando em vez de a região piscar
   vazia. Zero JavaScript — a classe é do htmx, a troca é de CSS.

   `.corpo-do-resultado` existe porque a regra abaixo que esconde
   `#resultado-do-modulo` durante a requisição precisa vencer a declaração do
   layout da `<section>`. Um atributo `style` inline tem prioridade sobre
   *qualquer* regra de folha de estilo externa, mesmo por `#id` — não é questão
   de especificidade de seletor. Com o layout preso ali, `display: none` desta
   folha nunca entraria em vigor e o conteúdo antigo ficaria empilhado sob o
   esqueleto em vez de escondido. Mover as três declarações para uma classe
   resolve pela cascata normal, sem `!important`.

   Medidas em `rem` e nos tokens `--space-*`: `px` em altura de texto quebra a
   escala de letra em silêncio. */
.corpo-do-resultado {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.regiao-de-resultado .esqueleto-do-resultado {
  display: none;
  /* Estado escondido explícito: sem ele não há de onde a opacidade partir, e o
     cruzamento com o resultado não acontece. */
  opacity: 0;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
}

/* A troca entre esqueleto e resultado era `display: none` <-> `display: flex`,
   e `display` não transiciona: a mudança acontecia num quadro só, o salto que a
   Issue #88 registra.

   `transition-behavior: allow-discrete` faz o navegador segurar o `display` até
   a opacidade terminar, o que dá o cruzamento sem tirar nada do fluxo -- manter
   os dois com `display` permanente empurraria o resultado para baixo pela altura
   do esqueleto. Onde `allow-discrete` não existe, o comportamento é exatamente o
   de antes: instantâneo. Degradação para o que já era. */
.regiao-de-resultado .esqueleto-do-resultado,
.regiao-de-resultado #resultado-do-modulo {
  transition: opacity var(--duracao-curta) var(--easing-saida),
              display var(--duracao-curta) var(--easing-saida) allow-discrete;
}
.regiao-de-resultado.htmx-request .esqueleto-do-resultado { display: flex; opacity: 1; }
.regiao-de-resultado.htmx-request #resultado-do-modulo { display: none; opacity: 0; }
.carregamento-barra {
  position: absolute; inset: 0 auto auto 0; width: 35%; height: 3px;
  background: var(--color-accent);
  animation: opshub-carregando 1.1s ease-in-out infinite;
}
[aria-busy="true"] { cursor: progress; }
[aria-disabled="true"] { pointer-events: none; opacity: 0.65; }
.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;
}
.skip-link {
  position: fixed; left: var(--space-3); top: var(--space-3); z-index: 200;
  transform: translateY(-180%); padding: var(--space-2) var(--space-3);
  background: var(--color-superficie-escura); color: var(--color-texto-sobre-escura);
  border: 2px solid var(--color-accent); text-decoration: none;
}
.skip-link:focus { transform: translateY(0); }

@keyframes opshub-carregando {
  from { transform: translateX(-110%); }
  to { transform: translateX(390%); }
}
@keyframes opshub-entrada-conteudo {
  from { opacity: 0; transform: translateY(5px); }
  to { opacity: 1; transform: translateY(0); }
}
/* `opshub-entrada-modal` saiu com a Issue #89: a gaveta entra e sai por
   transição de estado (`[open]` mais `@starting-style`), não por animação de
   entrada — animação só sabe entrar, e a saída era o que faltava.

   Saiu junto um resíduo **anterior** a esta Issue, que só apareceu porque ela
   mexeu aqui: um `to { opacity: 1; transform: scale(1); }` solto, seguido de
   uma chave, sem `@keyframes` que o abrisse. Era sobra de um bloco removido
   em alguma entrega passada; o navegador descartava, e nenhuma animação
   declarada nesta folha se referia a ele — as duas em uso, `opshub-carregando`
   e `opshub-entrada-conteudo`, continuam definidas acima. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    /* `transition-property: none`, e não só duração zerada. Zerar a duração
       deixa a transição **existindo** com ~0s, e uma propriedade em transição
       cujo valor vem de `var()` fica presa no valor anterior neste motor: era o
       que impedia a barra lateral de recolher, com a classe aplicada, a regra
       vencendo e a variável já resolvida. Desligar a propriedade também é o que
       "movimento reduzido" quer dizer de fato. */
    transition-property: none !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* Tela de entrada, fora da estrutura com barra lateral. */
.tela-entrada {
  min-height: 100vh; height: 100dvh; min-height: 100dvh; display: grid; place-items: center;
  padding: var(--space-6); background: var(--color-bg); color: var(--color-text);
}
.cartao-entrada {
  width: min(420px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
  border: 1px solid var(--color-divider); background: var(--color-surface);
  padding: var(--space-6);
}

/* ══════════════════════════════════════════════════════════════════════════
   Responsivo — os três pontos de quebra do protótipo
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 1240px) {
  /* Muda a **variável**, não o grid: o botão de recolher é posicionado a
     partir dela, e sobrescrever só o grid deixava os dois desalinhados —
     a barra encolhia para 176px e o botão continuava em 206px. */
  .aplicacao { --largura-da-barra: 176px; }
}

/* Abaixo de 1000px a barra **continua sendo barra lateral**, e deixa de virar
   faixa horizontal no topo. Decisão do responsável em 19/09/2026 (#530).

   O regime anterior punha marca, menu e sessão numa linha rolável acima do
   conteúdo, e abaixo de 760px em duas. Era funcional, e não era uma barra
   lateral: a navegação saía do lugar em que a pessoa aprendeu a procurá-la, os
   títulos de grupo desapareciam — `.titulo-grupo { display: none }` — e o menu
   virava uma fileira de itens sem hierarquia, rolando na horizontal.

   Nesta largura não cabem barra e conteúdo lado a lado, e a saída é a de
   qualquer aplicação: a barra sai do fluxo e entra **sobrepondo** o conteúdo, a
   pedido. Fora do fluxo ela não disputa largura com nada, então o menu volta
   inteiro — grupos, títulos e rótulos.

   **O regime fora do fluxo vive sob `.opshub-ativo`, e não é preferência de
   organização.** Quem abre a barra é o botão de menu, e ele nasce `hidden`:
   `.botao-menu[hidden]` só sai quando o bundle inicia. Sem JavaScript, barra
   oculta seria **menu inalcançável** nesta largura — e a versão sem JavaScript
   é a oficial deste portal. Sem o bundle, portanto, nada aqui se aplica e a
   barra segue no fluxo do grid, estreita, ocupando espaço: é o preço de
   continuar navegável, e é o preço certo.

   **Qual classe diz o quê, porque agora são duas.** `barra-recolhida` é a
   preferência de mesa, persistida em cookie, e significa trilha de ícones;
   `barra-sobreposta` é estado de sessão, sem cookie, e significa gaveta aberta
   aqui. Elas não se misturam: `sincronizarRegimeDaBarra`, em
   `frontend/src/main.ts`, tira a primeira ao entrar nesta largura e tira a
   segunda ao sair dela. Gravar cookie ao abrir a gaveta faria o telefone
   decidir como a barra nasce no monitor. */
@media (max-width: 1000px) {
  .aplicacao { --largura-da-barra: 176px; }

  /* Uma coluna só: a barra deixou de ocupar trilha do grid. */
  .opshub-ativo .aplicacao {
    grid-template-columns: minmax(0, 1fr);
    /* A gaveta não é `--largura-da-barra` porque não é largura de coluna: ela
       é uma medida de leitura, e `82vw` guarda a faixa de conteúdo que fica
       visível atrás — o que diz à pessoa que ela está *sobre* a página, e não
       noutra. */
    --largura-da-gaveta: min(17.5rem, 82vw);
    /* O chevron é posicionado a partir desta variável na regra base, com
       `translateX(calc(var(--largura-da-barra) - 0.875rem))`. Com a gaveta
       fechada ela vale o suficiente para ele encostar na margem esquerda do
       conteúdo; com a gaveta aberta, passa a valer a largura dela e o botão
       acompanha a borda, como no monitor. Uma variável, dois estados, e
       nenhuma regra de posição duplicada. */
    --largura-da-barra: 1.375rem;
  }
  .opshub-ativo .aplicacao.barra-sobreposta {
    --largura-da-barra: var(--largura-da-gaveta);
  }

  .opshub-ativo .barra-lateral {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 30;
    width: var(--largura-da-gaveta); height: 100vh; height: 100dvh;
    padding-right: 0;
    /* **Estado fechado escrito como base**, e não aplicado por classe ao
       fechar. É a regra que a #89 deixou: animação só sabe entrar, e quem
       precisa sair escreve o fechado como padrão. Aqui isso também elimina o
       piscar — a gaveta nunca chega aberta esperando o JavaScript fechá-la. */
    transform: translateX(-100%);
    /* **`visibility`, e não só `transform`.** Um elemento empurrado para fora
       da tela por `transform` continua na ordem de tabulação: sem isto, Tab a
       partir do cabeçalho percorreria os dezoito itens de um menu que não está
       na tela, e quem navega por teclado ficaria preso num vão invisível. É o
       modo de falha clássico da gaveta off-canvas, e ele não aparece em
       captura nenhuma.

       O atraso no `visibility` é o que deixa a saída ser vista: sem ele o
       elemento some no primeiro quadro e a animação de fechar não acontece.
       `0s linear <duração>` espera o deslizamento terminar para então
       esconder; ao abrir, a transição vai a `0s` e ele aparece imediatamente.
       `visibility` é uma das poucas propriedades discretas que transicionam
       assim, e é por isso que ela serve aqui e `display` não serviria. */
    visibility: hidden;
    transition: transform var(--duracao-longa) var(--easing-saida),
                visibility 0s linear var(--duracao-longa);
    box-shadow: var(--shadow-md);
  }
  .opshub-ativo .aplicacao.barra-sobreposta .barra-lateral {
    transform: translateX(0);
    visibility: visible;
    transition: transform var(--duracao-longa) var(--easing-saida),
                visibility 0s;
  }
  /* A trilha de ícones é da mesa. Aqui a gaveta é sempre larga, e o CSS não
     desfaz as quinze declarações de `.barra-recolhida` uma a uma: quem tira a
     classe nesta largura é o `sincronizarRegimeDaBarra`. Esta linha existe só
     para o caso de ela sobreviver a uma reinicialização do Alpine sobre
     conteúdo trocado — sem ela, a gaveta abriria com 3,5rem de largura. */
  .opshub-ativo .aplicacao.barra-recolhida .barra-lateral {
    width: var(--largura-da-gaveta);
  }

  /* O véu existe por três razões e nenhuma é decorativa: dá um alvo de toque
     para fechar que é a área inteira fora da gaveta, escurece o conteúdo o
     bastante para dizer que ele não é o que está em foco, e impede o clique de
     atravessar para um link da página atrás. É `<button>` e não `<div>` porque
     fechar é uma ação: assim ele entra na ordem de tabulação e tem nome
     acessível, sem `role` inventado. */
  .opshub-ativo .veu-da-barra {
    display: block; position: fixed; inset: 0; z-index: 20;
    border: 0; padding: 0; cursor: pointer;
    background: color-mix(in srgb, #201e1d 46%, transparent);
    /* **`visibility` aqui pela mesma razão da gaveta, e a simetria é o ponto**
       — achado da revisão de tela da #530, que encontrou esta metade tratada
       com uma solução diferente da outra. `opacity: 0` e `pointer-events:
       none` resolvem pintura e clique; nenhum dos dois tira o elemento do
       alcance de quem lê a página linearmente. Sem isto, um leitor de tela
       encontra um botão "Fechar o menu" de página inteira enquanto não há
       menu aberto para fechar — o `tabindex="-1"` do template cobre só a
       tecla Tab.

       Duas soluções diferentes para o mesmo problema, no mesmo bloco, é o que
       diverge na primeira correção. */
    opacity: 0; pointer-events: none; visibility: hidden;
    transition: opacity var(--duracao-longa) var(--easing-saida),
                visibility 0s linear var(--duracao-longa);
  }
  .opshub-ativo .aplicacao.barra-sobreposta .veu-da-barra {
    opacity: 1; pointer-events: auto; visibility: visible;
    transition: opacity var(--duracao-longa) var(--easing-saida),
                visibility 0s;
  }

  /* O chevron passa à frente da gaveta: fechado ele fica sobre o conteúdo,
     aberto ele encosta na borda dela. Nos dois casos precisa ficar clicável. */
  .opshub-ativo .botao-menu { z-index: 31; }
  /* **O ícone diz o que o clique vai fazer**, e aqui quem decide isso não é a
     mesma classe da mesa. Na trilha é `.barra-recolhida` que vira a seta; no
     regime de gaveta aquela classe é removida pelo módulo, então sem esta
     regra o chevron apontaria para dentro — "fechar" — com a gaveta já
     fechada. Medido em 390px: seta para a esquerda nos dois estados. */
  .opshub-ativo .aplicacao:not(.barra-sobreposta) .botao-menu-icone {
    transform: rotate(180deg);
  }

  /* O título do cabeçalho cede o canto ao chevron, que é fixo e passaria por
     cima dele. `padding-left`, e não `margin`, para o fundo do cabeçalho
     continuar alcançando a borda. */
  .opshub-ativo .cabecalho { padding-left: 2.75rem; }
}

@media (max-width: 760px) {
  .conteudo { padding: 14px; }
  .orientacoes { grid-template-columns: minmax(0, 1fr); }
  .cabecalho { flex-wrap: wrap; row-gap: 6px; }
  .logos { order: 3; }
  .grade-blocos-largos { grid-template-columns: minmax(0, 1fr); }
  .cabecalho-versoes { flex-direction: column; }
  .versao-destaque { width: 100%; }
  .historico-versoes { padding-left: var(--space-4); }
  .registro-versao::before { left: calc(-1 * var(--space-4) - 6px); }
  .botao-menu-texto { display: none; }
  /* A dica de tecla sai e o rótulo fica: quem está nesta largura provavelmente
     não tem teclado físico, e "Ir para…" continua dizendo o que o botão faz. */
  .botao-paleta-tecla { display: none; }
  .paleta-de-comandos { margin-top: var(--space-4); }
  /* Abaixo desta largura a gaveta ocupa a tela inteira: 560px de painel ao
     lado de 200px de grade não deixaria nem a gaveta nem o resultado legíveis,
     e o que a gaveta preserva num monitor — o contexto da linha ao lado — não
     existe aqui de qualquer forma. */
  .opshub-ativo .gaveta-detalhe { width: 100vw; }
  .gaveta-detalhe-cabecalho,
  .gaveta-detalhe-rodape { padding: var(--space-3); }
  .gaveta-detalhe-corpo { padding: var(--space-3); }
}

@media print {
  .barra-lateral, .cartao-filtros, .recorte { display: none; }
  .aplicacao { display: block; }
}

/* ── preferências de aparência ────────────────────────────────────────────── */

/* Controles de tema e tamanho de letra (parciais/preferencias.html). São um
   formulário comum: sem JavaScript eles continuam funcionando, porque quem
   grava a preferência e redesenha a página é o servidor. */
.preferencias {
  display: flex; align-items: center; gap: var(--space-1);
  margin: 0; flex: none;
}
.preferencias-botao {
  min-width: 2rem; min-height: 2rem;   /* alvo de toque acima do mínimo de 24px */
  display: inline-flex; align-items: center; justify-content: center;
  font-variant-numeric: tabular-nums;
}
.preferencias-botao[disabled] { opacity: 0.45; cursor: default; }

/* Fora da sessão o cabeçalho não existe, então os controles flutuam no canto.
   `position: fixed` para não empurrar o cartão de entrada, que é centralizado. */
.tela-entrada ~ .preferencias,
body > .preferencias {
  position: fixed; top: var(--space-2); inset-inline-end: var(--space-2);
  z-index: 6;
}

/* ── classes referenciadas em template e que faltavam ─────────────────────── */

/* As quatro estavam no markup e em CSS nenhum. Classe ausente não quebra nada
   visivelmente — o elemento só fica sem estilo —, e por isso passavam. */

/* Rótulo que envolve o campo, usado no filtro de checkbox. */
.radio {
  display: inline-flex; align-items: center; gap: var(--space-2);
  cursor: pointer;
}
.radio input { flex: none; }

/* Variante de aviso do bloco de estado: mesma caixa, régua de atenção.

   **Restrição de perfil, não falha de sistema** — as ações do leitor são
   opostas: aqui é "peça acesso", no `.estado-erro` é "tente de novo". Os dois
   dividiam `--color-atencao` e eram indistinguíveis por cor; hoje o que os
   separa é a **forma do glifo**, anel contra quadrado cheio, porque no tema
   escuro cor nenhuma os separa (a medição está na nota de `--color-erro`).

   Três formas, três estados, todas legíveis em escala de cinza: anel aqui,
   quadrado cheio no erro, círculo cheio no `.estado-processando`. O título por
   extenso continua sendo o canal que o leitor de tela recebe — o glifo é
   `content: ""` e não é anunciado. Issue #523. */
.estado-aviso { border-color: var(--color-atencao); }
.estado-aviso .estado-titulo::before {
  content: ""; display: inline-block; width: 8px; height: 8px;
  margin-right: var(--space-2); border-radius: 50%;
  border: 2px solid var(--color-atencao); box-sizing: border-box;
}

/* Variante de confirmação do bloco de estado: mesma caixa, régua marcada.

   Neutra, e não verde: o design-system não tem token de sucesso, e `.tag-ok`
   (linha 334) já resolve o mesmo problema com a rampa neutra, que inverte por
   inteiro em `[data-tema="escuro"]`. Criar o primeiro token semântico de
   sucesso do portal como efeito colateral de um conserto seria decidir design
   por acidente.

   O passo é o **600**, e não o 400 do esboço: a borda padrão de `.estado` é
   `--color-divider`, que resolve para ~`#9d9c9b` sobre a superfície clara. O
   400 (`#bab6b6`) é mais **claro** que isso — a variante ficaria mais apagada
   que a caixa neutra, que é o oposto de marcar. O 600 é mais forte que o
   divisor nos dois temas: `#7d7979` contra `#9d9c9b` no claro, `#8d8988`
   contra ~`#4b4948` no escuro. Issues #435 e #437. */
.estado-ok { border-color: var(--color-neutral-600); }

/* Lista de leitura corrida: a `<ul>` de responsabilidades do Aviso de
   Privacidade e a `<dl>` que explica os estados do diagnóstico. Uma classe para
   as duas porque o que elas pedem é o mesmo — o tipo pequeno do portal e um
   espaçamento previsível entre os itens.

   A classe era pedida pelos dois templates desde que existem e **não tinha
   regra em folha nenhuma**: achado da varredura da Issue #437.

   **O que faltava era respiro, não recuo**, e só a renderização lado a lado
   mostrou isso: o preflight do Tailwind (`opshub.css`) já zera marcador, recuo
   e margem de `ul`, `dl` e `dd`, então as duas listas não estavam caindo no
   padrão do navegador — estavam caindo no nada, com os itens colados. Uma
   primeira versão desta regra indentava a `<ul>`, o que daria recuo **sem**
   marcador, pior que as duas pontas. Marcador segue fora, como nas outras três
   listas desta folha (linhas 796, 1083 e 1115). */
.lista-simples { margin: var(--space-2) 0 0; font-size: 0.75rem; line-height: 1.55; }
.lista-simples li { margin-top: var(--space-1); }
.lista-simples dt { margin-top: var(--space-3); }
.lista-simples dt:first-child { margin-top: 0; }
.lista-simples dd { margin: var(--space-1) 0 0; }

/* Sobretítulo curto acima de um título de seção. */
.card-kicker {
  font-size: 0.625rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}

/* Gráfico do servidor, exibido enquanto o interativo não assume — e para sempre
   quando o JavaScript não roda. `aria-hidden` fica no template quando há
   alternativa textual ao lado. */
.grafico-fallback { display: block; max-width: 100%; }

/* ── Catálogo da Home, em três cartões ─────────────────────────────────────
   Issue #529, pelo mockup da #527. Substituiu onze cartões idênticos numa
   grade: o assunto vai ao topo do cartão e a contagem à direita, para
   responder "onde procuro" antes de "o que tem". A repartição é de
   `home.CARTOES_DO_CATALOGO`, com cobertura total conferida em teste. */
.grade-catalogo {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
  /* **`start`, e não o `stretch` padrão.** Os três cartões têm 4, 4 e 7 linhas,
     e esticados à altura do mais alto os dois primeiros ganham um vazio do
     tamanho de três linhas — que lê como conteúdo faltando, não como cartão
     menor. Cada um passa a ter a altura do que tem dentro. */
  align-items: start;
}
.cartao-catalogo {
  display: flex; flex-direction: column;
  border: 1px solid var(--color-divider); background: var(--color-surface);
  border-radius: var(--radius-lg); box-shadow: var(--elev-1); overflow: hidden;
}
.cartao-catalogo-topo {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  background: color-mix(in srgb, var(--color-text) 3%, var(--color-surface));
}
.cartao-catalogo-titulo {
  margin: 0; flex: 1;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--font-size-corpo); letter-spacing: 0.08em; text-transform: uppercase;
}
.cartao-catalogo-contagem {
  flex: none; font-size: var(--font-size-corpo);
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}
.lista-catalogo { margin: 0; padding: 0; list-style: none; }
.item-catalogo {
  display: flex; flex-direction: column; gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid color-mix(in srgb, var(--color-divider) 55%, transparent);
}
.item-catalogo:first-child { border-top: 0; }
.item-catalogo-topo { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.item-catalogo-nome {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--font-size-subtitulo); color: var(--color-text); text-decoration: none;
}
a.item-catalogo-nome:hover { color: var(--color-accent); text-decoration: underline; }
.item-catalogo-fonte {
  font-size: var(--font-size-nota);
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

/* ── A passada visual do 1.0 nos componentes que já existiam ───────────────
   Eles declaravam borda e fundo e **nunca** raio ou elevação. Issue #529. */
.bloco,
.bloco-restrito,
.cartao-filtros,
.estado,
.orientacoes { border-radius: var(--radius-lg); }

.bloco { box-shadow: var(--elev-1); }

.tira-kpi { border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--elev-1); }
/* `overflow: auto`, não `hidden`: qualquer valor diferente de `visible` já
   recorta no raio, e `hidden` anulava o `overflow-x: auto` da regra-base — a
   tabela larga era cortada em vez de rolar. Issue #582. */
.moldura-tabela { border-radius: var(--radius-md); overflow: auto; box-shadow: var(--elev-1); }

.btn,
.seg,
input[type="text"],
input[type="search"],
input[type="month"],
select { border-radius: var(--radius-sm); }

.ranking-trilho,
.ranking-barra { border-radius: 999px; }
.tag { border-radius: 999px; }

/* ── Etiqueta de estado do indicador ───────────────────────────────────────
   Issue #529, pelo mockup da #527 (`● atual`, `● crítico`). Diz em palavra, no
   topo do cartão, o que o corpo dele já dizia em símbolo — reticências,
   travessão ou "desativado" no lugar do número.

   **O ponto é `::before` e não caractere no template**, pelo mesmo motivo do
   `.selo-ambiente`: `●` colado no texto é lido em voz alta por leitor de tela
   como "círculo preto médio", e aqui ele é decoração de um rótulo que já diz
   tudo em palavra. Conteúdo gerado por CSS não entra na árvore de
   acessibilidade dos navegadores que importam a este parque.

   **Cor nunca é o único canal** (WCAG 1.4.1): a palavra é o canal, e a cor
   acompanha. `crítico` é o único que muda de matiz, porque é o único que pede
   ação. */
/* **`flex-wrap` e `overflow-wrap: normal` são os dois a não desfazer**, e o
   segundo é contraintuitivo. `.rotulo-campo` declara `overflow-wrap: anywhere`,
   e `anywhere` — ao contrário de `break-word` — **entra no cálculo do
   min-content**: o tamanho mínimo do rótulo passa a ser *um caractere*. Medido
   em 19/09/2026 com a escala de letra em 150%, que é o pior caso e não um
   detalhe (#300): `POSTOS ATIVOS` saía como `POSTO`/`S`/`ATIVO`/`S`, quatro
   linhas partidas no meio da palavra, porque a etiqueta ao lado tirou 61px da
   linha. Poder encolher não evita quebra — é a mesma lição da #300, e a
   mesma classe de defeito da #514, em que `anywhere` partia um numeral entre
   dois dígitos.

   Com `normal` a quebra volta a ser entre palavras, e com `wrap` a etiqueta
   desce para a linha seguinte quando não couber, em vez de espremer o rótulo.
   `anywhere` continua valendo fora daqui, que é onde ele protege de rótulo de
   palavra única estourando a caixa. */
.kpi-topo {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
.kpi-topo .rotulo-campo { flex: 1 1 auto; overflow-wrap: normal; }
.etiqueta-kpi {
  flex: none;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px 1px 5px; border-radius: 999px;
  font-size: var(--font-size-nota); font-weight: 600;
  letter-spacing: 0.02em; white-space: nowrap;
}
.etiqueta-kpi::before {
  content: ""; width: 5px; height: 5px; border-radius: 999px;
  background: currentColor; flex: none;
}
.etiqueta-kpi-atual {
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  background: color-mix(in srgb, var(--color-text) 7%, transparent);
}
.etiqueta-kpi-critica {
  color: var(--color-atencao);
  background: color-mix(in srgb, var(--color-atencao) 12%, transparent);
}
/* Os três estados de ausência compartilham a matiz esmaecida de propósito: a
   diferença entre eles está na palavra, e pintá-los em três cores diferentes
   sugeriria três graus de gravidade que não existem. `desativado` é o único
   permanente, e quem diz isso é o texto — aqui e na legenda da faixa.

   **A palavra é a mesma do corpo do cartão**, e ela já foi outra: a
   etiqueta nasceu dizendo "desligada" enquanto o valor do cartão dizia
   "desativado", duas grafias do mesmo fato no mesmo componente. Achado
   pela revisão de tela da #529; quem cedeu foi a etiqueta, porque
   `desativado` é a nomenclatura do estado desde a #330 e já está no
   corpo e na legenda. */
.etiqueta-kpi-preparando,
.etiqueta-kpi-ausente,
.etiqueta-kpi-desativada {
  color: color-mix(in srgb, var(--color-text) 45%, transparent);
  background: color-mix(in srgb, var(--color-text) 5%, transparent);
}
.etiqueta-kpi-desativada::before { background: none; box-shadow: inset 0 0 0 1px currentColor; }

/* ── Etiqueta de defasagem, no cabeçalho ───────────────────────────────────
   Issue #529. Substitui a faixa que abria o conteúdo de nove telas; o texto
   integral continua na página, no `sr-only` da macro. Ela herda o `sticky` do
   cabeçalho, então o aviso acompanha quem rola a tela — que a faixa não fazia.

   `--color-atencao` e mais o `::before`, pela mesma regra de dois canais: a
   palavra "atualizar" e a contagem carregam o sentido sem depender da cor. */
.etiqueta-defasagem {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 9px; border-radius: 999px;
  font-size: var(--font-size-nota); font-weight: 600;
  color: var(--color-atencao);
  background: color-mix(in srgb, var(--color-atencao) 12%, transparent);
  overflow-wrap: anywhere;
}
.etiqueta-defasagem::before {
  content: ""; width: 5px; height: 5px; border-radius: 999px;
  background: currentColor; flex: none;
}
.etiqueta-defasagem a { color: inherit; font-weight: 700; }

/* ══════════════════════════════════════════════════════════════════════════
   A tela de consulta pela prancha 09/10 — Issue #550

   A prancha descreve as sete telas de consulta de uma vez, porque as sete são
   o mesmo `templates/modulo.html`. O que muda aqui é composição: onde cada
   coisa fica e o que ela diz. Raio, elevação e tipografia já vieram na #529 e
   continuam saindo dos mesmos tokens.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Identidade da tela ────────────────────────────────────────────────────
   A etiqueta do grão passa a ser a primeira coisa da página, antes de qualquer
   pesquisa. Ela não é decoração: dizer "uma linha é uma pessoa" depois do
   resultado é dizer tarde. */
.identidade-da-tela {
  display: flex; flex-direction: column; gap: var(--space-3);
}
.identidade-da-tela-topo {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
.etiqueta-grao {
  display: inline-flex; align-items: center;
  padding: 3px 9px;
  font-size: var(--font-size-nota); font-weight: 600;
  color: var(--color-accent-800); background: var(--color-accent-100);
  border-radius: 999px;
}
.etiqueta-fonte {
  font-size: var(--font-size-nota);
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}

/* ── Cartão de filtros ─────────────────────────────────────────────────────
   `minmax(220px, …)` e não 170px: a 170 o cartão abria sete colunas numa tela
   de 1600px e o formulário virava uma tira de campos estreitos. Quatro por
   linha é o que a prancha desenha, e é o que 220px produz na largura útil do
   portal sem fixar contagem de coluna — que quebraria no celular. */
.cartao-filtros-grade {
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
/* Campo com valor. O reforço é **duplo** — borda de acento e fundo tênue —
   porque só a borda, sob a espessura de 1px deste sistema, some na varredura
   rápida que o cartão inteiro convida a fazer. */
.field-ativo > .input {
  border-color: var(--color-accent);
  background: var(--color-accent-100);
}
.cartao-filtros-rodape {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--color-divider);
}
.cartao-filtros-obrigacao {
  margin: 0; flex: 1 1 18rem; min-width: 0;
  font-size: var(--font-size-nota); text-wrap: pretty;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}
/* `margin-left: auto` mantém as duas ações à direita quando a frase encolhe, e
   `flex-wrap` acima garante que elas desçam inteiras em vez de espremer a
   frase até partir palavra — o modo de falha que a #300 mediu no cabeçalho. */
.cartao-filtros-acoes {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-left: auto; padding: 0;
}

/* ── Barra de resultado ────────────────────────────────────────────────────
   Contagem, chips do recorte e exportação numa linha só. */
.barra-de-resultado {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
}
.barra-de-resultado-contagem {
  margin: 0; font-size: var(--font-size-corpo); font-weight: 400;
  color: color-mix(in srgb, var(--color-text) 72%, transparent);
}
.barra-de-resultado-contagem strong {
  font-family: var(--font-heading); font-weight: 800;
  font-size: var(--font-size-destaque); color: var(--color-text);
}
.barra-de-resultado-posicao {
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.barra-de-resultado-acoes { margin-left: auto; }

/* ── Chips do recorte em vigor ─────────────────────────────────────────────
   Cada chip diz um filtro e o remove. O `×` é um `submit` do formulário de
   filtros — ver `macros.barra_de_resultado` —, então ele é um botão de
   verdade e herda foco e teclado do navegador. */
.chips-de-filtro {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin: 0; padding: 0; list-style: none; min-width: 0;
}
.chip-de-filtro {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 4px 2px 9px;
  font-size: var(--font-size-nota);
  border: 1px solid var(--color-divider); border-radius: 999px;
  background: var(--color-surface);
}
.chip-de-filtro-rotulo {
  color: color-mix(in srgb, var(--color-text) 58%, transparent);
}
.chip-de-filtro-valor {
  font-weight: 600; overflow-wrap: anywhere;
}
.chip-de-filtro-remover {
  display: inline-flex; align-items: center; justify-content: center;
  position: relative;
  width: 1.125rem; height: 1.125rem; padding: 0;
  font: inherit; line-height: 1; cursor: pointer;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
  background: transparent; border: 0; border-radius: 999px;
}
/* O alvo de toque leva o botão de 18px a 44px sem inflar o chip, do mesmo jeito
   que `.botao-menu` faz com o botão da barra lateral.

   **A conta é a guarda: 18 + 13 + 13 = 44.** Esta regra nasceu com
   `-0.75rem`, que dá 42px, sob um comentário afirmando 44 — a revisão de tela
   da #550 apanhou a diferença entre o texto e a aritmética. Os 44px são a WCAG
   2.5.5 (AAA); o mínimo exigível é 24px, da 2.5.8, e os 18px do glifo sozinho
   ficavam abaixo até dele. */
.chip-de-filtro-remover::after { content: ""; position: absolute; inset: -0.8125rem; }
.chip-de-filtro-remover:hover {
  color: var(--color-bg); background: var(--color-atencao);
}

/* ── O quadro da grade ─────────────────────────────────────────────────────
   A moldura rola na horizontal; o quadro em volta, não. É ele que carrega raio
   e elevação, e a moldura os perde por dentro para os dois lerem como uma peça
   só — senão o rodapé apareceria como um segundo cartão colado embaixo. */
.quadro-da-grade {
  border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--elev-1);
}
.quadro-da-grade > .moldura-tabela { border-radius: 0; box-shadow: none; }
.quadro-da-grade > .navegacao-de-paginas {
  margin-top: 0; padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-divider);
  background: var(--color-surface);
}

/* ── Notas da consulta ─────────────────────────────────────────────────────
   As três do pé da tela. Elas usam a mesma dupla de papéis do `.aviso` — traço
   neutro e traço de atenção —, e não uma paleta nova: verde e azul do mockup
   entrariam como tokens de estado que este portal ainda não mediu, e a #523
   deixou escrito o custo de tratar cor como se fosse de graça. */
.notas-da-consulta {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-3);
}
.nota-da-consulta {
  border: 1px solid var(--color-divider);
  border-left: 3px solid var(--color-divider);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
}
.nota-da-consulta h3 {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-nota); letter-spacing: 0.02em;
}
.nota-da-consulta p {
  margin: 0; font-size: var(--font-size-nota); text-wrap: pretty;
  color: color-mix(in srgb, var(--color-text) 68%, transparent);
}
.nota-da-consulta p + p { margin-top: var(--space-2); }
.nota-da-consulta-atencao { border-left-color: var(--color-atencao); }
.nota-da-consulta-atencao h3 { color: var(--color-atencao); }
