/* ═══════════════════════════════════════════════════════════════
   ROTIER CITIES — DESIGN SYSTEM
   Combinação A (Cívica Clara · light) + B (Técnica Dual · dark)
   Camada visual sobre Bootstrap 5.
   ═══════════════════════════════════════════════════════════════ */

/* ── Tokens ──────────────────────────────────────────────────── */
:root {
  /* --- Brand (sage Rotier Geotecnologias — ver docs/rotier-design-system) --- */
  --brand-50:  #eef3ec;
  --brand-100: #dae8d4;  /* sage-pale */
  --brand-200: #bacbb0;  /* sage-light */
  --brand-300: #8fa882;  /* sage */
  --brand-400: #7a8c74;  /* sage-mid */
  --brand-500: #5e7257;  /* sage-dark — accent/ação principal */
  --brand-600: #52634c;
  --brand-700: #45533f;
  --brand-800: #3a4636;
  --brand-900: #2f3835;  /* charcoal-dark */

  /* --- Técnico (azul/ciano) --- */
  --tech-blue:   #3b82f6;
  --tech-blue-2: var(--info);
  --tech-cyan:   #06b6d4;
  --tech-cyan-2: #0891b2;

  /* --- Acentos --- */
  --accent-warm:  #d97742;
  --accent-red:   #b8391f;
  --accent-amber: #eab308;

  /* --- Status --- */
  --ok:     #5a8a5a;
  --warn:   #d97742;
  --danger: #b8391f;
  /* `--danger-subtle` era referenciado em `app_shell.css` e nunca definido em
     lugar nenhum: o que pintava o hover era a reserva do `var()`, um
     `rgba(184,57,31,.08)` inventado ali. Reserva que sempre entra não é
     reserva, é o valor — escondido de quem procura a paleta. O tom vem do
     `--color-error-subtle` da fonte canônica de tokens. */
  --danger-subtle: #F5DCDC;
  --info:   #3b82f6;

  /* --- Primeiro plano sobre preenchimento saturado ---
     Texto e ícone dentro de botão, badge sólido ou chip de cor. **Não** é
     `--surface`: no tema escuro a superfície vira `#172027`, e trocar um pelo
     outro deixaria o texto escuro sobre fundo escuro. Branco sobre sage ou
     sobre vermelho continua legível nos dois temas, então este token não é
     sobrescrito no escuro — de propósito. */
  --on-accent: #ffffff;

  /* --- Tinta de marca (translúcida) ---
     **Isto conserta um bug, não é arrumação.** `base_gov.html` sobrescreve
     `--brand-500` com a `cor_primaria` do município (Regra do Tenant), mas
     havia dez `rgba(94,114,87,…)` com o RGB do sage em número cru. No dia em
     que um município escolhesse a cor dele, o anel de foco, a sombra dos botões
     e o fundo dos chips continuariam verde-sage enquanto o resto da interface
     virasse a cor dele.

     Está invisível hoje só porque o município no ar usa exatamente o `#5e7257`
     padrão — e é justamente esse tipo de defeito que dorme até o primeiro
     cliente que exerce a configuração.

     Nomeados por papel, não por alfa: `--brand-a10` diria o valor e esconderia
     a intenção. O valor literal fica como piso, e o `@supports` abaixo troca
     por `color-mix`, que lê o token e portanto acompanha o município. */
  --brand-wash:    rgba(94, 114, 87,.07);  /* fundo de pílula em hover/ativa */
  --brand-tint:    rgba(94, 114, 87,.10);  /* fundo de chip de marca */
  --brand-line:    rgba(94, 114, 87,.30);  /* borda de chip de marca */
  --brand-glow:    rgba(94, 114, 87,.45);  /* sombra de botão preenchido */
  --brand-glow-hi: rgba(94, 114, 87,.50);  /* idem, em hover */
  --brand-focus:   rgba(94, 114, 87,.85);  /* anel de foco — .85 é o que passa 3:1 */

  /* --- Status: par suave (badge, alert) ---
     Os valores são exatamente os que já estavam literais nas famílias
     `.badge.*` e `.alert-*`, que os duplicavam entre si. Existem como token
     porque ninguém podia responder "qual a cor de fundo de um aviso?" sem
     copiar hex de outra regra. */
  --st-ok-bg:      #e8f3ed;
  --st-ok-fg:      var(--brand-700);
  --st-ok-bd:      #c9e3d2;
  --st-warn-bg:    #fbeadb;
  --st-warn-fg:    #8b4a20;
  --st-warn-bd:    #f0d5b5;
  --st-danger-bg:  #f6dbd4;
  --st-danger-fg:  #7a240f;
  --st-danger-bd:  #ecb9a8;
  --st-info-bg:    #e0ecfd;
  --st-info-fg:    #1e4a99;
  --st-info-bd:    #bfd6fa;

  /* --- Status: par saturado (chip de ícone dos KPIs) ---
     Escala **distinta** da de cima, e continua distinta de propósito: o chip
     do KPI carrega mais peso que um badge inline. Unificar as duas mudaria
     pixel em todo painel e é decisão à parte. */
  --kpi-ok-bg:       #d3efd6;  --kpi-ok-fg:       #1f7a37;
  --kpi-warn-bg:     #fbe2bf;  --kpi-warn-fg:     #a05c0d;
  --kpi-danger-bg:   #f7d4ca;  --kpi-danger-fg:   #a63122;
  --kpi-neutral-bg:  #e2e6ea;  --kpi-neutral-fg:  #44555f;
  --kpi-info-bg:     #d2ebe5;  --kpi-info-fg:     #0f6e62;
  --kpi-plum-bg:     #e8dcf0;  --kpi-plum-fg:     #6a3d8a;
  --kpi-gold-bg:     #f5e7c0;  --kpi-gold-fg:     #8a6a12;
  --kpi-blue-bg:     #d8e6f2;  --kpi-blue-fg:     #245d8a;
  --kpi-accent-bg:   #dcecd0;  --kpi-accent-fg:   #45662d;

  /* --- Neutrals (light — cream/charcoal Rotier) --- */
  --bg:        #f4f1ec;  /* cream */
  --bg-elev:   #faf8f3;
  --surface:   #ffffff;
  --surface-2: #f4f1ec;
  --surface-3: #eae6de;  /* cream-mid */
  --border:    #d8d2c6;  /* cream-dark */
  --border-2:  #eae6de;
  --fg:        #2f3835;  /* charcoal-dark */
  --fg-2:      #4a5651;  /* charcoal */
  --muted:     #6b7a74;  /* charcoal-light */
  --muted-2:   #8a847a;  /* stone-dark */

  /* --- Bootstrap overrides (mapeia brand → bs-primary) --- */
  --bs-primary:     var(--brand-500);
  --bs-primary-rgb: 94, 114, 87;

  /* --- Tipografia (DM Sans / DM Mono — Rotier Geotecnologias) --- */
  --app-font:      'DM Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  --app-font-mono: 'DM Mono', 'Geist Mono', ui-monospace, monospace;
  --font-display:  'Space Grotesk', 'DM Sans', system-ui, sans-serif;

  /* --- Serifa de manchete (direção editorial, DESIGN.md §3.1) ---
     **O token existe; a face não é carregada globalmente.** `base_gov.html` não
     a traz de propósito: a serifa vale só em manchete de tela de leitura e
     decisão, e cobrar uma terceira família de toda tela — inclusive das de
     conferência, que nunca a usam — é peso de rede sem contrapartida.

     Quem usa, carrega: a tela editorial põe o `<link>` da família no
     `{% block css %}` dela. Usar este token sem carregar a face **não degrada
     com elegância** — cai no Georgia do sistema, que é outra tipografia e
     parece decisão.

     Vale **apenas em manchete**: corpo é DM Sans, dado técnico é DM Mono. São
     três famílias, e a terceira precisa ser rara para valer.

     Peso 400 em toda a escala é a assinatura da direção, não economia de
     carregamento: a serifa sussurra autoridade em vez de gritar em negrito.
     Não existe `600` desta família no projeto — se alguém pedir, o navegador
     sintetiza um falso-negrito, que é pior que o problema que tentava resolver. */
  --font-serif:    'Source Serif 4', ui-serif, Georgia, 'Times New Roman', serif;

  /* --- Font sizes ---
     `--fs-2xs` e `--fs-nav` não são degraus novos inventados: são os dois
     tamanhos mais usados da faixa 10–13px, que o produto já usava sem que a
     escala os declarasse. Havia sete tamanhos em uso onde a escala descrevia
     dois — o problema não era desvio, era ausência de decisão. */
  --fs-3xs:  8px;     /* rótulo de seção da barra lateral, e só ele */
  --fs-2xs:  10px;    /* rótulo de eixo, legenda de gráfico */
  --fs-xs:   11px;
  --fs-sm:   12.5px;
  --fs-nav:  13px;    /* item de menu, rótulo de tabela densa */
  --fs-base: 14px;
  --fs-md:   15.5px;
  --fs-lg:   18px;
  --fs-xl:   22px;
  --fs-2xl:  28px;
  --fs-3xl:  36px;

  /* --- Escala editorial (direção do DESIGN.md §3.1) ---
     **Não substitui a `--fs-*` acima, e isto é decisão, não transição pela
     metade.** A direção editorial tem duas densidades por desenho: a folgada,
     para telas de leitura e decisão, e a compacta, para mesa de conferência —
     planilha, boletim, listagem financeira. A Regra da Mesa de Conferência
     nasceu de medição: com sete colunas numéricas em 1600px, a densidade
     folgada empurra a coluna do total para fora do campo de visão.

     Uma escala só obrigaria a escolher entre uma tela bonita e uma tela que se
     confere. Duas escalas nomeadas dizem qual usar e quando: `--fs-*` continua
     sendo o padrão do produto e a única permitida em mesa de conferência;
     `--fe-*` vale onde a tela é para ler e decidir. */
  --fe-caption:  15px;
  --fe-body:     17px;   /* corpo da tela editorial */
  --fe-body-lg:  20px;
  --fe-sub:      22px;   /* mesmo degrau da --fs-xl, de propósito */
  --fe-title:    26px;
  --fe-heading:  44px;   /* manchete de tela — serifa 400 */
  --fe-display:  64px;   /* manchete de abertura — serifa 400 */

  /* --- Spacing (4px grid) --- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;
  --sp-10: 40px;
  --sp-12: 48px;

  /* --- Radius --- */
  /* Peça de espessura de traço: tarja de item ativo, tarja do KPI, barra do
     medidor. Não é `--r-xs` arredondado — a barra tem 4px de altura, e 4px de
     raio a tornariam cápsula. Racional em DESIGN.md §7.6. */
  --r-2xs:  2px;
  --r-xs:   4px;
  --r-sm:   6px;
  --r-md:   8px;
  --r-lg:   10px;
  --r-xl:   14px;
  --r-2xl:  18px;
  --r-full: 999px;

  /* --- Legados (compatibilidade) --- */
  --app-radius:    var(--r-xl);
  --app-radius-sm: var(--r-md);
  --app-radius-xs: var(--r-xs);

  /* --- Superfícies escuras ---
     O produto é claro; duas superfícies fogem disso por necessidade, e cada
     uma tem um motivo diferente. Ficam aqui, nomeadas, porque estavam literais
     e espalhadas — e literal espalhado é o estado em que uma correção acerta
     metade dos lugares.

     **Casco.** A barra superior e o que abre a partir dela. Uma base e três
     opacidades, não quatro cores: havia `rgba(8,12,24,α)` na barra e
     `rgba(10,15,30,α)` no dropdown, dois azuis que ninguém distingue e que
     toda correção futura teria de aplicar duas vezes. O dropdown e o menu
     expandido também tinham .96 e .97, distinção sem razão — agora é um
     degrau só. */
  --casco-rgb:    8 12 24;
  --casco-barra:  rgb(var(--casco-rgb) / .92);
  --casco-vidro:  rgb(var(--casco-rgb) / .88);  /* barra rolada: mais translúcida */
  --casco-painel: rgb(var(--casco-rgb) / .96);  /* dropdown e menu expandido */

  /* **Sombra de peça sage preenchida.** Verde escuro, e não o preto da escala
     geral nem o `--brand-glow`: são jobs diferentes. O glow é um halo da cor da
     marca ao redor do botão; isto é a sombra que a peça projeta no cream. Havia
     três alfas literais (.20, .28, .30) da mesma cor em três lugares — o .30
     entra no degrau do .28, porque dois pontos de opacidade não são uma decisão
     de projeto, são uma digitação. */
  --brand-drop-rgb: 20 82 58;
  --brand-drop-sm:  rgb(var(--brand-drop-rgb) / .20);  /* peça pequena: marca, switch */
  --brand-drop:     rgb(var(--brand-drop-rgb) / .28);  /* botão, aba ativa */

  /* **Overlay sobre o mapa.** O único desfoque escuro permitido (DESIGN.md
     §4), porque é o único lugar com mapa embaixo para desfocar. Estava literal
     em onze pontos entre `design.css`, `app_shell.css` e `webgis.css`; o
     documento já o descrevia, faltava o token que o entrega. */
  --overlay-mapa:    rgba(17,24,32,.92);
  --overlay-mapa-fg: #e6edf3;

  /* --- Sombras ---
     Valores do Shadow Vocabulary do DESIGN.md §4, e não uma segunda escala.

     Existiam duas: esta, em uso, com preto puro e blur próprio; e
     `--shadow-xs..xl` em `docs/rotier-design-system/project/colors_and_type.css`,
     canônica, com a base verde e nunca referenciada por ninguém. O sistema
     documentado não estava implementado, e a "Regra da Sombra Verde" — toda
     sombra sobre rgba(42,50,45,α), nunca preto — era violada pelo próprio
     token que todo mundo consome.

     Alinhar aqui conserta os consumidores de uma vez: app_shell, webgis,
     landing e admin_theme. A sombra fica um pouco mais presente (α de .04–.12
     para .08–.16) e esverdeada, que é o que o documento sempre descreveu. */
  --sh-xs: 0 1px 3px rgba(42,50,45,.08);
  --sh-sm: 0 2px 8px rgba(42,50,45,.10);
  --sh-md: 0 4px 16px rgba(42,50,45,.12);
  --sh-lg: 0 8px 32px rgba(42,50,45,.14);
  --sh-xl: 0 16px 48px rgba(42,50,45,.16);
  /* Duas camadas, e as duas são necessárias (WCAG 2.1 SC 1.4.11 pede 3:1):
     2px de `--surface` colados no botão separam o anel do preenchimento —
     sem esse vão, o anel sage sobre o `.btn-primary` sage some; 4px de
     `--brand-focus` fazem o contraste contra o fundo da página.
     MEDIDO no pixel renderizado, não estimado: o anel antigo (3px a 25%)
     dava 1,36:1 contra o fundo, e a única outra pista era a borda
     escurecendo a 2,99:1 — reprovava por pouco no melhor sinal e por muito
     no indicador dedicado. A 85%: 3,83:1 no branco, 3,59:1 no creme. */
  --sh-focus: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand-focus);
  --sh-focus-tech: 0 0 0 3px rgba(59,130,246,.3);

  /* --- Legados sombra --- */
  --app-shadow-xs: var(--sh-xs);
  --app-shadow-sm: var(--sh-sm);
  --app-shadow:    var(--sh-md);
  --app-shadow-md: var(--sh-lg);
  --app-shadow-lg: var(--sh-xl);

  /* --- Motion --- */
  --dur-fast:   120ms;
  --dur-base:   180ms;
  --dur-slow:   280ms;
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --app-ease:   var(--ease-out);
  --app-duration: var(--dur-base);
  --app-transition: var(--dur-base) var(--ease-out);

  /* --- Layout --- */
  --header-h: 72px;
  --z-sticky:  10;
  --z-overlay: 40;
  --z-modal:   60;
  --z-toast:   80;
  --z-tooltip: 100;

  /* --- Map --- */
  --map-control-size: 36px;
}

/* ── Dark mode (tela do mapa / operacional) ───────────────────── */
.theme-dark,
/* ── A tinta de marca segue o município ───────────────────────────────────
   `color-mix` lê `var(--brand-500)`, que o shell sobrescreve com a
   `cor_primaria` da prefeitura. Com o `rgba()` literal acima como piso, a
   tinta continuava sage para sempre.

   Vive num `@supports` de propósito: sem `color-mix` (Chrome < 111, Safari <
   16.2, Firefox < 113) a declaração seria inválida, o token resolveria para
   vazio e o **anel de foco desapareceria** — regressão de acessibilidade, não
   de estilo. Assim o navegador antigo fica com o sage literal, que é o
   comportamento de hoje, e o moderno ganha a cor do município.

   `in srgb` com alfa pré-multiplicado: misturar com `transparent` devolve a
   cor no alfa pedido, sem escurecer. */
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  :root {
    --brand-wash:    color-mix(in srgb, var(--brand-500)  7%, transparent);
    --brand-tint:    color-mix(in srgb, var(--brand-500) 10%, transparent);
    --brand-line:    color-mix(in srgb, var(--brand-500) 30%, transparent);
    --brand-glow:    color-mix(in srgb, var(--brand-500) 45%, transparent);
    --brand-glow-hi: color-mix(in srgb, var(--brand-500) 50%, transparent);
    --brand-focus:   color-mix(in srgb, var(--brand-500) 85%, transparent);
  }

  /* Camada de status: as famílias que derivam da marca acompanham também.
     `warn`, `danger` e `info` não derivam de `--brand-500` — são matizes
     próprios do sistema e continuam literais de propósito. */
  [data-theme="dark"] {
    --st-ok-bg:    color-mix(in srgb, var(--brand-500) 15%, transparent);
    --st-ok-bd:    color-mix(in srgb, var(--brand-500) 30%, transparent);
    --kpi-ok-bg:   color-mix(in srgb, var(--brand-500) 18%, transparent);
    --brand-line:  color-mix(in srgb, var(--brand-500) 30%, transparent);
    --brand-focus: color-mix(in srgb, var(--brand-500) 85%, transparent);
  }
}

[data-theme="dark"] {
  --bg:        #0b0f14;
  --bg-elev:   #111820;
  --surface:   #172027;
  --surface-2: #1d2830;
  --surface-3: #222d38;
  --border:    #222d38;
  --border-2:  #2d3a47;
  --fg:        #e6edf3;
  --fg-2:      #b4c0cc;
  --muted:     #7b8a99;
  --muted-2:   #5a6876;

  --brand-500: #8fa882;  /* sage — mais claro p/ dark mode */
  --brand-800: #bacbb0;  /* sage-light */

  /* Status no escuro — mesma receita que `.badge.*` já usava no ar: fundo em
     rgba da cor a ~15% e texto numa versão clara do matiz. Antes essa receita
     vivia duplicada em quatro regras `[data-theme="dark"] .badge.*`; agora é
     o token que muda, e todo mundo que o usa acompanha.

     Os chips de KPI **ganham tema escuro aqui pela primeira vez**. Eles tinham
     fundo pálido fixo, então no escuro ficavam creme e lavanda sobre uma
     superfície `#172027`. Não era escolha: era ausência de regra. */
  --st-ok-bg:      rgba(94,114,87,.15);   --st-ok-fg:      #6cba8a;
  --st-ok-bd:      rgba(94,114,87,.30);
  --st-warn-bg:    rgba(234,179,8,.12);   --st-warn-fg:    #eab308;
  --st-warn-bd:    rgba(234,179,8,.30);
  --st-danger-bg:  rgba(184,57,31,.15);   --st-danger-fg:  #ef6b4b;
  --st-danger-bd:  rgba(184,57,31,.30);
  --st-info-bg:    rgba(59,130,246,.15);  --st-info-fg:    #60a5fa;
  --st-info-bd:    rgba(59,130,246,.30);

  --kpi-ok-bg:       rgba(94,114,87,.18);   --kpi-ok-fg:       #6cba8a;
  --kpi-warn-bg:     rgba(217,119,66,.18);  --kpi-warn-fg:     #e0a56f;
  --kpi-danger-bg:   rgba(184,57,31,.18);   --kpi-danger-fg:   #ef6b4b;
  --kpi-neutral-bg:  rgba(123,138,153,.18); --kpi-neutral-fg:  #a8b6c2;
  --kpi-info-bg:     rgba(6,182,212,.16);   --kpi-info-fg:     #5ec8d8;
  --kpi-plum-bg:     rgba(139,92,180,.18);  --kpi-plum-fg:     #c3a3dd;
  --kpi-gold-bg:     rgba(234,179,8,.16);   --kpi-gold-fg:     #d9bc5a;
  --kpi-blue-bg:     rgba(59,130,246,.18);  --kpi-blue-fg:     #7db3f0;

  --sh-sm: 0 1px 2px rgba(0,0,0,.3);
  --sh-md: 0 4px 12px rgba(0,0,0,.35);
  --sh-lg: 0 12px 32px rgba(0,0,0,.45);
  --sh-xl: 0 24px 56px rgba(0,0,0,.6);
  /* Mesma opacidade do claro (.85): a 35% dava 1,93:1 contra `--surface`,
     abaixo dos 3:1 do WCAG 2.1 SC 1.4.11. A 85% dá 4,98:1. */
  --brand-focus: rgba(143,168,130,.85);
  --brand-line:  rgba(143,168,130,.30);
  --sh-focus: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand-focus);
  --sh-focus-tech: 0 0 0 3px rgba(59,130,246,.4);
}

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

html { scroll-behavior: smooth; }

body {
  font-family: var(--app-font);
  font-size: var(--fs-base);
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "ss01", "cv11";
  padding-top: var(--header-h);
}

::selection { background: var(--brand-200); color: var(--brand-900); }
[data-theme="dark"] ::selection { background: var(--brand-line); color: var(--fg); }

button, input, select, textarea { font: inherit; color: inherit; }

/* Preloader — estilos críticos estão inline no base.html para garantir
   exibição imediata antes do carregamento deste arquivo. */

/* ── Typography ──────────────────────────────────────────────── */
h1,h2,h3,h4,h5,h6 {
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--fg);
}

.mono { font-family: var(--app-font-mono); }

/* Degraus micro da escala, como classe.
   Existiam só como token, e por isso apareciam como `style="font-size: 10px"`
   embutido no HTML — o número certo, escrito no lugar errado: quem ajusta a
   escala depois muda o token e não alcança o que está grudado na marcação. */
.fs-2xs { font-size: var(--fs-2xs); }
.fs-3xs { font-size: var(--fs-3xs); }

.text-muted { color: var(--muted) !important; }
.text-brand { color: var(--brand-500) !important; }
.text-tech  { color: var(--tech-blue) !important; }

/* ── Header / Navbar ─────────────────────────────────────────── */
#header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1030;
  height: var(--header-h);
  padding: 10px 16px 0;
  pointer-events: none; /* permite clicar no conteúdo sob o gap */
}

/* Sem glassmorphism. O PRODUCT.md o lista como anti-referência nominal, e a
   exceção do DESIGN.md é só para painel sobre mapa — esta barra flutua sobre
   fundo cream LISO, onde `blur(20px)` não tem o que desfocar.

   Não foi só higiene: `rgba(8,12,24,.55)` sobre cream dava um cinza médio, e
   texto branco ali fica na fronteira de 4,5:1. Opaco, passa folgado. */
.app-navbar {
  height: 56px;
  pointer-events: all;
  background: var(--casco-barra);
  border: 1px solid rgba(255,255,255,.1);
  border-radius: var(--r-2xl);
  padding: 0 8px;
  transition: background var(--app-transition), box-shadow var(--app-transition),
              border-color var(--app-transition);
}

.app-navbar.scrolled {
  background: var(--casco-vidro);
  border-color: rgba(255,255,255,.14);
  box-shadow: 0 8px 32px rgba(0,0,0,.35), 0 1px 0 rgba(255,255,255,.06) inset;
}

.app-navbar .container-xl { height: 100%; }

.app-navbar .navbar-brand img { height: 32px; width: auto; }
.app-navbar .navbar-brand span {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--on-accent);
  letter-spacing: .01em;
}

/* Links nav */
.app-navbar .nav-link {
  color: rgba(255,255,255,.7) !important;
  font-size: var(--fs-nav);
  font-weight: 500;
  padding: .35rem .7rem !important;
  border-radius: var(--r-md);
  position: relative;
  transition: color var(--app-transition), background var(--app-transition);
  letter-spacing: .01em;
}
.app-navbar .nav-link:hover {
  color: rgba(255,255,255,.95) !important;
  background: rgba(255,255,255,.08);
}
.app-navbar .nav-link.active {
  color: var(--on-accent) !important;
  background: rgba(255,255,255,.1);
}
/* Acento verde embaixo do link ativo */
.app-navbar .nav-link.active::after {
  content: '';
  position: absolute;
  bottom: 2px; left: 50%;
  transform: translateX(-50%);
  width: 16px; height: 2px;
  background: var(--brand-400);
  border-radius: var(--r-full);
}

/* Separador vertical entre nav e actions */
.navbar-sep {
  width: 1px;
  height: 20px;
  background: rgba(255,255,255,.12);
  margin: 0 6px;
  align-self: center;
  flex-shrink: 0;
}

/* Dropdown */
.app-navbar .dropdown-menu {
  background: var(--casco-painel);
  border: 1px solid rgba(255,255,255,.1);
  border-radius: var(--r-lg);
  padding: .4rem;
  box-shadow: var(--sh-xl), 0 0 0 1px rgba(255,255,255,.04) inset;
  margin-top: .5rem !important;
}
.app-navbar .dropdown-item {
  color: rgba(255,255,255,.75);
  font-size: var(--fs-nav);
  border-radius: var(--r-sm);
  padding: .42rem .7rem;
  transition: background var(--app-transition), color var(--app-transition);
}
.app-navbar .dropdown-item:hover {
  background: rgba(255,255,255,.1);
  color: var(--on-accent);
}
.app-navbar .dropdown-divider { border-color: rgba(255,255,255,.1); }

/* CTA mapa */
.btn-nav-map {
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: .38rem 1rem;
  border-radius: var(--r-full);
  background: var(--brand-500);
  border: none;
  color: var(--on-accent) !important;
  letter-spacing: .02em;
  transition: background var(--app-transition), transform var(--app-transition),
              box-shadow var(--app-transition);
  box-shadow: 0 2px 10px var(--brand-glow);
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  text-decoration: none;
}
.btn-nav-map:hover {
  background: var(--brand-600);
  transform: translateY(-1px);
  box-shadow: 0 4px 18px var(--brand-glow-hi);
  color: var(--on-accent) !important;
}

/* Toggle mobile */
.app-navbar .navbar-toggler {
  border: 1px solid rgba(255,255,255,.15);
  border-radius: var(--r-md);
  padding: .28rem .5rem;
  color: rgba(255,255,255,.85);
  transition: background var(--app-transition);
}
.app-navbar .navbar-toggler:hover  { background: rgba(255,255,255,.08); }
.app-navbar .navbar-toggler:focus  { box-shadow: none; }

/* Menu mobile expandido */
@media (max-width: 991.98px) {
  #header { padding: 10px 12px 0; }

  .app-navbar .navbar-collapse {
    background: var(--casco-painel);
    border-top: 1px solid rgba(255,255,255,.08);
    margin: 6px -8px 0;
    padding: .6rem 1rem 1rem;
    border-radius: 0 0 var(--r-2xl) var(--r-2xl);
    box-shadow: 0 16px 32px rgba(0,0,0,.5);
  }

  .navbar-sep { display: none; }
}

/* ── Offline Banner ──────────────────────────────────────────── */
#offline-banner {
  font-size: var(--fs-nav);
  background: linear-gradient(90deg,var(--warn),var(--danger)) !important;
  color: var(--on-accent) !important;
  border: none !important;
}

/* ── Layout ──────────────────────────────────────────────────── */
.main { min-height: calc(100vh - var(--header-h)); }

/* ── Logo mark ───────────────────────────────────────────────── */
.logo {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--app-font); font-weight: 600;
  font-size: var(--fs-lg); letter-spacing: -.01em;
  color: var(--fg); text-decoration: none;
}
.logo .mark {
  width: 28px; height: 28px; border-radius: var(--r-md);
  background: var(--brand-500);
  display: grid; place-items: center;
  color: var(--on-accent); font-size: var(--fs-base); font-weight: 700;
  position: relative;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px var(--brand-drop-sm);
}
.logo .mark::after {
  content: ''; position: absolute; inset: 6px;
  border: 1.5px solid rgba(255,255,255,.85);
  border-radius: var(--r-xs);
  transform: rotate(45deg);
  box-sizing: border-box;
}

/* ── Page title ──────────────────────────────────────────────── */
.page-title {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 1rem 0;
}
.page-title h1 {
  font-size: var(--fs-lg);
  font-weight: 700;
  color: var(--fg);
  margin: 0;
}

/* ── Section ─────────────────────────────────────────────────── */
.section { padding: 1.5rem 0 2.5rem; }

/* ── Cards ───────────────────────────────────────────────────── */
.card {
  border-radius: var(--r-xl) !important;
  border: 1px solid var(--border) !important;
  background: var(--surface);
  box-shadow: var(--sh-sm) !important;
  transition: box-shadow var(--app-transition), transform var(--app-transition);
}

.card-hover:hover {
  box-shadow: var(--sh-md) !important;
  transform: translateY(-2px);
}

.card-header {
  background: var(--surface) !important;
  border-bottom: 1px solid var(--border) !important;
  border-radius: calc(var(--r-xl) - 1px) calc(var(--r-xl) - 1px) 0 0 !important;
  padding: .85rem 1.1rem;
  font-weight: 600;
  font-size: var(--fs-base);
}

.card-footer {
  background: var(--surface-2) !important;
  border-top: 1px solid var(--border) !important;
  border-radius: 0 0 calc(var(--r-xl) - 1px) calc(var(--r-xl) - 1px) !important;
  padding: .75rem 1.1rem;
}

/* A tarja lateral saiu (§8.3). `border-left: 3px` colorido como acento é o
   adorno mais associado a interface montada sem critério — e, pior que isso,
   não informa nada: quem olha não sabe o que a listra distingue. A ênfase passa
   a ser o fundo tonal, que separa o cartão do resto da página sem fingir que
   carrega significado. A classe fica, para não quebrar as telas que a usam. */
.card-accent-left {
  background: color-mix(in srgb, var(--brand-500) 5%, var(--surface)) !important;
  border: 1px solid var(--border) !important;
}

/* ── Cabeçalho de página (card "Gestão de …") ──────────────────────────────
   Faixa de abertura das telas de módulo: o que é esta tela, o número que
   resume o momento e as ações principais. Substitui o background inline que
   existia só no painel de obras. */
.page-hero {
  /* Branco, e não o creme `--surface-2`: a moldura já separa o herói do fundo
     da página (que é creme), e um creme sobre creme dava uma diferença de
     tom tão pequena que a moldura parecia sombra em vez de cartão. Branco é o
     mesmo papel dos outros cartões da tela — o herói passa a ser um cartão
     entre cartões, o primeiro deles. */
  background: var(--surface);
  border: 1px solid var(--border);
}
/* Título em cima, ações embaixo, tudo encostado À ESQUERDA.
   Era `space-between`: o título ficava na esquerda e as ações voavam para a
   borda direita da tela. Em monitor largo isso põe a ação primária a 1200px do
   título que a explica, e o olho tem de atravessar o vazio para ligar as duas
   coisas. Empilhado, a leitura é uma coluna: onde estou, o que é, o que posso
   fazer. Custa altura, que a página tem, em vez de distância, que ela não
   perdoa. */
.page-hero .page-hero-body {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: .8rem;
}
.page-hero h1, .page-hero .page-hero-titulo {
  /* `--fs-xl` (22px) e não os 1.25rem antigos: o herói existe para o título ser
     o primeiro a ser lido, e em 20px ele empatava com os `<h6>` de seção que
     vêm logo abaixo. 20px também não era degrau da rampa — a escala vai
     18 → 22 → 28, e o token evita reabrir essa discussão na próxima tela. */
  font-size: var(--fs-xl); font-weight: 700; color: var(--fg); margin: 0 0 .15rem;
  letter-spacing: -.01em;
}
.page-hero .page-hero-sub { font-size: var(--fs-nav); color: var(--muted); }
.page-hero .page-hero-acoes { display: flex; flex-wrap: wrap; gap: .5rem; }

@media (max-width: 575.98px) {
  /* No celular as ações ocupam a linha inteira, na ordem em que aparecem —
     botão de tamanhos diferentes na mesma faixa fica desalinhado. */
  .page-hero .page-hero-acoes { width: 100%; }
  .page-hero .page-hero-acoes > .btn { flex: 1 1 100%; justify-content: center; }
}

/* ── KPI — cartão canônico ÚNICO (Terreno Firme) ─────────────────
   Estrutura: .kpi-card > .kpi-icon (tint) + .kpi-value + .kpi-label.
   Detalhe dos elementos internos e tints mais abaixo ("KPI card inner elements").
   Faixa pronta de 4 cards: {% include 'core/_kpi_strip.html' with kpis=lista %}.
   Padrões antigos .stat-card e .kpi bare foram removidos — não reintroduzir. */
.kpi-card {
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--sh-sm);
  transition: box-shadow var(--app-transition), transform var(--app-transition);
  /* Coluna: ícone no topo, valor+rótulo ancorados na base → cartões alinham
     entre si mesmo quando o rótulo quebra em 2 linhas. */
  display: flex;
  flex-direction: column;
  min-height: 148px;
}
.kpi-card:hover { box-shadow: var(--sh-md); transform: translateY(-2px); }

/* KPI que navega. O número é a pergunta ("8 obras em execução"), a lista é a
   resposta — sem link, quem lê o painel tem que voltar ao menu e procurar.
   Envolve o cartão em vez de virar o cartão num <a> para não mexer no layout
   flex já resolvido acima.

   Herda a cor do texto: sublinhado e azul de link num cartão de dado o
   descaracterizariam. A affordance vem do cursor, da elevação no hover — que o
   `.kpi-card` já tem — e do anel de foco, que precisa existir porque sem
   sublinhado o teclado não teria outra pista. */
/* `a` E `button`: o número às vezes leva a outra TELA (link) e às vezes a
   outra ABA da mesma tela (troca no cliente, via Alpine). Um `<a href="#">`
   que só executa JavaScript mente sobre o destino e suja o histórico; para
   isso o elemento certo é `<button>`. As duas formas têm de parecer a mesma
   coisa, porque para quem lê são a mesma coisa: um número que leva à resposta. */
.kpi-link {
  display: block;
  height: 100%;
  width: 100%;
  color: inherit;
  text-decoration: none;
  /* Zera a aparência de botão: a affordance vem do cartão, não da moldura. */
  background: none;
  border: 0;
  padding: 0;
  text-align: inherit;
  font: inherit;
}
.kpi-link:hover, .kpi-link:focus-visible { color: inherit; }
.kpi-link:hover .kpi-card { border-color: var(--brand-500); }
.kpi-link:focus-visible { outline: none; }
.kpi-link:focus-visible .kpi-card {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* ── Badges ──────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: .3em .75em;
  font-family: var(--app-font-mono);
  font-size: var(--fs-xs); font-weight: 500;
  letter-spacing: .04em; text-transform: uppercase;
  border-radius: var(--r-sm);
  background: var(--surface-2); color: var(--fg-2);
  border: 1px solid var(--border);
}
.badge.ok, .badge-ok     { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-bd); }
.badge.warn, .badge-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-bd); }
.badge.danger, .badge-danger { background: var(--st-danger-bg); color: var(--st-danger-fg); border-color: var(--st-danger-bd); }
.badge.info, .badge-info { background: var(--st-info-bg); color: var(--st-info-fg); border-color: var(--st-info-bd); }
.badge.tech, .badge-tech { background: transparent; color: var(--tech-blue); border-color: var(--tech-blue); }
.badge.dot::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor;
}

/* O override de badge no escuro saiu: os quatro pares viviam duplicados
   aqui e nas regras do tema claro. Agora quem muda no escuro é o token
   `--st-*`, e badge e alert acompanham sem repetir receita. */

/* Status pill (ocorrências) */
.status-pill {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .28em .75em;
  border-radius: var(--r-full);
  font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: .03em; line-height: 1.5;
}
.status-pill::before {
  content: '';
  width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex-shrink: 0;
}

/* ── Tabs ────────────────────────────────────────────────────── */
.tabs {
  display: flex; gap: 2px;
  border-bottom: 1px solid var(--border);
  font-family: var(--app-font);
  /* Sem quebra, a última aba passava da borda e o overflow-x:hidden do
     .app-content a engolia — no celular ela simplesmente não existia. */
  flex-wrap: wrap;
}
.tab-btn {
  /* `inline-flex` para o ícone e o rótulo ficarem centrados um no outro. Com o
     `me-1` do Bootstrap e alinhamento por baseline, o ícone assentava um fio
     abaixo do texto — some numa aba, aparece nas seis lado a lado. */
  display: inline-flex; align-items: center; gap: .4rem;
  padding: 10px 14px;
  font-size: var(--fs-nav); font-weight: 500;
  background: transparent; color: var(--muted);
  border: none; border-bottom: 2px solid transparent;
  margin-bottom: -1px; cursor: pointer;
  transition: all var(--dur-fast);
  /* A barra existe como `<button>` (edital, contrato) e como `<a>` (as seções
     das configurações, que são navegação de verdade — cada aba tem URL). O
     Bootstrap sublinha todo `<a>`, e só a variante de link saía sublinhada:
     mesma barra, duas aparências, conforme a tela. */
  text-decoration: none;
  /* Arredondar só em cima: a aba encosta na linha da barra, e canto embaixo
     descolaria dela. */
  border-radius: var(--r-sm) var(--r-sm) 0 0;
}
/* O alvo é a aba inteira, e não o texto: 6px de recuo em cima e o cursor já
   está sobre a próxima. */
.tab-btn:hover { color: var(--fg); background: var(--surface-2); }
.tab-btn.active {
  color: var(--fg); font-weight: 600;
  border-bottom-color: var(--brand-500);
  /* Fundo transparente no ativo, de propósito: com fundo, o hover da aba
     vizinha ficaria igual ao ativo e a barra perderia o "onde estou". */
  background: transparent;
}
.tab-btn.active:hover { background: var(--surface-2); }
.tab-btn:focus-visible { outline: none; box-shadow: var(--sh-focus); border-radius: var(--r-xs); }

/* Contador de erros na aba. Não é `.badge` de status: badge de status pinta a
   cor **a partir** do estado do domínio (`|badge_class`), e aqui o estado é
   sempre o mesmo — erro é erro. O que varia é a quantidade.

   `aria-hidden` no número e o texto de verdade ao lado: "3" solto depois do
   nome da aba não diz nada a quem ouve a tela. */
.tab-erros {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--r-full);
  background: var(--st-danger-bg); color: var(--st-danger-fg);
  border: 1px solid var(--st-danger-bd);
  font-size: var(--fs-xs); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* O aviso do rascunho automático, ao lado do botão de salvar.
   Discreto por desenho: quem está escrevendo não precisa que a tela pisque a
   cada gravação — precisa saber, ao olhar, que o texto não se perdeu. */
.aviso-rascunho {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-family: var(--app-font-mono);
}
.aviso-rascunho[data-estado="salvo"] { color: var(--st-ok-fg); }
/* Erro é a única mensagem que precisa ser vista: significa que o texto NÃO
   está salvo, e a pessoa tem de clicar em Salvar antes de fechar a aba. */
.aviso-rascunho[data-estado="erro"] {
  color: var(--st-warn-fg);
  background: var(--st-warn-bg);
  border: 1px solid var(--st-warn-bd);
  border-radius: var(--r-sm, 4px);
  padding: .15rem .45rem;
  font-family: var(--app-font);
}
/* Painéis controlados por .tabs (JS alterna .active) */
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fadeSlideUp .25s var(--ease-out) both; }

/* Campos que ocupam a largura da tela, em colunas.

   Os formulários de documento (DFD, ETP, edital) empilhavam um campo por linha
   dentro de uma coluna de 7/12: num monitor de repartição, dois terços da tela
   ficavam vazios e o servidor rolava a página para ver seis campos curtos que
   caberiam lado a lado. Preencher um DFD é tarefa de conferência — quem preenche
   precisa ver o conjunto, não uma fila.

   `auto-fit` com mínimo de 340px resolve sem breakpoint: uma coluna no celular,
   duas no notebook, três ou quatro no monitor grande.

   Campo de texto longo ocupa a linha inteira, mas com teto de 100ch. Caixa de
   justificativa com 1900px de largura é ilegível — a linha longa demais faz o
   olho perder o começo da seguinte, e o texto aqui é redigido, não digitado em
   uma palavra. A largura total serve à visão de conjunto, não ao parágrafo. */
.campos-fluidos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  gap: 0 var(--sp-4, 1rem);
  align-items: start;
}
.campos-fluidos > *:has(textarea),
.campos-fluidos > *:has(select[multiple]) {
  grid-column: 1 / -1;
}
.campos-fluidos > *:has(textarea) textarea {
  max-width: 100ch;
}

.tabs-pill {
  display: inline-flex; gap: 2px; padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.tab-pill {
  padding: 6px 12px;
  font-size: var(--fs-sm); font-weight: 500;
  background: transparent; color: var(--muted);
  border: none; border-radius: var(--r-sm);
  cursor: pointer; transition: all var(--dur-fast);
}
.tab-pill.active { background: var(--surface); color: var(--fg); box-shadow: var(--sh-sm); }

/* ── Tabelas ─────────────────────────────────────────────────── */
.table { font-size: var(--fs-base); width: 100%; border-collapse: separate; border-spacing: 0; }
.table th {
  text-align: left;
  padding: 10px 12px;
  font-family: var(--app-font-mono); font-size: var(--fs-xs); font-weight: 500;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  border-top: 1px solid var(--border);
  white-space: nowrap;
}
.table th:first-child { border-left: 1px solid var(--border); border-top-left-radius: var(--r-md); }
.table th:last-child  { border-right: 1px solid var(--border); border-top-right-radius: var(--r-md); }
.table td {
  padding: .75rem 1rem;
  vertical-align: middle;
  border-bottom: 1px solid var(--border-2);
  color: var(--fg-2);
  font-variant-numeric: tabular-nums;
}
.table td.mono { font-family: var(--app-font-mono); font-size: var(--fs-sm); color: var(--fg); }
/* `th` também, e não só `td`. A regra cobria apenas a célula de corpo, então
   todo cabeçalho de coluna numérica e todo total em `<th>` de rodapé ficavam no
   alinhamento padrão — rótulo centralizado sobre números à direita, e o total
   do rodapé 45px fora da coluna que ele soma (medido em pixels na planilha da
   obra). Vale para 25 templates, incluindo o `core/list_base.html` que é base
   de lista de módulo inteiro. */
.table td.num, .table th.num { text-align: right; }
.table tbody tr { transition: background var(--app-transition); }
.table-hover tbody tr:hover { background: var(--surface-2) !important; }
.table-light, .table-light th { background: var(--surface-2) !important; }
.table-row-link { cursor: pointer; }

/* ── Formulários ─────────────────────────────────────────────── */
.form-control,
.form-select {
  border-radius: var(--r-md) !important;
  border: 1px solid var(--border) !important;
  font-size: var(--fs-base);
  color: var(--fg);
  background: var(--surface);
  transition: border-color var(--app-transition), box-shadow var(--app-transition);
  padding: .5rem .85rem;
}

.form-control:focus,
.form-select:focus {
  border-color: var(--brand-500) !important;
  box-shadow: var(--sh-focus) !important;
  outline: none;
}

.form-control::placeholder { color: var(--muted-2); }

.form-label {
  font-size: var(--fs-nav);
  font-weight: 600;
  color: var(--fg-2);
  margin-bottom: .35rem;
  letter-spacing: .01em;
}

.form-control.form-control-sm,
.form-select.form-select-sm { font-size: var(--fs-nav); padding: .35rem .7rem; }

/* ── A SETA DO SELETOR ────────────────────────────────────────────────────
   Campo de escolha precisa PARECER campo de escolha. Sem a seta, um `<select>`
   é visualmente idêntico a um campo de digitar, e a pessoa clica esperando o
   cursor — são 119 seletores nos templates e mais 145 que o Django monta.

   A causa era uma linha aqui em cima: `background: var(--surface)` no atalho.
   Atalho de `background` zera o `background-image`, e é ali que o Bootstrap
   punha o chevron. A regra tirou a seta de todo seletor do produto sem tocar em
   nenhum deles.

   A cor entra por token, e não cravada, porque no tema escuro uma seta
   charcoal sobre fundo `#172027` desaparece. `%23` é o `#` escapado — dentro de
   uma `url()` o `#` inicia fragmento, e a cor seria ignorada.

   `[multiple]` e `[size]` ficam de fora: ali a caixa mostra várias linhas e não
   abre nada, então a seta prometeria um gesto que não existe.

   A VARIÁVEL GUARDA A IMAGEM INTEIRA, e não só a cor. A primeira tentativa foi
   `stroke='var(--cor)'` dentro do `url()`, e não funciona: o conteúdo de um
   data URI é texto opaco para o CSS — não há substituição de variável ali —, e
   o SVG embutido não enxerga as propriedades do documento. O resultado foi
   seletor sem seta nenhuma, que é o defeito original de volta. Verificado no
   navegador, não deduzido.

   `%23` é o `#` escapado: dentro de `url()` o `#` inicia fragmento, e a cor
   seria descartada. */
:root { --seta-do-seletor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7a74' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --seta-do-seletor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237b8a99' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
}
[data-theme="dark"] { --seta-do-seletor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237b8a99' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }

.form-select:not([multiple]):not([size]) {
  background-image: var(--seta-do-seletor);
  background-repeat: no-repeat;
  background-position: right .75rem center;
  background-size: 14px 14px;
  /* Espaço à direita para o texto não passar por baixo da seta. Sem isto,
     opção longa fica ilegível justamente no fim, que é onde costuma estar o
     que distingue uma opção da outra. */
  padding-right: 2.2rem;
  /* `appearance: none` para o navegador não desenhar a SUA seta ao lado da
     nossa — no Chrome apareciam duas. */
  appearance: none;
  -webkit-appearance: none;
}
.form-select.form-select-sm:not([multiple]):not([size]) {
  background-position: right .6rem center;
  background-size: 12px 12px;
  padding-right: 1.9rem;
}

/* Input com ícone */
.input-icon-wrap { position: relative; }
.input-icon-wrap .bi {
  position: absolute;
  left: .75rem; top: 50%; transform: translateY(-50%);
  color: var(--muted); font-size: var(--fs-md);
  pointer-events: none;
}
.input-icon-wrap .form-control { padding-left: 2.2rem; }

/* Checkbox/radio estilizados */
.check {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-nav); cursor: pointer;
}
.check input {
  appearance: none;
  width: 16px; height: 16px;
  border: 1.5px solid var(--border); border-radius: var(--r-xs);
  background: var(--surface);
  display: grid; place-items: center;
  cursor: pointer; flex-shrink: 0;
}
.check input[type="radio"] { border-radius: 50%; }
.check input:checked { background: var(--brand-500); border-color: var(--brand-500); }
.check input:checked::after {
  content: ''; width: 8px; height: 8px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 5l2 2 4-4' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat center / contain;
}
.check input[type="radio"]:checked::after {
  content: ''; width: 6px; height: 6px; background: var(--on-accent); border-radius: 50%;
}

/* Toggle switch */
.switch {
  position: relative; width: 36px; height: 20px;
  background: var(--border); border-radius: var(--r-full);
  cursor: pointer; transition: background var(--dur-fast);
  flex-shrink: 0; display: inline-block;
}
.switch::after {
  content: ''; position: absolute;
  top: 2px; left: 2px; width: 16px; height: 16px;
  background: var(--on-accent); border-radius: 50%;
  transition: transform var(--dur-fast) var(--ease-out);
  box-shadow: 0 1px 2px rgba(0,0,0,.2);
}
.switch.on { background: var(--brand-500); }
.switch.on::after { transform: translateX(16px); }

/* ── Botões ──────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 12px 24px;
  font-family: var(--app-font);
  font-size: var(--fs-md); font-weight: 600;
  line-height: 1.1;
  border: 1px solid transparent;
  border-radius: var(--r-md) !important;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
  white-space: nowrap;
  letter-spacing: .005em;
}
.btn:active  { transform: scale(.97); }
.btn:hover { transform: translateY(-1px); }
.btn:focus-visible { outline: none; box-shadow: var(--sh-focus); }
.btn[disabled], .btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }

/* ── O botão não pode voltar a ser Bootstrap quando recebe foco ──────────────
   O Bootstrap 5.2.3 tem, literalmente:

       .btn:focus-visible { background-color: var(--bs-btn-hover-bg);
                            border-color: var(--bs-btn-hover-border-color); }

   As famílias abaixo pintam `background` direto e nunca definiram as
   `--bs-btn-*`, então essas variáveis seguiam com o valor compilado do
   framework. Resultado medido (diff de pixels, mesma página com e sem foco):
   `.btn-primary` ia de sage #5E7257 para o azul #0b5ed7, e `.btn-outline-primary`
   deixava de ser contornado e virava um botão **azul sólido**. Também
   `.btn-secondary` e `.btn-outline-secondary` caíam no cinza do framework.

   Não é só feio: é a Regra do Tenant quebrando no pior momento. A cor da
   prefeitura sumia exatamente quando um usuário de teclado agia — e teclado é
   requisito de eMAG, não preferência. Nenhuma suíte pegaria: `:focus-visible`
   não é estado que teste de servidor exercite.

   A correção é declarar as variáveis que o framework lê, em vez de tentar
   vencer a regra dele com outra pintura. O valor de cada uma é o mesmo do
   `:hover` já definido em cada família — foco e passagem de mouse dizem "esta é
   a peça sob a sua mão", e não havia razão para divergirem. */
.btn-primary {
  --bs-btn-hover-bg: var(--brand-600);
  --bs-btn-hover-border-color: var(--brand-600);
  --bs-btn-hover-color: var(--on-accent);
}
.btn-outline-primary {
  --bs-btn-hover-bg: var(--brand-500);
  --bs-btn-hover-border-color: var(--brand-500);
  --bs-btn-hover-color: var(--on-accent);
}
.btn-secondary, .btn-outline-secondary {
  --bs-btn-hover-bg: var(--surface);
  --bs-btn-hover-border-color: var(--muted);
  --bs-btn-hover-color: var(--fg);
}
.btn-danger {
  --bs-btn-hover-bg: var(--danger);
  --bs-btn-hover-border-color: var(--danger);
  --bs-btn-hover-color: var(--on-accent);
}
.btn-tech {
  --bs-btn-hover-bg: var(--tech-blue-2);
  --bs-btn-hover-border-color: var(--tech-blue-2);
  --bs-btn-hover-color: var(--on-accent);
}

.btn-primary {
  background: var(--brand-500); color: var(--on-accent);
  border-color: var(--brand-500);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18), 0 2px 8px var(--brand-drop);
}
.btn-primary:hover:not([disabled]):not(:disabled) {
  background: var(--brand-600); border-color: var(--brand-600);
  box-shadow: 0 6px 18px var(--brand-glow);
  color: var(--on-accent);
}
.btn-primary:active { background: var(--brand-700); }

/* outline-primary sem override caía no azul compilado do Bootstrap — alinha ao sage */
.btn-outline-primary {
  background: transparent; color: var(--brand-600);
  border-color: var(--brand-500); box-shadow: none;
}
.btn-outline-primary:hover:not([disabled]):not(:disabled) {
  background: var(--brand-500); color: var(--on-accent); border-color: var(--brand-500);
}

.btn-secondary, .btn-outline-secondary {
  background: var(--surface); color: var(--fg);
  border-color: var(--border);
  box-shadow: var(--sh-sm);
}
.btn-secondary:hover, .btn-outline-secondary:hover { border-color: var(--muted); }

.btn-tech {
  background: var(--tech-blue); color: var(--on-accent);
  border-color: var(--tech-blue);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(29,78,216,.25);
}
.btn-tech:hover:not([disabled]) {
  background: var(--tech-blue-2); border-color: var(--tech-blue-2);
}
.btn-tech:focus-visible { box-shadow: var(--sh-focus-tech); }

.btn-danger {
  background: var(--danger); color: var(--on-accent); border-color: var(--danger);
}
.btn-danger:hover:not([disabled]) { filter: brightness(1.08); color: var(--on-accent); }

/* A variante contornada da ação destrutiva. Faltava, e é usada em 19 lugares —
   excluir foto, remover item, abrir processo de sanção: todos saíam como texto
   vermelho solto, sem corpo de botão, porque `.btn-outline-danger` não casava
   com regra nenhuma. Segue a forma da `.btn-outline-secondary` (fundo de
   superfície, sombra rasa) e só troca a cor para a do perigo, que é o que a
   variante contornada quer dizer: a ação existe, mas não é a primária da tela. */
.btn-outline-danger {
  background: var(--surface); color: var(--danger);
  border-color: var(--danger); box-shadow: var(--sh-sm);
}
.btn-outline-danger:hover:not([disabled]):not(:disabled) {
  background: var(--danger); color: var(--on-accent); border-color: var(--danger);
}

.btn-ghost {
  background: transparent; color: var(--brand-800); border-color: transparent;
}
[data-theme="dark"] .btn-ghost { color: var(--tech-cyan); }
.btn-ghost:hover:not([disabled]) { background: var(--surface-2); }

.btn-sm { padding: 10px 18px; font-size: var(--fs-base); font-weight: 600; border-radius: var(--r-sm) !important; }
.btn-lg { padding: 15px 28px; font-size: var(--fs-lg); border-radius: var(--r-lg) !important; }
.btn-xs { font-size: var(--fs-nav); padding: .4rem .8rem; border-radius: var(--r-sm) !important; }

/* Faixa de ações no corpo (create/editar/etc migram do topo para cá) */
.page-actions{ display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  gap: 10px; margin: 0 0 20px; }
.page-actions:not(:has(*)){ display: none; margin: 0; }
@media (max-width: 640px){ .page-actions{ justify-content: stretch; }
  .page-actions > .btn, .page-actions > a.btn, .page-actions form{ flex: 1 1 auto; }
  .page-actions form > .btn{ width: 100%; } }
/* Botão de ação SÓ-ÍCONE. Quadrado de alvo fixo, e não texto invisível com
   padding: sem isto o mesmo lápis media larguras diferentes conforme a classe
   de tamanho que a tela tivesse posto, e uma fileira de ações saía desalinhada.
   `inline-flex` centra o glifo nos dois eixos — `line-height` de fonte de ícone
   não é confiável, e o desencontro de um ou dois pixels aparece justamente numa
   fileira, onde há com o que comparar. */
.btn-icon {
  padding: 0; width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm) !important;
  line-height: 1;
  /* SEM borda em repouso. Numa fileira de três ou quatro ações, cada moldura
     é um retângulo a mais competindo com o dado da linha — e o dado é o que a
     pessoa veio ler. O alvo de clique continua o mesmo: 34px de caixa, que é
     o que o dedo acerta, e não a borda que o olho via. */
  border-color: transparent !important;
  background-color: transparent !important;
}
/* A moldura volta no HOVER e no FOCO, que é quando ela serve: dizer onde o
   clique vai cair. No foco ela é obrigatória — teclado sem indicação visível
   é WCAG 2.4.7 quebrado. */
.btn-icon:hover, .btn-icon:focus-visible, .btn-icon.active, .btn-icon[aria-expanded="true"] {
  border-color: var(--border) !important;
  background-color: var(--surface-3) !important;
}
/* A intenção continua na COR do glifo: excluir é vermelho mesmo sem moldura. */
.btn-icon.btn-outline-danger,  .btn-icon.btn-danger  { color: var(--danger); }
.btn-icon.btn-outline-primary, .btn-icon.btn-primary { color: var(--brand-500); }
.btn-icon.btn-outline-danger:hover,  .btn-icon.btn-danger:hover  { background-color: var(--danger-subtle) !important; }
.btn-icon.btn-outline-primary:hover, .btn-icon.btn-primary:hover { background-color: var(--brand-100) !important; }
.btn-icon.btn-sm { width: 28px; height: 28px; }
.btn-icon.btn-xs { width: 24px; height: 24px; }
/* Botão só-ícone não tem rótulo visível, então o nome acessível é obrigatório —
   é o único jeito de alguém no leitor de tela saber o que ele faz. Quem cobra
   isso é `docs/design/lint-ui.sh`, e não uma regra daqui: um contorno de aviso
   num CSS estático não tem como saber que não está em produção, e apareceria
   para o usuário. */

/* ── Paginação ───────────────────────────────────────────────── */
.pagination { gap: .2rem; }
.page-link {
  border-radius: var(--r-xs) !important;
  border: 1px solid var(--border);
  color: var(--fg-2);
  font-size: var(--fs-nav); padding: .35rem .65rem;
  transition: all var(--app-transition);
}
.page-link:hover { background: var(--surface-2); color: var(--fg); border-color: var(--border); }
.page-item.active .page-link {
  background: var(--brand-500); border-color: var(--brand-500); color: var(--on-accent);
}
.page-item.disabled .page-link { color: var(--muted); }

/* ── Alertas ─────────────────────────────────────────────────── */
.alert {
  border-radius: var(--r-md) !important;
  border: none;
  font-size: var(--fs-base);
  padding: .75rem 1rem;
}
.alert-success { background: var(--st-ok-bg); color: var(--st-ok-fg); }
.alert-warning { background: var(--st-warn-bg); color: var(--st-warn-fg); }
.alert-danger  { background: var(--st-danger-bg); color: var(--st-danger-fg); }
.alert-info    { background: var(--st-info-bg); color: var(--st-info-fg); }

/* ── Modal ───────────────────────────────────────────────────── */
.modal-content {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-xl);
}
.modal-header { border-bottom: 1px solid var(--border-2); padding: 1.1rem 1.25rem; }
.modal-footer { border-top: 1px solid var(--border-2); padding: .85rem 1.25rem; background: var(--surface-2); }
.modal-title { font-size: var(--fs-md); font-weight: 600; letter-spacing: -.01em; }

/* ── Popup (Leaflet-style) ───────────────────────────────────── */
.app-popup {
  width: 260px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg);
  overflow: hidden;
  font-family: var(--app-font);
}
.app-popup-head {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-2);
  display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;
}
.app-popup-head .title { font-size: var(--fs-base); font-weight: 600; }
.app-popup-head .id {
  font-family: var(--app-font-mono); font-size: var(--fs-xs); color: var(--muted);
}
.app-popup-body { padding: 10px 12px; font-size: var(--fs-sm); color: var(--fg-2); display: grid; gap: 6px; }
.app-popup-body .row { display: flex; justify-content: space-between; gap: 8px; }
.app-popup-body .row .k {
  color: var(--muted); font-family: var(--app-font-mono);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em;
}
.app-popup-body .row .v { font-weight: 500; }
.app-popup-foot {
  padding: 8px 10px;
  border-top: 1px solid var(--border-2);
  display: flex; gap: 6px;
  background: var(--surface-2);
}

/* ── Tooltip ─────────────────────────────────────────────────── */
.app-tooltip {
  display: inline-block;
  padding: 6px 9px;
  background: var(--fg); color: var(--bg);
  font-size: var(--fs-xs);
  border-radius: var(--r-sm);
  position: relative;
  box-shadow: var(--sh-md);
}
.app-tooltip::after {
  content: ''; position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--fg);
}

/* ── Mapa (Leaflet) ──────────────────────────────────────────── */
.leaflet-container { font-family: var(--app-font) !important; }
.map-rounded { border-radius: 0 0 var(--r-xl) var(--r-xl); overflow: hidden; }

/* ── Painéis sobre o mapa ────────────────────────────────────────────────
   UM recipiente só: `rgba(17,24,32,.92)` + `blur(12px)`. Estava em quatro
   desfoques diferentes (10, 12, 14 e 20px) para a mesma família de painel —
   deriva, não decisão.

   Escuro, e não o cream do §"Elevação" do DESIGN.md: estes painéis têm texto
   claro (o hover é `rgba(255,255,255,.08)`), e trocar o fundo por claro poria
   texto claro sobre fundo claro. O documento passou a registrar as DUAS
   receitas; antes descrevia uma que o código não usava em lugar nenhum.

   É a exceção nominal ao veto de glassmorphism, e vale só aqui: painel que
   flutua sobre mapa precisa deixar o mapa entrever, senão esconde o dado. */

/* Toolbar do mapa */
.map-toolbar {
  display: inline-flex;
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 3px; gap: 1px;
  box-shadow: var(--sh-md);
}
.map-tool {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  background: transparent; color: var(--overlay-mapa-fg);
  border: none; border-radius: var(--r-sm);
  cursor: pointer; transition: all var(--dur-fast);
  font-size: var(--fs-nav);
}
.map-tool:hover  { background: rgba(255,255,255,.08); }
.map-tool.active { background: var(--tech-blue); color: var(--on-accent); }

/* Controles de zoom */
.map-ctl {
  position: absolute;
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--sh-md);
}
.map-ctl button {
  width: var(--map-control-size); height: var(--map-control-size);
  border: none; background: transparent;
  color: var(--overlay-mapa-fg); font-size: var(--fs-md);
  border-bottom: 1px solid var(--border);
  cursor: pointer; transition: background var(--dur-fast);
  display: grid; place-items: center;
}
.map-ctl button:last-child { border-bottom: none; }
.map-ctl button:hover { background: rgba(255,255,255,.08); }

/* Painel de camadas */
.layer-panel {
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  width: 260px; overflow: hidden;
  box-shadow: var(--sh-lg);
}
.layer-head {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  display: flex; justify-content: space-between; align-items: center;
  font-size: var(--fs-nav); font-weight: 600; color: var(--fg);
}
.layer-group-label {
  padding: 9px 14px 5px;
  font-family: var(--app-font-mono);
  font-size: var(--fs-2xs); font-weight: 500;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
}
.layer-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 14px; font-size: var(--fs-sm); color: var(--fg-2);
  cursor: pointer; transition: background var(--dur-fast);
}
.layer-row:hover { background: rgba(255,255,255,.04); }
.layer-row .layer-sw { width: 12px; height: 12px; border-radius: var(--r-xs); flex-shrink: 0; }
.layer-row .layer-count { font-family: var(--app-font-mono); font-size: var(--fs-xs); color: var(--muted); }

/* Legenda */
.map-legend {
  position: absolute;
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 10px 12px;
  font-family: var(--app-font-mono); font-size: var(--fs-xs);
  color: var(--fg); display: grid; gap: 4px;
  box-shadow: var(--sh-md); min-width: 180px;
}
.map-legend .legend-title {
  font-family: var(--app-font); font-size: var(--fs-xs); font-weight: 600;
  color: var(--muted); letter-spacing: .08em; text-transform: uppercase;
  margin-bottom: 4px;
}
.map-legend .row { display: flex; gap: 8px; align-items: center; color: var(--fg-2); }
.map-legend .sq { width: 12px; height: 12px; border-radius: var(--r-xs); flex-shrink: 0; border: 1px solid rgba(255,255,255,.2); }

/* Chip de coordenadas */
.coord-chip {
  position: absolute;
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 4px 8px;
  font-family: var(--app-font-mono); font-size: var(--fs-xs); color: var(--muted);
  display: inline-flex; gap: 8px; align-items: center;
}

/* Barra de escala */
.scale-bar {
  position: absolute;
  background: var(--overlay-mapa); backdrop-filter: blur(12px);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 6px 10px;
  font-family: var(--app-font-mono); font-size: var(--fs-2xs); color: var(--fg-2);
  display: flex; align-items: center; gap: 8px;
}

/* ── OC card (mobile) ────────────────────────────────────────── */
.oc-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: 1rem;
  box-shadow: var(--sh-xs);
  transition: box-shadow var(--app-transition), transform var(--app-transition);
  text-decoration: none; color: inherit; display: block;
}
.oc-card:hover {
  box-shadow: var(--sh-md) !important;
  transform: translateY(-1px); color: inherit;
}
.oc-card .oc-tipo  { font-weight: 600; font-size: var(--fs-base); color: var(--fg); }
.oc-card .oc-meta  { font-size: var(--fs-sm); color: var(--muted); }

/* ── Foto grid ───────────────────────────────────────────────── */
.foto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: .5rem;
}
.foto-grid a { display: block; border-radius: var(--r-md); overflow: hidden; aspect-ratio: 1; }
.foto-grid img { width: 100%; height: 100%; object-fit: cover; transition: transform .3s var(--ease-out); }
.foto-grid a:hover img { transform: scale(1.05); }

/* Imagem nunca é mais larga que o espaço que tem — evita que uma foto enviada
   em resolução alta empurre a página inteira no celular. */
img { max-width: 100%; }

/* ── Filter pill ─────────────────────────────────────────────── */
.filter-pill {
  cursor: pointer;
  border-radius: var(--r-full);
  padding: .3rem .85rem;
  font-size: var(--fs-nav); font-weight: 500;
  border: 1.5px solid var(--border);
  background: var(--surface); color: var(--fg-2);
  transition: all var(--app-transition);
  display: inline-flex; align-items: center; gap: .3rem;
}
.filter-pill:hover,
.filter-pill.active {
  border-color: var(--brand-500);
  background: var(--brand-wash);
  color: var(--brand-500);
}

/* ── Empty state ─────────────────────────────────────────────── */
.empty-state {
  text-align: center; padding: 3rem 1.5rem; color: var(--muted);
}
.empty-state > .bi { font-size: 2.5rem; opacity: .4; display: block; margin-bottom: .75rem; }
/* `>` e não descendente: sem ele a regra alcançava também o ícone DENTRO do
   botão de ação (`cta_icon` do `core/_empty_state.html`), que saía a 40px,
   em bloco e a 40% de opacidade — um X gigante desbotado ao lado do rótulo.
.empty-state p { font-size: var(--fs-base); margin: 0; }
.empty-state .empty-state-title { color: var(--fg); font-weight: 600; font-size: var(--fs-md); }
.empty-state .empty-state-text  { color: var(--muted); margin-top: .25rem; max-width: 42ch; margin-inline: auto; }

/* ── Lista canônica (core/list_base.html) ────────────────────── */
/* Grade de filtros que nunca estoura 12 colunas: cada campo ocupa no mínimo
   180px e o navegador acomoda quantos couberem por linha. */
.list-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--sp-2);
  align-items: end;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
  box-shadow: var(--sh-sm);
}
.list-filters .lf-actions {
  display: flex; gap: var(--sp-2);
  grid-column: 1 / -1;   /* botões numa linha própria, sempre */
}
.list-filters .lf-actions .btn { white-space: nowrap; }

/* Linha clicável acessível: o <a> real da 1ª célula recebe o foco; o foco
   destaca a linha inteira. Sem depender de onclick no <tr>. */
.table-row-link { cursor: pointer; }
.table-row-link:hover { background: var(--surface-2); }
.table-row-link:focus-within { background: var(--surface-2); outline: 2px solid var(--brand-500); outline-offset: -2px; }
.table-row-link a:focus { outline: none; }  /* o realce fica na linha, não só no link */

.list-pagination .page-link { color: var(--fg-2); border-color: var(--border); }
.list-pagination .page-item.disabled .page-link { color: var(--muted); background: var(--surface-2); }
.list-pagination .page-link:hover { background: var(--surface-2); color: var(--fg); }

/* ── Skeleton loader ─────────────────────────────────────────── */
.skeleton {
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.4s ease infinite;
  border-radius: var(--r-sm);
}
@keyframes skeleton-loading {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ── PWA install button ──────────────────────────────────────── */
#btn-instalar-pwa button {
  border-radius: var(--r-full);
  font-size: var(--fs-nav); font-weight: 600;
  box-shadow: var(--sh-lg);
}

/* ── Portal cards ────────────────────────────────────────────── */
.portal-card {
  border: 1px solid var(--border) !important;
  border-radius: var(--r-xl) !important;
  transition: transform .22s var(--ease-out), box-shadow .22s var(--ease-out);
  cursor: pointer; text-decoration: none; color: inherit; display: block;
}
.portal-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--sh-lg) !important;
  color: inherit;
}
.portal-card .icon-wrap {
  width: 58px; height: 58px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.45rem; margin: 0 auto 14px;
}

/* ── Animações de entrada ────────────────────────────────────── */
@keyframes fadeSlideUp {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
.fade-in-up { animation: fadeSlideUp .35s var(--ease-out) both; }
.fade-in-up:nth-child(1) { animation-delay: 0s; }
.fade-in-up:nth-child(2) { animation-delay: .06s; }
.fade-in-up:nth-child(3) { animation-delay: .12s; }
.fade-in-up:nth-child(4) { animation-delay: .18s; }

/* ── Scrollbar ───────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--r-lg); }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ── Responsividade ──────────────────────────────────────────── */
@media (max-width: 767px) {
  :root { --header-h: 56px; }
  .page-title h1 { font-size: var(--fs-md); }
  .section { padding: 1rem 0 2rem; }
  .hide-mobile  { display: none !important; }
  .show-mobile  { display: block !important; }
  .card { border-radius: var(--r-md) !important; }
  .card-header { padding: .7rem .9rem; }
  /* Overrides de KPI no mobile ficam no bloco após as definições-base
     (.kpi-card padding/icon/value), senão a ordem de origem os anula. */
}

@media (min-width: 768px) {
  .show-mobile { display: none !important; }
}

/* ── Dark-mode placeholder ───────────────────────────────────── */
@media (prefers-color-scheme: dark) {
  /* Reservado para implementação automática do dark mode */
}

/* ── KPI card inner elements (landing + shared) ──────────────── */
/* Anatomia do north-star (docs/design/mockups/empreendimento-detalhe.html):
   faixa sage à esquerda, RÓTULO em cima, número em DM Mono, e — quando o
   template fornecer — barra e nota de rodapé.

   O rótulo sobe por `order`, não por ordem no HTML: 26 dos 33 templates
   escrevem `.kpi-card` à mão (só 7 usam `core/_kpi_strip.html`), e reordenar
   no CSS alcança os 33 sem editar um por um. O partial canônico tem o rótulo
   antes do valor também no DOM — quem lê por voz ouve a mesma ordem que quem
   vê. Nos 26 restantes a voz ainda ouve "18, Checklist"; é compreensível, e
   fica assim até cada um migrar para o partial. */
.kpi-card { padding: 18px 20px; position: relative; }
/* `d-block` do Bootstrap é `!important` e MATA o `display:flex` da base — e com
   ele morrem o `order` e o `margin-top:auto` abaixo. O cartão então alinha ao
   topo enquanto os vizinhos alinham embaixo, e a faixa fica torta.

   Dez templates escrevem `class="kpi-card d-block"` (o `<a>` que envolve o
   cartão), inclusive `core/_kpi_strip.html`. Reafirmar aqui com `!important`
   custa uma linha; caçar dez templates custa dez, e o décimo primeiro nasce
   torto de novo. */
.kpi-card { display: flex !important; }
/* Filho sem classe `kpi-*` é ordenado por ÚLTIMO, não por primeiro.
   Sem esta linha ele herda `order: 0` e sobe acima do rótulo — foi o que
   aconteceu com a barra de progresso do "Execução média" em /obras/lista, que
   apareceu no topo do cartão. Nota de rodapé, barra e link extra são
   complemento do número: vêm depois dele, sempre. */
.kpi-card > * { order: 5; }
.kpi-card::before {
  content: "";
  position: absolute; left: 0; top: 16px; bottom: 16px;
  width: 2px; border-radius: var(--r-2xs);
  background: var(--brand-500);
}
.kpi-card .kpi-icon {
  order: 0;
  /* Menor que o antigo bloco de 52px: no north-star o dado é a manchete e o
     ícone é etiqueta. Grande demais, ele disputava a leitura com o número. */
  width: 34px; height: 34px;
  border-radius: var(--r-lg);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.05rem;
  margin-bottom: .6rem;
}
/* `landing_gov.html` e `index.html` embrulham valor+rótulo num `.kpi-body`.
   `order` só vale entre irmãos: sem tornar o embrulho um contexto flex, a
   reordenação passaria batida justo na landing. */
.kpi-card .kpi-body { display: flex; flex-direction: column; }
.kpi-card .kpi-label {
  order: 1;
  margin-top: auto; /* empurra rótulo+valor p/ base da coluna */
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--muted);
}
.kpi-card .kpi-value {
  order: 2;
  font-family: var(--app-font-mono);
  font-size: 2rem;
  font-weight: 500;
  letter-spacing: -.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  margin-top: .35rem;
}
/* Dinheiro é longo: "R$ 2.750.000,00" a 2rem estoura o cartão. Cinco telas
   resolviam isso com `style="font-size:1.5rem"` copiado — 24px, degrau que não
   existe na rampa da DESIGN.md (…18, 22, 28…). Aqui ele tem nome e cai no
   degrau que existe. */
.kpi-card .kpi-value--dinheiro { font-size: var(--fs-xl); }

/* "Sem checklist" ocupando o corpo de 2rem reservado ao dado vira manchete de
   uma ausência — e ainda estoura a largura do cartão. Frase é frase. */
.kpi-card .kpi-value--frase {
  font-size: 1.05rem;
  line-height: 1.35;
  color: var(--muted);
}
/* Unidade/moeda dentro do número: "R$" e "/ 27" são contexto, não grandeza. */
.kpi-card .kpi-value .u {
  font-size: .47em;
  font-weight: 400;
  color: var(--muted);
}
/* Nota de rodapé: o "por quê" do número (`8 obrigatórios · 6 condicionais`).
   Opcional — só aparece quando o template a fornece. */
.kpi-card .kpi-foot {
  order: 3;
  margin-top: .35rem;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--muted);
}
/* Barra de progresso. `scaleX` em vez de `width`: largura repagina, transform
   só compõe. Origem à esquerda para crescer no sentido da leitura. */
.kpi-card .kpi-meter {
  order: 4;
  /* Ocupa a MESMA faixa que `.kpi-foot` ocuparia — margem igual e a altura de
     uma linha de rodapé, com a barra centrada dentro. Antes a barra era mais
     baixa que uma linha de texto, e numa faixa onde uns cartões têm rodapé e
     outros têm barra o conteúdo ancorado na base subia ~9px só naquele: os
     números da faixa saíam de linhas diferentes. Visto em /obras/lista. */
  margin-top: .35rem;
  min-height: calc(11.5px * 1.35);
  display: flex; align-items: center;
}
.kpi-card .kpi-meter > i {
  height: 4px; border-radius: var(--r-2xs);
  background: var(--border);
  overflow: hidden;
  width: 100%;
  position: relative;
}
.kpi-card .kpi-meter > i::after {
  content: '';
  display: block; height: 100%; border-radius: inherit;
  background: var(--brand-500);
  transform-origin: left center;
  /* `--pct` chega 0..100 (o template escreve o número cru). `scaleX` exige
     número puro — `scaleX(65%)` é inválido e a barra some —, daí o /100. */
  transform: scaleX(calc(var(--pct, 0) / 100));
  transition: transform .6s cubic-bezier(.16, 1, .3, 1);
}
@media (prefers-reduced-motion: reduce) {
  .kpi-card .kpi-meter > i::after { transition: none; }
}
/* Cartão-âncora: o último da faixa, escuro, para a faixa ter um fim visual em
   vez de quatro cartões idênticos. Cor fixa — é uma inversão deliberada, não
   segue `--surface`; no tema escuro ela afunda um tom para continuar se
   distinguindo do fundo. */
.kpi-card--anchor {
  background: #2f3835;
  border-color: #2f3835;
  color: #f4f1ec;
}
.kpi-card--anchor .kpi-value { color: #f4f1ec; }
.kpi-card--anchor .kpi-label { color: #8fa882; }
/* Creme rebaixado por alfa, não um cinza novo: a nota tem de ser secundária
   sem inventar um tom fora da paleta. Dá 6,4:1 sobre o âncora. */
.kpi-card--anchor .kpi-foot { color: rgba(244, 241, 236, .72); }
.kpi-card--anchor .kpi-meter { background: rgba(244, 241, 236, .18); }
/* Os tints (`kpi-neutral` etc.) são fundos claros — no cartão escuro o ícone
   sumiria dentro do próprio quadrado. Aqui ele vira vazado. */
.kpi-card--anchor .kpi-icon {
  background: rgba(244, 241, 236, .1) !important;
  color: #8fa882 !important;
}
/* Sem override no tema escuro de propósito. O primeiro rascunho afundava o
   âncora para um tom mais escuro, e esse tom era cor inventada — não está na
   paleta. Charcoal-dark já se separa do `--surface` escuro (#172027) por ser
   mais claro e mais verde, que é tudo que o âncora precisa: ser o cartão
   diferente. Um tom novo para resolver isso seria dívida, não solução. */
/* KPI como link (filtra a lista) + estado ativo do filtro selecionado */
a.kpi-card { text-decoration: none; color: inherit; } /* mantém flex-column da base */
.kpi-card.is-active { border-color: var(--brand-500); box-shadow: 0 0 0 1px var(--brand-500), var(--sh-sm); }

/* KPI no mobile — declarado após as bases p/ vencer por ordem de origem
   (media query não adiciona especificidade). */
@media (max-width: 767px) {
  .kpi-card { padding: .9rem 1rem; min-height: 146px; }
  .kpi-card::before { top: 12px; bottom: 12px; }
  .kpi-card .kpi-icon { width: 28px; height: 28px; border-radius: var(--r-md); font-size: .95rem; margin-bottom: .45rem; }
  .kpi-card .kpi-value { font-size: 1.55rem; }
  .kpi-card .kpi-label { font-size: 10px; letter-spacing: .07em; }
  /* Dois cartões por linha no celular: a nota de rodapé quebraria em 3–4
     linhas e empurraria o número para fora do cartão. O número e o rótulo
     bastam nessa largura. */
  .kpi-card .kpi-foot { display: none; }
  /* Strips de KPI em grade: todas as linhas com a altura da mais alta, mesmo
     quando um card tem sub-linha (ex.: "N alertas") — sem número mágico.
     Cobre o strip canônico (.kpi-strip) e, via :has(), qualquer .row de
     .kpi-card nos dashboards. Navegadores sem :has() mantêm a min-height. */
  .kpi-strip,
  .app-content .row:has(> [class*="col"] > .kpi-card),
  .sistema-page-content .row:has(> [class*="col"] > .kpi-card) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .75rem;
    grid-auto-rows: 1fr;
    margin: 0 0 1.5rem;
  }
  .kpi-strip > [class*="col"],
  .app-content .row:has(> [class*="col"] > .kpi-card) > [class*="col"],
  .sistema-page-content .row:has(> [class*="col"] > .kpi-card) > [class*="col"] {
    width: auto; max-width: none; padding: 0;
  }
  .kpi-strip .kpi-card,
  .app-content .row:has(> [class*="col"] > .kpi-card) .kpi-card,
  .sistema-page-content .row:has(> [class*="col"] > .kpi-card) .kpi-card {
    height: 100%;
  }
}

/* ── Painel de trabalho com contexto ao lado ──────────────────
   A tabela é a coluna de trabalho; o painel (histórico) é contexto. O north-star
   os põe lado a lado, e a grade de 12 colunas não serve aqui: em 9/3 a tabela
   pedia 852px e recebia 800 a 1440px de viewport — a última ação da linha saía
   da tela. A regra de verdade não é uma fração, é um custo fixo: o painel come
   300px, e só se paga quando sobra largura para a tabela inteira.

   `minmax(0, 1fr)` e não `1fr`: item de grade tem `min-width: auto`, então uma
   tabela larga empurraria a coluna além da fração em vez de rolar dentro dela.

   1530px de viewport: medido, é onde a tabela inteira (841px de largura mínima)
   ainda cabe ao lado dos 300px do painel, descontadas a barra lateral e as
   margens. Abaixo disso o painel desce e a tabela fica com a largura inteira —
   trabalho primeiro, contexto depois.

   Era 1560 quando as ações da linha tinham rótulo. Passá-las a só-ícone devolveu
   11px, e não mais: o gargalo nunca foi a coluna de ações, são PROJETO e
   RESPONSÁVEL, cujo conteúdo mínimo ("Arquitetônico + acessibilidade (NBR
   9050)", "Ana Fiscal Administrativa") define o piso. Encurtar botão não move
   esse piso; o que moveria é truncar nome, que troca um problema de layout por
   um de leitura. */
.painel-com-contexto {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}
@media (min-width: 1530px) {
  .painel-com-contexto { grid-template-columns: minmax(0, 1fr) 300px; }
}

/* ── Coluna de tabela que encolhe até o conteúdo ──────────────
   `width:1%` + `nowrap` num `table-layout:auto` é o jeito de dizer "só o
   necessário": a largura pedida é absurda, o navegador cede o mínimo, e a
   sobra toda vai para as colunas sem largura. Serve para código, data,
   status e ações — as curtas — quando a tabela divide a tela com um painel
   ao lado e a coluna de nome está quebrando em quatro linhas. */
.col-justa { width: 1%; white-space: nowrap; }

/* ── Galeria de fotos (empreendimento) ───────────────────────── */
.foto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-3, 16px);
}
.foto-item { display: flex; flex-direction: column; }
.foto-thumb {
  position: relative; display: block; width: 100%; aspect-ratio: 4 / 3;
  padding: 0; border: 1px solid var(--border); border-radius: var(--r-lg, 12px);
  /* `--cream-mid` nunca existiu: o fallback sempre vencia. O valor
     `#EAE6DE` é exatamente `--surface-3`, que segue o tema. */
  overflow: hidden; background: var(--surface-3); cursor: zoom-in;
}
.foto-thumb img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .3s cubic-bezier(.22,1,.36,1); }
.foto-thumb:hover img { transform: scale(1.04); }
.foto-thumb:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.foto-cat { position: absolute; top: .4rem; left: .4rem; }
.foto-flag {
  position: absolute; top: .4rem; right: .4rem;
  width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(26,30,28,.55); color: var(--on-accent); font-size: var(--fs-xs);
}
.foto-cap { display: flex; align-items: baseline; gap: .4rem; padding: .4rem .1rem 0; }
.foto-legenda { flex: 1; min-width: 0; font-size: var(--fs-base); color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.foto-meta {
  font-size: var(--fs-xs); color: var(--muted);
  /* Coluna estreita + nowrap jogava o hash e o botão de excluir para fora do
     cartão. A linha quebra e o hash, que não tem espaço para quebrar, parte. */
  display: flex; flex-wrap: wrap; align-items: center; gap: .15rem .4rem;
  min-width: 0; overflow-wrap: anywhere;
}
.foto-del { margin-left: auto; padding: .05rem .3rem; color: var(--danger, var(--danger)); }

/* ── Diário de fotos do empreendimento ──────────────────────────────────
   A grade de miniaturas respondia "que fotos existem". A pergunta do servidor
   é "o que aconteceu na obra, e quando" — e para respondê-la ele lia a data em
   letra miúda sob cada miniatura, uma por uma.

   Aqui o dia é a unidade: cabeçalho com data, quem esteve no canteiro e
   quantas fotos, e abaixo as daquele dia. O fio à esquerda amarra as entradas
   numa linha do tempo sem virar decoração — ele existe para dizer que as
   entradas são a mesma sequência, e some quando há só uma. */
/* ── Galerias de foto — um box por conjunto ───────────────────
   O diário abaixo é uma LINHA DO TEMPO: fio vertical, um ponto por dia. A
   galeria não é sequência, é conjunto — cada uma tem nome, data e dono, e vive
   ao lado das outras. Daí cartão em grade, e não entrada em fio.

   `auto-fill` com piso de 320px: cabem duas por linha em tela cheia e uma no
   celular, sem media query. O piso é o que garante que a fila de miniaturas
   dentro do box ainda mostre duas lado a lado. */
.galeria-grade {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1rem;
  /* Cada box com a altura do próprio conteúdo. Sem isto a linha da grade
     iguala todas à mais alta, e uma galeria recém-aberta (sem foto) vira um
     retângulo branco de 400px ao lado de uma cheia — o vão parecia defeito de
     carregamento. Galerias não se comparam por altura; a de 3 fotos é maior
     que a de nenhuma, e está certo que pareça. */
  align-items: start;
}
.galeria-box {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--sh-sm);
}
/* Mesma faixa sage da lajota de KPI: no sistema, estado e pertencimento moram
   na margem esquerda. Aqui ela diz "isto é um conjunto nomeado". */
.galeria-box::before {
  content: "";
  position: absolute;
  left: 0; top: 16px; bottom: 16px;
  width: 2px; border-radius: var(--r-2xs);
  background: var(--brand-500);
}
.galeria-cabeca {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .5rem .75rem;
}
.galeria-titulo {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--fg);
}
.galeria-data { font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; }
.galeria-descricao {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--muted);
}
/* Herda a grade de miniaturas do diário — é a mesma miniatura, o mesmo
   recorte de legenda e o mesmo par autor+excluir. Duplicar as regras seria
   criar um segundo jeito de desenhar a mesma coisa. */
.galeria-fila {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: .65rem;
}
.galeria-fila .foto-cap {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: .1rem .35rem;
}
.galeria-fila .foto-legenda {
  grid-column: 1 / -1;
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.35;
}
.galeria-fila .foto-del { margin-left: 0; }
.galeria-vazia {
  margin: 0;
  padding: .85rem 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* O pé carrega a autoria. Fica no rodapé e não no cabeçalho porque a pergunta
   "quem abriu" vem depois de "o que é" — e no cabeçalho disputaria com o
   título, que é o que se procura ao varrer a grade. */
.galeria-pe {
  display: flex; align-items: center; justify-content: space-between;
  gap: .5rem;
  margin-top: auto;
  padding-top: .6rem;
  border-top: 1px solid var(--border);
  font-size: var(--fs-xs);
  color: var(--muted);
}

.diario-fotos { display: flex; flex-direction: column; gap: var(--sp-5, 1.5rem); }

.diario-dia {
  position: relative;
  padding-left: 1.15rem;
}
.diario-fotos .diario-dia::before {
  content: '';
  position: absolute;
  left: 3px; top: .45rem; bottom: 0;
  width: 1px;
  background: var(--border);
}
/* Entrada única não é sequência: o fio viraria enfeite. */
.diario-fotos .diario-dia:only-child::before { display: none; }
.diario-dia::after {
  content: '';
  position: absolute;
  left: 0; top: .38rem;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--brand-500);
}

.diario-cabeca {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: .3rem .6rem;
  margin-bottom: .55rem;
}
.diario-data {
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}
.diario-quem { font-size: var(--fs-sm); color: var(--fg-2); }
.diario-conta { font-size: var(--fs-xs); color: var(--muted); margin-left: auto; }
/* A data veio do envio, não de declaração de quem fotografou. Dizer isso é o
   que separa registro de suposição — e o tom é de aviso, não de erro, porque
   não é defeito: é o que se sabe.

   Tokens `--st-warn-*`, os mesmos do `.badge.warn`. Escrevi `--warn-fg` /
   `--warn-bg` na primeira versão e nenhum dos dois existe — seria o defeito
   que este arquivo já documenta duas vezes acima (`--ink-600`, `--cream-mid`):
   token inexistente, fallback vencendo sempre, cor fora da paleta. */
.diario-aviso {
  font-size: var(--fs-xs);
  color: var(--st-warn-fg);
  background: var(--st-warn-bg);
  border: 1px solid var(--st-warn-bd);
  border-radius: var(--r-sm, 4px);
  padding: .05rem .35rem;
}

/* As fotos do dia. `auto-fill` com piso de 150px: em 390px cabem duas, no
   desktop quantas couberem — sem breakpoint, que é a regra do projeto para
   grade responsiva. */
.diario-fila {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: .65rem;
}

/* Legenda acima, autor e excluir abaixo — e não os três disputando uma linha.
   `.foto-cap` é `flex` em linha e `.foto-legenda` tem `nowrap` + `ellipsis`:
   numa coluna de 150px isso espremia a legenda até sobrar uma letra —
   "Concretagem do trecho 3" virava "C". Lado a lado só funciona em cartão
   largo; aqui a coluna é estreita por desenho, porque o dia mostra várias
   fotos.

   Grade de duas colunas em vez de `flex-direction: column`: o botão de excluir
   é irmão de `.foto-meta`, não filho, então em coluna ele cairia numa terceira
   linha ocupando a largura toda. Assim a legenda toma a primeira linha inteira
   e autor + excluir dividem a segunda. */
.diario-fila .foto-cap {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: .1rem .35rem;
}
.diario-fila .foto-legenda {
  grid-column: 1 / -1;
  white-space: normal;
  /* Duas linhas: a terceira desalinharia as miniaturas vizinhas, e legenda de
     foto que precisa de três linhas é descrição, não legenda. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.35;
}
/* `margin-left: auto` era o que empurrava o botão para a direita no flex; na
   grade a coluna `auto` já faz isso, e a margem só somaria folga. */
.diario-fila .foto-del { margin-left: 0; }

.foto-lightbox {
  border: none; padding: 0; background: transparent;
  max-width: min(92vw, 1100px); max-height: 92vh;
}
.foto-lightbox::backdrop { background: rgba(26,30,28,.86); }
.foto-lightbox-inner { position: relative; }
.foto-lightbox img { max-width: 100%; max-height: 82vh; object-fit: contain;
  display: block; margin: 0 auto; border-radius: var(--r-md, 8px); }
.foto-lightbox figcaption {
  display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  /* `--cream` idem. `#F4F1EC` é `--bg` no tema claro — mas aqui o texto
     fica sobre a caixa escura do lightbox, então o papel é "claro sobre
     escuro" e não "fundo da página": `--on-accent`. */
  padding: .6rem .2rem 0; color: var(--on-accent); font-size: var(--fs-base);
}
.foto-lightbox-close {
  position: absolute; top: .5rem; right: .5rem; z-index: 1;
  width: 34px; height: 34px; border: none; border-radius: 50%;
  background: rgba(26,30,28,.6); color: var(--on-accent); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.foto-lightbox-close:hover { background: rgba(26,30,28,.85); }
.foto-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 1;
  width: 42px; height: 42px; border: none; border-radius: 50%;
  background: rgba(26,30,28,.6); color: var(--on-accent); cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 1.2rem;
}
.foto-nav:hover { background: rgba(26,30,28,.85); }
.foto-nav-prev { left: .5rem; }
.foto-nav-next { right: .5rem; }
@media (max-width: 767px) {
  .foto-nav { width: 36px; height: 36px; font-size: var(--fs-md); }
}
@media (prefers-reduced-motion: reduce) {
  .foto-thumb img { transition: none; }
  .foto-thumb:hover img { transform: none; }
}

/* Números de destaque em geral (valores/%) — fonte display maior */
.value-lg { font-family: var(--font-display); font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: -.02em; }
/* KPI icon tints — Terreno Firme, saturados p/ dar cor (curados, sem Tailwind cru) */
.kpi-accent  { background: var(--kpi-accent-bg);  color: var(--kpi-accent-fg); }
.kpi-ok      { background: var(--kpi-ok-bg);  color: var(--kpi-ok-fg); }
.kpi-warn    { background: var(--kpi-warn-bg);  color: var(--kpi-warn-fg); }
.kpi-danger  { background: var(--kpi-danger-bg);  color: var(--kpi-danger-fg); }
.kpi-neutral { background: var(--kpi-neutral-bg);  color: var(--kpi-neutral-fg); }
.kpi-info    { background: var(--kpi-info-bg);  color: var(--kpi-info-fg); }
.kpi-plum    { background: var(--kpi-plum-bg);  color: var(--kpi-plum-fg); }
.kpi-gold    { background: var(--kpi-gold-bg);  color: var(--kpi-gold-fg); }
/* nomes legados -> hues variados (não mais tudo sage) */
.kpi-blue    { background: var(--kpi-blue-bg);  color: var(--kpi-blue-fg); }
.kpi-green   { background: var(--kpi-ok-bg);  color: var(--kpi-ok-fg); }
.kpi-amber   { background: var(--kpi-warn-bg);  color: var(--kpi-warn-fg); }
.kpi-teal    { background: var(--kpi-info-bg);  color: var(--kpi-info-fg); }
.kpi-red     { background: var(--kpi-danger-bg);  color: var(--kpi-danger-fg); }
.kpi-purple  { background: var(--kpi-plum-bg);  color: var(--kpi-plum-fg); }

/* ── Tint no CARTÃO, e não no ícone ────────────────────────────
   As classes acima são tinta de ÍCONE: um quadradinho pastel de 34px. Algumas
   telas as puseram no `.kpi-card`, e aí o pastel virou o fundo do cartão
   inteiro — quatro blocos coloridos gritando ao lado de uma faixa de KPI que
   é branca e sóbria. Em `aba_dashboard.html` (atividades do empreendimento)
   isso ficava ao lado da faixa nova e a diferença era escandalosa.

   Aqui o fundo volta a ser branco e a cor vai para a FAIXA DA ESQUERDA, que é
   onde o north-star põe estado. A cor não se perde, deixa de ser preenchimento:
   estado é uma marca na margem, não uma tinta no papel. O ícone recebe o pastel
   original, do tamanho que ele foi feito, para o par cor+forma continuar de pé
   para quem não distingue a cor.

   Especificidade (0,2,0) vence as classes nuas (0,1,0); vale onde as duas
   estiverem no mesmo elemento, e não toca no ícone tintado corretamente. */
.kpi-card.kpi-accent, .kpi-card.kpi-ok, .kpi-card.kpi-warn,
.kpi-card.kpi-danger, .kpi-card.kpi-neutral, .kpi-card.kpi-info,
.kpi-card.kpi-plum, .kpi-card.kpi-gold, .kpi-card.kpi-blue,
.kpi-card.kpi-green, .kpi-card.kpi-amber, .kpi-card.kpi-teal,
.kpi-card.kpi-red, .kpi-card.kpi-purple {
  background: var(--surface);
  color: var(--fg);
}
.kpi-card.kpi-accent::before,
.kpi-card.kpi-green::before   { background: var(--kpi-accent-fg); }
.kpi-card.kpi-ok::before      { background: var(--kpi-ok-fg); }
.kpi-card.kpi-warn::before,
.kpi-card.kpi-amber::before,
.kpi-card.kpi-gold::before    { background: var(--kpi-warn-fg); }
.kpi-card.kpi-danger::before,
.kpi-card.kpi-red::before     { background: var(--kpi-danger-fg); }
.kpi-card.kpi-neutral::before { background: var(--kpi-neutral-fg); }
.kpi-card.kpi-info::before,
.kpi-card.kpi-teal::before,
.kpi-card.kpi-blue::before    { background: var(--kpi-info-fg); }
.kpi-card.kpi-plum::before,
.kpi-card.kpi-purple::before  { background: var(--kpi-plum-fg); }
/* O ícone dentro de um cartão tintado herda o pastel do próprio tint, porque o
   template já não tem onde declará-lo (a classe está no cartão). */
.kpi-card.kpi-accent  .kpi-icon { background: var(--kpi-accent-bg);  color: var(--kpi-accent-fg); }
.kpi-card.kpi-ok      .kpi-icon { background: var(--kpi-ok-bg);      color: var(--kpi-ok-fg); }
.kpi-card.kpi-warn    .kpi-icon { background: var(--kpi-warn-bg);    color: var(--kpi-warn-fg); }
.kpi-card.kpi-danger  .kpi-icon { background: var(--kpi-danger-bg);  color: var(--kpi-danger-fg); }
.kpi-card.kpi-neutral .kpi-icon { background: var(--kpi-neutral-bg); color: var(--kpi-neutral-fg); }
.kpi-card.kpi-info    .kpi-icon { background: var(--kpi-info-bg);    color: var(--kpi-info-fg); }

/* ── Dashboard de obras — cards em destaque, progresso, distribuição ──── */
.obra-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden;
  display: flex; flex-direction: column; height: 100%;
  transition: box-shadow var(--app-transition), transform var(--app-transition);
}
.obra-card:hover { box-shadow: var(--sh-md) !important; transform: translateY(-2px); }
.obra-card__media { height: 168px; overflow: hidden; background: #eae6de; }
/* Sem `filter`. Eram duas coisas ruins somadas.
   DESIGN: a foto é o registro da obra pública, e dessaturar é decoração sobre
   evidência — o cartão mostrava a obra em preto e branco até alguém passar o
   mouse. CUSTO: `filter` não é composto, e medido em trace no Chrome ele
   respondia por ~26% do raster da tela (amostras 498-540 com filtro contra
   331-428 sem, sem sobreposição). A transição piorava: repintava a imagem
   inteira a cada quadro, e rolar a grade varre dezenas de cartões. */
/* `font-size: 0`: mesma razão do `.emp-card__media img` abaixo — capa
   quebrada não pode virar texto alternativo gigante no cartão. */
.obra-card__media img { width: 100%; height: 100%; object-fit: cover; font-size: 0; }
.obra-card__media--empty { display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 2rem; }
.obra-card__desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.35; }
/* Empreendimento em box (mesmo efeito do obra-card, sem foto: faixa com ícone) */
.emp-card { overflow: hidden; }
.emp-card__media { height: 120px; overflow: hidden; display: flex; align-items: center;
  justify-content: center; color: rgba(255,255,255,.92); font-size: 1.9rem;
  background: var(--brand-500, #5e7257); }
/* Sem `filter`, pelo mesmo motivo do `.obra-card__media img` acima.
   O `brightness(1.04)` do hover saiu inteiro: 4% de brilho ninguém vê, e
   custava uma segunda camada de filtro além da da imagem. */
/* `font-size: 0` no IMG, e não no contêiner: o contêiner precisa dos 1.9rem
   para desenhar o ícone de quando não há foto. Quando o arquivo some, o
   navegador desenha o TEXTO ALTERNATIVO no lugar da imagem — e ele herdava
   aqueles 1.9rem, saía por cima do cartão e empurrava o resto da grade.
   Visto em 04/09/2026 na carteira de Sousa, com uma capa que respondia 404.
   Zerado, a capa quebrada cai no fundo da marca, que é o mesmo destino de
   quem nunca teve foto. */
.emp-card__media img { width: 100%; height: 100%; object-fit: cover; font-size: 0; }
.obra-ref { font-size: var(--fs-xs); font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); }
.obra-ref--alerta { color: var(--danger); }

.progress-track { background: var(--border); border-radius: var(--r-full); height: 5px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: var(--r-full); background: var(--brand-500); }
.progress-fill--fin { background: #8A847A; }
.progress-fill--alerta { background: var(--danger); }
.progress-cap { font-size: var(--fs-xs); font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); }

.seg-bar { display: flex; height: 8px; border-radius: var(--r-full); overflow: hidden; background: var(--border); }
.seg-bar > span { height: 100%; display: block; }
.seg-accent  { background: var(--brand-500); }
.seg-ok      { background: var(--ok); }
.seg-warn    { background: #A08030; }
.seg-neutral { background: #B8B2A6; }
.seg-dot { width: 8px; height: 8px; border-radius: var(--r-full); display: inline-block; }

.trend-up   { color: var(--ok); font-weight: 600; }
.trend-down { color: var(--danger); font-weight: 600; }

/* Tab-strip de navegação da obra (subseções) — pill segmentado destacado */
.obra-tabs { display: flex; gap: 4px; background: var(--surface-2, var(--surface-2));
  border: 1px solid var(--border); border-radius: var(--r-lg); padding: 5px;
  margin-bottom: 20px; overflow-x: auto; box-shadow: inset 0 1px 2px rgba(0,0,0,.04); }
.obra-tabs a { padding: 10px 18px; font-size: var(--fs-base); font-weight: 600; color: var(--muted);
  border-radius: var(--r-md); white-space: nowrap; text-decoration: none;
  transition: background .15s ease, color .15s ease, box-shadow .15s ease; }
.obra-tabs a:hover { color: var(--fg); background: rgba(0,0,0,.04); }
.obra-tabs a.on { color: var(--on-accent); background: var(--brand-500);
  box-shadow: 0 2px 8px var(--brand-drop); }
.obra-tabs a.on:hover { background: var(--brand-600, var(--brand-500)); color: var(--on-accent); }
/* Faixa de métricas da obra (header) */
.obra-metrics { background: var(--surface-2); border-radius: var(--r-md); padding: 16px 18px; }

/* Menu longo (os 14 destinos do desarquivamento): rola dentro de si em vez de
   crescer além da janela. Sem isto, num telefone os últimos destinos ficam
   fora do alcance — e "Cancelado", o último, é o que some primeiro. */
.dropdown-menu--rolavel { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }

/* Header da obra com imagem de fundo (banner sutil) */
/* Sem `overflow: hidden` aqui. O corte existia só para a imagem de fundo não
   vazar dos cantos arredondados — e cortava junto TODO dropdown do cabeçalho,
   que é `position: absolute` e cai para fora do cartão. Com o menu de
   desarquivamento (14 destinos) o estrago virou visível; com "Mudar status"
   (4 itens) já acontecia, sem ninguém notar. O corte agora é da imagem. */
.obra-hero { position: relative; }
.obra-hero__bg { position: absolute; inset: 0; background-size: cover; background-position: center;
  border-radius: inherit; opacity: .10; z-index: 0; pointer-events: none; }
.obra-hero > .card-body { position: relative; z-index: 1; }

/* Status "hero" — grande e vívido (header da obra) */
.status-hero { display: inline-flex; align-items: center; gap: 7px; padding: 6px 14px;
  border-radius: var(--r-full); font-family: var(--font-display); font-size: var(--fs-nav); font-weight: 600;
  letter-spacing: -.01em; border: 0; color: var(--on-accent); background: var(--brand-500);
  white-space: nowrap; }
/* Sem sombra colorida: `status-hero` INFORMA um estado, nao oferece uma acao.
   Com a sombra de forca de accent ele virava o elemento mais alto da tela e
   disputava com o botao que de fato move o processo — e "uma acao accent por
   tela" (DESIGN.md) some quando o que nao e acao grita igual. */
.status-hero::before { content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: rgba(255,255,255,.95); box-shadow: 0 0 0 4px rgba(255,255,255,.28);
  animation: status-pulse 1.8s ease-in-out infinite; }
.status-hero.info    { background: var(--info); }
.status-hero.ok      { background: var(--ok); }
.status-hero.warn    { background: var(--warn); }
.status-hero.danger  { background: var(--danger); }
@keyframes status-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
/* Animacao INFINITA sem guarda de movimento — achado ao arrumar o cabecalho.
   Ponto que pisca para sempre e o caso classico de WCAG 2.2.2, e este piscava
   para todo status, inclusive "Concluido". */
@media (prefers-reduced-motion: reduce) {
  .status-hero::before { animation: none; }
}

/* Barra de progresso animada (shimmer live) */
.progress-fill--anim { position: relative; overflow: hidden; transition: width .8s cubic-bezier(.22,1,.36,1); }
.progress-fill--anim::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
  transform: translateX(-100%); animation: progress-shimmer 1.8s ease-in-out infinite; }
@keyframes progress-shimmer { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
  .status-hero::before { animation: none; }
  .progress-fill--anim::after { animation: none; }
}
/* Zona de upload drag-drop */
.drop-zone { display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* `--border-strong` nunca existiu e `#B8B2A6` não está na paleta —
     ficava a 27 de distância de `--brand-200`. Borda tracejada de área de
     soltar arquivo é borda: `--border`, com o tracejado dando o destaque. */
  border: 1.5px dashed var(--border); border-radius: var(--r-md);
  padding: 26px 16px; background: var(--surface-2); cursor: pointer; text-align: center;
  transition: border-color .15s, background .15s; margin: 0; position: relative; }
/* A moldura É o controle; o campo nativo fica atrás dela. Regra própria, e não
   `.visually-hidden` do Bootstrap, por dois motivos: a zona de soltar é
   componente do sistema e não deve depender de utilitário de terceiro para não
   exibir lixo, e o dia em que o Bootstrap sair o campo nativo reapareceria
   dentro da moldura, com o "Escolher arquivos | Nenhum arquivo escolhido" do
   navegador por cima do texto da zona.

   Escondido por recorte, nunca por `display:none`: o campo continua focável, e
   é assim que a zona existe para quem navega por teclado. */
.drop-zone input[type="file"] {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}
.drop-zone:hover { border-color: var(--brand-500); }
.drop-zone--over { border-color: var(--brand-500); background: var(--brand-50); }
/* O campo de arquivo fica `visually-hidden`, e não `d-none`. A diferença é que
   `d-none` tira o controle da ordem de tabulação: a zona de soltar vira uma
   área que só o mouse alcança, e enviar arquivo deixa de existir para quem
   navega por teclado — falha direta de eMAG. Escondido desta forma o campo
   continua focável, e o realce abaixo mostra o foco na moldura da zona, que é
   o que a pessoa vê. */
.drop-zone:has(input[type="file"]:focus-visible) { border-color: var(--brand-500); }
/* Vale para as DUAS molduras. No invólucro o campo costuma ser o seletor nativo,
   que traz o próprio realce — mas há caso em que ele está escondido atrás de um
   botão (a capa do empreendimento), e sem esta linha o foco de teclado ficaria
   invisível: a pessoa tabula até um controle que não dá sinal de estar em foco. */
[data-drop-zone]:has(input[type="file"]:focus-visible) {
  outline: 2px solid var(--brand-500); outline-offset: 2px;
}
.drop-zone--recusado { border-color: var(--danger); background: var(--danger-subtle); }

/* INVÓLUCRO — a outra moldura possível. Um `<div data-drop-zone>` em volta de
   um campo comum não troca o controle: o seletor nativo continua lá, e o que
   entra é a fila, a barra e a possibilidade de soltar arquivo em cima.

   É a forma certa onde o campo é um entre vários numa linha (`col-md-4` das
   telas de entregável, anexo e diário) e onde o campo abre a câmera do
   aparelho — uma caixa tracejada em qualquer um dos dois casos seria um bloco
   de 80px de altura no meio de uma fileira de campos de 38px.

   Sem o realce abaixo a área aceitaria o arquivo em silêncio: funcionaria, e
   ninguém descobriria. O anel só aparece com arquivo sendo arrastado por cima,
   que é exatamente o momento em que a informação vale. */
[data-drop-zone]:not(.drop-zone) {
  border-radius: var(--r-md);
  transition: box-shadow .15s var(--ease-out), background .15s var(--ease-out);
}
[data-drop-zone]:not(.drop-zone).drop-zone--over {
  box-shadow: 0 0 0 2px var(--brand-500);
}

@media (prefers-reduced-motion: reduce) {
  [data-drop-zone]:not(.drop-zone) { transition: none; }
}

/* ── Envio de arquivo: a fila e o andamento ──────────────────────────────
   O que a zona de soltar não dizia: QUAIS arquivos entraram, quanto pesam e
   em que pé está a subida. Sem isso, arrastar três fotos e ver a moldura
   piscar é indistinguível de arrastar nenhuma. */
.envio-lista { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.envio-lista:empty { display: none; }

.envio-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
}
.envio-item-corpo { flex: 1; min-width: 0; }
.envio-item-nome {
  color: var(--fg); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Tamanho de arquivo é dado técnico: DM Mono e formato pt-BR, pela Regra do
   Dado Exato e pela Regra do Formato Brasileiro. `1,4 MB`, nunca `1.4 MB`. */
.envio-item-peso {
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  color: var(--muted); letter-spacing: .02em;
}
.envio-item-tirar {
  flex-shrink: 0; border: 0; background: none; color: var(--muted);
  line-height: 1; padding: 4px; border-radius: var(--r-sm); cursor: pointer;
}
.envio-item-tirar:hover { color: var(--fg); background: var(--surface-2); }
.envio-item-tirar:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 1px; }

.envio-item--erro { border-color: var(--danger); }
.envio-item--erro .envio-item-peso { color: var(--danger); }

/* A BARRA. Nasce ausente: arquivo escolhido e ainda não enviado não tem
   andamento nenhum, e uma barra parada em zero durante a digitação da legenda
   parece um envio travado. Ela só aparece quando o envio começa. */
.envio-item-barra {
  display: none;
  height: 3px; margin-top: 6px;
  background: var(--surface-2); border-radius: var(--r-full); overflow: hidden;
}
.envio-item--enviando .envio-item-barra { display: block; }
.envio-item-barra b {
  display: block; height: 100%; width: 0;
  background: var(--brand-500); border-radius: inherit;
}

/* MEDIDO — o envio sabe quantos bytes faltam, e a largura é o número real.
   Vem do `htmx:xhr:progress`. Transição curta pelo mesmo motivo da barra do
   topo: com os 280ms do resto do sistema ela fica sempre atrás do progresso
   e chega ao fim depois do upload. */
.envio-item--medindo .envio-item-barra b { transition: width .12s linear; }

/* INDETERMINADO — o envio NÃO sabe. Formulário que posta a página inteira não
   expõe progresso nenhum ao JavaScript: o navegador não emite evento de
   upload para `<form method="post">`. Aqui a barra é honesta e não finge
   porcentagem: um traço curto atravessa o trilho, comunicando "em curso" sem
   afirmar "faltam 25%". Fingir número é o defeito que a barra do topo deste
   mesmo sistema já foi consertada para não cometer. */
.envio-item--indeterminado .envio-item-barra b {
  width: 35%; animation: envio-atravessa 1.1s var(--ease-in-out) infinite;
}
@keyframes envio-atravessa {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(340%); }
}

.envio-item--pronto .envio-item-barra b { width: 100% !important; animation: none; }

/* Recusa de tipo ou de tamanho. Fica junto da zona, não em alerta no topo:
   o erro é do arquivo que a pessoa acabou de soltar, e ler a explicação a
   uma tela de distância do gesto não conserta o gesto. */
.envio-recusa {
  margin-top: 8px; padding: 8px 10px;
  border-left: 3px solid var(--danger);
  background: var(--danger-subtle);
  color: var(--fg); font-size: var(--fs-sm); border-radius: var(--r-sm);
}
.envio-recusa:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .envio-item--indeterminado .envio-item-barra b {
    animation: none; width: 100%; opacity: .45;
  }
  .envio-item--medindo .envio-item-barra b { transition: none; }
}

/* ── Geoservicos table / badges / buttons / toast ────────────── */
.geo-table {
  border-collapse: separate;
  border-spacing: 0;
  background: var(--surface);
  border-radius: var(--r-xl);
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: var(--sh-sm);
}
.geo-table thead th {
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  padding: .75rem 1rem;
  border-bottom: 1px solid var(--border);
}
.geo-table tbody td {
  padding: .7rem 1rem;
  font-size: var(--fs-base);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  color: var(--fg);
}
.geo-table tbody tr:last-child td { border-bottom: none; }
.geo-table tbody tr:hover { background: rgba(0,0,0,.025); }
[data-theme="dark"] .geo-table tbody tr:hover { background: rgba(255,255,255,.04); }

.geo-badge {
  display: inline-flex;
  align-items: center;
  padding: .2em .6em;
  border-radius: var(--r-full);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.geo-badge-car    { background: color-mix(in srgb, var(--ok) 12%, transparent);   color: var(--ok); }
.geo-badge-sudema { background: color-mix(in srgb, var(--info) 12%, transparent); color: var(--info); }

.geo-btns {
  display: inline-flex;
  gap: .4rem;
  flex-wrap: wrap;
  justify-content: center;
}
.geo-btn {
  display: inline-flex;
  align-items: center;
  padding: .3em .75em;
  border-radius: var(--r-md);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .04em;
  cursor: pointer;
  border: 1px solid transparent;
  text-decoration: none;
  transition: filter var(--dur-fast), transform var(--dur-fast);
  white-space: nowrap;
}
.geo-btn:hover:not(:disabled) { filter: brightness(1.12); transform: translateY(-1px); }
.geo-btn-wms { background: color-mix(in srgb, var(--info) 15%, transparent); color: var(--info);
               border-color: color-mix(in srgb, var(--info) 30%, transparent); }
.geo-btn-wfs { background: color-mix(in srgb, var(--ok) 15%, transparent);   color: var(--ok);
               border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
.geo-btn-kml { background: color-mix(in srgb, var(--warn) 15%, transparent); color: var(--warn);
               border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
.geo-btn-disabled,
.geo-btn:disabled { opacity: .35; cursor: not-allowed; pointer-events: none; }

.geo-toast {
  position: fixed;
  bottom: 1.75rem;
  left: 50%;
  transform: translateX(-50%) translateY(1rem);
  background: var(--fg);
  color: var(--bg);
  padding: .55rem 1.2rem;
  border-radius: var(--r-full);
  font-size: var(--fs-nav);
  font-weight: 500;
  box-shadow: var(--sh-lg);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s, transform .2s;
  z-index: 9999;
}
.geo-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ═══════════════════════════════════════════════════════════════
   FOOTER GOVERNAMENTAL — compartilhado em todas as páginas
   ═══════════════════════════════════════════════════════════════ */
.gov-footer {
  font-family: var(--app-font);
  background: var(--bg);
  border-top: 3px solid var(--brand-500);
}

.gov-footer-top {
  background: var(--bg);
  padding: 28px 0;
  border-bottom: 1px solid var(--border);
}

.gov-footer-logo {
  height: 52px;
  width: auto;
}

.gov-footer-inst-name {
  font-size: var(--fs-md);
  font-weight: 700;
  color: var(--fg);
  margin: 0 0 2px;
  letter-spacing: -.01em;
}

.gov-footer-sys-name {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0;
}

.gov-footer-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--brand-tint);
  border: 1px solid var(--brand-line);
  color: var(--brand-500);
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: 5px 14px;
  border-radius: var(--r-full);
  letter-spacing: .02em;
}

.gov-footer-body {
  padding: 48px 0 32px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.gov-footer-heading {
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--brand-500);
  margin-bottom: 16px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.gov-footer-text {
  font-size: var(--fs-base);
  color: var(--muted);
  margin-bottom: 6px;
  line-height: 1.6;
}

.gov-footer-links {
  list-style: none;
  padding: 0;
  margin: 0;
}

.gov-footer-links li {
  margin-bottom: 8px;
}

.gov-footer-links li a {
  font-size: var(--fs-base);
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-fast);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.gov-footer-links li a::before {
  content: '›';
  color: var(--brand-500);
  font-weight: 700;
  flex-shrink: 0;
  transition: transform var(--dur-fast);
}

.gov-footer-links li a:hover {
  color: var(--brand-500);
}

.gov-footer-links li a:hover::before {
  transform: translateX(3px);
}

.gov-footer-bottom {
  padding: 16px 0;
  background: var(--bg);
}

.gov-footer-copy {
  font-size: var(--fs-nav);
  color: var(--muted);
  margin: 0;
}

/* ── Âncoras de seção do RDO ──────────────────────────────────────
   O topbar do shell é fixo: sem a margem, o navegador rola a seção
   para debaixo dele e o usuário acha que nada aconteceu. */
.rdo-ancora { display: block; position: relative; top: -84px; visibility: hidden; }

/* KPI clicável: o card inteiro é o alvo, e o ativo se identifica. */
a:has(> .kpi-card) { display: block; color: inherit; }
.kpi-card { transition: border-color var(--app-transition), transform var(--app-transition); }
a:hover > .kpi-card { border-color: var(--brand-500); transform: translateY(-1px); }
a:focus-visible > .kpi-card { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.kpi-card--ativo { border-color: var(--brand-500); box-shadow: inset 0 0 0 1px var(--brand-500); }


/* ── Breadcrumbs ──────────────────────────────────────────────────────────
   O componente é um `<ol>` e não tinha estilo nenhum: o navegador desenhava a
   numeração padrão, e a trilha aparecia como "1. Início  2. Dashboard". Numerar
   caminho de navegação não informa nada — o que informa é a ordem visual e o
   separador. `<ol>` continua sendo a marcação certa (a ordem é semântica), só
   que sem o marcador. */
/* Sem valor de reserva no `var()`: os tokens são definidos no `:root` DESTE
   arquivo, então a reserva nunca é usada — e eu tinha escrito `#6b7671` onde o
   DESIGN.md diz `#6B7A74`. Reserva que não roda é lugar onde a deriva mora
   sem ninguém ver. */
.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.breadcrumbs li + li::before {
  content: "/";
  margin-right: 6px;
  color: var(--border);
}
.breadcrumbs a { color: var(--muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--fg); text-decoration: underline; }
.breadcrumbs .current { color: var(--fg); font-weight: 600; }

/* ═══════════════════════════════════════════════════════════════════
   CARREGANDO — o sistema nunca fica mudo

   Eram 88 requisições HTMX em 41 telas e NENHUM indicador: a pessoa
   clicava, a tela não mudava, e "processando" era indistinguível de
   "travou". Feedback aqui é global de propósito — resolver botão a botão
   deixaria de fora o 89º.
   ═══════════════════════════════════════════════════════════════════ */

/* A barra fina do topo saiu daqui. Ela virou a SEGUNDA coisa dizendo a mesma
   frase: desde que o `.loader` entrou, uma requisição em voo acendia um traço
   de 3px na borda superior E o conjunto de pontos. Dois indicadores para um
   fato só não somam informação — dividem a atenção, e o que fica longe do olho
   é o que deixa de ser lido.
   O que ela cobria virou `#carga-conteudo`, mais abaixo. */

/* O controle que disparou fica visivelmente ocupado. O HTMX marca com
   `.htmx-request` quem originou a requisição — sem precisar de atributo
   em cada botão.

   `.enviando` é o mesmo estado para o formulário que NÃO usa HTMX, que é a
   maioria: 328 telas ainda salvam por POST de página inteira, e nelas o
   botão de salvar não mudava de aparência nenhuma. Quem marca é
   `core/static/js/estados-carregando.js`. As duas classes compartilham o
   desenho de propósito — pelo botão, a pessoa não deve conseguir dizer qual
   das duas tecnologias está por trás. */
.htmx-request, .enviando { cursor: progress; }
button.htmx-request, .btn.htmx-request,
button.enviando, .btn.enviando { opacity: .65; pointer-events: none; }
button.htmx-request::after, .btn.htmx-request::after,
button.enviando::after, .btn.enviando::after {
  content: ""; display: inline-block; width: .8em; height: .8em;
  margin-left: .5em; vertical-align: -.08em;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: carregando-gira .7s linear infinite;
}
@keyframes carregando-gira { to { transform: rotate(360deg); } }

/* No botão SÓ-ÍCONE o giro SUBSTITUI o glifo, não se acomoda ao lado dele.
   A regra acima acrescenta um `::after` com `margin-left` — num botão comum ele
   entra depois do texto e tudo bem. No `.btn-icon`, que é uma caixa fixa de
   34px com `padding: 0` e `display: inline-flex`, o `::after` vira um SEGUNDO
   filho flex disputando a mesma largura: os dois se espremem, o ícone sai do
   lugar e a fileira inteira desalinha. Era o "spinner quebrando o css". */
.btn-icon.htmx-request > i, .btn-icon.enviando > i,
.btn-icon.htmx-request > svg, .btn-icon.enviando > svg { display: none; }
.btn-icon.htmx-request::after, .btn-icon.enviando::after {
  margin-left: 0; width: 1em; height: 1em; vertical-align: 0;
}

/* O BLOCO QUE VAI SER TROCADO esmaece — não vira esqueleto.

   Esqueleto é a escolha certa para primeira carga, quando não há nada no
   lugar. Aqui quase nunca é o caso: o HTMX deste sistema troca aba, tabela
   filtrada e página de lista, e nos três já existe conteúdo na tela. Trocar
   uma tabela de 20 linhas por barras cinza tira da pessoa a linha que ela
   estava lendo e faz a altura do bloco saltar quando o resultado volta com
   outro número de linhas. Esmaecido, o conteúdo antigo continua legível e
   nada se move; quem diz que há requisição em voo é a barra do topo.

   `pointer-events: none` porque conteúdo esmaecido ainda é clicável, e um
   segundo clique no filtro enquanto o primeiro corre é a origem da resposta
   fora de ordem. */
.bloco-carregando {
  opacity: .45;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* ── Esqueleto da SEÇÃO em troca ──────────────────────────────────────
   Só sobre o bloco que o HTMX vai substituir — não sobre a página. Mover uma
   atividade na aba de execução troca a tabela daquela aba; a barra lateral, as
   abas e o cabeçalho continuam onde estavam, e é isso que diz "é esta lista que
   está mudando", e não "o sistema está recarregando".

   SOBREPOSTO e não substituindo: o bloco antigo mantém a própria altura, então
   nada salta quando a resposta volta com outro número de linhas.
   Ver core/static/js/estados-carregando.js.  */
.carga-secao {
  position: absolute; inset: 0;
  /* TRANSLÚCIDO COM DESFOQUE, e os dois juntos ou nenhum.
     Só translúcido deixaria o texto antigo legível POR BAIXO das barras do
     esqueleto — dois textos disputando o mesmo espaço, que é pior que o véu
     opaco. O desfoque destrói a legibilidade do que está atrás, e aí a
     translucidez passa a dizer o que se quer dela: "é ISTO aqui que está
     mudando, e o resto continua onde estava".
     A cor é `--surface-3` (#eae6de no claro), o creme mais fechado da paleta
     — pedido explícito. `color-mix` sobre o TOKEN e não `#f4f1ec` cravado: assim o tema
     escuro acompanha sem uma segunda regra, e o dia em que a paleta mudar o
     véu muda junto. */
  background: color-mix(in srgb, var(--surface-3) 78%, transparent);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  /* 1010, e o número tem vizinhos nomeados.
     O Leaflet empilha até 1000 nos próprios controles (`.leaflet-top` e
     `.leaflet-bottom`), então um véu em 5 ficava POR BAIXO do mapa: a seção
     desfocava e o mapa continuava nítido e clicável em cima dela. Acima de
     1000 e abaixo de 1050 (painel do sino) e do modal — véu de SEÇÃO não tem
     por que cobrir um modal aberto. */
  z-index: 1010;
  display: flex; align-items: flex-start; justify-content: center;
  padding: var(--sp-4);
  overflow: hidden;
  border-radius: inherit;
}
/* O bloco por baixo já está em `opacity: .45`; o véu compensa para o esqueleto
   não sair lavado. */
.bloco-carregando .carga-secao { opacity: 1; }

.carga-secao .esqueleto { width: 100%; }
/* `fixed`, com o topo calculado pelo JS na INTERSEÇÃO entre a seção e a janela.
   Numa tabela mais alta que a tela, o centro da seção pode estar fora de vista —
   e loader que não se vê é o mesmo que não existir. `sticky` não serve aqui: o
   `overflow: hidden` do véu cria um scrollport que não rola, e grudado ele se
   comporta como relativo. Ver core/static/js/estados-carregando.js. */
.carga-secao .loader {
  position: fixed; left: 50%;
  /* `scale` e não uma variável de tamanho: o `.loader` é desenhado com
     `box-shadow` de valores fixos, e não existe token que ele leia. `scale`
     encolhe o desenho inteiro, sombras inclusive.
     Menor que o da página: aqui ele comenta uma seção, não a tela. */
  transform: translate(-50%, -50%) translateY(-4.5px) scale(.72);
}

/* O MEDIDOR — espera longa, a quinta e última peça deste vocabulário.

   As quatro anteriores respondem "há algo em voo" numa escala de segundos. Esta
   responde numa escala de minutos: conversão de DWG e IFC, envio ao PNCP,
   consulta ao Painel de Preços. Nessa escala o giro de 0,7s do `.htmx-request`,
   lido por quarenta segundos, deixa de dizer "processando" e passa a dizer
   "travou" — é o mesmo defeito que fez a barra do topo parar de fingir 75%.
   Ciclo de 2,4s comunica processo, não instante.

   O desenho é reprodução do `morphing-spinner` de Jatin Yadav (21st.dev). O
   fonte dele é fechado — iframe de outra origem, registro exigindo autenticação
   —, então isto é o comportamento observado, não o código dele: um quadrado
   gira enquanto o raio dos cantos respira entre firme e macio. Nunca vira outra
   figura, e é essa quietude que o faz funcionar ao lado de texto.

   `transform` é composta pela GPU. `border-radius` repinta, mas num elemento de
   44px isso não custa nada — e o commit que trocou `width` por `transform` na
   barra do KPI já fixou qual é o caminho da casa. */
.espera {
  width: var(--espera-tam, 44px); height: var(--espera-tam, 44px);
  flex-shrink: 0;
  background: var(--brand-500);
  animation: espera-gira var(--espera-ciclo, 2.4s) var(--ease-in-out) infinite;
}
@keyframes espera-gira {
  0%   { border-radius: 26%; transform: rotate(0deg); }
  25%  { border-radius: 42%; transform: rotate(90deg); }
  50%  { border-radius: 26%; transform: rotate(180deg); }
  75%  { border-radius: 42%; transform: rotate(270deg); }
  100% { border-radius: 26%; transform: rotate(360deg); }
}
/* Vazado, para tela cheia onde a massa sólida disputaria atenção com o conteúdo
   em vez de comentar sobre ele. Mesmo movimento. */
.espera--vazado {
  background: none; box-sizing: border-box;
  border: 3px solid var(--brand-500);
}

/* A GRADE — a página inteira ainda está chegando.

   Quatro por três pontos que piscam fora de ordem. É a peça de CARGA INICIAL,
   e por isso não se confunde com as outras: as quatro primeiras respondem "há
   algo em voo" com a tela já montada, e o `.espera` responde "isto vai levar
   minutos". Esta responde "ainda não há tela".

   Desenho de https://css-loaders.com (l29), com uma mudança: o preto vira
   `currentColor`, e a cor vem do token. Assim a Regra do Tenant continua
   valendo — a prefeitura com primária própria vê a carga na cor dela — e o tema
   escuro não recebe doze pontos pretos sobre fundo `#0b0f14`.

   Os `#0000` dos quadros são transparente explícito: é o apagar de cada ponto,
   e trocá-los por token acenderia a grade inteira.

   O elemento tem 9px, mas o desenho ocupa 45px — as sombras saem da caixa nos
   dois eixos. Quem centraliza precisa contar isso, e é o que `.carga-grade`
   faz com a margem. */
.loader {
  width: 9px; height: 9px;
  background: currentColor;
  color: var(--brand-500);
  box-shadow:
    -18px -9px, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px, 0  9px, 18px  9px,
    -18px 18px, 0 18px, 18px 18px;
  animation: l29 2s infinite;
}
@keyframes l29 {
  10% { box-shadow:
    -18px -9px, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px #0000, 0  9px, 18px  9px #0000,
    -18px 18px, 0 18px, 18px 18px; }
  20% { box-shadow:
    -18px -9px, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px, 0  9px, 18px  9px #0000,
    -18px 18px #0000, 0 18px, 18px 18px; }
  30% { box-shadow:
    -18px -9px, 0 -9px #0000, 18px -9px,
    -18px   0  #0000,         18px   0 ,
    -18px  9px, 0  9px #0000, 18px  9px,
    -18px 18px #0000, 0 18px, 18px 18px #0000; }
  40% { box-shadow:
    -18px -9px, 0 -9px, 18px -9px,
    -18px   0  #0000,         18px   0 #0000,
    -18px  9px, 0  9px, 18px  9px,
    -18px 18px, 0 18px, 18px 18px; }
  50% { box-shadow:
    -18px -9px, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0  #0000,
    -18px  9px #0000, 0  9px, 18px  9px,
    -18px 18px, 0 18px, 18px 18px; }
  60% { box-shadow:
    -18px -9px, 0 -9px #0000, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px #0000, 0  9px, 18px  9px,
    -18px 18px #0000, 0 18px, 18px 18px; }
  70% { box-shadow:
    -18px -9px, 0 -9px #0000, 18px -9px #0000,
    -18px   0 ,         18px   0 ,
    -18px  9px, 0  9px, 18px  9px,
    -18px 18px #0000, 0 18px, 18px 18px; }
  80% { box-shadow:
    -18px -9px #0000, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px, 0  9px, 18px  9px,
    -18px 18px, 0 18px, 18px 18px #0000; }
  90% { box-shadow:
    -18px -9px #0000, 0 -9px, 18px -9px,
    -18px   0 ,         18px   0 ,
    -18px  9px, 0  9px #0000, 18px  9px,
    -18px 18px, 0 18px, 18px 18px #0000; }
}

/* O véu de carga inicial. `opacity`/`visibility` e não `display`, para a saída
   poder ser suave — e `pointer-events: none` no fim, senão o véu invisível
   continua comendo os cliques da tela que já chegou. */
#carga-veu {
  position: fixed; inset: 0; z-index: 2500;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg);
  transition: opacity .25s var(--ease-out), visibility .25s var(--ease-out);
}
#carga-veu.pronto { opacity: 0; visibility: hidden; pointer-events: none; }
/* O desenho NÃO é simétrico em torno da caixa: as linhas ficam em -9, 0, +9 e
   +18, então o centro visual está 4,5px abaixo do centro do elemento. Centrar
   por flex alinha a CAIXA, e a grade nasce baixa. `transform` corrige sem mexer
   no layout — margem entraria na conta do próprio alinhamento e o desvio
   sobraria pela metade. Medido no navegador: 0,0 de desvio depois disto. */
#carga-veu .loader,
#page-preloader .loader { transform: translateY(-4.5px); }

@media (prefers-reduced-motion: reduce) {
  /* A grade para acesa e completa: quem pediu menos movimento continua vendo
     que a página está chegando. */
  .loader { animation: none; }
  #carga-veu { transition: none; }
}

/* A faixa em que o `.espera` mora numa tela de lista (core/_faixa_espera.html).
   Fica ACIMA da tabela, e não dentro dela: o que está em curso é o processo, não
   uma linha — pendurar o aviso numa linha faria a pessoa procurar qual. */
.faixa-espera {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 18px; margin-bottom: 14px;
  background: var(--brand-50);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}
.faixa-espera strong { display: block; color: var(--fg); font-weight: 600; }
.faixa-espera span   { color: var(--muted); font-size: var(--fs-sm); }

@media (prefers-reduced-motion: reduce) {
  #carga-conteudo .esqueleto > * { animation: none; }
  button.htmx-request::after, .btn.htmx-request::after,
  button.enviando::after, .btn.enviando::after {
    animation: none; border-right-color: currentColor; opacity: .5;
  }
  /* O esmaecido continua: é estado, não animação. Só deixa de esmaecer
     gradualmente. */
  .bloco-carregando { transition: none; }
  /* O `.espera` para no meio-termo do raio — nem o quadrado firme, nem o mole.
     Quem diz que há processo em curso passa a ser a legenda ao lado, que é
     texto e não movimento. */
  .espera { animation: none; border-radius: 34%; }
}


/* Ação de inserir foto dentro do box da galeria. Alinhada à esquerda, junto do
   título, porque é a ação DAQUELA galeria — na direita ela pareceria a ação da
   grade inteira, que é o botão "Adicionar fotos" do topo da aba. */
.galeria-inserir { align-self: flex-start; }

/* ═══════════════════════════════════════════════════════════════════
   ROTEIRO — o herói da tela de panorama

   Vem de `docs/design/mockups/gov-variacao.html`, aprovado em 17/08. O /gov
   abria com "Bem-vindo(a), Nome" e cinco totais soltos; boas-vindas não é
   informação, e total solto não responde a pergunta de quem abre um hub, que é
   ONDE ESTÃO OS PROCESSOS. O roteiro de contratação é a espinha do produto e a
   única coisa que este sistema sabe dizer e um painel genérico não.

   Três regras nomeadas do DESIGN.md governam o que está aqui: a Regra do
   Gradiente que Desenha (a régua), a Regra do Traço que Mede (o medidor de 1s)
   e a Regra da Profundidade com Papel (um cartão-herói por tela).
   ═══════════════════════════════════════════════════════════════════ */
.roteiro {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  /* Um degrau acima da grade que vem abaixo. Segue a Regra da Sombra Verde. */
  box-shadow: 0 2px 4px rgba(42, 50, 45, .06), 0 12px 28px rgba(42, 50, 45, .10);
  padding: 22px 24px 18px;
  margin-bottom: 20px;
}
.roteiro-cabeca {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 12px; margin-bottom: 4px;
}
.roteiro-titulo {
  font-size: var(--fs-xl); font-weight: 700;
  letter-spacing: -.015em; margin: 0; color: var(--fg);
}
.roteiro-nota { font-size: var(--fs-sm); color: var(--muted); }

/* A RÉGUA. Marca de 1px a cada 1/12 da largura: cada uma é um mês. Gradiente
   como técnica de desenho — remova e a tela perde a escala contra a qual os
   medidores abaixo são lidos. */
.roteiro-regua {
  position: relative; height: 13px; margin: 16px 0 0;
  background-image: linear-gradient(90deg, var(--border) 0, var(--border) 1px, transparent 1px);
  background-size: calc(100% / 12) 100%;
  border-bottom: 1px solid var(--border);
}
.roteiro-regua span {
  position: absolute; top: -13px;
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  color: var(--muted); letter-spacing: .04em;
}

/* `auto-fit`, e não `repeat(5, 1fr)`: quantas etapas existem é DADO, não
   constante. Município que contratou dois módulos tem duas etapas, e numa
   grade de cinco colunas fixas elas ficariam encolhidas à esquerda com três
   vãos vazios — a tela pareceria estar carregando o resto. */
.roteiro-etapas {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 10px; margin-top: 14px;
}
.roteiro-etapa {
  position: relative;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 12px 13px 13px;
  color: inherit; text-decoration: none; display: block;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s;
}
.roteiro-etapa:hover {
  transform: translateY(-2px); box-shadow: var(--sh-sm);
  border-color: var(--brand-500); color: inherit;
}
/* Cartão sobre cartão marca ONDE O TRABALHO ESTÁ CONCENTRADO sem gastar cor —
   a cor está ocupada dizendo estado (âmbar = etapa com processo velho). */
.roteiro-etapa.foco {
  background: var(--surface); border-color: var(--brand-500);
  box-shadow: 0 2px 3px rgba(42, 50, 45, .05), 0 10px 22px rgba(94, 114, 87, .14);
  transform: translateY(-3px);
}
.roteiro-etapa .ordem {
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  letter-spacing: .1em; color: var(--muted);
}
.roteiro-etapa .nome { font-size: var(--fs-nav); font-weight: 600; margin: 2px 0 8px; }
.roteiro-etapa .fig {
  /* `--fs-2xl` (28px) e não os 26px que vieram do mockup: 26 não é degrau da
     rampa, que vai 22 → 28. Mockup pode inventar tamanho; sistema não. */
  font-family: var(--app-font-mono); font-size: var(--fs-2xl); font-weight: 500;
  line-height: 1; letter-spacing: -.02em; color: var(--fg);
}
.roteiro-etapa .pe { font-size: var(--fs-xs); color: var(--muted); margin-top: 7px; }
/* O medidor é COMPARTILHADO: mede a etapa contra a régua de doze meses no
   roteiro, e o empenhado contra o contratado na faixa de dinheiro. A marca da
   metade abaixo continua só no roteiro — lá ela é o limiar dos seis meses, e na
   faixa de dinheiro 50% não quer dizer nada. */
.medidor {
  /* 5px, e não 3. A 3px um traço de 8% da régua — um mês de doze — media
     quatro pixels de comprimento por três de altura: some contra o trilho
     cinza, e a faixa inteira parecia "só as linhas da trena". A grandeza é
     a mesma; o que mudou é ela caber no olho. */
  height: 5px; border-radius: var(--r-full); background: var(--border);
  margin-top: 10px; overflow: hidden;
  position: relative;
}
/* Marca da metade da régua — seis meses, o limiar em que a etapa vira
   atenção. Sem ela o traço não tem contra o que ser lido, e barra sem escala
   é lida como proporção do número acima, que não é o que ela mede. */
.roteiro-etapa .medidor::after {
  content: ""; position: absolute; left: 50%; top: 0; bottom: 0;
  width: 1px; background: var(--surface);
}
.medidor > i {
  display: block; height: 100%; background: var(--brand-500);
  transform-origin: left center;
  transform: scaleX(calc(var(--pct, 0) / 100));
  /* 1s com desaceleração forte: a Regra do Traço que Mede. O olho acompanha o
     traço até onde ele para, e é onde ele para que informa. Monotônico — não
     ultrapassa o valor final. */
  transition: transform 1s cubic-bezier(.22, 1, .36, 1);
}
/* Etapa com processo velho: o medidor quase cheio JÁ diz isso, e a cor
   confirma. Cor sozinha nunca carrega o recado. */
.roteiro-etapa.atencao .medidor > i { background: var(--warn); }
.roteiro-etapa.atencao .fig { color: var(--warn); }

/* `auto-fit` com piso de 170px já cai para uma coluna sozinho abaixo de
   ~380px; media query aqui seria regra brigando com regra. */
@media (prefers-reduced-motion: reduce) {
  .roteiro-etapa, .medidor > i { transition: none; }
  .roteiro-etapa.foco, .roteiro-etapa:hover { transform: none; }
}

/* ── Chip de atenção ──────────────────────────────────────────
   Existe só quando tem o que contar: chip com zero é alarme que dispara sem
   motivo, e treina o olho a ignorar o vermelho. Quem decide é o view. */
.chip-atencao {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-full); padding: 6px 13px 6px 9px;
  font-size: var(--fs-sm); text-decoration: none; color: var(--fg);
  transition: border-color .15s, background .15s;
}
.chip-atencao:hover { border-color: var(--brand-500); background: var(--surface-2); color: var(--fg); }
.chip-atencao .cont {
  font-family: var(--app-font-mono); font-size: var(--fs-xs); font-weight: 500;
  padding: 1px 7px; border-radius: var(--r-full);
  background: var(--surface-2); color: var(--muted);
}
.chip-atencao.perigo .cont { background: var(--kpi-danger-bg); color: var(--kpi-danger-fg); }
/* Três gravidades, e cada uma quer dizer uma coisa: `perigo` — prazo ou
   dinheiro já perdido; `atencao` — vai perder se ninguém agir; sem
   modificador — espera decisão, sem relógio correndo. A cor mora só na
   contagem, não no chip inteiro: seis chips coloridos lado a lado viram uma
   faixa decorativa, e nenhum deles chama mais do que o outro. */
.chip-atencao.atencao .cont { background: var(--kpi-warn-bg); color: var(--kpi-warn-fg); }
/* O detalhe ("3 sem paralisação registrada") precisa se separar do rótulo:
   colados, os dois são lidos como uma frase só, e o número do detalhe se
   confunde com o do chip. */
.chip-atencao .detalhe {
  color: var(--muted); font-size: var(--fs-xs);
  padding-left: 7px; margin-left: 1px;
  border-left: 1px solid var(--border);
}
.chip-atencao.alerta .cont { background: var(--kpi-warn-bg); color: var(--kpi-warn-fg); }
.chip-atencao.bom    .cont { background: var(--kpi-ok-bg); color: var(--kpi-ok-fg); }

/* ── Faixa de dinheiro do hub ─────────────────────────────────
   Separador por BORDA com margem negativa, e não por vão de 1px sobre um fundo
   cinza: com o vão, a última linha de uma grade ímpar deixa a célula que falta
   pintada da cor da borda — um retângulo cinza que parece bloco quebrado. Foi o
   que aconteceu na carteira de contratos, que tem três blocos. A margem de -1px
   empurra a borda da primeira caixa de cada linha para fora do contêiner, onde
   o `overflow: hidden` a corta.

   `auto-fit` pela mesma razão do roteiro: quantos blocos existem é DADO.
   Município que não contratou obras tem dois, e três colunas fixas deixariam
   um vão vazio parecendo carregamento. */
.dinheiro-blocos {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  background: var(--surface); overflow: hidden;
  border-radius: 0 0 calc(var(--r-xl) - 1px) calc(var(--r-xl) - 1px);
}
.dinheiro-bloco {
  background: var(--surface); padding: 15px 18px;
  border-left: 1px solid var(--border); border-top: 1px solid var(--border);
  margin: -1px 0 0 -1px;
}
.dinheiro-bloco .rotulo { font-size: var(--fs-sm); color: var(--muted); }
.dinheiro-bloco .fig {
  /* Dinheiro em DM Mono: coluna de valor que não alinha é coluna que não se
     compara. Um degrau abaixo do número do roteiro (28px), porque a etapa é o
     herói da tela e o dinheiro é a resposta seguinte, não a primeira. */
  font-family: var(--app-font-mono); font-size: var(--fs-xl); font-weight: 500;
  letter-spacing: -.02em; margin-top: 5px; color: var(--fg);
}
.dinheiro-bloco .medidor { margin-top: 9px; }
.dinheiro-bloco .pe { font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; }

/* ── Painel de controle: decisão ao lado da agenda ────────────
   1.6fr para a fila porque ela carrega quatro colunas e a agenda carrega duas.
   Abaixo de 992px vira coluna única: espremida, a fila perderia a coluna de
   idade, que é justamente o dado que ordena a lista. */
.painel-controle {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 16px; align-items: start;
}
@media (max-width: 991.98px) { .painel-controle { grid-template-columns: 1fr; } }

/* ── Item da fila de decisão ──────────────────────────────────
   Linha inteira clicável: o alvo é a decisão, não um link de três palavras no
   fim dela. */
.fila-item {
  /* 110px porque o rótulo mais longo do sistema é "ABRIR SESSÃO": a 96px ele
     quebrava em duas linhas e a pílula ficava mais alta que a linha inteira. */
  display: grid; grid-template-columns: 110px minmax(0, 1fr) auto 18px;
  align-items: center; gap: 12px; padding: 11px 18px;
  border-top: 1px solid var(--border); color: var(--fg); text-decoration: none;
  transition: background .15s;
}
.fila-item:first-child { border-top: 0; }
.fila-item:hover { background: var(--surface-2); color: var(--fg); }
.fila-acao {
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  letter-spacing: .08em; text-align: center; padding: 3px 8px;
  border-radius: var(--r-full);
  background: var(--kpi-neutral-bg); color: var(--kpi-neutral-fg);
}
/* A cor mora no rótulo do ato, nunca na linha inteira — cinco linhas coloridas
   viram faixa decorativa e nenhuma chama mais que a outra. É a mesma regra do
   chip de atenção, dez blocos acima. */
.fila-item.atencao .fila-acao { background: var(--kpi-warn-bg); color: var(--kpi-warn-fg); }
.fila-item.perigo  .fila-acao { background: var(--kpi-danger-bg); color: var(--kpi-danger-fg); }
.fila-corpo { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fila-corpo .titulo { font-size: var(--fs-nav); font-weight: 600; }
.fila-corpo .detalhe {
  font-size: var(--fs-xs); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fila-idade { font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; }
/* Trinta dias parado: o número que ordena a fila também muda de cor, senão só
   o rótulo do ato carrega o recado. */
.fila-item.perigo .fila-idade { color: var(--kpi-danger-fg); font-weight: 600; }
.fila-abrir { color: var(--muted); font-size: var(--fs-sm); }
.fila-item:hover .fila-abrir { color: var(--brand-500); }
/* Anel do sistema, e não o contorno do navegador: a linha inteira é o alvo, e
   o contorno padrão desenha um retângulo colado no texto que some contra a
   borda do cartão. */
.fila-item:focus-visible, .agenda-item:focus-visible {
  outline: none; box-shadow: var(--sh-focus); border-radius: var(--r-md);
}
@media (max-width: 575.98px) {
  /* Em 390px as quatro colunas espremem o objeto até ele virar reticências: a
     idade desce para baixo do título e a seta sai. */
  .fila-item { grid-template-columns: 110px minmax(0, 1fr); }
  .fila-idade { grid-column: 2; }
  .fila-abrir { display: none; }
}

/* ── Fila do PNCP ─────────────────────────────────────────────
   Três estados, um número cada. Publicar no PNCP é requisito de eficácia do
   ato, não integração de infraestrutura: o painel precisa dizer se a fila está
   andando, e o ponto colorido é a etiqueta do estado — o número ao lado é que
   carrega o recado. */
.pncp-linha {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; padding: 5px 0;
}
.pncp-linha .rot {
  display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-nav);
}
.pncp-linha .ponto { display: block; width: 8px; height: 8px; border-radius: var(--r-full); }
.pncp-linha .cont {
  font-family: var(--app-font-mono); font-size: var(--fs-lg); font-weight: 500;
}

/* ── Agenda ───────────────────────────────────────────────────
   Sem cartão por item e sem borda por linha: a lista já é curta (seis itens no
   máximo), e o rótulo de semana em versalete faz a separação que a borda faria
   com mais tinta. */
.agenda-grupo + .agenda-grupo { border-top: 1px solid var(--border); margin-top: 10px; }
.agenda-rotulo {
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  letter-spacing: .1em; color: var(--muted); padding: 12px 0 8px;
}
.agenda-item {
  display: grid; grid-template-columns: 58px minmax(0, 1fr); gap: 12px;
  padding: 7px 0; color: var(--fg); text-decoration: none;
}
.agenda-item:hover { color: var(--fg); }
.agenda-item:hover .titulo { color: var(--brand-500); }
.agenda-data .dia {
  display: block; font-family: var(--app-font-mono);
  font-size: var(--fs-md); font-weight: 500;
}
.agenda-data .falta { display: block; font-size: var(--fs-xs); color: var(--muted); }
.agenda-corpo { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.agenda-corpo .tipo {
  font-family: var(--app-font-mono); font-size: var(--fs-2xs);
  letter-spacing: .08em; color: var(--muted);
}
.agenda-corpo .titulo { font-size: var(--fs-nav); font-weight: 600; }
.agenda-corpo .detalhe {
  font-size: var(--fs-xs); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── Ações que recolhem no celular ────────────────────────────
   O cabeçalho do empreendimento tem oito ações. Em 390px elas quebram em três
   linhas e empurram o conteúdo para fora da dobra.

   UM DOM SÓ, e é o ponto. O padrão fácil seria escrever as ações duas vezes —
   uma inline para desktop, outra dentro de um menu para celular — e este
   repositório já registra noutro lugar por que isso é ruim: "as duas TÊM de
   andar juntas: se divergirem, a versão antiga esconde a porta que a outra
   oferece". Aqui a mesma lista vira faixa no desktop e painel no celular,
   trocando só o CSS.

   O que NÃO recolhe: a ação accent e o status. Quem abre a tela precisa ver o
   que move o processo adiante sem procurar. */
/* `flex-wrap` é obrigatório: sem ele, em 390px o status mais a ação accent
   mais o "…" somam além da largura e o "…" sai da tela — o menu inteiro fica
   inalcançável justamente no tamanho para o qual ele foi feito. */
.acoes-cabecalho { position: relative; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.acoes-toggle { display: none; }
.acoes-lista { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

@media (max-width: 767.98px) {
  .acoes-toggle { display: inline-flex; }
  .acoes-lista {
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
    flex-direction: column; align-items: stretch; flex-wrap: nowrap;
    min-width: 232px; padding: 8px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: 0 2px 4px rgba(42, 50, 45, .06), 0 12px 28px rgba(42, 50, 45, .12);
  }
  /* Fechado é `hidden`, não `opacity: 0`: painel invisível que continua no
     fluxo de foco faz o teclado passear por botões que ninguém vê. */
  .acoes-lista[hidden] { display: none; }
  /* Dentro do painel cada ação ocupa a linha inteira — botão de larguras
     diferentes empilhado fica em escada. */
  .acoes-lista > form, .acoes-lista > .dropdown { width: 100%; }
  .acoes-lista .btn { width: 100%; justify-content: flex-start; }
}

/* ── Marca da Rotier no mapa ──────────────────────────────────────────────
   Canto inferior direito, discreta: ela diz de quem é o sistema, e não disputa
   atenção com o dado. Fundo creme translúcido porque o mapa por baixo pode ser
   claro ou escuro, e um PNG sem fundo desaparece sobre imagem de satélite. */
.marca-rotier {
  background: rgba(244, 241, 236, .88);
  border: 1px solid var(--rotier-cream-dark, #D8D2C6);
  border-radius: 6px;
  padding: 4px 8px;
  margin: 0 10px 10px 0 !important;
  line-height: 0;
  box-shadow: 0 1px 3px rgba(47, 56, 53, .18);
}
.marca-rotier a { display: block; line-height: 0; }
.marca-rotier img { display: block; height: 28px; width: auto; }
.marca-rotier a:focus-visible {
  outline: 2px solid var(--rotier-sage-dark, #5E7257); outline-offset: 3px;
}
.marca-rotier:hover { border-color: var(--rotier-sage-dark, #5E7257); }

@media (max-width: 576px) {
  /* Em tela de celular o mapa é a ferramenta, e a assinatura cede espaço. */
  .marca-rotier img { height: 20px; }
}

/* ── Resultado do Identificar, no mapa ────────────────────────────────────
   Tabela e não lista de "chave: valor" corrida: quem identifica compara campos
   entre camadas, e coluna alinhada é o que permite ler na diagonal. */
/* Tamanho padronizado: mesma altura e mesma largura a cada clique, com o
   conteúdo rolando por dentro e a ação ancorada embaixo. Popup que muda de
   forma obriga a reencontrar o botão a cada consulta, e quem identifica clica
   dezenas de vezes seguidas. */
.info-identificar {
  font-size: .82rem;
  display: flex; flex-direction: column;
  height: 300px; max-height: 60vh;
}
.info-identificar .info-abas { flex: 0 0 auto; }
.info-identificar .info-painel.ativo {
  display: block;
  flex: 1 1 auto; overflow-y: auto; min-height: 0;
  scrollbar-width: thin;
}
.info-identificar .info-acoes { flex: 0 0 auto; margin-top: auto; }
.info-identificar .info-bloco + .info-bloco { margin-top: 10px; }
.info-identificar .info-camada {
  font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  font-size: .68rem; color: var(--rotier-sage-dark, #5E7257);
  border-bottom: 1px solid var(--rotier-cream-dark, #D8D2C6);
  padding-bottom: 3px; margin-bottom: 4px;
}
.info-identificar table { width: 100%; border-collapse: collapse; }
.info-identificar th {
  text-align: left; font-weight: 500; color: var(--rotier-charcoal-light, #6B7A74);
  padding: 2px 10px 2px 0; white-space: nowrap; vertical-align: top;
}
.info-identificar td {
  padding: 2px 0; font-variant-numeric: tabular-nums; word-break: break-word;
}
.info-identificar .info-acao {
  display: block; margin-top: 10px; padding: 7px 10px; text-align: center;
  background: var(--rotier-sage-dark, #5E7257); color: #fff; border-radius: 6px;
  text-decoration: none; font-weight: 600;
}
.info-identificar .info-acao:hover { filter: brightness(1.08); color: #fff; }
.info-vazio { color: var(--rotier-charcoal-light, #6B7A74); font-size: .85rem; }



/* ── Resumo da área desenhada ─────────────────────────────────────────────
   O número de imóveis e o de "sem ficha" saem destacados: são os dois que
   decidem para onde a equipe vai na semana seguinte. */
.resumo-area { font-size: .82rem; min-width: 210px; }
.resumo-area .resumo-medida {
  font-weight: 600; margin-bottom: 6px; padding-bottom: 4px;
  border-bottom: 1px solid var(--rotier-cream-dark, #D8D2C6);
}
.resumo-area .resumo-carregando { color: var(--rotier-charcoal-light, #6B7A74); }
.resumo-area table { width: 100%; border-collapse: collapse; }
.resumo-area th {
  text-align: left; font-weight: 500; padding: 2px 10px 2px 0;
  color: var(--rotier-charcoal-light, #6B7A74); white-space: nowrap;
}
.resumo-area td { text-align: right; font-variant-numeric: tabular-nums; padding: 2px 0; }
.resumo-area td.destaque { font-weight: 700; color: var(--rotier-charcoal-dark, #2F3835); }


.info-identificar .info-nota {
  margin-top: 8px; padding-top: 8px; font-size: .76rem;
  border-top: 1px solid var(--rotier-cream-dark, #D8D2C6);
  color: var(--rotier-charcoal-light, #6B7A74);
}
.info-identificar .info-acao--fraca {
  background: transparent;
  color: var(--rotier-sage-dark, #5E7257);
  border: 1px solid var(--rotier-sage-dark, #5E7257);
  margin-top: 6px;
}
.info-identificar .info-acao--fraca:hover {
  background: rgba(94, 114, 87, .1); color: var(--rotier-sage-dark, #5E7257);
}

/* ── Abas do Identificar ──────────────────────────────────────────────────
   Um clique acerta quatro camadas empilhadas; a lista corrida obrigava a rolar
   procurando de qual camada era cada campo. Com uma camada só não há aba: aba
   única é decoração que rouba altura. */
.info-identificar .info-abas {
  display: flex; gap: 2px; margin: -2px -2px 8px;
  border-bottom: 1px solid var(--rotier-cream-dark, #D8D2C6);
  overflow-x: auto; scrollbar-width: thin;
}
.info-identificar .info-aba {
  border: 0; background: transparent; cursor: pointer;
  padding: 6px 10px; font-size: .72rem; font-weight: 600; white-space: nowrap;
  color: var(--rotier-charcoal-light, #6B7A74);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.info-identificar .info-aba:hover { color: var(--rotier-charcoal-dark, #2F3835); }
.info-identificar .info-aba.ativa {
  color: var(--rotier-sage-dark, #5E7257);
  border-bottom-color: var(--rotier-sage-dark, #5E7257);
}
.info-identificar .info-aba:focus-visible {
  outline: 2px solid var(--rotier-sage-dark, #5E7257); outline-offset: -2px;
}
.info-identificar .info-painel { display: none; }
.info-identificar .info-acoes {
  margin-top: 10px; padding-top: 8px;
  border-top: 1px solid var(--rotier-cream-dark, #D8D2C6);
}
.info-identificar .info-acoes .info-acao { margin-top: 0; }

/* ── Aviso de cookies ─────────────────────────────────────────────────────
   Rodapé estreito, e não caixa central com sobreposição: não há decisão a
   tomar aqui — é informação. Caixa que bloqueia a tela para dizer algo que não
   exige resposta é pedágio, não transparência. */
/* **Começa onde o conteúdo começa.** Era `left: 0`, e a faixa atravessava a
   largura toda passando por cima do sidebar: o texto nascia atrás da barra
   lateral, e a primeira frase ficava ilegível. Aqui `left` acompanha a largura
   da barra, e o `:has` cobre o estado recolhido — a barra encolhe de 280px
   para 52px, e uma faixa que ignorasse isso deixaria um vão à esquerda.

   Abaixo de 992px o sidebar é off-canvas (não ocupa espaço), então o aviso
   volta a `left: 0` — ver a media query no fim deste bloco. */
.aviso-cookies {
  position: fixed; left: 280px; right: 0; bottom: 0; z-index: 4000;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 16px;
  background: var(--rotier-charcoal-dark, #2F3835); color: #F4F1EC;
  font-size: .82rem; line-height: 1.45;
  box-shadow: 0 -2px 10px rgba(47, 56, 53, .25);
}
.aviso-cookies-texto { flex: 1 1 260px; }
.aviso-cookies a { color: #C7D6BF; text-decoration: underline; }
.aviso-cookies-botao {
  flex: 0 0 auto; border: 1px solid rgba(244, 241, 236, .5);
  background: transparent; color: #F4F1EC;
  border-radius: 6px; padding: 7px 18px; font-size: .82rem; font-weight: 600;
  cursor: pointer; min-height: 40px;
}
.aviso-cookies-botao:hover { background: rgba(244, 241, 236, .12); }
.aviso-cookies-botao:focus-visible { outline: 2px solid #C7D6BF; outline-offset: 2px; }

body:has(.app-sidebar.collapsed) .aviso-cookies { left: 52px; }
/* Telas sem o shell (login, portal público, mapa em tela cheia) não têm barra
   lateral nenhuma: sem isto a faixa nasceria deslocada 280px à direita. */
body:not(:has(.app-sidebar)) .aviso-cookies { left: 0; }
/* O mapa não usa `.app-sidebar`: tem trilho próprio (`#sidebar`, largura
   `--sb-min`), fixo e por cima da faixa. Sem esta linha a faixa nascia colada
   no zero e o trilho comia as primeiras letras — a frase começava em "…istema
   guarda no seu navegador". */
body:has(#sidebar) .aviso-cookies { left: var(--sb-min, 52px); }

@media (max-width: 991px) {
  /* Off-canvas: a barra deixa de ocupar espaço, e a faixa volta à largura
     inteira. */
  .aviso-cookies,
  body:has(.app-sidebar.collapsed) .aviso-cookies { left: 0; }
}

@media (max-width: 640px) {
  .aviso-cookies { padding: 12px; font-size: .78rem; }
  .aviso-cookies-botao { width: 100%; }
}

/* ── Confirmação (dialog único do casco) ─────────────────────────────
   Substitui o `confirm()` do navegador em todo o produto. O que ela precisa
   fazer, e o nativo não fazia: parecer parte do produto, NOMEAR o ato no botão
   e desenhar a exclusão diferente da confirmação inofensiva — quando as duas
   são idênticas a pessoa aprende a apertar sem ler.
   Ver core/static/js/confirmar.js e core/templates/core/_confirmar_dialogo.html.  */
dialog.confirmar {
  border: 0; padding: 0; background: transparent;
  max-width: 26rem; width: calc(100% - var(--sp-6));
  color: var(--fg);
}
dialog.confirmar::backdrop { background: rgba(26, 30, 28, .55); }

.confirmar__caixa {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-6);
  text-align: center;
}

.confirmar__x {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--muted);
  cursor: pointer;
}
.confirmar__x:hover { background: var(--surface-3); color: var(--fg); }

.confirmar__icone {
  display: block; font-size: 2.25rem;
  color: var(--muted-2);
  margin-bottom: var(--sp-3);
}
.confirmar__icone--perigo { color: var(--danger); }

.confirmar__pergunta {
  font-size: var(--fs-lg); font-weight: 600;
  margin: 0 0 var(--sp-2); text-wrap: balance;
}
.confirmar__detalhe {
  font-size: var(--fs-sm); color: var(--muted);
  margin: 0 0 var(--sp-2); text-wrap: pretty;
}
/* Pergunta de uma frase só não deve abrir buraco: o JS deixa este <p> vazio. */
.confirmar__detalhe:empty { display: none; }

.confirmar__acoes {
  display: flex; gap: var(--sp-3); justify-content: center;
  margin-top: var(--sp-5); flex-wrap: wrap;
}
/* A ordem visual é cancelar à ESQUERDA do confirmar no DOM invertido pelo
   `row-reverse`: a ordem de FOCO (Tab) segue o DOM, onde cancelar vem depois e
   já recebe o foco inicial; a ordem de LEITURA põe a ação à direita, que é
   onde o resto do produto a põe. */
.confirmar__acoes { flex-direction: row-reverse; }

@media (prefers-reduced-motion: no-preference) {
  dialog.confirmar[open] { animation: confirmar-entra 140ms var(--ease-out); }
  @keyframes confirmar-entra {
    from { opacity: 0; transform: translateY(-6px) scale(.98); }
    to   { opacity: 1; transform: none; }
  }
}

/* ── Linha do tempo ──────────────────────────────────────────────────
   Um componente, duas formas, e as duas dizem coisas diferentes:

   `.linha-tempo`            — o que ACONTECEU, em ordem. Histórico, auditoria,
                               edições. Cada marca é um fato consumado.
   `.linha-tempo--passos`    — por onde o processo PASSA. Estado de fluxo: o
                               que já foi, onde está e o que falta.

   Antes disto havia duas linhas do tempo escritas à mão dentro de <style> em
   telas de fiscalização, e o histórico dos outros módulos era tabela de três
   colunas — que ordena bem e não mostra NADA da forma do processo: dez linhas
   iguais não deixam ver que a obra ficou quatro meses parada entre duas delas.

   A linha vertical é `border-left` no <ol>, e a marca a cobre com um anel da
   cor do fundo (`box-shadow`), não com um pedaço de fundo opaco — assim ela
   funciona sobre `--surface` e sobre `--surface-2` sem saber onde está.  */
.linha-tempo {
  list-style: none; margin: 0; padding: 0;
  border-left: 1px solid var(--border);
}
.linha-tempo__item {
  position: relative;
  padding: 0 0 var(--sp-6) var(--sp-6);
}
.linha-tempo__item:last-child { padding-bottom: 0; }

.linha-tempo__marca {
  position: absolute; left: 0; top: 0;
  transform: translateX(-50%);
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .8rem; line-height: 1;
  background: var(--surface-3); color: var(--muted-2);
  box-shadow: 0 0 0 4px var(--anel-linha-tempo, var(--surface));
}
/* Concluído e "agora" são cores DIFERENTES de propósito: verde para o que já
   é fato, accent para onde o processo está. Pintar os dois igual apagaria a
   única informação que um stepper existe para dar. */
.linha-tempo__marca--feito { background: var(--brand-100); color: var(--ok); }
.linha-tempo__marca--agora { background: var(--brand-500); color: var(--on-accent); }
.linha-tempo__marca--alerta { background: var(--danger-subtle); color: var(--danger); }

.linha-tempo__titulo {
  margin: 0 0 2px; font-size: var(--fs-base);
  font-weight: 600; line-height: 1.3; color: var(--fg);
}
.linha-tempo__quando {
  display: block; font-size: var(--fs-xs);
  color: var(--muted); font-family: var(--app-font-mono);
}
.linha-tempo__corpo { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--fg-2); }
.linha-tempo__corpo:empty { display: none; }

/* Registro agrupado por DIA. O cabeçalho de data existe porque a pergunta que
   se faz a um histórico quase nunca é "qual foi o último evento" — é "o que
   aconteceu naquele dia". */
.linha-tempo-dia {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-5);
  margin-bottom: var(--sp-4);
  --anel-linha-tempo: var(--surface-2);
}
.linha-tempo-dia__data {
  display: block; font-size: var(--fs-lg); font-weight: 600;
  color: var(--fg); margin-bottom: var(--sp-4);
}

/* Passos: horizontal quando cabe, e a MESMA marcação vira vertical no celular
   — escrever as duas versões no template criaria duas que divergem na primeira
   alteração. */
@media (min-width: 769px) {
  .linha-tempo--passos {
    display: flex; border-left: 0;
    border-top: 1px solid var(--border);
    margin-top: 14px;   /* metade da marca, para ela montar em cima da linha */
  }
  .linha-tempo--passos .linha-tempo__item {
    flex: 1 1 0; min-width: 0;
    padding: var(--sp-6) var(--sp-3) 0 0;
  }
  .linha-tempo--passos .linha-tempo__marca {
    left: 0; top: 0; transform: translate(0, -50%);
  }
}


/* ── Esqueleto do conteúdo durante a navegação ────────────────────────
   Cobre SÓ a área de conteúdo: a barra lateral fica visível, e é isso que
   diz "você continua no sistema, é esta página que está trocando". Cobrir a
   tela inteira faria toda navegação parecer um recarregamento do produto.
   Ver core/static/js/navegacao-esqueleto.js.  */
#carga-conteudo {
  /* `position` e as bordas vêm do JS, medidas do retângulo do conteúdo — a
     lateral muda de largura entre recolhida, expandida e gaveta. Fixo e não
     absoluto: numa página mais alta que a tela, o absoluto rolava junto e quem
     estava no fim via a página de verdade por baixo.
     Ver core/static/js/navegacao-esqueleto.js. */
  position: fixed;
  /* Translúcido COM DESFOQUE. Aqui o desfoque tem um papel próprio: a página
     antiga continua atrás durante a espera, e vê-la nítida sugere que ainda dá
     para clicar nela — não dá, a navegação já saiu. Desfocada, ela vira o que
     é: contexto de onde se veio.
     `color-mix` e não `rgba()` cravado, para o tema escuro acompanhar sem uma
     segunda regra. */
  background: color-mix(in srgb, var(--surface-3) 82%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  /* 1060: acima do Leaflet (1000) e do painel do sino (1050). Este véu é de
     NAVEGAÇÃO — a página está saindo, e nada dela deve continuar por cima. A
     variante de tela cheia sobe para 2100, acima do tour (2000). */
  z-index: 1060;
  display: flex; align-items: flex-start; justify-content: center;
  padding: var(--sp-6);
  overflow: hidden;
}
/* O `.app-content` precisa ser a referência do `inset: 0` acima. */
.app-content { position: relative; }

/* Trocar de APP cobre tudo, barra lateral inclusa: ela vai mudar de itens, e
   mantê-la de pé durante a espera prometeria um menu que não é o que vem. O
   z-index passa dos 50 da lateral e dos 45 do véu da gaveta.
   Ver core/static/js/navegacao-esqueleto.js. */
#carga-conteudo.carga-conteudo--tela-cheia { z-index: 2100; }

#carga-conteudo .loader {
  position: absolute; top: 45%; left: 50%;
  /* -50% centra a CAIXA; os -4,5px corrigem o desenho, que é feito de
     `box-shadow` e não entra na caixa medida. Mesmo ajuste do `#carga-veu`. */
  transform: translate(-50%, -50%) translateY(-4.5px);
}

.esqueleto { width: 100%; max-width: 1100px; }
.esqueleto > * { margin-bottom: var(--sp-4); }

/* `--border` e não `--surface-3`: 1,10:1 sobre o fundo é invisível, e barra de
   esqueleto que não se vê é uma tela em branco — que se lê como defeito, não
   como espera. `--border` é o tom mais escuro que a paleta tem sem virar
   texto. */
.esq-linha {
  height: 14px; border-radius: var(--r-xs);
  /* A barra escureceu porque o VÉU escureceu. Contra `--surface-3` (#eae6de),
     `--border` sozinho dá 1,21:1 — some. A mistura com 30% de `--muted-2` põe
     em 1,53:1, que é a faixa em que esqueleto se lê como espera; acima de 2:1
     ele vira desenho e compete com o conteúdo que está chegando.
     Mistura de TOKENS e não hex cravado: o tema escuro acompanha. */
  background: color-mix(in srgb, var(--muted-2) 30%, var(--border));
}
.esq-linha--curta  { width: 62%; }

/* `.esq-linha--titulo` e `.esq-cartoes` saíram com o esqueleto de página. Só a
   troca de SEÇÃO desenha esqueleto agora, e ali o que existe são linhas de
   tabela e de lista — cartão e título eram a forma da PÁGINA, que deixou de ser
   desenhada. CSS que ninguém aplica é armadilha para quem lê depois. */

/* O brilho que atravessa. Sem ele o esqueleto é indistinguível de uma tela que
   carregou vazia — e tela vazia parada é lida como defeito. */
/* Sem `backdrop-filter`, translúcido vira o caso RUIM: o texto antigo fica
   legível por baixo das barras, dois textos no mesmo espaço. Onde o desfoque
   não existe, o véu volta a ser opaco — pior visualmente, correto de ler.
   `not all and (...)` é a forma de escrever "quando NÃO houver suporte". */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .carga-secao, #carga-conteudo { background: var(--surface-3); }
}

@media (prefers-reduced-motion: no-preference) {
  .esqueleto > * {
    background: linear-gradient(90deg,
      color-mix(in srgb, var(--muted-2) 30%, var(--border)) 0%,
      var(--border) 50%,
      color-mix(in srgb, var(--muted-2) 30%, var(--border)) 100%);
    background-size: 200% 100%;
    animation: esqueleto-brilho 1.4s ease-in-out infinite;
  }
  @keyframes esqueleto-brilho {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
  }
}

/* ── Par de dados (rótulo + valor técnico) ─────────────────────
   O padrão que isto substitui estava copiado à mão em cinco telas do Cadastro
   Técnico e em mais algumas fora dele:

       <span class="text-muted small d-block">Área do terreno</span>
       <span class="mono">450,00</span>

   Dois problemas, e nenhum dos dois é de gosto. O rótulo saía em `--fs-sm`
   regular, o mesmo tom e quase o mesmo peso do valor — nada dizia qual dos dois
   era o dado. E o valor ficava em `--fs-base`, isto é, no corpo de texto: um
   número que o município usa para lançar IPTU tinha exatamente o peso visual de
   uma frase de apoio ao lado.

   Aqui o rótulo passa a ser o MESMO micro-rótulo do `.kpi-label` (11px, 600,
   versalete, `.09em`) — que a tela já usa no KPI e no `<th>` da tabela, então
   não é vocabulário novo, é o vocabulário existente aplicado onde faltava. O
   valor sobe um degrau da rampa, para `--fs-lg`, em DM Mono com `tabular-nums`.

   E a grade: era `row > col-auto`, que dá colunas da largura do conteúdo. Numa
   sequência de quatro pares, cada um começava numa posição diferente e a
   segunda linha não alinhava com a primeira. `auto-fit` com piso de 170px
   alinha as colunas dentro do cartão e entre cartões vizinhos. */
.dados-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem 1.5rem;
}
/* Duas colunas fixas: cartão estreito com exatamente dois pares, onde o
   `auto-fit` colapsaria para uma coluna e deixaria metade do cartão vazia. */
.dados-grid.dados-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.dados-grid .dado { min-width: 0; }
.dado-rotulo {
  display: block;
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted);
  margin-bottom: .15rem;
}
.dado-valor {
  display: block;
  font-family: var(--app-font-mono);
  font-size: var(--fs-lg); font-weight: 500; line-height: 1.3;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  word-break: break-word;
}
/* Valor que é PALAVRA, não medida: "Esquina", "Alvenaria", um nome próprio.
   DM Mono existe para alinhar dígito com dígito; em texto corrido ela só deixa
   a leitura mais lenta. Um degrau abaixo porque nome de pessoa é longo. */
.dado-valor--texto {
  font-family: var(--app-font);
  font-size: var(--fs-md); font-weight: 500;
}
.dado-valor--vazio { color: var(--muted-2); }
/* Unidade dentro do valor ("m²", "R$"): contexto, não grandeza — mesma regra
   do `.kpi-value .u`, e o mesmo tamanho relativo. */
.dado-valor .u { font-size: .6em; font-weight: 400; color: var(--muted); }

/* A divergência colada no valor.
   Nasceu na tela do imóvel, onde o achado central da campanha — o cadastro
   tributário registra outro proprietário — saía como `.form-text`: cinza, 12px,
   do mesmo tom do rótulo acima. A informação que faz a tela existir, escrita no
   tom de uma legenda. Aqui ela usa o par suave de aviso que os badges e alertas
   já usam, e não inventa cor. */
.dado-nota {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: .35rem;
  font-size: var(--fs-sm); line-height: 1.35;
  color: var(--st-warn-fg);
  background: var(--st-warn-bg);
  border: 1px solid var(--st-warn-bd);
  border-radius: var(--r-sm);
  padding: 3px 8px;
}

/* Ícone do cabeçalho de cartão.
   Havia `style="color:var(--brand-500)"` em onze cabeçalhos — inline, repetido,
   e portanto fora do alcance de qualquer mudança de tema. Uma classe. */
.card-header-ico { color: var(--brand-500); flex-shrink: 0; }

/* Resumo do filtro, na própria faixa de ações da lista.
   Quantos resultados o filtro devolveu só existia no subtítulo do herói de
   algumas telas — e lá o número fala do acervo INTEIRO, então ele não muda
   quando a pessoa filtra e passa a mentir. Aqui ele mora ao lado do botão que
   o produziu. Some no celular: a faixa de ações lá já empilha. */
.list-filters .lf-actions .filtro-resumo {
  margin-left: auto; align-self: center;
  font-family: var(--app-font-mono); font-size: var(--fs-sm);
  color: var(--muted);
}

/* ── Lista de conferência em 390px ─────────────────────────────
   `base_gov.html` embrulha toda tabela do conteúdo num `.table-responsive`, e
   no telefone ela rola de lado. Para uma tabela de seis colunas isso significa
   que a INSCRIÇÃO — a coluna que identifica a linha — sai do campo de visão
   exatamente quando a pessoa arrasta para ler a área. Some a âncora e sobra o
   dado solto.

   A linha empilhada segue a grade que o `.agenda-item` e o `.fila-item` já
   usam: identificador em cima, contexto embaixo, número à direita. Alvo de
   toque de 44px, que é o piso para o dedo. Vale só onde a tabela existe em
   PARALELO — o `<table>` continua no HTML para o desktop e para leitor de tela;
   a lista é a mesma informação em outra forma, e uma das duas fica oculta. */
.lista-conferencia { display: flex; flex-direction: column; }
.lista-conferencia .lista-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 12px;
  padding: 14px 16px;
  min-height: 44px;
  align-items: center;
  border-bottom: 1px solid var(--border-2);
  color: var(--fg); text-decoration: none;
}
.lista-conferencia .lista-item:last-child { border-bottom: 0; }
.lista-conferencia .lista-item:hover { background: var(--surface-2); color: var(--fg); text-decoration: none; }
.lista-conferencia .lista-item:focus-visible { outline: 2px solid var(--brand-500); outline-offset: -2px; }
.lista-item .lista-chave {
  font-family: var(--app-font-mono); font-size: var(--fs-md);
  font-weight: 600; color: var(--fg);
}
.lista-item .lista-contexto { grid-column: 1; font-size: var(--fs-sm); color: var(--muted); }
.lista-item .lista-num {
  grid-row: 1 / 3; grid-column: 2; text-align: right;
  font-family: var(--app-font-mono); font-size: var(--fs-md);
  color: var(--fg); font-variant-numeric: tabular-nums;
}
.lista-item .lista-num small {
  display: block;
  font-family: var(--app-font); font-size: var(--fs-xs);
  color: var(--muted); letter-spacing: .06em; text-transform: uppercase;
}
/* A troca. Fora do celular a lista some e a tabela manda; no celular, o
   inverso. `!important` porque a tabela vive dentro do `.table-responsive` que
   o script do casco injeta, e é ele que precisa sumir junto. */
.so-movel { display: none; }
@media (max-width: 575.98px) {
  .so-movel { display: block; }
  .so-desktop { display: none !important; }
  /* Em 390px a faixa de ações não tem largura para botão + botão + resumo:
     o resumo espremia em duas linhas ao lado do 'Limpar filtros'. Linha
     própria, e o `.lf-actions` embrulha. */
  .list-filters .lf-actions { flex-wrap: wrap; }
  .list-filters .lf-actions .filtro-resumo { margin-left: 0; flex: 1 0 100%; }
}

/* A tabela desta lista tem seis colunas e um par delas só faz sentido junto
   (setor + quadra). Onde a lista empilhada assume, o cartão da tabela sai —
   `:not(.page-hero)` porque o herói também é `.card` e é filho direto. */
@media (max-width: 575.98px) {
  .list-page--conferencia > .card:not(.page-hero):not(.so-movel) { display: none; }
}
