/* FX Gateway — acesso local (Fluxo Equipamentos Eletrônicos)
 *
 * Identidade visual portada do Supervisório Aves (supervisorioavesV2.0): mesmo
 * idioma de vidro escuro, prata/branco neutro para "isto é interativo" e cor
 * reservada para estado. Quem opera os dois sistemas no mesmo turno passa a ver
 * a mesma interface no painel do servidor e no painel embarcado do gateway.
 *
 * O que veio de lá, literalmente:
 *   · a paleta (--bg #161415, superfícies de vidro, prata #ffffff→#a39d9a)
 *   · o fundo ambiente: clarão radial no topo + malha de 42 px
 *   · o cartão de vidro (blur + filete superior claro) de .module
 *   · as pílulas de estado de .status-pill e o clarão do rodapé (footer::before)
 *   · os logotipos do grupo em disco branco, de .group-footer-logos
 *
 * Três desvios deliberados, todos por causa do embarcado — e não por gosto:
 *
 *   1. TIPOGRAFIA. O supervisório carrega Oswald/Inter/JetBrains Mono do Google
 *      Fonts. Aqui não há internet: o operador acessa o AP do equipamento em pé
 *      no chão de fábrica. A saída não foi abrir mão das fontes, foi HOSPEDÁ-LAS
 *      — mas a partição só tem espaço para uma das três (ver www/fontes/LEIA.md
 *      para a conta), e a que veio é a Oswald, declarada logo abaixo.
 *
 *      É a escolha certa porque é a única sem substituto: Inter e JetBrains Mono
 *      caem em Segoe UI e Consolas, do mesmo gênero, e a tela não muda de
 *      caráter. A Oswald é condensada, e é ela que carrega a identidade em tudo
 *      que é caixa alta com entreletra — título em degradê, rótulo de indicador,
 *      item do trilho, selo. As páginas do SERVIDOR, que rodam num PC e não têm
 *      partição a respeitar, carregam as três (servidor/web/www/fontes.css).
 *
 *   2. ESCURO por padrão, CLARO por botão (§8 do PADRAO-VISUAL, 15/08). O
 *      automático — que mudava de identidade conforme o aparelho — segue fora;
 *      o tema claro é escolha explícita do operador, redefinindo os mesmos
 *      tokens no bloco html[data-tema="claro"] mais abaixo.
 *
 *   3. CONTRASTE. Duas cores do original não passam em 4,5:1 no texto pequeno
 *      deste painel: o vermelho de falha sobre a pílula tingida (3,84:1) e o
 *      cinza dos micro-rótulos (3,09:1). Aqui o vermelho de TEXTO é #f87171
 *      (4,86:1) e o cinza de rótulo é #8b8480 (4,61:1) — o #ef4444 do original
 *      continua sendo o sinal em si (borda, LED, piscada), onde não é texto.
 *
 * O que NÃO mudou, porque não é estética e sim leitura correta do dado:
 *   · Números em `tabular-nums` — sem isso os dígitos mudam de largura a cada
 *     atualização e a coluna "dança", o que cansa quem observa um processo.
 *   · "Sem leitura" nunca se parece com um valor: itálico, cinza, fonte de texto
 *     e não de número. Zero é medição; ausência não é.
 *
 * Nenhum HTML e nenhum JS foram tocados: as sete páginas compartilham o mesmo
 * vocabulário de classes, e os nomes de variável que o app.js escreve em estilo
 * inline (--ok, --er) e que o sistema.html usa (--t4) seguem existindo — só
 * apontam para a paleta nova.
 */

/* ---------------------------------------------------------------------------
   A FONTE DE EXIBIÇÃO — Oswald, servida da própria partição.
   Ver www/fontes/LEIA.md: procedência, licença (SIL OFL 1.1) e por que só ela.

   Arquivo VARIÁVEL, uma faixa 500–700 num único pedido: os dois pesos estáticos
   que este arquivo usa (600 nos rótulos, 700 nos títulos) custariam 4 KB a mais
   e duas requisições em vez de uma.

   `font-display: swap` é a parte que importa num painel embarcado: o texto pinta
   IMEDIATAMENTE na fonte da pilha e é substituído quando a Oswald chega. Com o
   `block` que é o padrão de fato, o cabeçalho ficaria invisível por até 3 s numa
   conexão ruim de AP — e um painel que abre sem título parece um painel quebrado.

   ⚠️ O `local()` vem primeiro de propósito: quem tiver a Oswald instalada na
   máquina (o PC do escritório costuma ter, via supervisório) não baixa nada.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: "Oswald";
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: local("Oswald"), url("/fontes/oswald.woff2") format("woff2");
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* ---- Prata/branco: a cor do "interativo" ----
     Vermelho é reservado a falha/erro. Todo o resto da interface usa o cinza
     neutro do logotipo Fluxo para indicar que algo responde ao toque. */
  --primary:      #ffffff;
  --primary-dark: #d8d4d1;
  --primary-08:   rgba(255,255,255,0.08);
  --primary-15:   rgba(255,255,255,0.14);
  --primary-30:   rgba(255,255,255,0.28);

  --bg:            #161415;
  --surface:       #201d1f;
  --surface-hover: #2a262a;
  --border-subtle: #3a3538;

  /* ---- Estado ---- */
  --success: #10b981;
  --warning: #f59e0b;
  --danger:  #ef4444;
  --serie:   #38bdf8;   /* linha dos gráficos — o azul dos charts do supervisório */

  /* ---- Compatibilidade ----
     Os nomes abaixo são consumidos fora deste arquivo (app.js escreve
     `color:var(--ok)` / `var(--er)` inline na tira; sistema.html usa --t4).
     Renomeá-los exigiria editar JS/HTML, que não é design — então eles ficam,
     apontando para a paleta nova. */
  --t1: #ece7e4;   /* texto principal      13,8:1 */
  --t2: #ded9d6;   /* texto secundário     11,7:1 */
  --t3: #a39d9a;   /* apoio / rótulo        6,3:1 */
  --t4: #8b8480;   /* micro-rótulo          4,6:1 */
  --t5: #6e6864;   /* separador decorativo — nunca texto que precise ser lido */

  --l1: rgba(255,255,255,0.09);   /* borda de superfície */
  --l2: rgba(255,255,255,0.05);   /* divisória interna   */

  --sup:   rgba(255,255,255,0.04);
  --sup2:  rgba(255,255,255,0.03);
  --fundo: #161415;

  --ok:   var(--success);  --ok-f: rgba(16,185,129,0.15);
  --av:   var(--warning);  --av-f: rgba(245,158,11,0.15);
  --er:   var(--danger);   --er-f: rgba(239,68,68,0.15);
  --er-txt: #f87171;       /* vermelho de TEXTO — o #ef4444 não passa em 4,5:1 */

  /* A marca continua existindo como token, mas sai dos elementos interativos:
     no idioma do supervisório, vermelho aceso significa falha. */
  --marca:      #B10C14;
  --marca-esc:  #8A090F;
  --marca-viva: #D4141E;

  --font-display: "Oswald", "Inter", system-ui, "Segoe UI", Roboto, sans-serif;
  --font-body:    "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Mono",
          "Roboto Mono", Menlo, Consolas, monospace;

  --r: 12px;
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --sombra: 0 8px 20px rgba(0,0,0,0.40);
  --sombra-alta: 0 12px 28px rgba(0,0,0,0.50);

  /* ---- Véus e texturas que dependem do fundo ----
     Eram literais espalhados; viraram tokens em 15/08/2026 para o tema claro
     poder invertê-los. Os valores abaixo são os ESCUROS de sempre — no tema
     escuro nada muda de aparência. */
  --malha:      rgba(255,255,255,0.02);  /* linhas da malha de 42 px do body */
  --rolagem:    #4a4547;                 /* polegar da barra de rolagem */
  --veu-nav:    rgba(0,0,0,0.30);        /* fundo do trilho de navegação */
  --veu-tabela: rgba(0,0,0,0.16);        /* fundo de tabela */
  /* Campo de formulário e toast: eram literais escuros, e no tema claro
     davam texto escuro sobre fundo escuro (ilegível) — achado da revisão de
     design de 15/08. Viraram token para o bloco claro poder invertê-los. A
     seta do <select> é a url() inteira, porque não dá para interpolar a cor
     dentro de um data URI. */
  --campo:      #1c191a;                 /* fundo dos input/select */
  --toast-bg:   rgba(22,20,21,0.95);     /* fundo do toast neutro */
  --seta: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
  --veu-thead:  rgba(0,0,0,0.28);        /* cabeçalho de tabela */
  --veu-aviso:  rgba(0,0,0,0.24);        /* alert-box */
  --veu-cheio:  rgba(0,0,0,0.45);        /* pílula de status e barra de progresso */
  --grad-titulo: linear-gradient(135deg, #ffffff 0%, #d8d4d1 55%, #a39d9a 100%);
}

/* ---------------------------------------------------------------------------
   TEMA CLARO — opcional, ligado pelo botão que o rodape.js injeta e persistido
   em localStorage ("fx-tema"); o tema.js no <head> o aplica antes da pintura.
   O padrão continua sendo o escuro (PADRAO-VISUAL.md §2).

   ⚠️ Contrastes MEDIDOS sobre #f4f1ef (WCAG 2.1), como a norma exige:
   t1 14,9 · t2 10,6 · t3 6,1 · t4 4,9 (mesma ressalva do §3: não usar em
   elemento com hover) · ok 4,9 · av 6,3 · er-txt 5,8 · serie 5,3. O --danger
   (4,3) segue a regra de sempre: preenchimento e borda, nunca texto.

   ⚠️ A tela de ENTRADA fica fora do tema, preta por decisão (15/08) — o
   rodape.js não injeta o botão lá. E o @media print vence este bloco pelo
   seletor duplicado lá embaixo: imprimir no tema claro não pode mudar o PDF. */
html[data-tema="claro"] {
  --primary:      #1a1718;
  --primary-dark: #3a3634;
  --primary-08:   rgba(0,0,0,0.07);
  --primary-15:   rgba(0,0,0,0.13);
  --primary-30:   rgba(0,0,0,0.28);

  --bg:            #f4f1ef;
  --surface:       #ffffff;
  --surface-hover: #eae6e3;
  --border-subtle: #d5d0cd;

  --success: #047857;
  --warning: #92400e;
  --danger:  #dc2626;
  --serie:   #0369a1;   /* o gêmeo claro vive no grafico.js, como o escuro */

  --t1: #201d1f; --t2: #3a3634; --t3: #5f5955; --t4: #6e6864; --t5: #948d89;

  --l1: rgba(0,0,0,0.12);
  --l2: rgba(0,0,0,0.07);
  --sup:   rgba(0,0,0,0.04);
  --sup2:  rgba(0,0,0,0.03);
  --fundo: #f4f1ef;

  --ok-f: rgba(4,120,87,0.13);
  --av-f: rgba(146,64,14,0.12);
  --er-f: rgba(220,38,38,0.12);
  --er-txt: #b91c1c;

  --malha:      rgba(0,0,0,0.035);
  --rolagem:    #b9b3af;
  --veu-nav:    rgba(255,255,255,0.55);
  --veu-tabela: rgba(255,255,255,0.45);
  --veu-thead:  rgba(0,0,0,0.05);
  --veu-aviso:  rgba(0,0,0,0.05);
  --veu-cheio:  rgba(255,255,255,0.65);
  --grad-titulo: linear-gradient(135deg, #1a1718 0%, #3a3634 55%, #5f5955 100%);

  /* Campo claro e toast claro; a seta do select vira tinta escura (a branca
     sumiria no campo claro). Contrastes sobre #fff: --t1 (texto do campo/toast)
     passa com folga; o campo tem borda --border-subtle para se destacar da
     superfície branca. */
  --campo:    #ffffff;
  --toast-bg: rgba(255,255,255,0.97);
  --seta: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%231a1718' d='M6 9L1 4h10z'/%3E%3C/svg%3E");

  --sombra: 0 8px 20px rgba(0,0,0,0.14);
  --sombra-alta: 0 12px 28px rgba(0,0,0,0.20);
}

/* As marcas do rodapé acompanham o tema como a do topo: cada uma tem variante
   de PAPEL (marcas/*-papel.png, wordmark escuro com o símbolo colorido intacto,
   geradas da arte oficial), trocadas pelo rodape.js junto com o tema. A faixa
   escura que existiu aqui por algumas horas saiu a pedido do usuário em 15/08. */

/* ---------------------------------------------------------------------------
   Fundo ambiente — clarão radial no topo sobre malha de 42 px, portado do
   supervisório. `fixed` para a malha não rolar junto e virar listra.
   --------------------------------------------------------------------------- */
html { background: var(--bg); }
body {
  font-family: var(--font-body);
  color: var(--t1);
  line-height: 1.5;
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  padding: 22px 18px 40px;
  min-height: 100vh;
  background:
    radial-gradient(circle at 50% -10%, var(--primary-08) 0%, transparent 55%),
    repeating-linear-gradient(0deg,  var(--malha) 0 1px, transparent 1px 42px),
    repeating-linear-gradient(90deg, var(--malha) 0 1px, transparent 1px 42px),
    var(--bg);
  background-attachment: fixed;
}

::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--rolagem); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--t5); }

/* ---------------------------------------------------------------------------
   Estrutura da página
   As páginas são arquivos separados (o firmware mapeia /sistema, /rede, …) e
   todas trazem o mesmo esqueleto. O grid monta o layout de duas colunas do
   supervisório — trilho à esquerda em altura cheia, conteúdo à direita — sem
   exigir uma linha de HTML a mais: só as áreas mudaram de arranjo.
   --------------------------------------------------------------------------- */
.wrap {
  max-width: 1180px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 234px minmax(0, 1fr);
  grid-template-areas:
    "marca topo"
    "nav   tira"
    "nav   card"
    "nav   rod";
  column-gap: 18px;
  row-gap: 14px;
}

/* ---------- Marca ----------
   Sem placa: o logotipo pousa direto no fundo do painel. Desde 15/08/2026 o
   `logo.png` é o LOGO OFICIAL do "Logos Site.pdf" (Fluxo Equipamentos
   Eletrônicos, 411x128, PNG-8 com alfa) — wordmark BRANCO desenhado para fundo
   escuro, swoosh e subtítulo no vinho da marca. A variante recolorida que
   existia antes (wordmark levantado para prata) deixou de ser necessária: o
   original já é a arte para fundo escuro. O mesmo arquivo assina o topo de
   TODAS as telas (placa e painel) e a tela de login do painel.

   As marcas do rodapé seguem a mesma regra desde 15/08/2026: logos oficiais do
   PDF, wordmark branco, sem o disco branco que as embrulhava (.grupo-f). */
.logo-container {
  grid-area: marca;
  display: flex;
  align-items: center;
  /* Centrado na PRÓPRIA coluna do grid, não encostado à esquerda.
   *
   * ⚠️ `flex-start` fazia sentido com a marca do GRUPO, quase quadrada
   * (380x151), que preenchia a célula. A da Fluxo Equipamentos Eletrônicos é
   * bem mais larga que alta — a 44px de altura ocupa ~135px de largura —, e
   * sobrava um vazio à direita que deixava o cabeçalho desequilibrado: a marca
   * parecia empurrada para o canto em vez de ocupar a coluna que é dela. */
  justify-content: center;
  padding: 6px 4px;
}
.logo {
  height: 44px;
  width: auto;
  max-width: 100%;
  display: block;
  /* Entrada em mola + leve reação ao passar o mouse, portadas de .login-box
     .logo img do supervisório (lá adaptadas do padrão do Framer Motion). */
  animation: logoSpringIn 0.7s var(--ease-out-expo) both;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.logo:hover { transform: scale(1.06) rotate(-1deg); }

@keyframes logoSpringIn {
  0%   { opacity: 0; transform: translateY(14px) scale(0.82); }
  60%  { opacity: 1; transform: translateY(-3px) scale(1.04); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

/* ---------- Barra de identificação ----------
   Cartão de vidro com o filete claro no topo (.module do supervisório) e título
   em degradê prata, o mesmo tratamento de .content-brand-name / .page-heading. */
.header {
  grid-area: topo;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background-color: var(--sup);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--l1);
  border-radius: 14px;
  box-shadow: inset 0 2px 0 0 var(--primary-15);
  padding: 14px 22px;
}
.header h1 {
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.2vw, 1.32rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.035em;
  line-height: 1.15;
  background: var(--grad-titulo);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}
.header p {
  font-size: 11.5px;
  color: var(--t3);
  font-family: var(--mono);
  margin-top: 4px;
}

/* ---------- Tira de estado ----------
   Resumo que vale em qualquer página: o operador não precisa voltar à aba de
   dados para saber se o equipamento responde e se a fila está escoando.
   Portada de .report-summary/.summary-card: ladrilhos de vidro soltos, no lugar
   das células emendadas por borda. */
.tira {
  grid-area: tira;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: 10px;
}
.tira:empty { display: none; }
.tira-c {
  background-color: var(--sup2);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--l1);
  border-radius: var(--r);
  padding: 11px 15px;
}
.tira-r {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--t4);
  margin-bottom: 4px;
}
.tira-v {
  font-family: var(--mono);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--t1);
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  gap: 7px;
}
.tira-v.nulo { color: var(--t4); font-style: italic; font-family: var(--font-body); font-weight: 400; }

/* ---------- Duas colunas ----------
   Medidas de um lado, alarmes e estados do outro.

   ⚠️ Grid num div comum, nunca dois `.card` irmãos: `.card` tem `grid-area:
   card`, e irmãos caem na mesma célula e se sobrepõem. O design foi desenhado
   para telas de UM equipamento, com um card por página e seções dentro.

   Colapsa para uma coluna abaixo de 720px: lado a lado num celular, a tabela de
   valores fica estreita demais e o número quebra em duas linhas — e número
   quebrado é justamente o que a fonte tabular existe para evitar. */
.duas-colunas {
  display: grid;
  gap: 8px 22px;
  grid-template-columns: 1fr 1fr;
  align-items: start;
}
.duas-colunas > div { min-width: 0; }
.duas-colunas h4 {
  font-family: var(--font-display);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--t3);
  margin: 18px 0 7px;
}
/* Uma coluna só — pedido do usuário em 18/08/2026, sobre as grandezas do
   processo. ⚠️ MODIFICADOR, não mudança na regra base: `.duas-colunas` também
   monta a seção Sistema logo abaixo, que segue em duas.
   ⚠️ E o corte instantânea × totalizador NÃO morreu com o layout: ele é
   `acumulador`, vem do vocabulário e sustenta a PRODUÇÃO do relatório
   (fim − início). O que mudou foi só onde as linhas aparecem na tela. */
.duas-colunas.uma-coluna { grid-template-columns: 1fr; }

@media (max-width: 720px) {
  .duas-colunas { grid-template-columns: 1fr; }
}

/* ---------- Indicadores ----------
   As grandezas marcadas como destaque em app.js — nos insensibilizadores são
   tensão, corrente e frequência, as três que a Portaria 365/2021 exige.
   Portado de .compact-metric: ladrilho de vidro com filete claro à esquerda. */
.kpis {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(152px, 1fr));
  margin-bottom: 16px;
}
.kpi {
  background: var(--sup2);
  border: 1px solid var(--l1);
  border-radius: var(--r);
  padding: 12px 14px;
  position: relative;
  overflow: hidden;
  transition: background-color .2s, border-color .2s;
}
.kpi:hover { background: var(--primary-08); border-color: var(--primary-15); }
.kpi::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--primary-30);
}
.kpi.mudo::before { background: var(--l1); }
.kpi-r {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--t4);
  margin-bottom: 6px;
}
.kpi-v {
  font-family: var(--mono);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -.025em;
  color: var(--primary);
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
}
.kpi-u { font-size: 11.5px; font-weight: 500; color: var(--t3); margin-left: 4px; }
.kpi-v.nulo { color: var(--t4); font-size: 17px; font-family: var(--font-body); font-style: italic; }

/* ---------- Trilho de navegação ----------
   A .sidebar do supervisório: painel de vidro em altura cheia, item ativo em
   prata tingido (nunca em vermelho — vermelho aqui quer dizer falha). */
nav {
  grid-area: nav;
  align-self: stretch;
  background: var(--veu-nav);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--l1);
  border-radius: 14px;
  padding: 14px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: 10px;
  color: var(--t3);
  text-decoration: none;
  white-space: nowrap;
  transition: background .2s, color .2s, border-color .2s;
}
nav a:hover { background: var(--primary-08); color: var(--primary); }
nav a.ativo {
  background: var(--primary-15);
  border-color: var(--primary-30);
  color: var(--primary);
}
nav a.ativo .ic { opacity: 1; }
.ic {
  width: 16px; height: 16px;
  flex-shrink: 0;
  opacity: .68;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Separadores do trilho: agrupam o menu em processo, equipamento e
   administração — injetados por app.js, ver montaIconesMenu(). Mesmo rótulo
   miúdo de .sidebar-nav-label. */
.trilho-sep {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--t4);
  padding: 16px 12px 5px;
}
.trilho-sep:first-child { padding-top: 4px; }

/* ---------- Pé do trilho ----------
   O `.sidebar-footer` do supervisório: o que encerra a sessão fica separado do
   que navega, por um filete, colado na base da barra. Montado pelo `sessao.js`
   e por isso só existe nas páginas do servidor — o acesso local não tem sessão
   nominal, a senha de manutenção é pedida por ação.

   `margin-top: auto` é o que empurra até a base, e funciona porque o `nav` é
   `flex-direction: column` com `align-self: stretch`, isto é, tem altura de
   sobra para empurrar contra. */
.trilho-pe {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--l1);
}
/* Prata tingido com borda, e não o link transparente dos itens de menu: é uma
   AÇÃO no meio de uma lista de destinos. Mesmo tratamento do
   `.sidebar-logout-btn` — e pela mesma razão de lá, nunca vermelho: neste
   sistema visual vermelho aceso quer dizer falha, não "cuidado". */
nav a.sair {
  justify-content: center;
  background: var(--primary-15);
  border-color: var(--primary-30);
  color: var(--primary);
}
nav a.sair:hover { background: var(--primary-30); color: var(--primary); }
nav a.sair .ic { opacity: 1; }

/* ---------- Conteúdo ----------
   O cartão de vidro de .module: fundo translúcido, blur e filete claro no topo
   que acende no hover. */
/* ⚠️⚠️ DOIS CARDS NA MESMA CÉLULA SE SOBREPÕEM — e o sintoma não parece
   sobreposição.
   `.card` fixa `grid-area: card`, e a grade tem UMA célula com esse nome. A
   página do receptor tem dois cards (a célula LoRa e a leitura), então o de
   baixo ficava desenhado ATRÁS do outro. Como `.card` tem fundo semitransparente
   e `backdrop-filter: blur(10px)`, o texto de trás vazava borrado: na tela
   aparecia como "manchas brancas no fundo", e a lista de transmissores parecia
   simplesmente não existir. Um defeito só, com dois sintomas que não se
   conectam à vista.

   ⚠️ É a MESMA armadilha registrada em 05/08 no painel do servidor, quando três
   cards irmãos saíram com os KPIs por cima dos botões e a tabela invisível.
   A classe governa a área, então todo card irmão a herda.

   A linha extra só existe quando há o que pôr nela: `montaRemotos` marca o
   `.wrap` e o CSS abre a fileira. Sem isso, as páginas de transmissor — que não
   têm célula — ganhariam um `row-gap` a mais. */
.wrap.com-celula {
  grid-template-areas:
    "marca topo"
    "nav   tira"
    "nav   celula"
    "nav   card"
    "nav   rod";
}
#card-celula { grid-area: celula; }

.card {
  grid-area: card;
  background-color: var(--sup);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--l1);
  border-radius: 14px;
  box-shadow: inset 0 2px 0 0 var(--primary-15), var(--sombra);
  padding: 22px 24px 26px;
}
.card h3 {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--t1);
  margin-bottom: 13px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--l2);
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
}
.card h3:not(:first-child) { margin-top: 28px; }

/* ---------- Tabelas ----------
   Rótulo à esquerda, valor à direita em fonte monoespaçada: a coluna de números
   fica alinhada e comparável de linha a linha. Tema escuro do supervisório —
   linhas transparentes, divisórias finas, cabeçalho miúdo em caixa alta. */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  border: 1px solid var(--l1);
  border-radius: var(--r);
  overflow: hidden;
  background: var(--veu-tabela);
}
thead th {
  background: var(--veu-thead);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--t4);
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--l1);
  white-space: nowrap;
}
tbody td { padding: 9.5px 14px; border-bottom: 1px solid var(--l2); }
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background .15s; }
tbody tr:hover { background: var(--primary-08); }
tbody td:first-child { color: var(--t2); width: 52%; }
tbody td:last-child {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--t1);
  font-weight: 600;
}
tbody td:only-child { width: auto; }

/* ---------- Tabela de LISTAGEM: rola dentro do card ----------
   As tabelas do acesso local são rótulo/valor (duas colunas) e cabem em
   qualquer tela. As do painel da Fluxo são listagens de 5 a 7 colunas — frota,
   plantas, relatórios, registradores, usuários — e num celular NÃO cabem: a
   largura mínima do conteúdo vence o `width: 100%` acima (em tabela, 100% é
   largura preferida, não teto), a tabela transborda o `.card`, que não tem
   `overflow`, e o vazamento empurra a PÁGINA INTEIRA para rolar de lado.
   Medido em 375 px, só com o cabeçalho: registradores vaza 210 px, usuários
   148, plantas 137, frota 121, relatórios 39 — e a frota com dados reais
   chegou a 615 px de tabela num card de 353 px.

   ⚠️ A saída NÃO é `table { display: block; overflow-x: auto }`: com `display:
   block` a tabela deixa de distribuir as colunas, a borda e o raio ficam na
   largura do bloco enquanto as linhas encolhem para o conteúdo, e as ~20
   tabelas de duas colunas que hoje estão corretas regrediriam para consertar
   cinco. O invólucro rola e a tabela continua sendo tabela.

   Em tela larga não muda nada: o invólucro tem a largura do card, a tabela
   ocupa 100% dela e não há o que rolar. */
.tabela-rolavel {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* A barra fina não pode comer a última linha da tabela quando aparece. */
  padding-bottom: 1px;
}

/* ⚠️ `tabindex` está no HTML, não aqui: região rolável precisa ser alcançável
   por teclado (WCAG 2.1.1) — sem isso, quem não usa ponteiro não chega às
   colunas escondidas. O foco só é visível quando ele existe de fato. */
.tabela-rolavel:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--r);
}

/* Coluna numérica que NÃO é a última (o `td:last-child` acima cobre as tabelas
   de duas colunas; nas de várias colunas — registradores, plantas — o valor é
   uma coluna do meio). Sem isto os dígitos usam fonte proporcional e a coluna
   "dança" a cada polling. */
.n {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Sem leitura é diferente de zero — e não pode se parecer com um número.
   Vale como `td.sem-dado` e como `.sem-dado` solto (span da saúde do remoto,
   célula de valor da tabela de registradores). */
.sem-dado {
  color: var(--t4);
  font-style: italic;
  font-family: var(--font-body);
  font-weight: 400;
}

/* ---------- Avisos ---------- */
.alert-box {
  border: 1px solid var(--l1);
  background: var(--veu-aviso);
  color: var(--t2);
  border-radius: var(--r);
  padding: 13px 16px;
  margin-top: 14px;
  font-size: 12.5px;
  line-height: 1.55;
}
.alert-box h4 {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--t1);
  margin-bottom: 7px;
}
.alert-box p { font-weight: 400; }
.alert-box ul { margin: 7px 0 0 17px; }
.alert-box li { margin: 3px 0; }

/* Variante vermelha, para ocorrências/falhas — usada em Gráficos
   (renderAnomalias) além do que já existia aqui antes. Fundo tingido + borda na
   cor, como .status-pill--danger; o texto usa o vermelho claro, que passa em
   4,5:1 sobre esse fundo. */
.falha-box {
  border-color: var(--er);
  background: var(--er-f);
  color: var(--er-txt);
}
.falha-box h4 { color: inherit; }
.falha-item {
  font-weight: 700;
  padding: 8px 12px;
  margin-top: 7px;
  border-radius: 9px;
  border-left: 3px solid var(--danger);
  background: rgba(239,68,68,0.10);
}

/* ---------- Aba de diagnóstico (admin) ----------
   Separada do dado validado por exigência de projeto, não por estética: num
   sistema regulado, grandeza experimental e parâmetro de insensibilização não
   podem ter a mesma autoridade visual. A faixa lateral marca o cartão inteiro
   como "outro tipo de dado" antes da leitura começar. */
.h3-diag { display: flex; align-items: center; gap: 9px; }
.card h3 .selo { vertical-align: middle; }
.card-diag {
  border-left: 3px solid var(--warning);
  box-shadow: inset 0 2px 0 0 rgba(245,158,11,0.30), var(--sombra);
}

.diag-aviso {
  font-size: 12px;
  line-height: 1.55;
  color: var(--t3);
  margin-bottom: 13px;
}
.diag-aviso strong { color: var(--t2); }

/* Tabela de bits do 0x46. Bit ligado ganha destaque tipográfico, não cor de
   alarme: é uma tabela de conferência, e um bit ligado num registrador cuja
   documentação ninguém validou não é uma falha confirmada do equipamento. */
.bit-n {
  font-family: var(--mono);
  color: var(--t3);
  width: 3.5em;
}
.bit-on {
  font-family: var(--mono);
  font-weight: 700;
  color: var(--primary);
}

/* ---------- Estado da conexão ----------
   O .footer-health-status do supervisório: pílula escura com LED que brilha. */
.status-container {
  margin-top: 22px;
  padding-top: 15px;
  border-top: 1px solid var(--l2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 700;
  padding: 5px 12px;
  border-radius: 30px;
  border: 1px solid var(--l1);
  background: var(--veu-cheio);
  color: var(--t3);
}
.status.connected    { background: var(--ok-f); border-color: rgba(16,185,129,0.40); color: var(--ok); }
.status.disconnected { background: var(--er-f); border-color: rgba(239,68,68,0.40);  color: var(--er-txt); }
.status.loading      { background: var(--av-f); border-color: rgba(245,158,11,0.40); color: var(--av); }
.timestamp {
  font-size: 11px;
  color: var(--t4);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.indicator {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 5px currentColor;
  flex-shrink: 0;
}
.indicator.active { animation: bat 2.2s ease-in-out infinite; }
@keyframes bat { 0%,100% { opacity: 1 } 50% { opacity: .22 } }

/* Ponto de estado usado na tira: pisca só quando o dado está chegando, para o
   "vivo" ser observável de longe sem exigir leitura. */
.pt {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 5px currentColor;
  flex-shrink: 0;
  display: inline-block;
}
.pt.vivo { animation: bat 2.2s ease-in-out infinite; }

/* ---------- Selos ----------
   .status-pill do supervisório: fundo tingido + borda + texto na cor semântica.
   Ler o estado num bloco de cor com forma cansa menos que num ícone solto. */
.selo {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 30px;
  border: 1px solid transparent;
  white-space: nowrap;
}
.selo.ok     { background: var(--ok-f); border-color: rgba(16,185,129,0.40); color: var(--ok); }
.selo.alerta { background: var(--av-f); border-color: rgba(245,158,11,0.40); color: var(--av); }
.selo.erro   { background: var(--er-f); border-color: rgba(239,68,68,0.40);  color: var(--er-txt); }
.selo.neutro { background: var(--primary-08); border-color: var(--l1); color: var(--t3); }

/* ---------- Formulários ---------- */
.campo { margin-bottom: 16px; }
.campo label {
  display: block;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--t4);
  margin-bottom: 6px;
}
.campo .ajuda {
  font-size: 12px;
  color: var(--t3);
  margin-top: 7px;
  line-height: 1.5;
}
input, select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  font-size: 16px;              /* 16px evita o zoom automático do iOS */
  font-family: inherit;
  background: var(--campo);
  color: var(--primary);
}
input::placeholder { color: var(--t4); }
input:focus, select:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 2px var(--primary-30);
}
/* Envio do .bin do OTA: o botão nativo do <input type="file"> vem branco e com
   a fonte do sistema — sobre o vidro escuro ele fica sendo a coisa mais clara
   da tela, disputando atenção com a ação principal ("Enviar e atualizar").
   Aqui ele vira o mesmo botão secundário do resto do painel. */
input[type="file"] {
  padding: 8px 10px;
  font-size: 12.5px;
  color: var(--t3);
  cursor: pointer;
}
input[type="file"]::file-selector-button {
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  margin-right: 12px;
  padding: 7px 14px;
  border-radius: 8px;
  cursor: pointer;
  border: 1px solid var(--l1);
  background: rgba(255,255,255,0.05);
  color: var(--t2);
  transition: background .2s, border-color .2s, color .2s;
}
input[type="file"]::file-selector-button:hover {
  background: rgba(255,255,255,0.12);
  border-color: var(--primary-30);
  color: var(--primary);
}

/* Seta do <select> desenhada em branco: a nativa é escura e some no campo. */
select {
  background-image: var(--seta);
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 34px;
  appearance: none;
  -webkit-appearance: none;
}

/* ---------- Botões ----------
   Vidro transparente por padrão (.btn do supervisório). Cor só entra em ação
   semântica — e a única aqui é a destrutiva. */
button {
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  padding: 10px 18px;
  border-radius: 11px;
  cursor: pointer;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* `isolation` e `overflow` existem para as duas camadas de brilho abaixo:
     elas ficam em `z-index: -1` e sem um contexto de empilhamento próprio
     escapariam para trás do cartão, onde não apareceriam. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border: 1px solid var(--primary-30);
  /* "Vidro líquido" do supervisório (.login-box button): degradê de cima para
     baixo com filete claro por dentro na borda superior e sombra interna
     embaixo. Sem isso um botão largo vira uma barra cinza chapada e some ao
     lado do secundário — o relevo é o que diz qual é a ação principal. */
  background: linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.05));
  color: var(--primary);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow:
    0 4px 14px rgba(0,0,0,0.35),
    inset 0 1px 0 rgba(255,255,255,0.38),
    inset 0 -2px 5px rgba(0,0,0,0.28);
  transition: background .2s, border-color .2s, transform .2s, box-shadow .2s, color .2s, filter .2s;
}

/* ---- As duas camadas que faltavam do "vidro líquido" ----
 * Portadas de `.login-box button::before/::after` do supervisório (lá adaptadas
 * do "Liquid Glass Button", 21st.dev/@designali-in, de React/Tailwind para CSS
 * puro). O degradê e as sombras internas já estavam aqui; SEM estas duas o botão
 * é um retângulo com gradiente — vidro é o que reflete, e o reflexo é isto.
 *
 * ::before é o VÉU: claro no topo, transparente no meio, escurecendo no pé.
 * `mix-blend-mode: overlay` faz o véu reagir ao que está atrás em vez de
 * chapar por cima — é o que dá profundidade em vez de mais uma camada de cinza.
 *
 * ::after é o RISCO DE LUZ, a faixa embaçada colada no topo, recuada 8% de cada
 * lado. É a peça que faz o olho ler "superfície curva com luz batendo em cima",
 * e a que some primeiro quando alguém tenta simplificar o efeito. */
button::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,0.35),
                              rgba(255,255,255,0) 45%, rgba(0,0,0,0.15) 100%);
  mix-blend-mode: overlay;
}
button::after {
  content: "";
  position: absolute;
  left: 8%; right: 8%; top: 6%;
  height: 30%;
  z-index: -1;
  border-radius: 999px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0));
  filter: blur(2px);
  opacity: .8;
  transition: opacity .2s;
}
/* Desabilitado não reflete: o `opacity: .40` do botão inteiro apagaria o texto
   junto e deixaria o brilho relativamente MAIS forte que ele — o botão morto
   ficaria mais chamativo que o vivo ao lado. */
button:disabled::before, button:disabled::after { display: none; }
/* Os chapados não têm relevo por decisão (ver o bloco de `button.secundario`
   abaixo): é a ausência do vidro que diz qual é a ação principal. Deixar o
   reflexo neles devolveria justamente a hierarquia que se quis tirar. */
button.secundario::before, button.secundario::after,
button.perigo::before, button.perigo::after,
.g-periodo button::before, .g-periodo button::after { display: none; }

button:hover {
  background: linear-gradient(180deg, rgba(255,255,255,0.26), rgba(255,255,255,0.09));
  border-color: rgba(255,255,255,0.45);
  transform: translateY(-2px);
  filter: brightness(1.06);
  box-shadow:
    0 6px 16px rgba(0,0,0,0.40),
    inset 0 1px 0 rgba(255,255,255,0.45),
    inset 0 -2px 5px rgba(0,0,0,0.28);
}
button:active { transform: scale(0.97); }
button:disabled {
  opacity: .40;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
button.largo { width: 100%; }
/* Secundário e perigo ficam CHAPADOS de propósito: sem o relevo do vidro
   líquido, a hierarquia entre "a ação" e "a outra ação" se lê à distância. */
button.secundario {
  background: rgba(255,255,255,0.05);
  color: var(--t2);
  border-color: var(--l1);
  box-shadow: none;
}
button.secundario:hover {
  background: rgba(255,255,255,0.12);
  border-color: var(--primary-30);
  color: var(--primary);
  filter: none;
  box-shadow: 0 5px 12px rgba(0,0,0,0.30);
}
/* Ação destrutiva: só ganha o vermelho cheio no hover, para não convidar ao
   clique acidental num painel que também tem "reiniciar". */
button.perigo {
  background: rgba(239,68,68,0.10);
  color: var(--er-txt);
  border-color: rgba(239,68,68,0.40);
  box-shadow: none;
}
button.perigo:hover {
  background: var(--danger);
  color: var(--primary);
  border-color: var(--danger);
  filter: none;
  box-shadow: 0 5px 12px rgba(0,0,0,0.35);
}

.acao {
  border: 1px solid var(--l1);
  background: var(--sup2);
  border-radius: var(--r);
  padding: 14px 16px;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  transition: background .2s, border-color .2s;
}
.acao:hover { background: var(--primary-08); border-color: var(--primary-15); }
.acao > :first-child { flex: 1; min-width: 200px; }
.acao .desc {
  font-size: 12px;
  color: var(--t3);
  margin: 4px 0 0;
  line-height: 1.5;
}
.msg { font-size: 12.5px; margin-top: 11px; font-weight: 600; color: var(--t3); }
.msg.ok  { color: var(--ok); }
.msg.err { color: var(--er-txt); }

/* ---------- Barra de progresso (OTA) ---------- */
.progresso {
  width: 100%;
  height: 8px;
  background: var(--veu-cheio);
  border: 1px solid var(--l1);
  border-radius: 999px;
  overflow: hidden;
  margin-top: 12px;
  display: none;
}
.progresso .barra {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #a39d9a 0%, #ffffff 100%);
  transition: width .3s;
}

/* ---------- Rodapé ----------
   O clarão que sobe do rodapé, portado de footer::before — no original um arco
   subindo do fundo, aqui neutro pela mesma razão que lá: cor só existe para
   indicar estado, então o rodapé não pode virar decoração colorida. */
.footer {
  grid-area: rod;
  position: relative;
  overflow: hidden;
  margin-top: 6px;
  border: 1px solid var(--l1);
  border-radius: 14px;
  background-color: var(--sup2);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding: 18px 20px;
  text-align: center;
  font-size: 11.5px;
  color: var(--t3);
  line-height: 1.6;
  box-shadow: var(--sombra);
}
.footer > * { position: relative; z-index: 1; }
.footer::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -70%;
  width: 140%;
  aspect-ratio: 1;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(255,255,255,0.09), rgba(255,255,255,0.03) 45%, transparent 72%);
  filter: blur(30px);
}
.footer strong { color: var(--t1); }

/* Faixa institucional do Grupo Fluxo (injetada por rodape.js).
   ⚠️ O disco branco por marca SAIU em 15/08/2026: as artes antigas eram
   recortes com preto sobre fundo claro e precisavam do disco; as atuais são os
   LOGOS OFICIAIS do "Logos Site.pdf" — wordmark branco sobre transparência,
   desenhados para fundo escuro. Embrulhá-los num círculo branco apagaria o
   wordmark. As alturas vêm inline do rodape.js (uma por marca), e cada logo é
   um LINK para o site da empresa. */
.grupo {
  border-top: 1px solid var(--l1);
  margin: 16px -20px -18px;
  padding: 18px 20px 19px;
}
.grupo-t {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--t4);
  margin-bottom: 12px;
}
.grupo-f {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 12px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--t3);
}
.grupo-f a { display: block; }
.grupo-f img {
  display: block;
  object-fit: contain;
  opacity: .88;
  transition: transform .2s, opacity .2s;
}
.grupo-f a:hover img {
  transform: translateY(-2px);
  opacity: 1;
}
.grupo-n { margin-top: 13px; font-size: 10.5px; color: var(--t4); }

/* ---------------------------------------------------------------------------
   Gráficos (SVG puro — sem biblioteca externa: no acesso local não há internet
   para CDN, e a partição de páginas não comporta 200-500 KB de biblioteca)
   Cartões e seletor de período portados de .chart-card e .chip.
   --------------------------------------------------------------------------- */
.g-periodo { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.g-periodo button {
  flex: 0 1 auto;
  font-size: 11.5px;
  font-weight: 600;
  padding: 6px 15px;
  border-radius: 30px;
  background: rgba(255,255,255,0.05);
  color: var(--t3);
  border-color: var(--border-subtle);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.g-periodo button:hover {
  background: var(--primary-15);
  border-color: var(--primary);
  color: var(--primary);
  transform: none;
  box-shadow: none;
}
.g-periodo button.ativo {
  background: var(--primary-15);
  border-color: var(--primary);
  color: var(--primary);
}

.g-card {
  background: var(--sup2);
  border: 1px solid var(--l1);
  border-radius: var(--r);
  padding: 14px 16px 10px;
  margin-bottom: 12px;
  transition: background .2s, transform .2s;
}
.g-card:hover { background: var(--primary-08); transform: translateY(-2px); }
.g-topo {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 9px;
}
.g-titulo {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--t3);
}
.g-valor {
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 600;
  color: var(--primary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.g-unidade { font-size: 11.5px; font-weight: 400; color: var(--t3); margin-left: 4px; }
/* O SVG tem viewBox 600×160 e NÃO declara preserveAspectRatio, então com altura
   fixa ele era encaixado no meio do cartão e sobrava faixa vazia dos dois lados
   — o gráfico usava pouco mais da metade da largura disponível. Deixando a
   altura seguir a proporção natural, ele preenche o cartão sem esticar: a linha
   de 2 px continua com 2 px em todo o traçado, que uma escala não-uniforme
   (preserveAspectRatio="none") estragaria.

   O teto de 200 px tem função: como o viewBox inteiro escala junto, sem limite
   os rótulos do eixo cresceriam até competir com o texto da página. Em 200 px a
   ampliação para de 1,25× — rótulo de eixo fica em ~13 px, abaixo do corpo, e a
   sobra dos lados vira margem, não buraco. A espessura da linha não depende
   disso: o traçado usa vector-effect="non-scaling-stroke" (grafico.js) e fica
   em 2 px exatos em qualquer largura, do celular ao monitor. */
.grafico {
  width: 100%;
  height: auto;
  aspect-ratio: 600 / 160;
  max-height: 200px;
  display: block;
  overflow: visible;
}
.g-extremos {
  font-size: 11px;
  color: var(--t4);
  text-align: right;
  margin-top: 5px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.g-data {
  background: var(--sup2);
  border: 1px solid var(--l1);
  border-radius: var(--r);
  padding: 14px 16px;
  margin-bottom: 14px;
}
.g-data label {
  display: block;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--t4);
  margin-bottom: 7px;
}
.g-data-linha { display: flex; gap: 8px; flex-wrap: wrap; }
.g-data-linha input { flex: 1; min-width: 150px; }
.g-data-linha button { flex: 0 0 auto; }
.g-data .ajuda { font-size: 12px; color: var(--t3); margin-top: 9px; line-height: 1.5; }

/* ---------------------------------------------------------------------------
   MOVIMENTO — o que faltava trazer do supervisório

   Lá são quatro @keyframes: logoSpringIn (na marca, acima), rowIn, blinkBorder
   e toastIn. O toast não tem equivalente aqui — este painel não emite aviso
   flutuante —, então ficaram estes dois, mais o fundo ambiente do canvas.

   Regra que veio junto: movimento entra na CHEGADA do dado e no ALARME, nunca
   no dado que está mudando. Um valor que pulsa a cada leitura é ruído numa tela
   que alguém observa por turno inteiro.
   --------------------------------------------------------------------------- */

/* Entrada das superfícies, de .compact-card. `backwards` segura o estado
   inicial durante o atraso — sem isso o cartão pisca visível antes de começar.
 *
 * ⚠️ A ANIMAÇÃO VAI NOS RECIPIENTES, NUNCA NOS LADRILHOS.
 * `.tira-c` e `.kpi` são reescritos por `tira.innerHTML` e por `montaKpis()` a
 * CADA leitura — de dois em dois segundos. Animar o ladrilho faria a fileira
 * inteira renascer e desaparecer em laço perpétuo, que é precisamente o "não
 * animar o dado que está mudando" da abertura desta seção. Os recipientes
 * (.tira, .card, nav, .footer) nascem uma vez com a página e animam uma vez.
 *
 * `.g-card` é exceção legítima: a página de Gráficos não faz polling — os
 * cartões só são remontados quando o operador troca o período, e aí o
 * movimento comunica que a consulta foi refeita. */
@keyframes rowIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.logo-container, .header, .tira, nav, .card, .footer, .g-card, .acao {
  animation: rowIn 0.45s var(--ease-out-expo) backwards;
}
/* A página compõe de cima para baixo, na ordem em que se lê. */
.header       { animation-delay: 0.05s; }
.tira         { animation-delay: 0.10s; }
nav           { animation-delay: 0.14s; }
.card         { animation-delay: 0.18s; }
.footer       { animation-delay: 0.24s; }
/* Dentro de Gráficos, um cartão depois do outro. */
.g-card:nth-of-type(2) { animation-delay: 0.05s; }
.g-card:nth-of-type(3) { animation-delay: 0.10s; }
.g-card:nth-of-type(4) { animation-delay: 0.15s; }
.g-card:nth-of-type(n+5) { animation-delay: 0.20s; }

/* Alarme ativo pisca a borda, de .compact-card.falha-ativa. Só aparece quando
   há falha de verdade: a caixa de "sem sinal" nasce com display:none e o
   .falha-box só é montado quando o período tem ocorrência. */
@keyframes blinkBorder {
  0%, 100% { border-color: var(--danger); box-shadow: 0 0 8px rgba(239,68,68,0.55); }
  50%      { border-color: rgba(239,68,68,0.25); box-shadow: 0 0 2px transparent; }
}
#caixa-sem-sinal, .alert-box.falha-box {
  animation: blinkBorder 1.6s ease-in-out infinite;
}

/* Fundo ambiente — pontos claros à deriva atrás de tudo. O canvas é criado pelo
   rodape.js (único script que as duas frentes carregam), então nenhuma página
   precisou de marcação nova. Autônomo: não reage ao cursor, para não competir
   com o dado que muda ao vivo. */
.fundo-ambiente {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}
/* ---------- Botão de tema (rodape.js) ----------
   Chip de vidro fixo, fora do fluxo — presente em toda tela menos a de entrada.
   Prata como todo interativo (§1); ícone de traçado com currentColor. O ícone
   mostra o tema PARA ONDE o clique leva: sol no escuro, lua no claro. */
.tema-botao {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 60;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--sup);
  backdrop-filter: blur(10px);
  border: 1px solid var(--l1);
  border-radius: 50%;
  color: var(--t3);
  cursor: pointer;
  box-shadow: var(--sombra);
  transition: color .2s, border-color .2s, transform .2s;
}
.tema-botao:hover { color: var(--primary); border-color: var(--primary-30); transform: translateY(-2px); }
.tema-botao .ic { width: 18px; height: 18px; fill: none; stroke: currentColor;
                  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.tema-botao .ic-lua { display: none; }
html[data-tema="claro"] .tema-botao .ic-sol { display: none; }
html[data-tema="claro"] .tema-botao .ic-lua { display: block; }
@media print { .tema-botao { display: none; } }

/* ⚠️ Fora do PDF. O relatorio.py carrega o rodape.js para montar o documento, e
   o Chrome imprime com `--print-to-pdf`, que aplica estilo de impressão. Ponto
   decorativo não entra em relatório de conformidade — nem a malha e o clarão do
   fundo, que só gastariam tinta. */
@media print {
  /* Redeclarar os TOKENS, e não só o fundo: `body { background:#fff }` sozinho
     deixaria o texto (--t1, quase branco) invisível no papel. Trocando a
     origem, tudo que herda deles acompanha — tabela, selo, rótulo, borda. */
  /* `html[data-tema="claro"]` junto: sem ele, a especificidade do tema claro
     (0,1,1) venceria a do :root (0,1,0) e IMPRIMIR NO TEMA CLARO mudaria o
     PDF — os tokens de papel têm de valer nos dois temas. */
  :root, html[data-tema="claro"] {
    --t1: #0f1115; --t2: #3a414b; --t3: #5b626c; --t4: #6b7280; --t5: #8a9099;
    --l1: #d7dbe0; --l2: #e8eaee;
    --sup: #ffffff; --sup2: #f7f8fa; --fundo: #ffffff;
    --primary-08: rgba(0,0,0,0.04);
    --primary-15: rgba(0,0,0,0.08);
    --primary-30: rgba(0,0,0,0.18);
    --er-txt: #a81c1c;
    --sombra: none;
  }
  html, body { background: #fff !important; color: var(--t1); }
  .fundo-ambiente { display: none; }
  /* O título usa degradê recortado no texto (color: transparent) — em papel
     isso imprime nada. Volta a ser tinta. */
  .header h1 {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: var(--t1);
  }
  .header, .card, .footer, .tira-c, .kpi, .g-card {
    background: #fff !important;
    box-shadow: none !important;
    backdrop-filter: none;
  }
  /* A marca do painel é a variante prateada, que some no branco. Em papel o
     relatório usa outro arquivo (logo-papel.png, ver relatorio.py); aqui, que é
     impressão avulsa de uma tela, esconder é melhor que imprimir um borrão. */
  .logo { display: none; }
  nav { display: none; }
}

/* ---------------------------------------------------------------------------
   TELA DE ENTRADA — um cartão só, no centro

   Portada de .login-overlay/.login-box do supervisório. O painel usa a grade de
   duas colunas (.wrap); a entrada não: é tela de primeiro contato, sem trilho,
   sem tira de estado e sem nada a comparar. Um retângulo no meio do escuro.

   Aqui o cartão absorve o que antes eram três blocos empilhados — o formulário,
   as notas e o rodapé institucional viraram corpo e pé DE DENTRO da mesma caixa.

   A MARCA É A EXCEÇÃO: fica fora, acima do cartão. Ela não é campo nem aviso,
   é a assinatura da tela — e com ela do lado de fora o cartão passa a conter
   exatamente aquilo que se preenche ou se lê para entrar. Por isso a coluna:
   marca em cima, cartão embaixo, ambos centrados sobre o campo de estrelas.
   --------------------------------------------------------------------------- */
.login-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Centrado por margem automática, e não por justify-content: com a marca
     fora, a coluna ficou ~90 px mais alta, e `justify-content: center` num
     container que rola corta o TOPO sem deixar rolar até ele. Margem auto
     centra quando sobra espaço e cede quando falta. */
  justify-content: flex-start;
  z-index: 2000;
  /* Reflexo diagonal, tipo vidro/metal — sem cor, só brilho.
     ⚠️ O fundo é PRETO PURO, e os três lugares que o pintam têm de casar: este,
     o `.login-success-overlay` e o rastro do canvas de partículas (o
     `rgba(0,0,0,0.10)` do login.html). O grafite #121011 que estava aqui era
     mais claro que o preto e produzia contraste estranho contra a animação —
     e, se um dos três divergir, o rastro acumula uma mancha visível sobre o
     fundo, que é a mesma falha vista de outro jeito. */
  background:
    linear-gradient(128deg, transparent 40%, rgba(255,255,255,0.05) 50%,
                    rgba(255,255,255,0.02) 58%, transparent 68%),
    #000000;
  /* A caixa cresceu com o rodapé embutido: em janela baixa o flex centralizado
     cortaria topo e base por igual, sem como rolar até lá. */
  overflow-y: auto;
  padding: 24px 16px;
}
/* Campo de estrelas — canvas escrito pixel a pixel (ver o <script> do
   login.html). Fica atrás da caixa e não recebe clique. */
.login-starfield {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
.login-box {
  position: relative;
  z-index: 1;
  margin-bottom: auto;          /* o par da margem de cima da marca — ver acima */
  width: min(380px, 100%);
  background: rgba(255,255,255,0.04);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 14px;
  padding: 34px 32px 26px;
  box-shadow: inset 0 2px 0 0 var(--primary-15), var(--sombra-alta);
  animation: rowIn 0.5s var(--ease-out-expo) backwards;
  animation-delay: 0.08s;
}
/* Fora do cartão: `position/z-index` para ficar sobre o campo de estrelas, que
   é irmão dela agora, e `flex-shrink: 0` para a marca não achatar em janela
   baixa — antes ela era filha do cartão e nada disso era preciso. */
.login-logo {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  margin-top: auto;
  text-align: center;
  margin-bottom: 22px;
  /* ⚠️ A largura é obrigatória, não enfeite. Como item flex a caixa encolheria
     para o tamanho do conteúdo, e o `max-width: 70%` da imagem abaixo passaria
     a ser 70% dos próprios 148 px — a marca sairia com ~104 px. Dentro do
     cartão isso não acontecia, porque lá ela era um bloco de 380 px. Casar com
     a largura do cartão devolve a conta de antes e alinha os dois. */
  width: min(380px, 100%);
}
.login-logo img {
  width: 148px;
  max-width: 70%;
  height: auto;
  display: inline-block;
  animation: logoSpringIn 0.7s var(--ease-out-expo) both;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.login-logo img:hover { transform: scale(1.06) rotate(-1deg); }
.login-box h2 {
  text-align: center;
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: var(--grad-titulo);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}
.login-sub {
  text-align: center;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--t3);
  margin: 4px 0 24px;
}
.login-box .campo { margin-bottom: 16px; }
/* O botão de ENTRAR leva o tratamento de exibição — Oswald, caixa alta,
   entreletra —, e não a tipografia do botão comum do painel.
   É o que o supervisório faz nos dois botões de "primeiro contato" (ativação e
   login) e em nenhum outro: lá dentro, "Salvar configuração" e "Enviar e
   atualizar" são ações entre dezenas numa tela densa, e caixa alta em todas
   viraria ruído. Aqui há UMA ação na tela inteira, e ela é a porta — o mesmo
   lugar onde o título acima já está em degradê prata. */
.login-box button {
  width: 100%;
  margin-top: 4px;
  padding: 13px;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
/* Notas de rodapé do formulário: existem para explicar uma regra do sistema,
   não para serem lidas antes dele. Ficam abaixo do botão, miúdas. */
.login-nota {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--t3);
  margin-top: 14px;
}
.login-erro {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--er-txt);
  background: var(--er-f);
  border: 1px solid rgba(239,68,68,0.40);
  border-radius: 10px;
  padding: 9px 12px;
  margin-top: 14px;
}
/* O rodapé institucional entra AQUI dentro, como pé da própria caixa — era o
   terceiro cartão da tela. Sem borda e sem fundo próprio: é crédito, não
   seção. */
.login-box .grupo {
  border-top: 1px solid var(--l1);
  margin: 22px -32px -26px;
  padding: 16px 24px 18px;
  text-align: center;
}
.login-box .grupo-f { gap: 9px; }
.login-box .grupo-f img { width: 30px !important; height: 30px !important; }
.login-box .grupo-t { margin-bottom: 9px; }
.login-box .grupo-n { margin-top: 10px; }

@media (max-width: 420px) {
  .login-box { padding: 26px 20px 20px; }
  .login-box .grupo { margin: 20px -20px -20px; padding: 14px 16px 16px; }
}

/* TRANSIÇÃO DE ENTRADA — as partículas montando a marca.
   Portada de .login-success-overlay. Cobre a tela inteira em cima do login
   (z-index acima do overlay de entrada, que é 2000) e some por opacidade
   depois que o painel já foi pedido. Mesmo PRETO da tela de trás, para o
   overlay aparecer sem salto de fundo — se as duas cores divergirem, o salto
   acontece no instante em que a animação começa, que é onde mais se olha. */
.login-success-overlay {
  position: fixed;
  inset: 0;
  background: #000000;
  z-index: 25000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  transition: opacity 0.45s ease;
}
.login-success-overlay.fade-out { opacity: 0; }
.login-success-overlay canvas { width: 100%; height: 100%; display: block; }

/* ---------------------------------------------------------------------------
   PERGUNTA E AVISO — o que substituiu `confirm()`, `prompt()` e `alert()`

   Portados de `.modal-backdrop`/`.modal` e `.toast`/`toastIn` do supervisório.

   ⚠️ Isto não é preciosismo. Até aqui, TODA ação destrutiva do painel embarcado
   — reiniciar, gravar firmware, descartar a fila, apagar o histórico, esquecer a
   rede Wi-Fi — e a senha de manutenção passavam por caixa NATIVA do navegador.
   O painel inteiro é vidro escuro com Oswald e prata; a decisão de apagar o
   histórico aparecia numa caixa branca do Chrome, com a fonte do sistema e o
   nome do host no título. A identidade quebrava exatamente no momento em que a
   tela mais precisa ser inequívoca, e num celular a mesma caixa vem colada no
   topo, longe do botão que a abriu.

   Havia um segundo motivo, este funcional: `confirm()` e `prompt()` travam o
   laço de eventos da página. Num painel que faz polling a cada 2 s, a tira de
   estado congela enquanto a caixa está aberta — o operador que hesita diante de
   "Reiniciar?" está olhando um valor parado sem nenhum aviso disso.
   --------------------------------------------------------------------------- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(0,0,0,0.85);
  opacity: 0;
  transition: opacity .12s ease;
}
.modal-backdrop.open { opacity: 1; transition-duration: .18s; }
.modal {
  width: 100%;
  max-width: 460px;
  background: var(--bg);
  border: 1px solid var(--l1);
  border-radius: 14px;
  padding: 24px;
  box-shadow: var(--sombra-alta);
  transform: scale(.96) translateY(8px);
  transition: transform .18s var(--ease-out-expo);
}
.modal-backdrop.open .modal { transform: scale(1) translateY(0); }
.modal h2 {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--t1);
  margin-bottom: 10px;
}
.modal p {
  font-size: 13px;
  line-height: 1.55;
  color: var(--t2);
}
.modal p + p { margin-top: 8px; }
/* O detalhe de uma ação destrutiva é o que evita o clique por reflexo — fica
   visível, não escondido atrás de um "saiba mais". */
.modal p.detalhe { color: var(--t3); font-size: 12.5px; }
.modal .campo { margin: 16px 0 0; }
.modal-actions {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}
/* Abaixo disso os dois botões lado a lado ficam estreitos e o rótulo quebra;
   empilhados, a ação principal fica embaixo — mais longe do polegar que acabou
   de tocar em "Apagar", o que é o comportamento desejado aqui. */
@media (max-width: 400px) {
  .modal-actions { flex-direction: column-reverse; }
  .modal-actions button { width: 100%; }
}

/* Aviso flutuante — o que era `alert()`. Um só por vez, some sozinho.
   z-index acima do modal: o aviso de "senha incorreta" nasce no instante em que
   a caixa de senha ainda está fechando. */
.toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 30000;
  max-width: min(440px, 90vw);
  padding: 12px 18px;
  border-radius: 10px;
  background: var(--toast-bg);
  border: 1px solid var(--border-subtle);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--t1);
  font-size: 12.5px;
  line-height: 1.45;
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
  animation: toastIn .25s var(--ease-out-expo);
}
/* O toast flutua com backdrop-filter: o fundo herda o --toast-bg opaco (que
   troca com o tema) e o sinal de erro fica na borda e no texto — uma tinta
   translúcida deixaria o aviso vazar o conteúdo borrado atrás. */
.toast-erro { border-color: var(--er); color: var(--er-txt); }
.toast-ok   { border-color: var(--ok); }
@keyframes toastIn {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Sai por opacidade, sem deslizar: o aviso já foi lido quando começa a sumir. */
.toast.saindo { opacity: 0; transition: opacity .25s ease; }

/* ---------------------------------------------------------------------------
   Foco visível — o teclado precisa enxergar onde está em todo elemento
   alcançável, não só nos campos de formulário.
   --------------------------------------------------------------------------- */
button:focus-visible,
nav a:focus-visible,
a:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Telas estreitas: o trilho vira tira rolável e o painel ocupa a largura toda
   --------------------------------------------------------------------------- */
@media (max-width: 820px) {
  .wrap {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "marca" "topo" "tira" "nav" "card" "rod";
    row-gap: 12px;
  }
  .trilho-sep { display: none; }
  nav {
    flex-direction: row;
    overflow-x: auto;
    padding: 8px;
    gap: 6px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  nav::-webkit-scrollbar { display: none; }
  nav a { border-radius: 30px; padding: 9px 14px; }
  /* Na tira horizontal o pé deixa de ser pé: `margin-top: auto` não empurra
     nada numa linha, e o filete superior viraria um risco solto em cima do
     botão. O filete passa para a ESQUERDA, onde continua fazendo o mesmo
     trabalho — dizer que dali em diante não é mais navegação. */
  .trilho-pe {
    margin-top: 0;
    margin-left: auto;
    padding-top: 0;
    padding-left: 10px;
    border-top: 0;
    border-left: 1px solid var(--l1);
    flex: 0 0 auto;
  }
  .header { align-items: flex-start; }
}

@media (max-width: 560px) {
  body { padding: 14px 10px 28px; }
  .logo-container { padding: 12px 16px; border-radius: 12px; }
  .logo { height: 40px; }
  .header { padding: 13px 16px; }
  .card { padding: 18px 15px 20px; border-radius: 12px; }
  tbody td { padding: 9px 11px; }
  tbody td:first-child { width: 48%; }
  .grupo { margin: 16px -20px -18px; }
  .grupo-f img { width: 34px !important; height: 34px !important; }
}

/* ---------------------------------------------------------------------------
   Movimento reduzido — o alarme continua vermelho e com destaque para quem
   pediu menos movimento: só a piscada (desconfortável para sensibilidade a
   movimento) sai, nunca o sinal em si.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .indicator.active, .pt.vivo { opacity: 1; }
  button:hover, .g-card:hover, .grupo-f img:hover, .logo:hover { transform: none; }
  .fundo-ambiente { display: none; }
  /* ⚠️ O ALARME NÃO PODE SUMIR JUNTO COM A PISCADA. Zerar a duração faria a
     animação terminar e o elemento voltar ao estilo base — borda neutra, falha
     invisível. Aqui o vermelho fica cravado, parado: sai o movimento, nunca o
     sinal. */
  #caixa-sem-sinal, .alert-box.falha-box {
    animation: none !important;
    border-color: var(--danger);
    box-shadow: 0 0 8px rgba(239,68,68,0.55);
  }
}
