/* Sistema de temas (pedido do usuário em 17/08/2026: "modo de exibição
   Claro/Escuro/Noturno" + "paleta de destaque" com pelo menos 6 cores,
   trocados por atributo no <html>, persistidos de verdade no banco —
   ver app/services/configuracoes_service.py e frontend/js/tema.js).
   Mantém os MESMOS NOMES de variável já usados em vários lugares
   (inline em index.html/equipamentos.html e em HTML gerado por JS:
   busca.js, facial.js, eventos.js, popup-pessoa.js,
   duplicidade-facial.js, cadastro.js) — só os VALORES mudam por tema/
   cor, nunca os nomes (evita ter que caçar var(--xxx) por todo lado).

   Claro é o padrão de fábrica agora (era escuro antes desta rodada,
   17/08/2026, ao aplicar o visual dos novos prints fornecidos).
   [data-tema="escuro"]/[data-tema="noturno"] sobrescrevem só as
   variáveis de fundo/superfície/texto; [data-cor="..."] sobrescreve só
   as variáveis de cor de destaque — são independentes uma da outra
   (qualquer cor funciona com qualquer modo).

   --*-bg/--*-text (verde/vermelho/âmbar) e --accent-dim/--accent-text
   usam color-mix() pra se adaptarem automaticamente ao tema E à cor
   escolhida, em vez de precisar de 18 combinações hard-coded (3 temas
   × 6 cores) — suportado por todo navegador relevante desde 2023. */

/* Fonte de exibição (pedido do usuário em 27/08/2026: "quero algo
   futurista e bonito") — só pra títulos/rótulos/números (Chakra Petch
   é geométrica, levemente angular, o mesmo estilo usado em painéis
   reais de controle de acesso/biometria), nunca pro texto corrido
   (tabelas/listas/formulários continuam na fonte de sistema, que lê
   melhor em bloco denso de dados). @import precisa vir antes de
   qualquer regra (só comentários podem vir antes) — por isso está
   aqui no topo. Sem internet no momento do load, cai no fallback
   sans-serif abaixo, sem quebrar nada. */
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600;700&display=swap');

:root {
  --font-display: "Chakra Petch", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  /* Claro (padrão) */
  --bg: #f3f4f8;
  --card-bg: #ffffff;
  --surface-2: #f1f3f7;
  --border: #e3e6ee;
  --text: #1c2130;
  --text-muted: #6b7280;
  --text-3: #9aa1b0;
  --shadow: 0 1px 3px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.05);

  /* Cor de destaque — roxo é o padrão de fábrica */
  --primary: #7c6ff0;
  --primary-hover: #6a5ce0;
  /* Cor de texto sobre a cor de destaque (botão primário, aba ativa) —
     branco pras cores escuras/saturadas, quase-preto pras claras (verde/
     amber/ciano), senão o texto fica ilegível nelas. Ajustada por
     [data-cor="..."] abaixo; este valor cobre o roxo (padrão de fábrica). */
  --primary-fg: #ffffff;

  /* Semânticas base (mesma tonalidade nos 3 temas — só o fundo/texto
     "tintado" ao redor delas muda, via color-mix abaixo) */
  --success: #16a34a;
  --danger: #dc2626;
  --warning: #d97706;

  --radius: 10px;

  /* Derivadas via color-mix — nunca hard-code um --*-bg/--*-text novo
     em outro lugar do arquivo; se precisar de mais um tom, derive daqui. */
  --accent-dim: color-mix(in srgb, var(--primary) 16%, var(--card-bg));
  --accent-text: color-mix(in srgb, var(--primary) 75%, var(--text));
  --green-bg: color-mix(in srgb, var(--success) 18%, var(--card-bg));
  --green-text: color-mix(in srgb, var(--success) 60%, var(--text));
  --red-bg: color-mix(in srgb, var(--danger) 18%, var(--card-bg));
  --red-text: color-mix(in srgb, var(--danger) 60%, var(--text));
  --amber-bg: color-mix(in srgb, var(--warning) 20%, var(--card-bg));
  --amber-text: color-mix(in srgb, var(--warning) 55%, var(--text));
}

/* Escuro — mesma paleta fria (azul-acinzentada) que o tema único de
   antes desta rodada. */
:root[data-tema="escuro"] {
  --bg: #0b0e1a;
  --card-bg: #141a2b;
  --surface-2: #1b2338;
  --border: #262f47;
  --text: #eef1f8;
  --text-muted: #8d94ab;
  --text-3: #5b6480;
  /* Achado ao comparar com a referência visual em 17/08/2026: "none"
     deixava todo card chapado, sem profundidade nenhuma nos temas
     escuros — a referência mantém uma sombra difusa mesmo no modo
     escuro/noturno. */
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 14px 40px rgba(0, 0, 0, 0.45);
}

/* Noturno — escuro, mas com paleta QUENTE (menos luz azul, pensado pra
   uso à noite — pedido do usuário em 17/08/2026) em vez da paleta fria
   do "escuro" acima. */
:root[data-tema="noturno"] {
  --bg: #1a1512;
  --card-bg: #241d18;
  --surface-2: #2e251e;
  --border: #3d322a;
  --text: #f0e6da;
  --text-muted: #a89686;
  --text-3: #7a6b5c;
  --shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
}

/* Paleta de destaque — 6 cores (pedido do usuário em 17/08/2026),
   independente do tema acima. Hex de verde/amber/ciano ajustados em
   17/08/2026 pra bater com a referência visual comparada. Cada cor
   também define --primary-fg (cor do texto EM CIMA da cor de destaque —
   branco pras cores escuras/saturadas, quase-preto pras claras, senão o
   texto de botão primário/aba ativa fica ilegível nas cores claras). */
:root[data-cor="roxo"]  { --primary: #7c6ff0; --primary-hover: #6a5ce0; --primary-fg: #ffffff; }
:root[data-cor="azul"]  { --primary: #3b82f6; --primary-hover: #2563eb; --primary-fg: #ffffff; }
:root[data-cor="verde"] { --primary: #16b981; --primary-hover: #10996c; --primary-fg: #04150e; }
:root[data-cor="amber"] { --primary: #f0a12a; --primary-hover: #d98f1f; --primary-fg: #1e1405; }
:root[data-cor="rosa"]  { --primary: #ec4899; --primary-hover: #db2777; --primary-fg: #ffffff; }
:root[data-cor="ciano"] { --primary: #22b8cf; --primary-hover: #1a94a8; --primary-fg: #04181d; }

* { box-sizing: border-box; }

/* Barra de rolagem fina, na cor de destaque (pedido do usuário em
   27/08/2026: "futurista e bonito") — troca a barra grossa/cinza padrão
   do navegador nos painéis que rolam por dentro (ex.: .log-scroll). Só
   Chrome/Edge (WebKit) entendem essas propriedades; em outro navegador
   cai na barra padrão do sistema, sem quebrar nada. */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--primary) 35%, var(--border)); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--primary) 55%, var(--border)); }

/* Anel de foco visível, na cor de destaque (pedido do usuário em
   27/08/2026: "futurista e bonito", + acessibilidade de brinde) — só
   aparece navegando por teclado (:focus-visible), nunca ao clicar com
   mouse, então não muda nada do visual de clique já existente. */
:focus-visible { outline: 2px solid color-mix(in srgb, var(--primary) 60%, transparent); outline-offset: 2px; }

/* Viewport fixo, sem scroll de página inteira (pedido do usuário em
   14/08/2026: "a lista não pode passar da caixa") — cada painel (log de
   acessos, ativos e expirando) rola por DENTRO de si mesmo via
   .log-scroll, nunca a página toda. A aba Configurações é a única
   exceção (tem mais cards empilhados do que cabe numa tela) — ver o
   "overflow-y:auto" inline no próprio .main daquela página, que
   sobrescreve isto com especificidade maior. */
html, body { height: 100%; overflow: hidden; }
body {
  margin: 0;
  font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  /* Profundidade de fundo (pedido do usuário em 27/08/2026: "futurista e
     bonito") — dois brilhos suaves na cor de destaque atual (funciona com
     qualquer uma das 6 cores, é só color-mix a partir de --primary) mais
     uma grade de pontos bem discreta (tipo "grade de sensor"), lembrando
     um esquema técnico — combina com o tema de biometria/controle de
     acesso do programa. Só aparece de verdade ATRAVÉS dos cards, que
     agora são translúcidos (ver .card abaixo); atrás dos elementos
     opacos normais nem se nota que existe. */
  background-image:
    radial-gradient(ellipse 900px 560px at 88% -8%, color-mix(in srgb, var(--primary) 16%, transparent), transparent 65%),
    radial-gradient(ellipse 700px 520px at -8% 108%, color-mix(in srgb, var(--primary) 10%, transparent), transparent 65%),
    radial-gradient(color-mix(in srgb, var(--text-3) 40%, transparent) 1px, transparent 1px);
  background-size: auto, auto, 26px 26px;
  background-repeat: no-repeat, no-repeat, repeat;
}

/* ===== Chrome geral: sidebar + conteúdo (pedido do usuário em
   14/08/2026, seguindo mockup_v6.html) — antes era só uma .topbar com
   abas; agora as duas páginas (Cadastro/index.html e Configurações/
   equipamentos.html) compartilham este mesmo wrapper. Nav só troca de
   página de verdade (href normal) — não é uma SPA — a classe "active" já
   vem marcada no HTML de cada página, igual antes. */
.app { display: grid; grid-template-columns: 210px 1fr; height: 100vh; transition: grid-template-columns .18s ease; }
.app.collapsed { grid-template-columns: 60px 1fr; }

.sidebar {
  /* Vidro fosco (pedido do usuário em 27/08/2026: "futurista e bonito")
     — translúcida + blur em vez de cor sólida, deixando o brilho de
     fundo do body (ver body acima) aparecer por trás, meio embaçado.
     @supports abaixo cobre navegador sem backdrop-filter: fica sólida
     como era antes, sem nenhuma perda visual. */
  background: color-mix(in srgb, var(--card-bg) 80%, transparent);
  backdrop-filter: blur(20px) saturate(150%);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  border-right: 1px solid color-mix(in srgb, var(--border) 70%, var(--primary) 15%);
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sidebar { background: var(--card-bg); }
}
.sidebar-top { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
/* Marca do programa (pedido do usuário em 28/08/2026: "quero adicionar a
   marca do programa com a logomarca, nome do programa vai ser contrô")
   — ícone + nome no topo da barra lateral, em TODA página que tem
   sidebar (chrome compartilhado, mas HTML duplicado por página — ver
   frontend/js/sidebar.js). Diferente da logo do CONDOMÍNIO
   (.logo-banner-cadastro, configurável em Identidade visual) — esta é a
   marca do PRÓPRIO programa, fixa, sempre presente. flex:1 empurra o
   botão de recolher/expandir pra ponta direita sozinho, sem precisar de
   nenhum ajuste no .toggle-btn. Escondida com o menu recolhido (mesmo
   espírito de .nav-label ali embaixo) — 60px de largura não sobra
   espaço pra ícone + texto + botão de recolher juntos. */
.marca-programa { display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0; overflow: hidden; }
/* 22px -> 30px -> 40px (pedido do usuário em 28/08/2026, duas vezes:
   "o icone... precisa ser um pouco maior", depois "o icone do programa
   precisa ser mais visivel e nitido... quero maior"). O anel/brilho na
   cor de destaque (box-shadow, de um pedido anterior — "faça o logo
   acompanhar a paleta de cores") foi removido no mesmo pedido de
   28/08/2026: "sem a sombra da caixa" — o contorno nítido de
   .marca-programa-icone (filtro logo abaixo) já basta, o anel só
   competia visualmente num ícone tão pequeno. */
.marca-programa-icone {
  width: 40px; height: 40px; border-radius: 7px; flex-shrink: 0; object-fit: cover;
}
.marca-programa-texto {
  font-family: var(--font-display); font-weight: 700; font-size: 15.5px;
  color: var(--text); white-space: nowrap; letter-spacing: .02em;
}
.app.collapsed .marca-programa { display: none; }
.toggle-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  width: 28px; height: 28px; border-radius: 7px; cursor: pointer; font-size: 13px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.toggle-btn:hover { background: var(--border); }
/* "Voltar para Cadastros e Blocos" da página de Unidade — mesmo visual
   de .toggle-btn acima (quadrado, ícone só), mas é um <a> (link de
   verdade, não JS) — precisa do text-decoration/display próprios que
   um <button> já tem de graça. Pedido do usuário em 28/08/2026: "o
   botao Voltar... precisa virar botao de icone". */
.voltar-icone-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  width: 30px; height: 30px; border-radius: 7px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.voltar-icone-btn:hover { background: var(--border); color: var(--text); }
.voltar-icone-btn svg { width: 16px; height: 16px; }
/* Seta gira 180° ao recolher/expandir (pedido do usuário em 17/08/2026:
   "faça um [botão] em forma de seta animada"). */
.toggle-btn svg { transition: transform 0.25s ease; }
.app.collapsed .toggle-btn svg { transform: rotate(180deg); }

/* Caixa base reutilizada pelo preview de logo em Configurações
   (#logo-preview-banner, com tamanho sobrescrito inline) e pela faixa
   de logo acima da caixa de Cadastro (.logo-banner-cadastro) abaixo. */
.logo-banner {
  width: 100%; height: 56px; border-radius: 10px;
  background: linear-gradient(120deg, var(--surface-2), var(--bg));
  border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; position: relative;
  /* Brilho na cor de destaque atual (pedido do usuário em 28/08/2026:
     "faça o logo acompanhar a paleta de cores do programa de forma
     harmônica") — mesmo anel/glow de .marca-programa-icone acima, pra
     ler como a mesma família visual em toda aparição da marca. Só
     decorativo — nunca aparece quando .vazio (sem logo configurado, ver
     mais abaixo), senão viraria um contorno brilhando ao redor de nada. */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 30%, transparent),
              0 4px 18px color-mix(in srgb, var(--primary) 22%, transparent);
}
.logo-banner.vazio { box-shadow: none; }

/* Logo da tela de Login (pedido do usuário em 28/08/2026) — mesmo anel/
   glow na cor de destaque das outras 2 aparições da marca acima, pra
   ler como a mesma família visual em toda a tela em que aparece. */
.marca-login {
  display: block; width: 100%; max-width: 220px; margin: 0 auto 14px;
  border-radius: var(--radius);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 30%, transparent),
              0 4px 18px color-mix(in srgb, var(--primary) 22%, transparent);
}
/* Fundo removido + halo claro fixo, não troca por tema (pedido do
   usuário em 28/08/2026: primeiro "a logomarca precisa se adaptar a cor
   de fundo da pagina" — achado: os PNGs da marca tinham fundo
   azul-marinho OPACO embutido, não transparente, aparecendo como uma
   caixa escura em cima de fundo claro; DEPOIS refinado: "o nome da logo
   nao ficou na cor oposta do plano de fundo... priorize sempre o azul,
   mas nao deixe o nome ficar oculto e nem apagado quando a cor da
   pagina mudar" — a 1ª tentativa trocava pra uma variante de letra clara
   no tema escuro/noturno via content:url() condicionado a
   [data-tema="claro"]; o pedido é justamente NUNCA fazer essa troca —
   sempre as letras navy (frontend/img/*-claro.png, já no src="" do
   HTML), com um halo claro fixo (não depende de var() de tema — é
   fixo de propósito, funciona igual em qualquer fundo) garantindo
   contraste mesmo em cima de um fundo escuro, onde navy sozinho quase
   some. drop-shadow (não box-shadow) porque precisa seguir o contorno
   real das letras/PNG transparente, não um retângulo. Duas camadas
   empilhadas = halo mais grosso/visível que uma só. */
.marca-login {
  filter: drop-shadow(0 0 2px rgba(245, 246, 250, .85)) drop-shadow(0 0 2px rgba(245, 246, 250, .85));
}
/* Ícone da barra lateral é BEM menor (30px) que a logo do login (220px)
   — o mesmo halo de 2px de desfoque, proporcionalmente, borrava o
   desenho inteiro nesse tamanho (pedido do usuário em 28/08/2026: "o
   icone do programa precisa ser mais visivel e nitido", depois de já
   ter pedido a cor navy fixa com garantia de contraste). Troca de
   "brilho suave" por "contorno fino": raio bem menor (0.6px) e 3 camadas
   bem opacas em vez de 2 mais fracas — no tamanho do ícone isso lê como
   uma borda nítida ao redor de cada letra, não uma mancha embaçada, mas
   ainda garante contraste em cima do fundo escuro do tema escuro/
   noturno (var(--card-bg) neles é #141a2b/#241d18 — bem perto do navy
   #0a1640 da própria letra; sem esse contorno ela quase some ali). */
.marca-programa-icone {
  filter: drop-shadow(0 0 .6px rgba(245, 246, 250, 1)) drop-shadow(0 0 .6px rgba(245, 246, 250, 1)) drop-shadow(0 0 .6px rgba(245, 246, 250, 1));
}
/* object-fit: cover — restaurado em 28/08/2026 (pedido do usuário:
   "volte com a caixa de logo inicial antes dessas últimas mudanças de
   logo", desfazendo a troca pra contain feita mais cedo no mesmo dia).
   Preenche toda a área da caixa; com a logo REAL da marca (~3:2, bem
   mais larga que alta) isso volta a cortar um pouco topo/base pra caber
   na largura — mesmo comportamento de antes de 28/08/2026. Se isso
   incomodar visualmente, revisitar (a alternativa testada foi contain,
   que nunca corta mas deixa tarja vazia nas laterais sem uma caixa
   proporcional à imagem). */
.logo-banner img { width: 100%; height: 100%; object-fit: cover; }
.logo-banner span { font-size: 11.5px; color: var(--text-muted); letter-spacing: .05em; white-space: nowrap; }
/* Ferramenta de enquadramento da logo (pedido do usuário em 17/08/2026:
   "preciso de uma ferramenta de enquadramento para a foto do meu
   logo") — mesmo cropper inline da foto de cadastro, ver
   frontend/js/cropper.js (criarCropper) e logo.js. */
.logo-banner canvas { width: 100%; height: 100%; object-fit: contain; cursor: crosshair; display: block; }
/* Virou popup em 28/08/2026 (pedido do usuário: "quando eu colocar uma
   logo nova o programa abra a caixa original da logo como um popup e
   permita que possa ajustar a imagem dentro da caixa... do tamanho que
   eu quiser") — antes a caixinha de preview (#logo-preview-banner,
   220×70) crescia temporariamente (.cropping) pra abrir espaço pra
   cortar; agora ela só mostra o logo já salvo/"SEM LOGO" (nunca mais
   cresce) e o corte inteiro (canvas + pré-visualização) mora dentro de
   #logo-editor-modal abaixo, já nascendo bem maior. */
.popup-backdrop {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(0, 0, 0, .55);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.logo-editor-dialog {
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: var(--shadow); padding: 18px;
  max-width: 92vw;
  animation: toast-entrada .15s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .logo-editor-dialog { animation: none; }
}
.logo-editor-titulo {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 14px;
  font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--text);
}
.logo-editor-titulo button {
  background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 4px; line-height: 1;
}
.logo-editor-titulo button:hover { color: var(--text); }
/* Mesma proporção recomendada da faixa real (3.5:1, cfg.proporcaoAlvo em
   logo.js — ver texto de ajuda "3:1 a 4:1" em equipamentos.html), só que
   numa caixa BEM maior que os 220×70 de antes — o objetivo é justamente
   dar espaço de sobra pra ajustar com precisão (zoom, arrastar, ajustar
   à caixa), coisa que a caixinha da linha de Configurações nunca teve
   tamanho pra fazer bem. margin-bottom:0 sobrescreve o padrão de
   .logo-banner-cadastro (não se aplica aqui, é só reuso da caixa base). */
.logo-editor-caixa {
  /* height:auto sobrescreve o height:56px fixo de .logo-banner (base
     compartilhada) — sem isto, um height explícito qualquer sempre
     vence aspect-ratio (que só preenche dimensão em auto), e a caixa
     nunca cresceria de verdade. */
  width: min(720px, 78vw); height: auto; aspect-ratio: 3.5 / 1; max-height: 60vh;
  margin: 0 auto;
}

/* Caixa de foto do cadastro rápido de unidade (pedido do usuário em
   21/08/2026: "a ferramenta de corte de foto dentro de unidades... esta
   bugada, pode ser o tamanho da caixa de foto") — normalmente 120×120
   (pequena, só pra ocupar pouco espaço na lista), cresce pra um
   quadrado maior enquanto corta, dando espaço de sobra pra arrastar a
   seleção com precisão (mesma ideia da logo acima, mas quadrada em vez
   de paisagem, já que é foto de rosto). Valor fixo (com limite pra
   telas estreitas via min()), não percentual — mesma armadilha do item
   acima, width/height:100% encolheria em vez de crescer. */
#unidade-foto-caixa.cropping { width: min(320px, 90vw) !important; height: min(320px, 90vw) !important; flex-shrink: 0; }

/* Logo em formato paisagem, DENTRO da coluna do meio, acima da caixa de
   cadastro (pedido do usuário em 17/08/2026, com fotos de exemplo
   mostrando o padrão exato: a faixa fica só acima da coluna do meio —
   "Log de acessos" e "Status de cadastro do dia" continuam começando no
   topo normalmente, sem encolher). Por isso mora dentro de .form-col,
   não no topbar compartilhado. Ver frontend/js/sidebar.js. */
/* Reduzido de 26%/90-220px pra 12%/56-130px em 20/08/2026 (pedido do
   usuário: "suba mais a caixa geral de cadastro") — sem logo configurado
   esse espaço ficava reservado vazio (ver .vazio abaixo) e empurrava a
   caixa de Cadastro pra baixo à toa; o tamanho continua IGUAL com ou sem
   logo (não viola o "não quero que estique" de mais acima), só ficou
   menor nos dois casos. */
/* 28/08/2026: três rodadas de ajuste fino no mesmo dia (aspect-ratio 3/2
   pra bater a proporção real da logo → trocar % por vh → travar largura
   com max-width, cada uma resolvendo um problema mas criando outro —
   cropping, depois camera quase sumindo, depois vão vazio nas laterais)
   foram desfeitas a pedido do usuário: "volte com a caixa de logo
   inicial antes dessas últimas mudanças de logo". De volta ao
   comportamento estável de 20/08/2026 (object-fit também voltou pra
   `cover`, ver .logo-banner img acima) — e, no mesmo dia, mais um
   pedido: "deixe a caixa um pouco maior" (56/12%/130 → 64/14%/150,
   +14~15% em cada limite do clamp, mantendo a mesma lógica).

   max-width tentado e desfeito no mesmo dia ainda (achado pelo usuário,
   com prints em janela maximizada/tela cheia: "em tela cheia o logo nao
   esta ficando assim... quando eu movo a proporção da caixa" — o corte
   da letra em cima/embaixo com object-fit:cover numa proporção muito
   esticada) — a correção tentada foi travar a LARGURA da caixa
   (max-width:520px), mas o pedido seguinte foi claro: "não altere o
   tamanho da caixa do logo". A caixa continua width:100% sempre, do
   jeito que já era; quem precisa evitar corte agressivo em telas muito
   largas é a IMAGEM em si — pré-encolhida/com preenchimento próprio via
   o editor de logo em popup (zoom-/"ajustar à caixa" na proporção
   3.5:1 recomendada, ver frontend/js/logo.js e o texto de ajuda em
   equipamentos.html), não a caixa de destino. */
.logo-banner-cadastro {
  flex: 0 0 auto;
  height: clamp(64px, 14%, 150px);
  margin-bottom: 12px;
}
/* .vazio (sem logo configurado/removido) em vez de .hidden (pedido do
   usuário em 20/08/2026: "quando eu removo o logo a câmera estica, não
   quero que estique, quero que fique do mesmo tamanho") — com .hidden
   (display:none) a faixa saía do flex e os 26% de altura que ela
   reservava iam sozinhos pro .form-card debaixo (e de lá pro
   .cam-triple, que também é flex:1) — a câmera crescia toda vez que a
   logo era removida. Mantendo o espaço reservado (mesmo flex-basis de
   sempre) e só apagando a cor/borda, a câmera nunca muda de tamanho,
   com ou sem logo — só fica um vão em branco no lugar da faixa. */
.logo-banner-cadastro.vazio {
  background: transparent; border-color: transparent;
  /* Sem NENHUMA logo configurada, não faz sentido reservar até 150px de
     vão vazio (o teto do clamp() acima) — trava no mínimo (64px, mesmo
     valor de sempre) só quando não há nada pra mostrar. */
  height: 64px;
}

.nav-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px;
  color: var(--text-muted); font-family: var(--font-display); font-size: 13.5px; font-weight: 500; text-decoration: none;
  margin-bottom: 4px; white-space: nowrap; overflow: hidden;
}
.nav-item:hover { background: var(--surface-2); }
/* Gradiente + indicador lateral em vez de fundo sólido plano (achado ao
   comparar com a referência visual em 17/08/2026). */
.nav-item.active {
  background: linear-gradient(90deg, color-mix(in srgb, var(--primary) 22%, transparent), color-mix(in srgb, var(--primary) 6%, transparent));
  color: var(--accent-text);
  box-shadow: inset 3px 0 var(--primary);
}
.nav-item .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .7; flex-shrink: 0; }
/* Ícone no lugar do "dot" (pedido do usuário em 19/08/2026: "quero que
   implemente ícones... seguindo a lógica do programa") — herda a cor do
   próprio link (cinza-muted parado, cor de destaque quando .active),
   igual o dot já fazia, então não precisou de nenhuma regra de cor
   própria. Com o menu recolhido (.app.collapsed) o ícone passa a ser a
   ÚNICA coisa visível na linha — antes era só o dot, pouco informativo. */
.nav-item .nav-icon { display: flex; flex-shrink: 0; transition: transform .15s ease; }
.nav-item .nav-icon svg { width: 18px; height: 18px; }
.nav-item:hover .nav-icon { transform: scale(1.1); }
.app.collapsed .nav-item { justify-content: center; padding: 9px 0; }
.app.collapsed .nav-label { display: none; }

.sidebar-foot { margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); font-size: 11px; }
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); flex-shrink: 0; display: inline-block; }
.status-dot.off { background: var(--danger); }
/* Sinal discreto de sincronização com a nuvem (pedido do usuário em
   08/09/2026: "um sinal discreo mas funcional") — âmbar pra "tem algo
   esperando pra ser enviado, mas nada quebrado" (.off/vermelho continua
   reservado só pra falha de verdade). */
.status-dot.pendente { background: var(--warning); }
.status-ok { display: flex; align-items: center; gap: 7px; color: var(--text-muted); white-space: nowrap; }
.status-off-row { display: flex; align-items: center; gap: 7px; color: var(--red-text); font-weight: 600; white-space: nowrap; }
.status-off-meta { display: block; color: var(--text-3); font-size: 10px; margin: 2px 0 0 14px; }
.app.collapsed .sidebar-foot * { display: none; }
.app.collapsed .sidebar-foot { display: flex; justify-content: center; padding-top: 12px; }
.app.collapsed .sidebar-foot::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--success); display: block; }
/* Versão do programa no rodapé da sidebar (pedido do usuário em
   10/09/2026: "nas proximas versoes de patch mostre a versao do
   programa") — discreta, nunca compete com os sinais de status acima;
   escondida junto com o resto quando a sidebar está recolhida (mesma
   regra .app.collapsed .sidebar-foot * já existente). */
.sidebar-versao { margin-top: 8px; color: var(--text-3); opacity: .75; }

.main { height: 100vh; overflow: hidden; padding: 16px 24px; display: flex; flex-direction: column; }
.topbar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; flex-shrink: 0; }
.topbar h1 { font-family: var(--font-display); font-size: 21px; margin: 0; font-weight: 700; letter-spacing: 0.01em; }

.card {
  /* Vidro fosco (pedido do usuário em 27/08/2026: "futurista e bonito")
     — mesma ideia da .sidebar acima: translúcido + blur em vez de cor
     sólida. Borda com um toque da cor de destaque (em vez de --border
     puro) pra dar aquele "brilho fino na borda do vidro". @supports
     cobre navegador sem backdrop-filter (cai pra sólido, como antes). */
  background: color-mix(in srgb, var(--card-bg) 78%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 75%, var(--primary) 15%);
  border-radius: 16px;
  box-shadow: var(--shadow), inset 0 1px 0 color-mix(in srgb, white 10%, transparent);
  backdrop-filter: blur(20px) saturate(150%);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  padding: 16px 18px;
  margin-bottom: 16px;
  /* Entrada suave (pedido do usuário em 27/08/2026: "moderno e
     elegante... efeitos") — só roda uma vez, quando o card aparece na
     tela (troca de página, ou um card novo entrando via JS); nunca de
     novo só porque o CONTEÚDO de dentro mudou (reescrever innerHTML não
     recria o elemento .card em si, então a animação não reinicia à
     toa). Deliberadamente SEM opacidade (achado em bancada em
     27/08/2026: numa aba em segundo plano, o navegador pode congelar o
     relógio da animação em 0% — se o "de" fosse opacity:0, o card
     ficaria invisível até alguém interagir com a aba; só a POSIÇÃO
     anima, então mesmo travada em 0% o card continua 100% visível, só
     6px "mais baixo" até a animação resolver). */
  animation: card-entrada .35s cubic-bezier(.16, 1, .3, 1) both;
}
@keyframes card-entrada {
  from { transform: translateY(6px); }
  to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .card { animation: none; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .card { background: var(--card-bg); }
}
/* Título discreto em caixa alta (achado ao comparar com a referência
   visual em 17/08/2026) — antes era um título grande/comum, destoando do
   resto da interface, que é toda compacta. Fonte de exibição adicionada
   em 27/08/2026 (ver --font-display no :root) — reforça o ar "painel
   técnico" nos rótulos de seção, sem tocar no texto corrido. */
.card h2, .card h3 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-muted);
}

label { display: block; font-size: 12.5px; color: var(--text-muted); margin-bottom: 4px; font-weight: 500; }

/* Rótulo compacto em caixa alta usado no "Destino"/"Modo de acesso" da
   tela de Cadastro (classe já existia no HTML, mas sem regra própria —
   achado ao comparar com a referência visual em 17/08/2026). */
.field-label {
  font-family: var(--font-display);
  font-size: 10px;
  color: var(--text-muted);
  margin: 0 0 6px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* input[type="date"]/[type="time"] adicionados em 21/08/2026 (pedido do
   usuário: "o formato de calendário e relógio do programa é feio, tem
   algum mais bonito?") — ficaram de fora desta regra desde que a
   validade virou dois campos separados (Data / Horário) em vez de um
   datetime-local só; sem isso, os dois ficavam 100% no visual padrão
   cru do navegador (caixa pequena, sem borda no nosso estilo), bem
   diferente de todo o resto dos campos da tela. */
input[type="text"], input[type="datetime-local"], input[type="date"], input[type="time"],
input[type="number"], select, input[type="password"] {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 9px;
  font-size: 14px;
  background: var(--surface-2);
  color: var(--text);
}
input::placeholder { color: var(--text-3); }
/* O ícone de calendário/relógio nativo vem sempre "escuro" (o navegador
   não sabe do nosso tema) — com :root { color-scheme } já ajustando o
   POPUP do seletor (linha abaixo), só falta este ícone pequeno dentro
   da própria caixa acompanhar o tema, senão ele desaparece ou destoa em
   fundo escuro. Chrome/Edge (WebKit): filtro de cor no próprio ícone. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  border-radius: 5px;
  padding: 3px;
  filter: opacity(0.65);
}
input[type="date"]::-webkit-calendar-picker-indicator:hover,
input[type="time"]::-webkit-calendar-picker-indicator:hover {
  filter: opacity(1);
  background: var(--border);
}
:root[data-tema="escuro"] input[type="date"]::-webkit-calendar-picker-indicator,
:root[data-tema="escuro"] input[type="time"]::-webkit-calendar-picker-indicator,
:root[data-tema="noturno"] input[type="date"]::-webkit-calendar-picker-indicator,
:root[data-tema="noturno"] input[type="time"]::-webkit-calendar-picker-indicator {
  filter: invert(1) opacity(0.75);
}
:root[data-tema="escuro"] input[type="date"]::-webkit-calendar-picker-indicator:hover,
:root[data-tema="escuro"] input[type="time"]::-webkit-calendar-picker-indicator:hover,
:root[data-tema="noturno"] input[type="date"]::-webkit-calendar-picker-indicator:hover,
:root[data-tema="noturno"] input[type="time"]::-webkit-calendar-picker-indicator:hover {
  filter: invert(1) opacity(1);
}
/* Halo suave translúcido em vez de contorno sólido reto (achado ao
   comparar com a referência visual em 17/08/2026). */
input:focus, select:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent);
}

/* Pedido do usuário em 17/08/2026: "no nome do equipamento, em modo
   escuro... não dá pra ler" — a causa mais comum disso é o navegador
   forçando um fundo claro/amarelo no autofill (ex: Chrome "lembrar
   valores digitados") por cima da nossa cor de fundo escura, ou o
   próprio navegador aplicando esquema de cor nativo dos controles sem
   saber que a página já tem tema escuro. As duas correções abaixo
   cobrem os dois casos, nos 3 temas. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  -webkit-text-fill-color: var(--text) !important;
  -webkit-box-shadow: 0 0 0px 1000px var(--surface-2) inset !important;
  caret-color: var(--text);
  transition: background-color 999999s ease-in-out 0s;
}
:root { color-scheme: light; }
:root[data-tema="escuro"], :root[data-tema="noturno"] { color-scheme: dark; }

.field { margin-bottom: 14px; position: relative; }

/* Campos "Nome"/"Documento" da tela de Cadastro — classe já existia no
   HTML sem regra própria (achado ao comparar com a referência visual em
   17/08/2026). */
.tall-input { min-height: 48px; font-size: 14px; }

.busca-wrap { position: relative; }
/* Destino (bloco) / Unidade como busca+popup (pedido do usuário em
   20/08/2026) — mesmo princípio do .busca-wrap acima. */
.destino-busca-wrap { position: relative; }

/* Liberação manual de porta/catraca (pedido do usuário em 18/08/2026:
   "o botão é feio, coloque um mais bonito") — botão com ícone de cadeado
   aberto (mesmo estilo de traço fino usado no ícone da câmera/foto),
   cor âmbar (ação de exceção/override manual, distinta do azul/roxo
   padrão dos botões primários) em vez de um <button class="primary">
   genérico. */
.liberacao-manual-wrap { position: relative; display: flex; align-items: center; gap: 4px; }
/* v1.5.67 — setinha ao lado do botão "Liberar acesso": abre a lista para escolher UM equipamento
   (o botão grande libera todos os configurados). Só aparece com mais de um configurado. */
.btn-liberacao-manual-lista {
  background: var(--amber-bg); color: var(--amber-text); border: 1px solid transparent;
  border-radius: 8px; padding: 7px 8px; font-size: 12px; line-height: 1; cursor: pointer;
}
.btn-liberacao-manual-lista:hover { filter: brightness(0.95); }
.btn-liberacao-manual {
  display: flex; align-items: center; gap: 7px;
  background: var(--amber-bg); color: var(--amber-text);
  border: 1px solid transparent; border-radius: 8px;
  padding: 7px 14px; font-size: 13px; font-weight: 600; cursor: pointer;
  transition: filter .15s;
  /* Pulso contínuo (pedido do usuário em 19/08/2026: pode "ganhar um
     ícone/animação mais chamativo já que é uma ação física real") — um
     anel que respira ao redor do botão, chamando atenção de longe pra
     essa ação (abre a porta/catraca de verdade, sem reconhecimento
     facial). Anima box-shadow, não transform, pra não brigar com a
     animação genérica de hover/clique de TODO botão (ver regra `button`
     e `button svg` mais acima neste arquivo). */
  animation: liberacao-pulso 2.4s ease-in-out infinite;
}
.btn-liberacao-manual:hover { filter: brightness(0.95); animation-play-state: paused; }
.btn-liberacao-manual:disabled { opacity: .6; cursor: not-allowed; animation: none; }
@keyframes liberacao-pulso {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--amber-text) 45%, transparent); }
  50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--amber-text) 0%, transparent); }
}
/* Botão único de alternância de tema, do lado do relógio na tela de
   Cadastro (pedido do usuário em 20/08/2026 — ver #btn-tema-toggle em
   index.html e frontend/js/tema.js). Mesmo tamanho/formato redondo do
   .toggle-btn (recolher menu), só que o ícone troca dinamicamente pro
   modo atual (Claro/Escuro/Noturno) em vez de ficar fixo. */
.tema-toggle-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  width: 26px; height: 26px; border-radius: 7px; cursor: pointer; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; transition: filter .15s, background .15s;
}
.tema-toggle-btn:hover { background: var(--border); }

@media (prefers-reduced-motion: reduce) {
  .btn-liberacao-manual { animation: none; }
}
.liberacao-manual-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 8px; border-radius: 7px; font-size: 13.5px;
}
.liberacao-manual-item:hover { background: var(--card-bg); }
.liberacao-manual-item button {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 28px; height: 28px; padding: 0; border-radius: 6px;
  background: var(--amber-bg); color: var(--amber-text); border: none; cursor: pointer;
}
.liberacao-manual-item button:hover { filter: brightness(0.95); }
.liberacao-manual-item button:disabled { opacity: .5; cursor: not-allowed; }
.liberacao-manual-item .resultado { font-size: 11px; margin-top: 2px; }

.popup {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow);
  z-index: 30;
  max-height: 280px;
  overflow-y: auto;
  margin-top: 4px;
  padding: 4px;
}
.popup .item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  cursor: pointer;
  border-radius: 7px;
}
.popup .item:hover,
/* Navegação por teclado (achado na auditoria de 05/09/2026) — mesmo
   destaque do :hover, só que controlado via JS (ativarNavegacaoTecladoPopup
   em api.js) enquanto o mouse não está envolvido. */
.popup .item.ativo { background: var(--card-bg); }
.popup .item img, .popup .item .avatar-vazio {
  width: 32px; height: 32px; border-radius: 50%; object-fit: cover; background: var(--bg); flex-shrink: 0;
}
.popup .vazio { padding: 12px; color: var(--text-muted); font-size: 13px; }

/* Informações da unidade — ícone "ℹ️" discreto + popover (pedido do
   usuário em 01/10/2026: "um ícone discreto de informação... pra não
   poluir a visualização"). .unidade-info-wrap precisa de
   position:relative pra .popup (top:100%, ver regra acima) se ancorar
   bem embaixo do ícone, nunca do layout inteiro da página. Ver
   frontend/js/unidade-info.js. */
.unidade-info-wrap { position: relative; display: inline-block; vertical-align: middle; }
.unidade-info-btn {
  background: none; border: none; cursor: pointer; padding: 0 2px;
  font-size: 13px; line-height: 1; opacity: .55; vertical-align: middle;
}
.unidade-info-btn:hover { opacity: 1; }
/* position:fixed (não a regra base .popup, absolute) — calculada em JS
   por UnidadeInfo._posicionar, pra flutuar livre de qualquer ancestral
   com overflow:hidden/auto (ex.: a lista de resultados da busca de
   destino, destino.js). top/left ficam só inline, aqui só o resto. */
.unidade-info-popup { position: fixed; width: 220px; max-height: none; padding: 10px; right: auto; margin-top: 0; z-index: 250; }
.unidade-info-conteudo { display: flex; flex-direction: column; gap: 7px; }
.unidade-info-linha { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px; }
.unidade-info-rotulo { color: var(--text-muted); flex-shrink: 0; }
.unidade-info-vazio { color: var(--text-3); font-style: italic; }
.unidade-info-campo { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--text-muted); }
.unidade-info-input { font-size: 12.5px; padding: 4px 7px; }
.unidade-info-acoes { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.unidade-info-editar-btn, .unidade-info-salvar-btn { font-size: 12px; padding: 4px 10px; align-self: flex-start; }
.unidade-info-status { font-size: 11.5px; color: var(--text-muted); }

/* Popup discreto no canto da tela (pedido do usuário em 21/08/2026) —
   ver Toast em frontend/js/api.js. Some sozinho depois de alguns
   segundos ou no clique do ✕; nunca bloqueia o resto da tela. */
#toast-container {
  position: fixed; bottom: 20px; right: 20px; z-index: 200;
  display: flex; flex-direction: column; gap: 8px; max-width: 340px;
}
.toast {
  display: flex; align-items: flex-start; gap: 8px;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 9px;
  padding: 10px 12px; font-size: 12.5px; line-height: 1.4;
  box-shadow: var(--shadow); animation: toast-entrada .18s ease-out;
}
.toast-aviso { border-color: var(--warning); }
.toast span { flex: 1; }
.toast button {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 13px; padding: 0; line-height: 1; flex-shrink: 0;
}
.toast button:hover { color: var(--text); }
@keyframes toast-entrada {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

.grid-3 { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 12px; }

.linha-dupla { display: flex; flex-direction: column; gap: 16px; }

/* 🐛 Achado em 28/08/2026 (pedido do usuário: "a camera nao pode perder
   a proporção mesmo mudando a resoluçaõ"): .camera-preview-linha/
   .webcam-box/.preview-foto-coluna eram do layout ANTIGO (14/08/2026,
   vídeo e foto lado a lado) — desde a fusão da caixa de foto com a
   câmera ao vivo (20/08/2026, ver .sym-box.cam mais abaixo), nenhuma
   dessas classes existe mais no HTML (confirmado por busca). Removidas
   aqui. O problema real estava na regra `video#webcam-video` que vinha
   junto: por ter um ID no seletor, ela ganhava de `.sym-box.cam video`
   (a regra "de verdade", sem ID) na especificidade do CSS mesmo estando
   morta/órfã — media o VÍDEO num aspect-ratio:3/4 FIXO (do layout
   antigo, lado a lado) em vez de acompanhar o tamanho real da caixa
   (que mudou de forma pra uma faixa larga desde então), estourando/
   distorcendo a proporção sempre que a janela/resolução mudava o
   tamanho real da caixa. #preview-foto/#preview-foto-vazio/
   #cropper-canvas tinham o mesmo tipo de regra antiga, mas essas têm
   sua PRÓPRIA versão nova com ID no seletor (empata em especificidade,
   ganha por ter mais classes) — só o vídeo tinha esse buraco. */
#preview-foto {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 10px;
  background: var(--bg);
  display: block;
}
#preview-foto-vazio {
  width: 100%;
  aspect-ratio: 3 / 4;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12px;
  border-radius: 10px;
  background: var(--bg);
  color: var(--text-muted);
  font-size: 13px;
}
#cropper-canvas {
  max-width: 100%;
  height: auto;
  border-radius: 10px;
  background: var(--bg);
  display: block;
}
.webcam-controles { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }

button {
  padding: 9px 16px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  /* Ícones + animação em todos os botões (pedido do usuário em
     19/08/2026: "quero que implemente ícones e botões animados seguindo
     a lógica do programa") — display:inline-flex/gap acomoda o <svg> do
     ícone (ver frontend/js/icones.js) sem afetar botões só-texto; a
     transição de leve "levantada" no hover + "afundada" no clique é a
     mesma em TODO botão do app, sem precisar repetir regra por tela. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  transition: background .15s ease, border-color .15s ease, transform .12s ease, box-shadow .15s ease;
}
button:hover { background: var(--border); transform: translateY(-1px); }
button:active:not(:disabled) { transform: translateY(0) scale(.96); }
/* color: var(--primary-fg), não branco fixo (achado ao comparar com a
   referência visual em 17/08/2026) — algumas paletas de destaque (verde/
   amber/ciano) são claras, e texto branco fica ilegível em cima delas. */
/* Brilho permanente no botão primário (pedido do usuário em 27/08/2026:
   "futurista e bonito") — antes só acendia no hover; agora fica sempre
   com um brilho leve na cor de destaque (qualquer uma das 6), acentua
   mais forte no hover. */
button.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); box-shadow: 0 2px 14px color-mix(in srgb, var(--primary) 30%, transparent); }
button.primary:hover { background: var(--primary-hover); box-shadow: 0 4px 20px color-mix(in srgb, var(--primary) 45%, transparent); }
button.danger { background: var(--red-bg); color: var(--red-text); border-color: var(--danger); }
button.danger:hover { background: color-mix(in srgb, var(--danger) 26%, var(--card-bg)); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button:disabled:hover { transform: none; box-shadow: none; }
/* Ícone dentro do botão acompanha a animação com um leve zoom próprio —
   dá a sensação de "resposta" mesmo em botões cujo texto não muda. Ver
   frontend/js/icones.js pro desenho de cada ícone (o <svg> não tem
   width/height próprios, só viewBox — o tamanho de exibição é sempre
   definido aqui, por CSS). */
button svg { width: 15px; height: 15px; flex-shrink: 0; transition: transform .15s ease; }
/* Botões compactos (linha de equipamento no PessoaCard, ações de card)
   usam ícone um pouco menor, proporcional ao texto de 12px deles. */
.pessoa-card-remover-equip svg, .pessoa-card-remover-todos svg, .pessoa-card-tornar-colaborador svg,
.unidade-card-acoes button svg, .bloco-acoes button svg { width: 12px; height: 12px; }
button:hover svg { transform: scale(1.12); }
button:active:not(:disabled) svg { transform: scale(0.9); }
@media (prefers-reduced-motion: reduce) {
  button, button svg { transition: none; }
  button:hover, button:active:not(:disabled) { transform: none; }
}

/* Botão "Salvar cadastro" — classe já existia no HTML sem regra própria
   (achado ao comparar com a referência visual em 17/08/2026). */
.save-btn {
  width: 100%;
  padding: 12px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 25%, transparent);
  transition: 0.16s ease;
}
.save-btn:hover { transform: translateY(-1px); filter: brightness(1.07); }

.upload-label {
  padding: 9px 16px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}
.upload-label:hover { background: var(--border); }

.hidden { display: none !important; }

/* 🐛 Achado em 03/09/2026 (relatado pelo usuário: "quando um operador
   com acesso limitado esta logado, ele ao mudar de função na aba de
   navegação lateral exibe todas as funções por um instante e some" —
   ex.: trocar de Cadastro pra Histórico mostrava Configurações e
   Blocos por um instante mesmo pra quem não tem permissão nenhuma
   delas). Causa: Auth._aplicarOcultacaoPorPermissao só decide o que
   esconder DEPOIS de confirmar a sessão (GET /api/auth/me, sempre
   assíncrono) — até a resposta voltar, o HTML estático de CADA página
   (com os 4 itens do menu sempre presentes, ver frontend/*.html) já
   tinha sido pintado 100% visível pelo navegador. Como este é um app
   multipágina (cada clique no menu é uma navegação de verdade, recarrega
   o HTML do zero), essa corrida acontecia em TODA troca de página, não
   só no primeiro carregamento. Corrigido invertendo a ordem: esconder
   por padrão via CSS (aplica antes de qualquer JS rodar, então o
   navegador nunca chega a pintar um frame com isso visível) — Auth.js
   (_aplicarOcultacaoPorPermissao) passou a só REVELAR
   (visibility:visible) o que o operador realmente pode ver, em vez de
   "mostra tudo, esconde o que não pode" depois. */
[data-permissao], [data-somente-admin-programa], [data-somente-sem-escopo-unidade] { visibility: hidden; }

.cropper-overlay {
  position: fixed; inset: 0; background: rgba(4, 6, 12, 0.72);
  display: flex; align-items: center; justify-content: center; z-index: 100;
}
.cropper-box { background: var(--card-bg); border: 1px solid var(--border); border-radius: 14px; padding: 16px; max-width: min(90vw, 640px); }
.cropper-box canvas { max-width: 100%; cursor: crosshair; border-radius: 8px; }
.cropper-acoes { display: flex; gap: 8px; margin-top: 12px; justify-content: flex-end; }

.painel-vazio { color: var(--text-muted); font-size: 12px; padding: 6px 0; }
/* Layout compacto e vertical (pedido do usuário em 14/08/2026: caixa
   simétrica com câmera/foto — bem mais estreita que o card cheio de
   antes) — foto pequena centralizada, nome, barra, e os 3 botões reais
   (Confirmar/Liberar/Negar) como ícones numa linha, não texto completo,
   pra caber na largura da caixa sem perder nenhuma função. */
/* Grade FIXA de 4 colunas (pedido do usuário em 20/08/2026, terceira
   rodada de ajuste no mesmo dia — histórico das duas tentativas
   anteriores, se precisar: 1) foto esticava pra ocupar o card inteiro,
   grande demais com 1 correspondência só; 2) foto de 34px circular
   (tamanho do histórico), pequena demais. Pedido final: "deixe maior de
   forma que caiba 4 fotos dentro da caixa... um do lado do outro com a
   barra de porcentagem menor dividindo a caixa em 4 fotos") — SEMPRE 4
   colunas iguais (não "auto-fit" adaptável como antes: 1 correspondência
   ocupa só 1/4 da largura, não estica sozinha), foto quadrada ocupando a
   coluna inteira, nome/barra/ações compactos embaixo. max_rostos_por_frame
   já tem teto de 4 (ver configuracoes_service.py) — a grade não fica sem
   uso mesmo se vier menos que 4 de uma vez. */
.painel-matches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; align-content: start; }
.painel-match {
  display: flex; flex-direction: column; align-items: center; gap: 3px; text-align: center;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 5px;
  min-width: 0;
}
.painel-match img, .painel-match .avatar-vazio { width: 100%; aspect-ratio: 1 / 1; height: auto; border-radius: 6px; object-fit: cover; background: var(--card-bg); }
.painel-match-info { width: 100%; min-width: 0; }
.painel-match-info > div:first-child { font-size: 10px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Barra de porcentagem MENOR aqui dentro (pedido do usuário) — mais
   fina e com menos respiro que a versão padrão (usada também no popup
   de duplicidade facial, que não muda). */
.painel-match .barra-similaridade-wrap { gap: 3px; margin: 3px 0 2px; }
.painel-match .barra-similaridade { height: 4px; }
.painel-match .badge-similaridade { font-size: 8.5px; }
/* Botões de ação redondos e coloridos por significado (pedido do usuário
   em 19/08/2026: "esses ícones estão feio e eu quero com mais
   animações") — antes eram os glifos ✓/⚡/✕ crus, os 3 na mesma cor
   genérica. Agora cada ação tem sua própria cor (verde=confirmar,
   destaque=liberar rápido, vermelho=negar), então dá pra diferenciar as
   3 só pela cor, sem precisar ler o ícone — e cada uma "acende" mais
   forte no hover, com um anel de brilho na própria cor. Menores aqui
   dentro (coluna estreita, 1/4 da caixa) — mesma cor/lógica, só o
   tamanho encolhe. */
.painel-match .acoes { display: flex; gap: 3px; margin-top: 2px; justify-content: center; flex-wrap: wrap; }
.painel-match .acoes .match-acao {
  width: 22px; height: 22px; padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.painel-match .acoes .match-acao svg { width: 11px; height: 11px; }
.match-confirmar { background: var(--green-bg); color: var(--green-text); }
.match-confirmar:hover { background: var(--success); color: #fff; box-shadow: 0 0 0 5px color-mix(in srgb, var(--success) 22%, transparent); }
.match-liberar { background: var(--accent-dim); color: var(--accent-text); }
.match-liberar:hover { background: var(--primary); color: var(--primary-fg); box-shadow: 0 0 0 5px color-mix(in srgb, var(--primary) 22%, transparent); }
/* O raio "treme" sozinho, baixinho, sugerindo energia/rapidez — só ele,
   os outros dois ficam parados até o operador interagir. */
.match-liberar svg { animation: raio-tremeluz 1.6s ease-in-out infinite; }
.match-liberar:hover svg { animation-play-state: paused; }
.match-negar { background: var(--red-bg); color: var(--red-text); }
.match-negar:hover { background: var(--danger); color: #fff; box-shadow: 0 0 0 5px color-mix(in srgb, var(--danger) 22%, transparent); }
@keyframes raio-tremeluz {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}
@media (prefers-reduced-motion: reduce) {
  .match-liberar svg { animation: none; }
}
.badge-similaridade { font-size: 10.5px; color: var(--text-muted); white-space: nowrap; }

.barra-similaridade-wrap { display: flex; align-items: center; gap: 8px; margin: 6px 0 4px; }
.barra-similaridade { flex: 1; height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.barra-similaridade-fill { height: 100%; border-radius: 999px; width: 0%; transition: width 0.3s ease, background-color 0.3s ease; }

/* Barra de progresso do "🔄 Sincronizar" por equipamento (pedido do
   usuário em 28/08/2026: "adicione um barrinha de porcentagem de
   sincronizar") — reaproveita .barra-similaridade acima (mesmo
   componente visual de "Cadastro em massa"), só com uma largura fixa
   pra caber ao lado dos botões na linha da tabela, sem esticar a coluna
   inteira. */
.sync-progresso { display: flex; align-items: center; gap: 6px; margin-top: 6px; width: 100%; max-width: 260px; }
.sync-progresso .barra-similaridade { height: 6px; }
.sync-progresso-texto { font-size: 11px; color: var(--text-muted); white-space: nowrap; min-width: 30px; text-align: right; }
/* Botão "Parar" (pedido do usuário em 28/08/2026: "quero que nesse
   processos mais longos, tenha um botao de parar") — pequeno, ao lado
   da barra, cor de perigo discreta (pra ficar claro que interrompe algo,
   sem chamar tanta atenção quanto um botão .danger cheio). */
.sync-progresso-parar {
  flex-shrink: 0; padding: 2px 8px; font-size: 10.5px;
  color: var(--red-text); background: var(--red-bg); border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
}
.sync-progresso-parar:hover { background: color-mix(in srgb, var(--danger) 22%, var(--card-bg)); }

.lista-equipamentos-check { display: flex; flex-direction: column; gap: 6px; }
.lista-equipamentos-check label { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--text); font-weight: 400; }
.lista-equipamentos-check input { width: auto; }

/* Popup de conflito de bloco (pedido do usuário em 20/08/2026) — uma
   linha por bloco conflitante, mesmo espírito visual do
   .lista-equipamentos-check acima, só com um subtítulo pequeno abaixo
   do nome do bloco explicando o que a checkbox faz. */
.conflito-bloco-item {
  display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2);
  cursor: pointer;
}
.conflito-bloco-item input { width: auto; margin-top: 3px; flex-shrink: 0; }

#status-salvar { margin-top: 10px; font-size: 13px; }
/* Entrada suave nas mensagens de status (pedido do usuário em
   27/08/2026: "efeitos") — toda confirmação/erro do programa passa por
   .ok/.erro (dezenas de telas), então esta única regra já poli tudo de
   uma vez, sem precisar mexer tela por tela. Sutil de propósito — só um
   leve "chegar" de baixo, sem quicar nem exagerar (pedido explícito:
   "nada muito estravagante"). */
/* Sem opacidade na animação de propósito (mesmo achado do .card acima)
   — uma mensagem de sucesso/erro precisa estar visível na hora, nunca
   correr risco de ficar "presa" invisível numa aba em segundo plano. */
.ok, .erro, .aviso { display: inline-block; animation: status-entrada .25s ease both; }
@keyframes status-entrada {
  from { transform: translateY(3px); }
  to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .ok, .erro, .aviso { animation: none; }
}
.ok { color: var(--success); }
.erro { color: var(--danger); }
/* .aviso já existia como classe solta em historico.js (sem cor própria,
   caía no texto padrão) — ganhou cor de verdade em 28/08/2026 de
   passagem, ao precisar do mesmo "amarelo de atenção" pra pré-
   visualização pendente do logo (ver logo.js), consistente com o mesmo
   tom já usado em .toast-aviso/.resultado-equip.status-pendente. */
.aviso { color: var(--warning); }

/* Resultado de sincronização por equipamento (pedido do usuário em
   28/08/2026: "deixe essa menssagem mais bonita" — sobre uma linha de
   erro que aparecia como texto cru "✗ Facial externo: Esta foto já
   está associada a..."). Ver renderizarLinhaResultadoEquipamento em
   frontend/js/api.js — usada por cadastro.js e facial.js. Estados
   simples (sincronizado/pendente/removido) ficam uma linha compacta com
   ícone; erro ganha um cartão (cabeçalho + corpo), porque a mensagem
   pode ser mais longa e às vezes vem com um popup de ação logo abaixo
   (.popup-conflito-duplicidade, ver api.js) — os dois precisam ler como
   UM bloco só, não dois soltos. */
.resultado-equip {
  display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; padding: 3px 0;
  animation: status-entrada .25s ease both;
}
.resultado-equip svg { width: 15px; height: 15px; flex-shrink: 0; }
.resultado-equip.status-ok { color: var(--success); }
.resultado-equip.status-pendente { color: var(--warning); }
.resultado-equip.status-neutro { color: var(--text-muted); }
/* v1.5.47 (pedido do usuário em 15/09/2026: "permita que os cadastros sejam
   feitos nas unidades mesmo sem equipamento associado ao bloco") — o aviso
   "bloco sem equipamento" usa este MESMO visual de vínculo pendente (ver
   renderizarAvisoSemEquipamento em frontend/js/api.js), só que a frase é
   longa (explica o que fazer), então o ícone fica alinhado ao topo em vez de
   ao centro de um parágrafo de 2-3 linhas. */
.resultado-equip.multilinha { align-items: flex-start; }
.resultado-equip.multilinha svg { margin-top: 2px; }
.resultado-equip.multilinha span { line-height: 1.45; }
/* Achado na bancada em 15/09/2026, montando o resumo da sincronização da
   unidade/bloco: um item de flex não encolhe abaixo do próprio conteúdo por
   padrão (min-width:auto), então a frase vazava para fora do balão em vez de
   quebrar linha. Vale pra todas as linhas .resultado-equip — as antigas eram
   curtas e nunca esbarraram nisso. */
.resultado-equip span { min-width: 0; overflow-wrap: break-word; }

.resultado-equip-erro {
  display: flex; flex-direction: column; gap: 4px;
  padding: 8px 10px; margin-top: 3px;
  background: color-mix(in srgb, var(--danger) 8%, var(--card-bg));
  border: 1px solid color-mix(in srgb, var(--danger) 30%, var(--border));
  border-radius: 9px;
  animation: status-entrada .25s ease both;
}
.resultado-equip-erro-cabecalho {
  display: flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 700; color: var(--danger);
}
.resultado-equip-erro-cabecalho svg { width: 15px; height: 15px; flex-shrink: 0; }
/* overflow-wrap/word-break (🐛 pedido do usuário em 28/08/2026, com
   print mostrando a câmera de cadastro quase sumindo): a mensagem crua
   de "equipamento inacessível" (ver app/adapters/intelbras.py) embute o
   erro de rede por baixo — trechos como "HTTPConnectionPool(host=
   '192.168.3.243'," ou "0x201742e2e00>," não têm NENHUM espaço, e sem
   isto aqui um <p> comum não consegue quebrar linha no meio de um token
   assim: a caixa esticava horizontalmente e empurrava o resto do
   layout (inclusive a câmera ao vivo). Ver também resumirErro/Balao em
   api.js, que corta o texto ANTES de renderizar — isto aqui é a segunda
   camada de segurança, pra nenhum texto futuro (resumido ou não)
   conseguir repetir o mesmo estouro. */
.resultado-equip-erro-texto {
  margin: 0; font-size: 12.5px; color: var(--text); line-height: 1.45;
  overflow-wrap: anywhere; word-break: break-word;
}
/* Botão "detalhes" (ver resumirErro/Balao em api.js) — inline, discreto,
   parece um link em vez de um botão cheio (ele mora dentro do mesmo <p>
   do resumo curto, não é uma ação separada de destaque). */
.resultado-equip-erro-detalhes-btn {
  background: none; border: none; padding: 0; margin-left: 2px;
  font: inherit; font-size: 11.5px; font-weight: 700; color: var(--danger);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.resultado-equip-erro-detalhes-btn:hover { color: var(--text); }

/* Balão de detalhe técnico (pedido do usuário em 28/08/2026: "torne
   esses tipos de avisos como popup balão discreto mas não no meio da
   tela e sim próximo da função... não pode atrapalhar o funcionamento") —
   diferente do #toast-container acima (canto fixo da tela,
   autodestrutivo): este balão fica ANCORADO perto de quem o abriu
   (posição calculada em JS a partir do getBoundingClientRect do próprio
   botão "detalhes", ver Balao em api.js) e só fecha por ação do
   operador. position:fixed por definição fica FORA do fluxo normal do
   layout — nenhum texto por dentro dele, não importa o tamanho,
   consegue empurrar mais nada ao redor (era exatamente essa a garantia
   que faltava: o texto cru antes ficava dentro do fluxo normal da
   tela). max-height+overflow-y é a mesma ideia de segurança, agora na
   vertical: mesmo um erro absurdamente longo fica com scroll próprio em
   vez de crescer sem limite. */
.balao-detalhe {
  position: fixed; z-index: 300;
  display: flex; align-items: flex-start; gap: 8px;
  background: var(--card-bg); border: 1px solid var(--danger); border-radius: 10px;
  box-shadow: var(--shadow); padding: 10px 12px;
  animation: toast-entrada .15s ease-out;
}
.balao-detalhe p {
  margin: 0; flex: 1; font-size: 12px; line-height: 1.45; color: var(--text);
  overflow-wrap: anywhere; word-break: break-word;
  max-height: 220px; overflow-y: auto;
}
.balao-detalhe button {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 13px; padding: 0; line-height: 1; flex-shrink: 0;
}
.balao-detalhe button:hover { color: var(--text); }

/* Painel de resultado de Salvar cadastro/Liberar acesso (pedido do
   usuário em 28/08/2026: "essas mensagens precisam sumir depois de um
   tempo, tem que ficar dentro de um balão popup" — antes ficavam presas
   pra sempre num <div id="status-salvar"> fixo no meio do layout, ver
   mostrarResultadoAcao em frontend/js/api.js). Mesma família visual do
   .balao-detalhe acima (fixed, fora do fluxo, nunca empurra o resto da
   tela), mas borda neutra (não é sempre um aviso de erro) e mais largo
   (guarda o resultado inteiro — cabeçalho + uma linha por equipamento —
   não só uma frase técnica). max-height+overflow-y: mesma segurança de
   sempre, mesmo com muitos equipamentos na lista. */
.balao-resultado {
  position: fixed; z-index: 300;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: var(--shadow); padding: 12px 26px 10px 14px;
  max-height: 70vh; overflow-y: auto;
  animation: toast-entrada .15s ease-out;
}
.balao-resultado-fechar {
  position: absolute; top: 8px; right: 8px;
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 13px; padding: 2px; line-height: 1;
}
.balao-resultado-fechar:hover { color: var(--text); }
.balao-resultado-corpo { font-size: 12.5px; }
/* Uma "seção" por chamada de mostrarResultadoAcao quando ACUMULA em vez
   de substituir (pedido do usuário em 05/09/2026: operador com permissão
   pra cadastrar em mais de um bloco, cadastrando a MESMA pessoa em
   blocos diferentes um de cada vez — o balão do bloco 1 não pode
   desaparecer quando o bloco 2 é salvo) — ver mostrarResultadoAcao em
   frontend/js/api.js. Só a partir da 2ª seção ganha a linha divisória;
   a 1ª fica exatamente como sempre foi (sem chaveAcumular, só existe 1
   seção, então esta regra nunca se aplica). */
.balao-resultado-secao + .balao-resultado-secao {
  border-top: 1px dashed var(--border); margin-top: 8px; padding-top: 8px;
}

/* Aviso "Editando {nome}"/"Já liberado" — passou por balão flutuante
   (pedido do usuário em 28/08/2026: "deixe ela como um balão popup em
   baixo de atualizar cadastro") mas um position:fixed calculado em JS
   saía do lugar toda vez que a janela mudava de tamanho, sem
   recalcular ("esta saindo da posição conforme a resolução muda").
   Pedido seguinte: "no lugar dele coloque um botao em baixo de salvar"
   — de volta ao fluxo NORMAL da página (embaixo de #btn-salvar, ver
   index.html), sem position/cálculo nenhum — nunca mais sai do lugar,
   porque flui sozinho junto com o resto do formulário, garantido pela
   própria natureza do fluxo normal (não por nenhuma lógica de
   reposicionamento que precisaria escutar resize). */
.aviso-editando-linha {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 8px; font-size: 12.5px; font-weight: 600; color: var(--accent-text);
}
@media (prefers-reduced-motion: reduce) {
  .resultado-equip, .resultado-equip-erro, .balao-detalhe, .balao-resultado { animation: none; }
}

.log-lateral { display: flex; flex-direction: column; min-height: 0; position: relative; }
.log-scroll, .log-lateral > .log-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.evento-item {
  display: flex; gap: 10px; align-items: center; padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.evento-item:last-child { border-bottom: none; }
.evento-item img, .evento-item .avatar-vazio, .avatar {
  width: 34px; height: 34px; border-radius: 50%; object-fit: cover; background: var(--surface-2); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 10.5px; color: var(--text-muted);
  border: 1px solid var(--border); font-weight: 600;
}
/* Snapshot da câmera do equipamento (pessoa não cadastrada) diferenciado
   visualmente da foto de um cadastro de verdade — achado ao comparar com
   a referência visual em 17/08/2026. Ver eventos.js. */
.evento-item img.cam-shot, .avatar.cam-shot { background: var(--amber-bg); border-color: var(--warning); color: var(--amber-text); }
.evento-item .info { flex: 1; min-width: 0; }
.evento-item .nome { font-size: 13.5px; font-weight: 600; }
.evento-item .meta { font-size: 11.5px; color: var(--text-muted); margin-top: 2px; }
.evento-item.nao-identificado .nome { color: var(--text-muted); font-style: italic; font-weight: 500; }
.evento-item.clicavel { cursor: pointer; border-radius: 8px; margin: 0 -8px; padding: 8px; }
/* Realce na cor de destaque em vez de cinza neutro (achado ao comparar
   com a referência visual em 17/08/2026). */
.evento-item.clicavel:hover { background: color-mix(in srgb, var(--primary) 12%, transparent); }
/* Foto clicável abre o popup discreto de imagem (pedido do usuário em
   17/08/2026) — separado do clique na linha, que abre o popup grande de
   detalhe (ver imagem-popup.js). */
.evento-item img { cursor: pointer; }

/* Sinalização no ícone da lista de Ativos (pedido do usuário em
   28/08/2026: "quero que no icone desse cadastro seja exibido algum
   tipo de sinalização" — Fila de espera do facial; depois estendido:
   "os cadastros que estao agendados... tbm ter uma sinalização") — um
   ponto pequeno ancorado no canto do avatar (não mais um badge de texto
   solto embaixo, que já existe pra outra coisa — até HH:MM/permanente/
   expira em Xmin). .avatar-wrap só existe pra dar um position:relative
   ao redor da foto/avatar-vazio sem mudar o tamanho deles (34px
   continua vindo de .evento-item img/.avatar-vazio acima). Anel na cor
   do CARD por trás (não branco fixo) pra separar visualmente do avatar
   em qualquer tema. Ver Ativos._linha em frontend/js/ativos.js. */
.avatar-wrap { position: relative; flex-shrink: 0; display: inline-flex; }
.avatar-sinal {
  position: absolute; bottom: -1px; right: -1px;
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--card-bg);
}
/* Âmbar — mesma cor já usada pro badge da aba Fila (.badge.soon) —
   consistência: é o mesmo estado, só que sinalizado no ícone agora. */
.avatar-sinal.sinal-fila { background: var(--warning); }
/* Neutro — "agendado" não é um problema, é só uma questão de tempo (o
   próprio job de ativação cuida sozinho quando chegar a hora); âmbar
   aqui competiria visualmente com o sinal de fila acima, que é o que
   realmente pede atenção. */
.avatar-sinal.sinal-agendado { background: var(--text-muted); }
/* Verde — sincronizado em todos os equipamentos, sem pendência nenhuma
   (pedido do usuário em 28/08/2026: "quero mais uma bolinha verde para
   quando estiver em todos os faciais") — mesmo tom de .badge.ok/
   .resultado-equip.status-ok, pra "tudo certo" ler igual em qualquer
   lugar do programa. */
.avatar-sinal.sinal-sincronizado { background: var(--success); }

/* Detalhamento por equipamento (pedido do usuário em 05/09/2026: "a
   bolinha de status precisa mostar os equipamentos dentro do card, por
   exemplo catraca 1 como o status verde mas a catraca 2 amarelo pq
   ainda nao esta no facial") — em vez da bolinha única acima (que só
   sabia dizer "tem pendência" ou "tudo sincronizado", nunca QUAL
   equipamento), este grupo mostra 1 bolinha pequena por equipamento,
   ainda ancorado no mesmo cantinho de sempre — ver
   Ativos._sinalPorEquipamento em frontend/js/ativos.js (que decide
   quando usar este grupo vs. a bolinha única acima, mantida como
   fallback pra cadastros de antes desta feature). Bolinhas menores que
   .avatar-sinal (8px, não 12px) pra 2-3 caberem lado a lado no mesmo
   espaço de sempre sem estourar visualmente o avatar de 34px. */
.avatar-sinal-grupo {
  position: absolute; bottom: -1px; right: -1px;
  display: flex; gap: 2px;
}
.avatar-sinal-equip {
  width: 8px; height: 8px; border-radius: 50%;
  border: 1.5px solid var(--card-bg);
  flex-shrink: 0;
}
/* Mesmas 3 cores semânticas de sempre (.avatar-sinal acima/.status-pill
   em style.css) — verde/âmbar/vermelho, nunca uma paleta nova só pra
   este grupo. */
.avatar-sinal-equip.sincronizado { background: var(--success); }
.avatar-sinal-equip.pendente { background: var(--warning); }
.avatar-sinal-equip.erro { background: var(--danger); }

/* Legendas explicativas longas escondidas em botão (pedido do usuário
   em 28/08/2026: "essas legendas explicativas... deixe todas elas
   ocultas em botõezinhos pequeno de detalhe") — abre o Balao (ver
   api.js) com o texto completo, ancorado perto do próprio botão. Ícone
   sozinho, sem fundo/borda — discreto de propósito, é só um "ⓘ" ao
   lado do campo/rótulo a que se refere, não uma ação de destaque. */
.legenda-info-btn {
  background: none; border: none; padding: 0; margin-left: 4px;
  color: var(--text-muted); cursor: pointer; line-height: 1;
  display: inline-flex; vertical-align: middle;
}
.legenda-info-btn:hover { color: var(--primary); }
.legenda-info-btn svg { width: 14px; height: 14px; }

/* Badge de método/resultado do evento (Facial/Negado) — pedido do
   usuário em 14/08/2026, ao pedir que o programa siga o mockup_v6.html
   fornecido (ele já mostrava esses badges no log de acessos). O dado
   (eventos_acesso.status: 0=falha, 1=sucesso) já existia e já era
   devolvido pela API — só não tinha representação visual até agora. */
.badge { display: inline-block; font-size: 9px; padding: 2px 6px; border-radius: 5px; margin-top: 3px; font-weight: 600; }
.badge.face { background: var(--green-bg); color: var(--green-text); }
.badge.negado { background: var(--red-bg); color: var(--red-text); }
.badge.manual { background: var(--amber-bg); color: var(--amber-text); }
.badge.soon { background: var(--amber-bg); color: var(--amber-text); }
.badge.ok { background: var(--surface-2); color: var(--text-muted); }
.badge.exp { background: var(--red-bg); color: var(--red-text); }
/* v1.5.47 — a linha da lista de Ativos passou a poder ter DOIS selos lado a
   lado (a validade + "sem equipamento", ver Ativos._linha): sem isto eles
   ficavam colados um no outro, parecendo um texto só. */
.badge + .badge { margin-left: 5px; }

#filtro-eventos, #filtro-ativos, #filtro-blocos, #filtro-unidades { font-size: 13px; margin-bottom: 10px; }

/* ===== Painel "Ativos e expirando" (pedido do usuário em 14/08/2026, ao
   pedir que o programa siga o mockup_v6.html fornecido) — não existia
   antes. Ver frontend/js/ativos.js e GET /api/cadastros/status_geral. */
.seg-control { display: flex; gap: 3px; background: var(--surface-2); border-radius: 8px; padding: 3px; margin-bottom: 10px; flex-shrink: 0; }
.seg-btn { flex: 1; background: transparent; border: none; color: var(--text-muted); font-size: 9.5px; padding: 7px 2px; border-radius: 6px; cursor: pointer; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; }
.seg-btn:hover { background: var(--border); }
.seg-btn.active { background: var(--primary); color: var(--primary-fg); }

/* Abas de categoria da tela de Configurações (pedido do usuário em
   04/09/2026 — ver ConfigAbas em frontend/js/equipamentos.js). Mesmo
   componente .seg-control/.seg-btn de sempre, só que com 8 itens em vez
   de 2-3 — precisa de um flex-basis mínimo pra não espremer demais os
   rótulos mais longos ("Cadastro e Regras de Acesso") e quebrar em mais
   de uma linha em telas estreitas, em vez de continuar tentando caber
   tudo numa linha só cada vez mais fina. */
#config-abas .seg-btn { flex: 1 1 150px; font-size: 11px; padding: 9px 8px; }
.ativos-panel { display: none; flex-direction: column; flex: 1; min-height: 0; position: relative; }
.ativos-panel.active { display: flex; }
/* Âncora do popup discreto de imagem (busca por nome adicionada em
   17/08/2026: "acesso ativo precisa... ter uma busca por nome"). */
.ativos-card { position: relative; }

.status-pill { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
.status-pill.online { background: var(--green-bg); color: var(--green-text); }
.status-pill.offline { background: var(--red-bg); color: var(--red-text); }
.status-pill.nao_configurado { background: var(--surface-2); color: var(--text-muted); }
/* v1.5.51 — equipamento sem login liberado (nada automático até "Logar"). */
.status-pill.sem_login { background: var(--amber-bg, #fff4d6); color: var(--amber-text, #8a5a00); }
.equip-login-aviso { font-size: 11.5px; color: var(--amber-text, #8a5a00); margin-top: 2px; }
.status-pill.pendente { background: var(--amber-bg); color: var(--amber-text); }
.status-pill.pendente_ativacao { background: var(--amber-bg); color: var(--amber-text); }
.status-pill.sincronizado { background: var(--green-bg); color: var(--green-text); }
.status-pill.erro { background: var(--red-bg); color: var(--red-text); }
.status-pill.erro_remocao { background: var(--red-bg); color: var(--red-text); }
.status-pill.erro_duplicado { background: var(--red-bg); color: var(--red-text); }
.status-pill.removido { background: var(--surface-2); color: var(--text-muted); }
/* v1.5.95 — confirmado por OUTRA máquina (credenciais_nuvem_espelho), nunca
   verificado por aqui: nem verde (não é "sincronizado" por este computador)
   nem âmbar/vermelho (não é pendência/erro daqui) — neutro de propósito. */
.status-pill.confirmado_outra_maquina { background: var(--surface-2); color: var(--text-muted); }

/* Achado em bancada em 17/08/2026: a tabela de equipamentos (nome +
   marca + 2 status + 4 botões de ação) ficava mais larga que o card,
   quebrando a borda arredondada e empurrando os botões pra fora da
   caixa ("os botões saem da caixa"). Toda tabela agora rola por dentro
   de si mesma (na horizontal) em vez de estourar o card. */
.table-scroll { overflow-x: auto; }
/* Duas listas de backup lado a lado, cada uma com altura FIXA e rolagem
   vertical própria (pedido do usuário em 19/08/2026: "quero que deixe
   com dois blocos separados... de forma que role na mesma caixa sem
   alterar o tamanho") — sem isso a lista de backups crescia pra sempre
   conforme se acumulavam (regra/backup automático "antes de restaurar"
   incluído). Ver frontend/equipamentos.html e frontend/js/backup.js. */
.backup-listas-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
/* min-width:0 é o que faz a coluna "1fr" respeitar a largura da grade —
   por padrão, um item de grid tem min-width:auto, ou seja, o próprio
   navegador nunca deixa a coluna encolher além da largura "natural" do
   conteúdo (o nome mais comprido do arquivo .zip + data + tamanho + os 2
   botões, tudo numa linha só). Sem isso, com backups de nome longo a
   grade toda estourava pra fora da caixa (bug relatado pelo usuário em
   20/08/2026, com print: a lista da direita ficava empurrada pra fora da
   tela e a barra de rolagem da esquerda aparecia solta/desalinhada) — a
   caixa achava que tinha 1230px de conteúdo dentro de um espaço de
   457px. Com min-width:0 a coluna aceita encolher e quem assume rolar
   o excesso é o .table-scroll (overflow-x:auto) de cada caixa,
   isoladamente — exatamente o "role na mesma caixa sem alterar o
   tamanho" pedido. */
.backup-listas-grid > div { min-width: 0; }
.backup-lista-titulo { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--text-muted); }
.backup-lista-scroll { max-height: 260px; overflow-y: auto; border: 1px solid var(--border); border-radius: 10px; }
@media (max-width: 720px) {
  .backup-listas-grid { grid-template-columns: 1fr; }
}
table { width: 100%; border-collapse: collapse; }
table th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
table td { padding: 10px; border-bottom: 1px solid var(--border); font-size: 13px; vertical-align: middle; }
table tr:last-child td { border-bottom: none; }
.td-sub { color: var(--text-muted); font-size: 10.5px; }

/* Slider nativo (pedido do usuário em 17/08/2026: sensibilidades da
   busca facial viram barra de slide em vez de campo numérico) — sem
   isto o navegador desenha um slider genérico que não combina com o
   resto da interface.

   Trilha preenchida + polimento visual (pedido do usuário em 27/08/2026:
   "moderno e elegante... sliders") — a trilha até o thumb vem pintada
   com a cor de destaque via var(--slider-fill), um gradiente de 2 cores
   com o ponto de corte na posição atual do valor. --slider-fill é uma
   PORCENTAGEM ("42%") mantida em sincronia pelo JS (ver
   frontend/js/icones.js::_wireSliders — genérico, funciona pra
   QUALQUER <input type="range"> da tela, atual ou futuro, sem precisar
   de código extra por tela); 50% é só o valor de fallback antes do JS
   rodar pela primeira vez. */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--primary) var(--slider-fill, 50%),
    var(--surface-2) var(--slider-fill, 50%)
  );
  outline: none;
  cursor: pointer;
  margin: 10px 0;
  transition: box-shadow .15s ease;
}
input[type="range"]:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 25%, transparent);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--primary);
  cursor: pointer;
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .25), 0 0 0 1px var(--primary);
  transition: transform .12s ease, box-shadow .15s ease;
  margin-top: 0;
}
input[type="range"]:hover::-webkit-slider-thumb {
  transform: scale(1.15);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .3), 0 0 0 5px color-mix(in srgb, var(--primary) 18%, transparent);
}
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.05);
}
input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--primary);
  cursor: pointer;
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .25), 0 0 0 1px var(--primary);
  transition: transform .12s ease, box-shadow .15s ease;
}
input[type="range"]:hover::-moz-range-thumb {
  transform: scale(1.15);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .3), 0 0 0 5px color-mix(in srgb, var(--primary) 18%, transparent);
}
input[type="range"]::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: transparent;
}
@media (prefers-reduced-motion: reduce) {
  input[type="range"], input[type="range"]::-webkit-slider-thumb, input[type="range"]::-moz-range-thumb {
    transition: none;
  }
}

/* Cards agrupados por categoria na aba Configurações (pedido do usuário
   em 17/08/2026: "o que faz parte de equipamentos permanece do lado de
   equipamentos, o que faz parte de estética visual fica do lado de
   estética visual, o que faz parte de banco de dados fica do lado de
   banco de dados... equipamentos em primeiro e estética visual em
   último") — 3 pares lado a lado (Novo equipamento+Equipamentos
   cadastrados, Busca facial+Liberação baseada, Local dos dados+Backup) e
   "Identidade visual" sozinha por último, sem par. align-items:start
   (não stretch) pra não forçar as duas caixas de um par a ficarem da
   mesma altura — cada categoria tem seu próprio tamanho natural. */
.config-grid-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; margin-bottom: 16px; }
.config-grid-2col > .card { margin-bottom: 0; min-width: 0; }
@media (max-width: 900px) {
  .config-grid-2col { grid-template-columns: 1fr; }
}

/* Tabela de equipamentos com altura própria — cabe ~10 linhas (pedido do
   usuário em 17/08/2026: "precisa ser maior para caber 10 equipamentos
   ... ao passar de 10 os excedentes vai ficar dentro da caixa com barra
   de rolagem"). Cabeçalho fixo (sticky) enquanto rola, pra não perder a
   referência de qual coluna é qual. Altura calculada a partir da altura
   real de uma linha (medida em bancada: ~71px com nome/direção
   quebrando em 2 linhas) × 10 + cabeçalho. */
.tabela-equipamentos-scroll { max-height: 745px; overflow-y: auto; }
.tabela-equipamentos-scroll thead th { position: sticky; top: 0; background: var(--card-bg); z-index: 1; }

/* Achado na auditoria de 05/09/2026 ("tabela de Histórico sem paginação,
   cabeçalho/coluna fixos") — mesmo padrão de .tabela-equipamentos-scroll
   acima, mais a 1ª coluna (foto+nome) fixa ao rolar horizontalmente
   também (a tabela tem 9 colunas, sem isso o nome some ao rolar pra ver
   o resto). Largura da coluna 1 (foto) fixada em 54px (34px avatar +
   10px de padding de cada lado) pra dar um `left` previsível à coluna 2. */
.historico-tabela-scroll { max-height: 700px; overflow-y: auto; }
.historico-tabela-scroll thead th { position: sticky; top: 0; background: var(--card-bg); z-index: 2; }
.historico-tabela th:nth-child(1), .historico-tabela td:nth-child(1) { width: 54px; }
.historico-tabela th:nth-child(1), .historico-tabela td:nth-child(1),
.historico-tabela th:nth-child(2), .historico-tabela td:nth-child(2) {
  position: sticky; background: var(--card-bg); z-index: 1;
}
.historico-tabela thead th:nth-child(1), .historico-tabela thead th:nth-child(2) { z-index: 3; }
.historico-tabela th:nth-child(1), .historico-tabela td:nth-child(1) { left: 0; }
.historico-tabela th:nth-child(2), .historico-tabela td:nth-child(2) { left: 54px; }

/* Cadastros e Blocos (pedido do usuário em 18/08/2026) — lista de blocos
   clicável (mesmo princípio do popup de busca: linha inteira reage ao
   clique), a selecionada fica destacada pra deixar claro qual bloco as
   unidades ao lado pertencem. */
.tabela-unidades-scroll { max-height: 520px; overflow-y: auto; }
.tabela-unidades-scroll a { color: var(--primary); text-decoration: none; font-weight: 600; }
.tabela-unidades-scroll a:hover { text-decoration: underline; }
/* v1.5.33 — blocos em cartões (frontend/blocos.html) — mesmo visual do
   .unidade-card; o cartão inteiro abre a página do bloco (/bloco?id=). */
.blocos-form { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.blocos-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.blocos-grid > p { grid-column: 1 / -1; }
.bloco-card { cursor: pointer; }
.bloco-item {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 12px; border-radius: 8px; cursor: pointer; font-size: 13.5px;
  border: 1px solid transparent; margin-bottom: 4px;
}
.bloco-item:hover { background: var(--surface-2); }
.bloco-item.selecionado { background: var(--accent-bg, var(--surface-2)); border-color: var(--accent, var(--border)); }
.bloco-item .bloco-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bloco-item .bloco-nome { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bloco-item .bloco-contagem { font-size: 11.5px; color: var(--text-muted); }
.bloco-item .bloco-acoes { display: flex; gap: 4px; flex-shrink: 0; }
.bloco-item .bloco-acoes button { padding: 3px 8px; font-size: 12px; }

/* Unidades em cards (pedido do usuário em 19/08/2026: "a exibição das
   unidades precisa ser em forma de card que terá quantos colaboradores
   quantos visitantes e quantos prestadores") — antes era uma tabela com
   só um total entre parênteses. */
.unidades-grid-scroll { max-height: 560px; overflow-y: auto; padding-right: 2px; }
.unidades-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
.unidade-card {
  display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px;
  background: var(--surface-2);
  /* Leve elevação ao passar o mouse (pedido do usuário em 27/08/2026:
     "moderno e elegante") — o card inteiro leva pra página da unidade
     (ver .unidade-card-nome a), então reagir ao hover no card inteiro,
     não só no link, deixa claro que é clicável. */
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.unidade-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--text) 8%, transparent);
  border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
}
@media (prefers-reduced-motion: reduce) {
  .unidade-card { transition: none; }
  .unidade-card:hover { transform: none; }
}
.unidade-card-nome { font-size: 14.5px; font-weight: 700; }
.unidade-card-nome a { color: var(--primary); text-decoration: none; }
.unidade-card-nome a:hover { text-decoration: underline; }
.unidade-card-contagens { display: flex; flex-direction: column; gap: 3px; font-size: 12px; }
.unidade-card-contagem-item { display: flex; justify-content: space-between; gap: 8px; color: var(--text-muted); }
.unidade-card-contagem-item strong { color: var(--text); font-weight: 600; }
.unidade-card-vazio { font-size: 12px; color: var(--text-muted); font-style: italic; }
.unidade-card-acoes { display: flex; gap: 6px; margin-top: 2px; }
.unidade-card-acoes button { flex: 1; padding: 4px 8px; font-size: 12px; }

/* 3 -> 4 colunas (pedido do usuário em 28/08/2026: "isso sobra espaço
   para deixar os cardes em sequências de 4 por linha", depois de o
   cartão virar compacto — ver opcoes.compacto em
   PessoaCard.renderizar/popup-pessoa.js) — history original de 24/08/2026
   abaixo. Só a ORGANIZAÇÃO no espaço muda aqui — nem a estrutura do
   PessoaCard nem o HTML que ele gera foram tocados. 4 colunas FIXAS
   (repeat(4, 1fr), diferente do auto-fill de .unidades-grid acima); as
   media queries abaixo caem pra 2 e depois 1 coluna em telas estreitas
   (mesmo espírito responsivo de .config-grid-2col/.backup-listas-grid
   neste arquivo), pra nunca cortar/espremer foto+nome demais.

   Comentário original (24/08/2026): "a exibiçaão dos cadastros dentro
   das unidades, ficam um do lado do outro em grupo de 3 na lista para
   economizar espaço" — antes cada .pessoa-card ficava empilhado
   sozinho, ocupando a largura toda do container. */
#unidade-lista-pessoas {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  align-items: start;
}
#unidade-lista-pessoas > .pessoa-card { margin-bottom: 0; min-width: 0; }
/* Achado na revisão adversarial de 25/08/2026: UnidadePagina._renderizarAbaAtual
   (frontend/js/unidade.js) injeta um <p> solto (mensagens tipo "Nenhuma
   pessoa cadastrada nesta unidade no momento."/"Nenhum(a) <categoria>
   nesta unidade.") dentro deste MESMO container quando a lista está
   vazia. Sem esta regra, esse <p> vira só mais um item do grid e fica
   confinado a 1 coluna (1/3 ou 1/2 da largura) em vez de ocupar a linha
   inteira como antes desta seção virar grid — caso comum (qualquer
   unidade/aba vazia), não uma borda rara. */
#unidade-lista-pessoas > p { grid-column: 1 / -1; }
/* Achado testando ao vivo (unidade.html com pessoas reais, 24/08/2026):
   a coluna de 3 acima fica mais estreita do que a linha de equipamento
   (nome + status + botões Liberar/Remover) precisa pra caber sem quebrar
   — aquela linha é gerada com style="" INLINE em
   PessoaCard.renderizar (frontend/js/popup-pessoa.js), sem flex-wrap e
   com o grupo de botões em flex-shrink:0, então ela estourava pra fora
   do cartão (cortada) em vez de acompanhar a coluna estreita. Como esta
   frente não pode mexer em popup-pessoa.js, o !important aqui vence só o
   style inline, e só DENTRO deste grid — o mesmo componente no popup de
   detalhe (fora deste container) continua com a linha numa coluna larga,
   sem precisar quebrar, e não é afetado por esta regra. */
#unidade-lista-pessoas .pessoa-card-equipamentos > div {
  flex-wrap: wrap !important;
  row-gap: 4px !important;
}
/* O grupo de botões (status + Liberar acesso + Remover), segundo <span>
   daquela mesma linha, também é flex (com flex-shrink:0 inline) e sozinho
   já passa dos ~253px úteis da coluna — quebrar só a linha de fora (regra
   acima) não bastava, esse grupo ainda estourava largo numa linha só.
   Aqui ele ganha o direito de quebrar (e de encolher) e fica alinhado à
   direita, mesmo com múltiplas linhas. */
#unidade-lista-pessoas .pessoa-card-equipamentos > div > span:last-child {
  flex-wrap: wrap !important;
  flex-shrink: 1 !important;
  justify-content: flex-end !important;
  row-gap: 4px !important;
}
/* Escala 4 -> 3 -> 2 -> 1 (degrau de 3 novo, container ficou mais largo
   — 1100px, ver unidade.html — pra não pular direto de 4 pra 2 numa
   tela intermediária). */
@media (max-width: 1000px) {
  #unidade-lista-pessoas { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 860px) {
  #unidade-lista-pessoas { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  #unidade-lista-pessoas { grid-template-columns: 1fr; }
}

/* Botão "Detalhes" do cartão compacto (pedido do usuário em 28/08/2026:
   "coloque um botao de detalhe e oculte as informações de cadastro
   dentro desse botao") — largura total do cartão (mais fácil de mirar
   com o mouse que um botãozinho pequeno) mas visual discreto (fundo
   transparente, borda tracejada) pra não competir com os botões de
   ação de verdade (Editar/Liberar/Remover etc., que só aparecem depois
   de expandir). Seta gira 180° ao abrir — mesma ideia de .toggle-btn
   da barra lateral. */
.pessoa-card-detalhe-toggle {
  width: 100%; justify-content: center; margin-top: 2px;
  background: transparent; border: 1px dashed var(--border); color: var(--text-muted);
  font-size: 11.5px; padding: 5px 8px;
}
.pessoa-card-detalhe-toggle:hover { background: var(--surface-2); color: var(--text); }
.pessoa-card-detalhe-toggle svg { transition: transform 0.2s ease; }
.pessoa-card-detalhe-toggle.aberto svg { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .pessoa-card-detalhe-toggle svg { transition: none; }
}
.pessoa-card-detalhe-corpo { margin-top: 10px; }

/* Resultado da busca de cadastro por nome/documento (pedido do usuário em
   19/08/2026, "seguindo a mesma linha de pesquisa do histórico de
   acesso") — cada resultado leva direto pro bloco/unidade da pessoa. */
.busca-cadastro-linha {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 8px; margin-bottom: 6px;
  text-decoration: none; color: inherit;
}
a.busca-cadastro-linha:hover { background: var(--surface-2); }
.busca-cadastro-linha.sem-destino { opacity: .75; cursor: default; }
.busca-cadastro-linha img, .busca-cadastro-linha .avatar-vazio {
  width: 32px; height: 32px; border-radius: 50%; object-fit: cover; background: var(--surface-2);
  border: 1px solid var(--border); flex-shrink: 0;
}
.busca-cadastro-linha .busca-cadastro-info { min-width: 0; flex: 1; }
.busca-cadastro-linha .busca-cadastro-nome { font-weight: 600; font-size: 13.5px; }
.busca-cadastro-linha .busca-cadastro-detalhe { font-size: 11.5px; color: var(--text-muted); }
.busca-cadastro-linha .busca-cadastro-destino { font-size: 12px; text-align: right; white-space: nowrap; }

.form-equipamento { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; max-width: 720px; }
.form-equipamento .full { grid-column: 1 / -1; }

/* Foto por linha na busca de cadastros do equipamento (pedido do
   usuário em 02/09/2026: "essa busca precisa mostrar nome id e foto de
   que esta cadastrado dentro do facial") — mesmo padrão de
   .busca-cadastro-linha acima (img + placeholder do mesmo tamanho),
   ver VarreduraSemFoto._renderizarLista em
   frontend/js/varredura-sem-foto.js. */
.varredura-sem-foto-foto, .varredura-sem-foto-foto-vazia {
  width: 28px; height: 28px; border-radius: 50%; object-fit: cover; background: var(--surface-2);
  border: 1px solid var(--border); flex-shrink: 0;
}

/* ===== CADASTRO: log | form | ativos (pedido do usuário em 14/08/2026,
   seguindo mockup_v6/v8.html) — três colunas do MESMO tamanho vertical
   (align-items padrão = stretch, não "start" — achado em 14/08/2026: com
   "start" cada card ficava do tamanho do seu CONTEÚDO, e como .cad-layout
   já tinha altura travada pelo .main de cima, o conteúdo que não coubesse
   simplesmente vazava pra fora da caixa em vez de rolar por dentro — "a
   lista não pode passar da caixa"). Três colunas independentes, cada uma
   rolando por dentro via .log-scroll. */
.cad-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr minmax(240px, 300px); gap: 16px; }
.cad-layout > .card, .form-col, .cad-layout-right { height: 100%; min-height: 0; margin-bottom: 0; }
.cad-layout > .card { display: flex; flex-direction: column; }
.cad-layout .card > h2, .cad-layout .card > h3 { flex-shrink: 0; }
/* Coluna direita empilhada — "Status de cadastro do dia" em cima, "Log
   de acessos" embaixo (pedido do usuário em 20/08/2026). Os dois cards
   não são mais filhos diretos de .cad-layout (por isso as regras acima,
   com seletor ">", não os alcançam mais) — ganham a mesma altura/rolagem
   própria aqui, um do lado do outro na vertical em vez de na horizontal. */
.cad-layout-right { display: flex; flex-direction: column; gap: 16px; }
.cad-layout-right > .card { flex: 1; min-height: 0; margin-bottom: 0; display: flex; flex-direction: column; }

@media (max-width: 1100px) {
  html, body { overflow: auto; }
  .main { height: auto; overflow: visible; }
  .cad-layout { grid-template-columns: 1fr; }
  .cad-layout > .card, .form-col, .cad-layout-right { height: auto; }
  .cad-layout-right > .card { flex: none; min-height: 260px; }
}

/* Coluna do meio (form de cadastro) — pedido do usuário em 14/08/2026:
   câmera / foto do cadastro / busca facial como três caixas simétricas
   dentro do próprio formulário (mesma estrutura do mockup_v6/v8.html:
   form-col > form-card > main-row > left-stack + cam-triple). */
.form-col { display: flex; flex-direction: column; min-height: 0; }
.form-card { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* Achado em bancada em 17/08/2026: com left-stack AO LADO das caixas de
   câmera, elas ficavam espremidas em ~70px de largura dentro de uma
   coluna de ~450px (190px iam pro left-stack) — virava uma tira alta e
   estreita em vez de uma caixa proporcional. O usuário pediu justamente
   "mude o dimensionamento das câmeras/foto/correspondência". Solução:
   left-stack agora é uma barra HORIZONTAL compacta ACIMA das caixas, que
   passam a usar a largura inteira do formulário. */
.main-row { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; flex: 1; min-height: 0; }
.left-stack { width: 100%; flex-shrink: 0; display: flex; flex-direction: row; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.left-stack > div { flex: 1; min-width: 108px; }
/* width fixo (pedido do usuário em 20/08/2026: "só botão salvar cadastro
   precisa ser sempre do mesmo tamanho, sem alterar") — antes esta div
   (e o botão .save-btn com width:100% dentro dela) media pelo próprio
   CONTEÚDO (flex-basis:auto = shrink-to-fit), então o botão mudava de
   largura/posição sozinho toda vez que o texto trocava entre "Salvar
   cadastro" (16 caracteres) e "Atualizar cadastro" (19 caracteres) —
   ver mostrarAvisoEditando/ocultarAvisoEditando em cadastro.js. 180px
   cabe "Atualizar cadastro" (o mais longo dos dois) com folga. */
.left-stack > div:last-child { flex: 0 0 auto; width: 180px; min-width: 0; }
/* 🐛 Achado em 24/09/2026 (usuário, com print: "popup tela de cadastro ultrapassa a margem da tela") — depois de
   puxar uma pessoa que já tem acesso em outro destino, esta coluna empilha o botão Salvar, "Editando…", o aviso
   cinza "já está ativo em" e o aviso laranja "já tem acesso em … e não tem cadastro no destino": ~446 px, sem teto
   (esta linha do `.left-stack` não encolhe, `flex-shrink: 0`). Em 1366x650 o laranja ia até y=743 numa janela de 650,
   o `.main { overflow: hidden }` cortava o que passava e a câmera (`.sym-box.cam`, que fica só com o que sobra) ficava
   com 2 px. Agora, SÓ enquanto um dos dois avisos aparece, a coluna tem teto de 32vh e rola por dentro (medido em
   1366x650, 1280x720, 1366x768 e 1920x1080: tudo dentro da janela, câmera de 74/122/154/366 px em vez de 2/2/2/266;
   sem os avisos a câmera mede exatamente o mesmo de antes: 190/260/308/642 px). Só com `:has()` para o overflow não
   cortar a sombra do botão Salvar no estado normal. */
.left-stack > div:last-child:has(.destinos-tambem:not(.hidden), .aviso-visita-destino:not(.hidden)) {
  max-height: 32vh; overflow-y: auto; overflow-x: hidden;
}
/* Destino tinha mais espaço que "Modo de acesso" (pedido do usuário em
   17/08/2026, quando Destino ainda cabia checkboxes de equipamento) —
   sem sentido agora que Destino é um campo de busca único (pedido do
   usuário em 20/08/2026: "diminua tbm o campo de destino deixando do
   mesmo tamanho do campo de nome") — teto de largura pra ficar do
   mesmo porte do campo Nome, em vez de esticar sozinho pela largura
   toda liberada pela Unidade que saiu. */
/* Especificidade ">div" (0,1,1) do .left-stack > div genérico é maior
   que uma classe só (0,1,0) — sem o combinador aqui, aquela regra
   ganhava e este flex nunca era aplicado. */
.left-stack > .destino-busca-wrap { flex: 0 1 330px; max-width: 340px; }
/* Lista de resultados menor (pedido do usuário em 20/08/2026: "faça com
   que ao clicar na busca de destino a lista de destino fique menor
   exibindo só 3 por vez na barra de rolagem") — cada item tem 2 linhas
   (nome da unidade + nome do bloco), ~54px de altura; 3 itens ≈ 165px.
   Sobrescreve o max-height:280px genérico de .popup (usado pelo
   busca-popup de nome/documento, que continua grande) só aqui. */
#cadastro_destino_popup { max-height: 165px; }

/* 🐛 Achado em 01/10/2026 (pedido do usuário: "Preciso que crie uma aba de
   observação no momento que tiver fazendo os cadastros... para documentar
   alguma informação sobre aquele cadastros") — campo compacto na coluna
   nova do `.left-stack` (ver index.html/unidade.html). min-height baixo +
   max-height com teto (nunca sem limite): qualquer crescimento aqui tira
   altura de verdade da câmera ao vivo (`.sym-box.cam`, que só fica com o
   que sobra de `.main-row`, flex-direction:column) — já quebrou 2x antes
   só com o aviso de "já tem acesso em outro destino" (v1.5.111, v1.5.129,
   ver comentário grande de `.left-stack > div:last-child:has(...)` acima).
   resize:vertical deixa o operador alargar um pouco se precisar, mas o
   navegador respeita o max-height como teto do próprio redimensionamento. */
.obs-operador-input {
  width: 100%;
  box-sizing: border-box;
  min-height: 40px;
  max-height: 72px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 9px;
  font: inherit;
  font-size: 12.5px;
  background: var(--surface-2);
  color: var(--text);
  resize: vertical;
}
.obs-operador-input::placeholder { color: var(--text-3); }
.obs-operador-input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent);
}

/* Rolagem própria em vez de crescer sem limite (pedido do usuário: "de
   forma otimizada") — com equipamentos + grupos futuros, a lista de
   chips poderia empurrar as caixas de câmera pra baixo; agora ela cabe
   ~3 linhas de chips e rola por dentro quando passar disso. */
.fila-chips { display: flex; gap: 6px; flex-wrap: wrap; max-height: 84px; overflow-y: auto; padding-right: 2px; }
.chip { display: flex; align-items: center; gap: 6px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 9px; font-size: 11px; }
.chip input { width: auto; }
.pill-row { display: flex; gap: 6px; }
.pill { flex: 1; text-align: center; padding: 7px 4px; border-radius: 8px; font-size: 11px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted); cursor: pointer; transition: 0.15s ease; }
.pill.selected { background: var(--accent-dim); border-color: var(--primary); color: var(--accent-text); }
/* Realce sutil ao passar o mouse (achado ao comparar com a referência
   visual em 17/08/2026) — nenhum dos dois tinha feedback de :hover. */
.chip:hover, .pill:hover { border-color: var(--primary); }

/* Seleção de fonte da câmera — webcam ou equipamento (pedido do usuário
   em 19/08/2026: "coloque uma legenda tbm em cada ícone mas que seja
   bonita e acompanhe o estilo") — MESMA classe .pill do "Modo de acesso"
   (herda cor/borda/seleção/hover de lá "de graça"), só ganha ícone ao
   lado do texto e não estica pra preencher a linha toda (equipamentos
   têm nomes de tamanhos bem diferentes). */
/* ⚠️ Achado ao testar no navegador real em 19/08/2026: o elemento tem
   id="fonte-camera-row" no HTML, não class — usar ".fonte-camera-row"
   aqui (seletor de CLASSE) nunca batia com nada, então o ícone (sem
   nenhum width/height aplicado — não é um <button>, então nem a regra
   genérica "button svg" alcançava) caía no tamanho padrão do navegador
   pra um <svg> sem width/height explícito (~300x150px!), estourando a
   pill inteira e empurrando as caixas de câmera/foto pra quase zero de
   altura. Corrigido pro seletor de ID (#). */
#fonte-camera-row .pill { flex: 0 1 auto; display: flex; align-items: center; gap: 5px; padding: 6px 12px; font-size: 12px; }
#fonte-camera-row .pill svg { width: 13px; height: 13px; flex-shrink: 0; }
/* Lista de equipamentos ao lado do pill "Webcam" (pedido do usuário em
   20/08/2026: "o botão da webcam fica padrão... já a câmera dos
   equipamentos fica em uma lista oculta igual ao campo de destino onde
   aparece a lista de blocos" — mesma ideia visual do <select> de
   "Destino (bloco)", só que compacto pra caber ao lado do pill em vez
   de ocupar a linha inteira). width:auto sobrescreve o width:100% do
   <select> genérico (ver regra global mais acima) — senão ele tomaria a
   largura inteira da .pill-row e empurraria o pill "Webcam" pra fora.
   flex:0 0 auto + width fixo (pedido do usuário em 20/08/2026: "a caixa
   onde exibe câmera equipamento tá muito longa, deixe do tamanho do
   botão de webcam") — antes flex:1 fazia esticar pra preencher toda a
   largura sobrando da linha; agora fica do mesmo porte do pill ao lado,
   só um pouco mais largo (nome de equipamento não cabe em 90px como
   "Webcam" cabe) — nome mais comprido corta com reticências (overflow)
   em vez de esticar a caixa. */
#fonte-camera-row select {
  flex: 0 0 auto; width: 130px; min-width: 0; padding: 6px 10px; font-size: 12px; border-radius: 8px;
  text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
}

/* As três caixas simétricas — câmera ao vivo, foto do cadastro,
   correspondências. Mesma altura e largura nas três (pedido do usuário
   em 17/08/2026: caixas maiores/mais proeminentes, preenchendo a altura
   disponível do formulário, em vez do tamanho pequeno com teto em cm de
   antes) — cada uma ocupa 1/3 da largura via flex:1 e a altura inteira
   de .main-row (que já estica pra ocupar o espaço restante do
   form-card). Só o CONTEÚDO de cada uma é real (vídeo de verdade,
   preview de foto de verdade, painel de matches com os botões reais de
   Confirmar/Liberar/Negar). */
.cam-triple { flex: 1; display: flex; gap: 10px; min-height: 0; }
.sym-box {
  flex: 1;
  height: 100%;
  min-width: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.sym-box .rec { position: absolute; top: 8px; left: 8px; display: flex; align-items: center; gap: 4px; font-size: 9.5px; color: #f77; z-index: 2; }
/* Pulso no ponto "ao vivo" (pedido do usuário em 27/08/2026: "futurista
   e bonito") — só transform (scale), nunca opacity, seguindo o mesmo
   cuidado já tomado no .card acima (aba em segundo plano pode congelar
   o relógio da animação; com scale, mesmo travado o ponto continua
   100% visível, só não pulsa até a aba voltar ao primeiro plano). */
.rec-dot {
  width: 6px; height: 6px; border-radius: 50%; background: #f44;
  box-shadow: 0 0 6px 1px rgba(255, 68, 68, .7);
  animation: rec-pulso 1.6s ease-in-out infinite;
}
@keyframes rec-pulso {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.35); }
}
@media (prefers-reduced-motion: reduce) {
  .rec-dot { animation: none; }
}
.sym-box.cam { align-items: center; justify-content: center; }
/* HUD de scanner biométrico (pedido do usuário em 27/08/2026: "futurista
   e bonito") — cantos em L, tipo mira/reticulo de scanner, mais uma
   linha de varredura subindo e descendo. Decoração pura: z-index baixo
   (fica atrás da toolbar/rótulo, na frente só do vídeo), pointer-events
   none (nunca rouba clique de nada). Só na câmera ao vivo, que é o
   momento mais "biométrico" da tela. Linha de varredura anima só
   "top" (posição) — nunca opacity — mesmo cuidado do .card acima; a
   opacidade fica fixa e quem "esconde" a linha nas pontas é o
   overflow:hidden do próprio .sym-box (herda da regra em cima). */
.sym-box.cam::before {
  content: "";
  position: absolute; inset: 10px;
  background-image:
    linear-gradient(var(--primary), var(--primary)), linear-gradient(var(--primary), var(--primary)),
    linear-gradient(var(--primary), var(--primary)), linear-gradient(var(--primary), var(--primary)),
    linear-gradient(var(--primary), var(--primary)), linear-gradient(var(--primary), var(--primary)),
    linear-gradient(var(--primary), var(--primary)), linear-gradient(var(--primary), var(--primary));
  background-repeat: no-repeat;
  background-size: 2px 18px, 18px 2px, 2px 18px, 18px 2px, 2px 18px, 18px 2px, 2px 18px, 18px 2px;
  background-position: top left, top left, top right, top right, bottom left, bottom left, bottom right, bottom right;
  opacity: .55;
  pointer-events: none;
  z-index: 1;
}
.sym-box.cam::after {
  content: "";
  position: absolute; left: 8px; right: 8px; height: 2px; top: -8%;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--primary) 85%, white), transparent);
  box-shadow: 0 0 8px 1px color-mix(in srgb, var(--primary) 70%, transparent);
  opacity: .65;
  pointer-events: none;
  z-index: 1;
  animation: scan-varredura 3.2s ease-in-out infinite;
}
@keyframes scan-varredura {
  0%, 100% { top: -8%; }
  50% { top: 106%; }
}
@media (prefers-reduced-motion: reduce) {
  .sym-box.cam::before { opacity: .35; }
  .sym-box.cam::after { display: none; }
}
/* object-fit: contain em vez de cover (pedido do usuário em 17/08/2026:
   "a câmera ficou com muito zoom, a resolução precisa ser a original mas
   obedecendo o limite da caixa") — "cover" corta as bordas pra preencher
   a caixa inteira (efeito de zoom quando a proporção da caixa não bate
   com a da webcam); "contain" mostra o vídeo inteiro, sem cortar nem
   ampliar além da resolução real, só encaixando dentro do limite da
   caixa (pode sobrar uma tarja vazia dos lados/em cima, mas nunca corta
   nem estica a imagem). */
.sym-box.cam video, .sym-box.cam img#equipamento-camera-preview { width: 100%; height: 100%; object-fit: contain; background: #000; }
.sym-box.cam .cam-label { position: absolute; bottom: 8px; left: 8px; right: 8px; font-size: 10px; color: var(--text-muted); text-align: center; }
/* Botão de capturar foto (pedido do usuário em 17/08/2026: "o ícone de
   foto... tem que ficar na câmera ao vivo") — mesma barra flutuante que
   já existia na caixa de foto, só que agora nesta caixa. */
.sym-box.cam .foto-toolbar { justify-content: center; }

/* Foto capturada/cortada + ferramenta de corte, FUNDIDAS na própria
   caixa da câmera (pedido do usuário em 20/08/2026: "remova a caixa do
   meio onde exibe a foto tirada e deixe somente a câmera de cadastro" —
   antes eram regras de .sym-box.foto, uma caixa separada; ver
   frontend/js/cropper.js e Foto em cadastro.js, que decidem qual desses
   elementos fica visível por cima do feed ao vivo a cada momento). */
.sym-box.cam img#preview-foto { width: 100%; height: 100%; object-fit: cover; }
.sym-box.cam .foto-vazio-texto { font-size: 11.5px; color: var(--text-muted); text-align: center; padding: 0 10px; }
.sym-box.cam canvas#cropper-canvas { width: 100%; height: 100%; object-fit: contain; cursor: crosshair; display: block; }
.cropper-acoes-inline {
  position: absolute; bottom: 0; left: 0; right: 0; z-index: 3;
  background: rgba(0,0,0,.45); display: flex; justify-content: center; gap: 6px; padding: 6px 0;
}
.cropper-acoes-inline button {
  background: var(--card-bg); border: 1px solid var(--border); color: var(--text);
  width: 28px; height: 28px; border-radius: 7px; font-size: 12px; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.cropper-acoes-inline button:hover { background: var(--border); }
.cropper-acoes-inline button.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.cropper-acoes-inline button.primary:hover { background: var(--primary-hover); }

.foto-toolbar { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,.4); display: flex; justify-content: center; gap: 10px; padding: 8px 0; z-index: 2; }
.foto-toolbar button, .foto-toolbar .upload-label-icon {
  background: var(--card-bg); border: 1px solid var(--border); color: var(--text);
  width: 30px; height: 30px; border-radius: 7px; font-size: 14px; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
}
.foto-toolbar button:hover, .foto-toolbar .upload-label-icon:hover { background: var(--border); }
/* Trava visual do upload durante a checagem de duplicidade (🐛 corrida
   corrigida em 28/08/2026, ver _travarControlesFoto em cadastro.js) —
   label não tem :disabled nativo como um <button>, por isso precisa de
   uma classe própria; mesmo opacity de button:disabled (regra global). */
.upload-label-icon.upload-desabilitado { opacity: 0.5; cursor: not-allowed; pointer-events: none; }

/* "Trocar foto" (pedido do usuário em 27/08/2026 — ver comentário no
   HTML, junto de #foto-trocar-label) — pill flutuando bem em cima do
   foto-toolbar (46px = 8px+30px+8px de altura dele), pra deixar claro
   que os ícones logo abaixo são a ação. pointer-events:none: é só um
   aviso, nunca deve roubar o clique de nada por baixo. */
.foto-trocar-label {
  position: absolute; bottom: 46px; left: 50%; transform: translateX(-50%);
  background: color-mix(in srgb, var(--primary) 88%, black);
  color: #fff; font-size: 11px; font-weight: 600;
  padding: 4px 10px; border-radius: 999px; white-space: nowrap;
  pointer-events: none; z-index: 2;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
  animation: card-entrada .3s ease both;
}

/* "Voltar para a câmera" (pedido do usuário em 28/08/2026) — mesmo
   visual/posição de .foto-trocar-label acima, mas CLICÁVEL de verdade
   (é um botão, não um aviso): sem pointer-events:none, com hover/active
   próprios, já que a ação é descartar a foto nova e voltar pro feed ao
   vivo (Foto.limpar()). */
.foto-voltar-btn {
  position: absolute; bottom: 46px; left: 50%; transform: translateX(-50%);
  background: color-mix(in srgb, var(--primary) 88%, black);
  color: #fff; font-size: 11px; font-weight: 600;
  padding: 5px 12px; border: none; border-radius: 999px; white-space: nowrap;
  cursor: pointer; z-index: 2;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
  animation: card-entrada .3s ease both;
  transition: background .15s ease, transform .12s ease;
}
.foto-voltar-btn:hover { background: color-mix(in srgb, var(--primary-hover) 88%, black); transform: translateX(-50%) translateY(-1px); }
.foto-voltar-btn:active { transform: translateX(-50%) translateY(0) scale(.96); }
@media (prefers-reduced-motion: reduce) {
  .foto-voltar-btn { animation: none; }
}

.sym-box.sim { padding: 10px; gap: 6px; overflow-y: auto; }
.sim-title { font-family: var(--font-display); font-size: 9.5px; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); font-weight: 600; margin: 0 0 2px; flex-shrink: 0; }

/* Popup discreto de imagem (pedido do usuário em 17/08/2026: clicar
   numa foto do "Log de acessos" ou "Ativos e expirados" mostra a
   imagem ampliada num popup pequeno, ancorado DENTRO da própria caixa
   do painel — não é o popup grande de detalhe da pessoa, que continua
   existindo ao clicar no nome/linha). Ver frontend/js/imagem-popup.js. */
.imagem-popup {
  position: absolute; z-index: 40;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,.28); padding: 6px;
}
.imagem-popup img { display: block; width: 160px; height: 160px; object-fit: cover; border-radius: 6px; }

.cfg-row { display: flex; align-items: center; justify-content: space-between; padding: 11px 0; border-bottom: 1px solid var(--border); gap: 20px; }
.cfg-row:last-child { border-bottom: none; }
/* Rótulo de cada linha de configuração ("Modo de exibição", "Paleta de
   destaque" etc.) — classe já existia no HTML sem regra própria (achado
   ao comparar com a referência visual em 17/08/2026). */
.cfg-label p { margin: 0; font-size: 13px; font-weight: 600; }
.cfg-label span { font-size: 11px; color: var(--text-muted); }

/* Switch on/off de verdade (substituiu o checkbox nativo em 17/08/2026,
   ao comparar com a referência visual — antes era um <input
   type="checkbox"> simples). Estado controlado via classe ".on" +
   atributo aria-checked, não .checked (é um <button>, não um checkbox)
   — ver frontend/js/equipamentos.js. */
.switch {
  width: 36px; height: 20px; border-radius: 20px;
  background: var(--surface-2); border: 1px solid var(--border);
  position: relative; cursor: pointer; flex-shrink: 0; padding: 0;
  transition: 0.18s ease;
}
.switch.on { background: var(--primary); }
.switch::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%; background: #fff;
  transition: 0.18s ease;
}
.switch.on::after { left: 17px; }

/* Relógio/status no topo (pedido do usuário em 17/08/2026, seguindo os
   prints fornecidos) — ao lado do título da página. */
.topbar { justify-content: space-between; }
.topbar-status { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-muted); }
.topbar-status .status-dot { background: var(--success); }

/* ===== Modo de exibição + paleta de destaque (pedido do usuário em
   17/08/2026) — aba Configurações, card "Identidade visual". Aplicado
   de verdade via atributos data-tema/data-cor no <html>, nunca só
   visual — ver frontend/js/tema.js. */
.modo-exibicao-row { display: flex; gap: 8px; flex-wrap: wrap; }
.modo-exibicao-btn {
  padding: 9px 16px; border-radius: 9px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font-size: 13px; font-weight: 500; cursor: pointer;
}
/* Ícone de verdade (sol/lua/meia-lua) em vez do emoji ☀️/🌙/🌗 cru
   (pedido do usuário em 19/08/2026: "esse tbm ta feio") + um "pop" ao
   selecionar — o resto da animação (hover/clique) já vem de graça da
   regra genérica de botão lá em cima neste arquivo. */
.modo-exibicao-btn.active {
  background: var(--accent-dim); border-color: var(--primary); color: var(--accent-text);
  animation: modo-selecionado .35s ease;
}
@keyframes modo-selecionado {
  0% { transform: scale(0.88); }
  60% { transform: scale(1.06); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .modo-exibicao-btn.active { animation: none; }
}

.paleta-cores-row { display: flex; gap: 8px; flex-wrap: wrap; }
.cor-swatch {
  width: 32px; height: 32px; border-radius: 9px; border: 2px solid transparent; cursor: pointer; padding: 0;
  transition: 0.15s ease;
}
.cor-swatch:hover { transform: scale(1.08); }
/* Anel na PRÓPRIA cor escolhida, não num tom neutro (achado ao comparar
   com a referência visual em 17/08/2026) — assim o swatch ativo se
   destaca com a cor que ele representa. */
.cor-swatch.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--card-bg), 0 0 0 4px var(--primary); }

/* ===== Marca d'água (pedido do usuário em 17/08/2026) — texto ou
   imagem, opacidade configurável, exibida com pointer-events:none (nunca
   atrapalha clique em nada) no canto inferior direito da tela, nas duas
   páginas. Ver frontend/js/marcadagua.js. */
#marca-dagua-overlay {
  position: fixed; right: 18px; bottom: 14px; z-index: 50;
  pointer-events: none; user-select: none;
  opacity: var(--marca-dagua-opacidade, 0.15);
  font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text);
}
#marca-dagua-overlay img { max-height: 40px; max-width: 160px; object-fit: contain; display: block; }

.marca-dagua-tipo-row { display: flex; gap: 8px; margin-bottom: 12px; }
.marca-dagua-preview {
  position: relative; width: 100%; max-width: 320px; height: 70px; border-radius: 10px;
  background: var(--bg); border: 1px dashed var(--border);
  display: flex; align-items: center; justify-content: flex-end; padding: 10px 14px;
  margin-bottom: 10px; overflow: hidden;
}

/* ===== Ferramenta de scanner do Incontrol (pedido do usuário em
   24/08/2026 — ver escopo completo em
   app/services/importar_incontrol_service.py). Só visualização: nenhum
   destes elementos dispara cadastro/envio de nada. Ver
   frontend/importar_incontrol.html e frontend/js/importar-incontrol.js. */

/* Cada linha de caminho de .zip (campo + Procurar + Remover) — igual ao
   campo de pasta já usado em Backup/Local dos dados, mas repetível
   (pedido do usuário: "precisa aceitar MAIS DE UM zip ao mesmo tempo"). */
.incontrol-zip-row { display: flex; gap: 8px; margin-bottom: 8px; }
.incontrol-zip-row input { flex: 1; min-width: 0; }

/* Resumo agregado no topo do resultado (total de pessoas, ativos/
   inativos, com/sem foto, com/sem CPF) — tiles simples, sem exigir
   nenhuma biblioteca de gráfico pra um punhado de números. */
.incontrol-resumo-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px; margin-bottom: 4px;
}
.incontrol-stat {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px;
}
.incontrol-stat .valor { font-size: 22px; font-weight: 700; line-height: 1.2; }
.incontrol-stat .rotulo { font-size: 11px; color: var(--text-muted); margin-top: 2px; }

/* Grupos por unidade/departamento — accordion colapsado por padrão
   (pedido do usuário: nunca carregar as ~1434 fotos de uma vez só). */
.incontrol-grupo { border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.incontrol-grupo-cabecalho {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; cursor: pointer; background: var(--surface-2); user-select: none;
}
.incontrol-grupo-cabecalho:hover { background: var(--accent-dim); }
.incontrol-grupo-cabecalho .nome { font-weight: 600; font-size: 13.5px; }
.incontrol-grupo-cabecalho .qtd { font-size: 11.5px; color: var(--text-muted); }
.incontrol-grupo-seta { transition: transform 0.15s ease; flex-shrink: 0; }
.incontrol-grupo.aberto .incontrol-grupo-seta { transform: rotate(90deg); }
.incontrol-grupo-corpo { padding: 14px; }

/* Grid de cartões de pessoa dentro de um grupo expandido. */
.incontrol-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.incontrol-card {
  border: 1px solid var(--border); border-radius: 10px; padding: 10px; background: var(--card-bg);
  display: flex; flex-direction: column; gap: 6px;
}
.incontrol-card-foto {
  width: 100%; aspect-ratio: 1 / 1; border-radius: 8px; object-fit: cover;
  background: var(--surface-2); display: block;
}
.incontrol-card-sem-foto {
  width: 100%; aspect-ratio: 1 / 1; border-radius: 8px; background: var(--surface-2);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: var(--text-3); gap: 4px;
}
.incontrol-card-sem-foto span { font-size: 10.5px; }
.incontrol-card .nome { font-size: 12.5px; font-weight: 600; line-height: 1.3; }
.incontrol-card .documento { font-size: 11px; color: var(--text-muted); }

/* v1.5.33 — página da unidade: abas ("Cadastrar na unidade" ... "Visibilidade"),
   quadro de avisos e cadastros recentes de outros operadores. */
#unidade-abas-categoria { flex-wrap: wrap; }
#unidade-abas-categoria .seg-btn { flex: 1 1 auto; font-size: 10.5px; padding: 8px 10px; white-space: nowrap; }
.unidade-mural { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 14px; margin-top: 14px; align-items: start; }
@media (max-width: 900px) { .unidade-mural { grid-template-columns: 1fr; } }
.unidade-mural > .card { margin-bottom: 0; min-width: 0; }
.quadro-avisos-form { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.quadro-avisos-form textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 64px; font: inherit; }
.quadro-avisos-lista { display: flex; flex-direction: column; gap: 10px; max-height: 520px; overflow-y: auto; }
.aviso-item { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; background: var(--surface-2); }
.aviso-cabeca { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; }
.aviso-autor { font-weight: 700; font-size: 13px; }
.aviso-meta { color: var(--text-muted); }
.aviso-cabeca .badge { margin-left: auto; }
.badge.aviso-geral { color: var(--primary); }
.aviso-remover { padding: 2px 7px; font-size: 11px; }
.aviso-texto { margin-top: 6px; white-space: pre-wrap; word-break: break-word; font-size: 13.5px; }
.aviso-fotos { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.aviso-fotos:empty { display: none; }
.aviso-fotos img { width: 84px; height: 84px; object-fit: cover; border-radius: 8px; cursor: zoom-in; border: 1px solid var(--border); }
.aviso-previa { position: relative; display: inline-block; }
.aviso-previa button { position: absolute; top: 2px; right: 2px; padding: 0 5px; font-size: 11px; line-height: 16px; }
.cadastros-recentes-lista { display: flex; flex-direction: column; gap: 6px; max-height: 560px; overflow-y: auto; }
.toast-info { border-color: var(--primary); } /* v1.5.33 — aviso de cadastro chegado de outra máquina */

/* v1.5.35 — aviso de origem de um cadastro vindo de outra máquina (popup da
   pessoa, lista de cadastros recentes e detalhe). */
.aviso-origem-cadastro {
  margin-top: 6px; padding: 7px 10px; border-radius: 8px; font-size: 12.5px; line-height: 1.4;
  background: color-mix(in srgb, var(--primary) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border));
}
/* v1.5.47 (pedido do usuário em 15/09/2026) — uma pessoa = UM cadastro com
   VÁRIOS destinos, "mas a credencial e a mesma". Selo dos destinos que o
   operador NÃO administra ("um operador so mexe nos destinos dos blocos que
   ele administra; os demais destinos ele ve apenas como informacao") e a
   caixinha "Também tem acesso em" do cartão compacto da Unidade. */
.destino-somente-info {
  display: inline-block; font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 5px;
  background: var(--surface-2); color: var(--text-muted); white-space: nowrap;
}
.destinos-tambem {
  margin-top: 8px; padding: 7px 10px; border-radius: 8px; font-size: 12.5px; line-height: 1.5;
  background: var(--surface-2); color: var(--text-muted);
  /* v1.5.68 — o aviso ficou curto (ver cadastro.js), mas com muitos destinos ele ainda pode passar
     da altura da coluna: rola por dentro em vez de ser cortado, e nome/destino comprido quebra
     em vez de estourar a largura. */
  max-height: 180px; overflow-y: auto; overflow-wrap: anywhere;
}
.destinos-tambem strong { color: var(--text); font-weight: 600; }

.recente-item { cursor: pointer; border-radius: 8px; }
.recente-item:hover { background: var(--surface-2); }
.recente-origem { font-size: 11px; line-height: 1.35; }

/* v1.5.36 — botão "Avisos" no topo da tela de Cadastro, popup de avisos,
   câmera num popup (webcam-popup.js) e foto ampliada. */
.topbar-acoes { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.btn-avisos-topo { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; }
.avisos-popup-box { width: min(92vw, 620px); max-height: 88vh; overflow-y: auto; }
.avisos-popup-cabeca { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.avisos-popup-linha { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 6px; }
.avisos-popup-linha select { max-width: 220px; }
.avisos-popup-subtitulo { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin: 6px 0 8px; }
#webcam-popup-overlay { z-index: 120; }
.webcam-popup-box { width: min(92vw, 640px); }
#webcam-popup-video { width: 100%; max-height: 60vh; border-radius: 10px; background: #000; display: block; }
.unidade-camera-btn { display: block; width: 100%; margin-top: 4px; font-size: 11.5px; padding: 5px 6px; }
.visualizador-foto { position: fixed; inset: 0; z-index: 130; background: rgba(4, 6, 12, .85); display: flex; align-items: center; justify-content: center; cursor: zoom-out; }
.visualizador-foto img { max-width: 92vw; max-height: 90vh; border-radius: 10px; }
.unidade-mural.sem-avisos { grid-template-columns: 1fr; } /* v1.5.37 — sem a permissão de avisos */
.btn-avisos-topo { position: relative; }
.avisos-badge { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  background: var(--danger, #e5484d); color: #fff; font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; }

/* v1.5.38 — prazo, lixeira e PDF dos avisos; PDF da lista da unidade. */
.btn-avisos-acao { padding: 3px 9px; font-size: 12px; }
.btn-avisos-acao.ativo { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.avisos-lixeira-cabeca { font-size: 12px; color: var(--text-muted); padding: 6px 2px 2px; }
.aviso-na-lixeira { opacity: .85; border-style: dashed; }
.lista-busca-linha { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 14px; }
.avisos-popup-subtitulo .btn-avisos-acao { float: right; text-transform: none; letter-spacing: 0; font-weight: 500; }

/* ===== Tela de Configurações reorganizada (v1.5.39 — pedido do usuário: "quero
   que faça uma organização de forma lógica de acordo com as funções na tela de
   configurações deixando visualmente mais simétrico e bonito"). Menu em blocos
   com ícone, cabeçalho por categoria e grades simétricas — ver o comentário no
   topo de frontend/equipamentos.html. ===== */
.config-nav { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 8px; margin-bottom: 18px; }
.config-nav-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px;
  min-height: 74px; padding: 12px 6px 10px; border-radius: 13px; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--border) 75%, var(--primary) 15%);
  background: color-mix(in srgb, var(--card-bg) 78%, transparent);
  color: var(--text-muted); font-size: 11.5px; font-weight: 600; line-height: 1.2; text-align: center;
  transition: transform .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease;
}
.config-nav-btn svg { width: 21px; height: 21px; margin: 0; flex-shrink: 0; }
.config-nav-btn:hover { color: var(--text); border-color: var(--primary); transform: translateY(-1px); }
.config-nav-btn.active {
  background: var(--primary); color: var(--primary-fg); border-color: var(--primary);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--primary) 32%, transparent);
}
.config-nav-btn { min-width: 0; }
.config-nav-btn span { max-width: 100%; overflow-wrap: anywhere; }
/* Tela mais estreita: 3 x 3 blocos (continua simétrico), ícone ao lado do nome. */
@media (max-width: 1180px) {
  .config-nav { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .config-nav-btn { flex-direction: row; justify-content: flex-start; min-height: 0; padding: 10px 14px; text-align: left; }
}
@media (max-width: 760px) {
  .config-nav-btn { flex-direction: column; justify-content: center; padding: 10px 4px; text-align: center; font-size: 10.5px; }
}

.config-painel-cabeca { display: flex; align-items: center; gap: 12px; margin: 0 0 14px; }
.config-painel-icone {
  width: 40px; height: 40px; border-radius: 12px; display: grid; place-items: center; flex-shrink: 0;
  background: color-mix(in srgb, var(--primary) 15%, transparent); color: var(--primary);
}
.config-painel-icone svg { width: 21px; height: 21px; }
.config-painel-cabeca h2 { margin: 0; font-family: var(--font-display); font-size: 16px; font-weight: 700; color: var(--text); letter-spacing: .01em; }
.config-painel-cabeca p { margin: 2px 0 0; font-size: 12.5px; color: var(--text-muted); }

/* Grade: cards da mesma linha com a MESMA altura e o rodapé (botão Salvar)
   alinhado no pé de todos. */
.config-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin-bottom: 16px; align-items: stretch; }
.config-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.config-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.config-grid > .card { margin: 0; min-width: 0; display: flex; flex-direction: column; }
.config-grid > .card > button, .config-grid > .card > a, .config-grid > .card > label { align-self: flex-start; }
.card-rodape { padding-top: 12px; }
.config-grid > .card > .card-rodape { margin-top: auto; }
@media (max-width: 1180px) { .config-grid.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 900px) { .config-grid.cols-2, .config-grid.cols-3 { grid-template-columns: minmax(0, 1fr); } }

.card.card-perigo { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.card.card-perigo > h2 { color: var(--danger); }
.config-zona-perigo {
  display: flex; align-items: center; gap: 10px; margin: 6px 0 12px;
  font-family: var(--font-display); font-size: 10.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--danger);
}
.config-zona-perigo::after { content: ""; flex: 1; height: 1px; background: color-mix(in srgb, var(--danger) 35%, transparent); }

.config-subgrid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.config-subitem {
  display: flex; flex-direction: column; min-width: 0; padding: 12px 14px; border-radius: 12px;
  border: 1px solid var(--border); background: color-mix(in srgb, var(--surface-2) 55%, transparent);
}
.config-subitem > .card-rodape { margin-top: auto; }
@media (max-width: 1180px) { .config-subgrid-3 { grid-template-columns: minmax(0, 1fr); } }

.config-campos-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; }
@media (max-width: 720px) { .config-campos-2 { grid-template-columns: minmax(0, 1fr); } }

/* Formulário de equipamento em 4 colunas (card na largura toda) e sliders da
   busca facial em 3. */
.form-equipamento.form-equipamento-largo { grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: none; }
.form-equipamento.form-3col { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: none; }
.form-equipamento-largo #eq_blocos_container { flex-direction: row !important; flex-wrap: wrap; column-gap: 20px; }
@media (max-width: 1180px) {
  .form-equipamento.form-equipamento-largo { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .form-equipamento.form-3col { grid-template-columns: minmax(0, 1fr); }
}

/* Credenciais do Supabase/R2 em 2 colunas no card da nuvem (largura toda). */
#sincronizacao-nuvem-supabase-r2-campos { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; }
#sincronizacao-nuvem-supabase-r2-campos > .field { max-width: none !important; }
#sincronizacao-nuvem-supabase-r2-campos > button { grid-column: 1 / -1; justify-self: start; }
@media (max-width: 900px) { #sincronizacao-nuvem-supabase-r2-campos { grid-template-columns: minmax(0, 1fr); } }

/* Linhas "rótulo + controle" (Aparência) quebram em cards mais estreitos. */
.config-grid.cols-2 .cfg-row { flex-wrap: wrap; }
.config-grid.cols-2 .cfg-label { flex: 1 1 220px; }

/* v1.5.42 — editor de perfil (Configurações → Operadores): permissões por perfil. */
.perfil-editor { display: flex; flex-direction: column; gap: 10px; font-size: 13px; }
.perfil-editor-secao {
  border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px;
  background: color-mix(in srgb, var(--surface-2) 45%, transparent);
}
.perfil-editor-titulo {
  font-family: var(--font-display); font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--primary); margin-bottom: 6px;
}
.perfil-editor-subtitulo { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin: 8px 0 4px; }
.perfil-editor-nota { font-size: 11.5px; color: var(--text-muted); margin: 0 0 6px; line-height: 1.45; }
.perfil-editor-item { display: flex; align-items: flex-start; gap: 7px; padding: 3px 0; cursor: pointer; font-size: 13px; color: var(--text); font-weight: 400; margin: 0; }
.perfil-editor-item input { width: auto; margin-top: 2px; flex-shrink: 0; }
.perfil-editor-sub { margin: 2px 0 6px 24px; padding-left: 10px; border-left: 2px solid var(--border); }
.perfil-editor-blocos { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0 12px; }
.perfil-editor-outras > summary { cursor: pointer; font-size: 12.5px; color: var(--text-muted); font-weight: 600; }
.perfil-editor-outras[open] > summary { margin-bottom: 4px; }

/* v1.5.43 — card "Perfis na nuvem": o que está na nuvem agora e conflitos de nome. */
.perfil-nuvem-aviso { color: var(--warning); font-size: 11.5px; }
.perfis-na-nuvem { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
.perfis-na-nuvem-titulo { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin-top: 6px; }
.perfis-na-nuvem-item { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 12.5px; padding: 4px 0; border-bottom: 1px dashed var(--border); }
.perfis-na-nuvem-item button { font-size: 11.5px; padding: 3px 8px; flex-shrink: 0; }
.perfis-na-nuvem-meta { color: var(--text-muted); font-size: 11.5px; }
.perfis-na-nuvem-nota { font-size: 11.5px; color: var(--text-muted); margin: 2px 0; }
.perfis-na-nuvem-conflitos { font-size: 12px; background: var(--amber-bg); color: var(--amber-text); border-radius: 10px; padding: 8px 10px; }
.perfis-na-nuvem-conflitos ul { margin: 4px 0 0 16px; padding: 0; }

/* v1.5.46 — "as fotos nunca devem cortar nada do rosto" (pedido do usuário, com print da testa e do
   queixo cortados no cartão da pessoa): toda foto de pessoa aparece INTEIRA (contain, com fundo
   neutro nas sobras), nunca recortada para caber na caixa; os avatares redondos das listas viraram
   quadrados arredondados (o círculo cortava as bordas da foto). O logo continua como estava. */
.popup .item img, #preview-foto, .sym-box.cam img#preview-foto, .painel-match img, .evento-item img, .avatar,
.busca-cadastro-linha img, .varredura-sem-foto-foto, .imagem-popup img, .incontrol-card-foto, .aviso-fotos img {
  object-fit: contain; background: var(--surface-2);
}
.popup .item img, .popup .item .avatar-vazio, .evento-item img, .evento-item .avatar-vazio,
.busca-cadastro-linha img, .busca-cadastro-linha .avatar-vazio, .varredura-sem-foto-foto, .varredura-sem-foto-foto-vazia {
  border-radius: 8px;
}
/* v1.5.46 — "Remover da nuvem" em cada item de "Puxar da nuvem com seleção". */
.nuvem-item-linha { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.nuvem-item-linha .nuvem-remover-item { font-size: 11px; padding: 1px 8px; flex-shrink: 0; }

/* v1.5.53 — pessoas no bloco/unidade: banco local x só na nuvem (discreto). */
.contagem-origem {
  font-size: 10.5px;
  font-weight: 400;
  color: var(--text-muted);
  margin-left: 4px;
  cursor: help;
  white-space: nowrap;
}

/* v1.5.54 — filtro de blocos do "Status de cadastro do dia". */
.filtro-blocos-status { position: relative; }
.filtro-blocos-botao { font-size: 11.5px; padding: 3px 8px; white-space: nowrap; }
.filtro-blocos-botao.ativo { border-color: var(--primary); color: var(--primary); font-weight: 600; }
.filtro-blocos-lista {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
  min-width: 190px; max-height: 260px; overflow-y: auto;
  background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); padding: 6px;
}
.filtro-blocos-item { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: normal; padding: 3px 4px; cursor: pointer; margin: 0; }
.filtro-blocos-item input { width: auto; margin: 0; }

/* v1.5.54 — "abrir a unidade" do cadastro, no cartão da pessoa. */
.link-destino-cadastro { font-size: 11.5px; margin-left: 6px; white-space: nowrap; color: var(--primary); text-decoration: none; }
.link-destino-cadastro:hover { text-decoration: underline; }

/* v1.5.54 — aviso "rosto muito perto da câmera" sobre a foto em corte. */
.aviso-rosto-perto {
  position: absolute; left: 8px; right: 8px; top: 8px; z-index: 5;
  background: var(--amber-bg, #fff4d6); color: var(--amber-text, #7a4b00);
  border: 1px solid var(--warning, #d98e04); border-radius: 8px;
  padding: 6px 10px; font-size: 12.5px; font-weight: 600; line-height: 1.35;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18); pointer-events: none;
}

/* v1.5.54 — Painel de cadastros e acessos. */
.painel-pagina { max-width: 1400px; display: flex; flex-direction: column; gap: 14px; }
.painel-filtros { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.painel-datas { display: flex; gap: 10px; align-items: center; }
.painel-datas label { display: flex; align-items: center; gap: 6px; font-size: 13px; margin: 0; font-weight: normal; }
.painel-datas input { width: auto; }
.painel-status { font-size: 12.5px; color: var(--text-muted); }
.painel-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
.painel-tile { margin: 0; }
.painel-tile-rotulo { font-size: 12.5px; color: var(--text-muted); }
.painel-tile-valor { font-size: 30px; font-weight: 700; color: var(--text); line-height: 1.2; margin-top: 4px; font-variant-numeric: tabular-nums; }
.painel-lista-categorias { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; }
.painel-cat { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
.painel-cat strong { font-size: 18px; font-variant-numeric: tabular-nums; }
.painel-sub { font-size: 12px; font-weight: normal; color: var(--text-muted); }
.painel-vazio { color: var(--text-muted); font-size: 13px; margin: 4px 0; }
.painel-barras { display: flex; flex-direction: column; gap: 4px; max-height: 320px; overflow-y: auto; }
.painel-barra-linha { display: grid; grid-template-columns: 58px 1fr 52px; align-items: center; gap: 8px; font-size: 12.5px; padding: 2px 0; }
.painel-barra-linha:hover { background: var(--surface-2); border-radius: 4px; }
.painel-barra-rotulo { color: var(--text-muted); }
.painel-barra-trilho { display: block; height: 12px; }
.painel-barra { display: block; height: 100%; background: var(--primary); border-radius: 0 4px 4px 0; }
.painel-barra-valor { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.painel-tabela { width: 100%; border-collapse: collapse; font-size: 13px; }
.painel-tabela th, .painel-tabela td { padding: 6px 8px; border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }
.painel-tabela th { font-size: 12px; color: var(--text-muted); font-weight: 600; }
.painel-tabela .num { text-align: right; font-variant-numeric: tabular-nums; }
.painel-tabela tfoot td { font-weight: 600; border-top: 2px solid var(--border); border-bottom: none; }
.painel-bloco { margin: 0; }
.painel-bloco-topo { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.painel-bloco-topo h2 { margin: 0; }
.painel-bloco-total { font-size: 13.5px; color: var(--text-muted); }
.painel-bloco-total strong { color: var(--text); font-size: 18px; }
.painel-h3 { font-size: 13px; margin: 12px 0 4px; color: var(--text-muted); font-weight: 600; }
.painel-unidades { margin-top: 10px; }
.painel-unidades summary { cursor: pointer; font-size: 13px; color: var(--text-muted); font-weight: 600; }
#painel-blocos { display: flex; flex-direction: column; gap: 14px; }

/* v1.5.55 — ações de cada equipamento quebram linha em vez de sair da tabela
   (🐛 print do usuário em 16/09/2026: Logar/Sincronizar/Remover cortados à direita). */
.equip-acoes-celula { min-width: 220px; }
.equip-acoes { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
.equip-acoes button { white-space: nowrap; }

/* v1.5.55 — consumo das nuvens no Painel. */
.painel-nuvem-item { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; }
.painel-nuvem-item .painel-h3 { margin-top: 0; }
.painel-nuvem-valor { font-size: 13.5px; color: var(--text-muted); display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.painel-nuvem-valor strong { font-size: 20px; color: var(--text); font-variant-numeric: tabular-nums; }
.painel-nuvem-estado { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.painel-nuvem-estado.alerta { color: var(--warning); }
.painel-nuvem-estado.critico { color: var(--danger); }
.painel-nuvem-trilho { height: 10px; border-radius: 5px; background: var(--surface-2); margin: 8px 0 6px; overflow: hidden; }
.painel-nuvem-barra { height: 100%; border-radius: 5px; background: var(--primary); }
.painel-nuvem-barra.alerta { background: var(--warning); }
.painel-nuvem-barra.critico { background: var(--danger); }
.painel-nuvem-detalhes { font-size: 12px; color: var(--text-muted); line-height: 1.4; }

/* v1.5.56 — equipamentos em cartões (Configurações → Equipamentos). */
.equip-marcar-todos-rotulo { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: normal; margin: 0 0 8px; cursor: pointer; }
.equip-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; max-height: 1100px; overflow-y: auto; padding: 2px; }
.equip-card { border: 1px solid var(--border); border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 8px; background: var(--card-bg); min-width: 0; }
.equip-card:has(.equip-chk:checked) { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
.equip-card-topo { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.equip-card-titulo { display: flex; align-items: flex-start; gap: 8px; margin: 0; cursor: pointer; min-width: 0; }
.equip-card-titulo input { width: auto; margin-top: 3px; flex-shrink: 0; }
.equip-nome { font-weight: 700; font-size: 14px; line-height: 1.3; word-break: break-word; color: var(--text); }
.equip-direcao { font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--text-muted); white-space: nowrap; }
.equip-status-linha { display: flex; gap: 14px; flex-wrap: wrap; }
.equip-status-linha > div { display: flex; align-items: center; gap: 6px; }
.equip-campos { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; }
.equip-campo { display: flex; flex-direction: column; min-width: 0; }
.equip-campo-largo { grid-column: 1 / -1; }
.equip-rotulo { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); font-weight: 600; }
.equip-valor { font-size: 13px; color: var(--text); word-break: break-word; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.equip-vazio { color: var(--text-muted); font-size: 12.5px; }
.equip-chip { font-size: 11.5px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--border); }

/* v1.5.133 — perfil de instalação por bloco (bloco.html / Configurações → Nuvem) e remoção de bloco da nuvem em cascata
   (Configurações → Nuvem). Tudo é painel na própria página e rola por dentro: nunca estoura a janela em resolução baixa. */
.perfil-bloco-caixa { margin: 0 0 12px; padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); }
.perfil-bloco-painel { margin-top: 10px; }
.perfil-bloco-lista { max-height: min(240px, 38vh); overflow-y: auto; display: flex; flex-direction: column; gap: 4px; border: 1px solid var(--border); border-radius: 8px; padding: 8px; }
.perfil-bloco-lista:empty { display: none; }
.perfil-bloco-item { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.perfil-bloco-item input { width: auto; flex: none; }
.perfil-bloco-cartao { padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; }
.cascata-painel { margin-top: 10px; padding: 12px; border: 1px solid var(--danger); border-radius: 10px; max-height: min(72vh, 600px); overflow-y: auto; }
.cascata-resumo { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; margin-bottom: 6px; }
.cascata-titulo { margin: 10px 0 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
.cascata-lista { margin: 0; padding-left: 18px; font-size: 12.5px; max-height: 150px; overflow-y: auto; }
.cascata-aviso { margin-top: 10px; padding: 8px 10px; border-radius: 8px; border: 1px solid var(--warning); font-size: 12.5px; }
.cascata-escolha { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }

/* v1.5.134 (Pacote 4b) — aba "Gestão remota": lista de computadores, monitor e editor. Painéis na própria página; tudo rola por dentro (sem popup). */
.gr-lista { max-height: min(420px, 55vh); overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.gr-pc { display: flex; flex-direction: column; gap: 6px; }
.gr-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.gr-chip { font-size: 11.5px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--border); }
.gr-chip-ok { border-color: var(--success, #168a52); color: var(--success, #168a52); }
.gr-chip-aviso { border-color: var(--warning); color: var(--warning); }
.gr-chip-erro { border-color: var(--danger); color: var(--danger); font-weight: 600; }
.gr-acoes { display: flex; flex-wrap: wrap; gap: 6px; }
.gr-monitor { max-height: min(60vh, 560px); overflow-y: auto; }
.gr-equip-grade { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 8px; }
.gr-equip { display: flex; flex-direction: column; gap: 3px; }
.gr-ponto { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--text-muted); }
.gr-ponto-online { background: var(--success, #168a52); }
.gr-ponto-offline { background: var(--danger); }
.gr-ponto-inativo { background: var(--text-muted); opacity: .6; }
.gr-editor { margin-top: 12px; padding: 12px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); max-height: min(72vh, 620px); overflow-y: auto; }
.gr-ed-grade { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0 12px; }
.equip-info { border-top: 1px solid var(--border); padding-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.equip-info-topo { display: flex; align-items: center; justify-content: space-between; }
.equip-info-atualizar { padding: 0 8px; font-size: 13px; line-height: 22px; }
.equip-capacidade { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.equip-capacidade > div { display: flex; flex-direction: column; align-items: center; padding: 4px; border-radius: 8px; background: var(--surface-2); }
.equip-capacidade strong { font-size: 15px; font-variant-numeric: tabular-nums; }
.equip-capacidade span { font-size: 10.5px; color: var(--text-muted); }
.equip-info-rodape { font-size: 11px; color: var(--text-muted); }
.equip-card .equip-acoes { justify-content: flex-start; border-top: 1px solid var(--border); padding-top: 8px; margin-top: auto; }
.equip-card .equip-acoes button { font-size: 12px; padding: 4px 8px; }
/* v1.5.98 — "Testar este facial" (item 280/2 do DOCUMENTACAO.md). */
.equip-teste-camera { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.equip-teste-camera-img { max-width: 100%; max-height: 160px; border-radius: 8px; border: 1px solid var(--border); }
.equip-card .equip-login-aviso { margin: 0; }

/* v1.5.60 — busca direta por nome/ID nos faciais. */
.varredura-busca-direta { border: 1px solid var(--border); border-radius: 8px; padding: 10px; margin-bottom: 14px; }

/* v1.5.61 — "Enviar para as máquinas" (operador). */
.envio-maquinas-dialog { width: 520px; max-width: calc(100vw - 32px); max-height: 86vh; overflow-y: auto; }
.envio-maquinas-ajuda { margin: 0 0 10px; font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
.envio-maquinas-lista { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.envio-maquinas-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; flex-wrap: wrap; }
.envio-maquinas-item input { width: auto; margin: 0; }
.envio-maquinas-item small { color: var(--text-muted); }
.envio-maquinas-resposta { margin-left: auto; font-size: 12px; color: var(--text-muted); }
.envio-maquinas-resposta.ok { color: var(--success, #2e7d32); }
.envio-maquinas-resposta.aviso { color: var(--warning, #b8860b); }
.envio-maquinas-status { font-size: 12.5px; min-height: 18px; margin-bottom: 10px; }
.envio-maquinas-acoes { display: flex; gap: 8px; flex-wrap: wrap; }

/* v1.5.61 — "Puxar" um item por vez em "Puxar da nuvem com seleção". */
.nuvem-item-linha .nuvem-puxar-item { font-size: 11px; padding: 1px 8px; flex-shrink: 0; margin-left: auto; }
.nuvem-item-puxado .nuvem-puxar-item { color: var(--success, #2e7d32); }

/* v1.5.62 — escolher quais cadastros baixar da nuvem (bloco/unidade). */
.baixar-escolha-dialog { width: 620px; max-width: calc(100vw - 32px); max-height: 86vh; display: flex; flex-direction: column; }
.baixar-escolha-ajuda { margin: 0 0 10px; font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
.baixar-escolha-topo { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.baixar-escolha-topo input[type=text] { flex: 1; min-width: 200px; }
.baixar-escolha-topo label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: normal; cursor: pointer; }
.baixar-escolha-topo input[type=checkbox], .baixar-escolha-item input { width: auto; margin: 0; }
.baixar-escolha-lista { overflow-y: auto; min-height: 60px; max-height: 50vh; display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.baixar-escolha-item { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; font-weight: normal; }
.baixar-escolha-item[hidden] { display: none; }
.baixar-escolha-item img, .baixar-escolha-item .avatar-vazio { width: 36px; height: 36px; border-radius: 6px; object-fit: contain; background: var(--bg); flex-shrink: 0; }
.baixar-escolha-texto { min-width: 0; font-size: 13px; line-height: 1.35; }
.baixar-escolha-texto small { color: var(--text-muted); font-size: 11.5px; }
.baixar-escolha-acoes { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.baixar-escolha-status { font-size: 12.5px; }

/* v1.5.63 — botão "Baixar" no card de um cadastro da nuvem (página da unidade). */
.pessoa-card-nuvem-acoes { display: flex; justify-content: flex-end; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.btn-baixar-card-nuvem { font-size: 12px; padding: 4px 10px; display: inline-flex; align-items: center; gap: 4px; }
.btn-baixar-card-nuvem svg { width: 14px; height: 14px; }

/* v1.5.64 — limpeza de unidades repetidas no mesmo bloco. */
.dup-grupo { border: 1px solid var(--border); border-radius: 8px; padding: 10px; margin-bottom: 8px; }
.dup-grupo-titulo { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; font-weight: normal; }
.dup-grupo-titulo input, .dup-unidade input { width: auto; margin: 0; }
.dup-grupo-titulo small, .dup-unidade small { display: block; color: var(--text-muted); font-size: 11.5px; }
.dup-ajuda { margin: 8px 0 4px; font-size: 12px; color: var(--text-muted); }
.dup-unidades { display: flex; flex-direction: column; gap: 4px; }
.dup-unidade { display: flex; align-items: flex-start; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; font-weight: normal; font-size: 13px; }
.dup-unidade:hover { background: var(--bg); }
.dup-relatorio { margin: 6px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--text-muted); }

/* v1.5.65 — aviso de visita num destino onde a pessoa não tem cadastro. */
.aviso-visita-destino {
  margin-top: 6px; padding: 8px 10px; border-radius: 8px; font-size: 12.5px; line-height: 1.45;
  background: var(--card-bg); border: 1px solid var(--warning); color: var(--text);
}

/* v1.5.117 — chip com o trecho da máquina e do operador no topo do Cadastro/Unidade (auth.js::_montarChipIdentidade).
   Medido com a câmera do Cadastro em 1280x720, 1366x768 e 1920x1080: nenhuma diferença com o chip. Em janelas de 1060 px ou menos o topo
   quebraria de linha (e a câmera fica só com a altura que sobra), então o chip some ali. */
@media (max-width: 1060px) {
  #auth-chip-identidade { display: none; }
}
