/* =============================================================================
   JARVIS — linguagem visual (contrato §12)

   Regras que este arquivo obedece, sem exceção:
   - Fundo quase preto #05070a com vinheta radial suave. O canvas É a página;
     os painéis flutuam por cima dele.
   - UM acento: esmeralda #22c55e. Marca ESTADO (ouvindo, ativo, em foco).
     Nunca decoração. Todo o resto é ardósia/cinza.
   - Painéis de vidro: rgba(10,14,20,.72) + backdrop-filter, 1px de borda a 12%,
     raio 14px, sem sombra projetada.
   - Régua, não caixa: listas separadas por fios de 1px. Nenhum cartão dentro de
     outro cartão. Nenhuma borda-listra lateral. Nenhum texto em degradê.
   - Escala de ESPAÇO: 4 8 12 16 24 32 48, nas fichas --s1..--s7. Governa três
     propriedades e só essas três: margin, padding e gap. Nelas não entra número
     solto — a única exceção é o `margin: -1px` de .sr-only e .seg-input, que não
     é espaço e sim o recorte de esconder da vista sem esconder do leitor de tela.
     O que a escala NÃO governa, e por isso está escrito em px no lugar onde
     vale: altura de linha (.chip 22px, .badge 20px, ambas cravadas para a
     pílula fechar em volta da letra), largura de coluna (.kv 92px, o rótulo mais
     comprido do inspetor), teto de tamanho (#reactor 160px) e peça
     desenhada (#stage::after, 180px de brilho). Esses seguem a letra e o
     desenho, não a régua do espaço, e ficam no seletor, nunca como ficha.
   - UM orçamento de altura para a resposta, guardado num sítio só: #bottom.
     O cartão não tem teto próprio. Ver a seção 8.
   - Nada aparece só no :hover. Isto roda no telemóvel, onde hover não existe.
   - Todo botão tem no mínimo 44x44 px. Todo controle tem anel de foco visível.
   - prefers-reduced-motion: reduce mata o movimento sem mexer no layout.

   Contraste MEDIDO no navegador, não calculado no papel: duas capturas da
   mesma tela, uma como ela é e outra com todo o texto tornado transparente. O
   pixel que muda entre as duas é pixel de LETRA, e o valor dele na segunda é o
   fundo que aquela letra tem por trás. Assim uma pastilha <kbd> ao lado da
   frase não entra na conta como se fosse o fundo dela.
     1440x900 ... vazio, Recuperar e Ambiente ...... 304 textos
      390x844 ... vazio, Recuperar, Filtro, Inspetor 122 textos
     426 no total, NENHUM abaixo de 4,5:1. Pior de todos: 4,93:1, o rótulo
     micro #7c8ba0 a 11px no cartão Recuperar de um telemóvel — é sempre ele.
     Extremo teórico, com uma chapa BRANCA por baixo de todo o vidro, coisa que
     o grafo nunca produz: o micro cai a 4,44:1 e o corpo fica em 7,4:1. Num
     telemóvel esse extremo deixou de existir: ali as folhas são opacas.
   O contrato pede #64748b nos micro-rótulos; a 10px isso dá 4,04:1, abaixo do
   mínimo legível de 4,5:1. Subimos para #7c8ba0, mesma família ardósia, e
   guardamos #64748b em --ink-dim só para traço e ponto, nunca para texto.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   1. Fichas (tokens). app.js e graph.js leem daqui com getComputedStyle.
   -------------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  /* — fundo — */
  --bg: #05070a;
  --bg-2: #070a0f;
  --bg-3: #0b1119;

  /* — superfícies — */
  --panel: rgba(10, 14, 20, .72);
  --panel-fallback: rgba(8, 11, 16, .95);
  /* A MESMA cor do vidro, sem transparência nenhuma. Existe para o telemóvel,
     onde os painéis são folhas de ponta a ponta por cima do grafo inteiro e
     não janelas flutuando na beira dele: ali «ver através» é o nome bonito de
     um nó do grafo a passar por baixo da palavra. Também veste a barra fixa
     dos filtros, que fica colada por cima da lista que rola. */
  --panel-solid: #0a0e14;
  /* O painel COMPOSTO, medido de uma captura real no meio de uma lista: o
     vidro mais o véu de --panel-lift dão (13,17,22) tanto no computador como
     no telemóvel. Serve à barra colada dos filtros, que precisa de tapar a
     lista sem se ver: um fundo 3 níveis mais escuro já é uma caixa dentro da
     caixa, e caixa dentro de caixa é proibido. */
  --panel-sticky: #0d1116;
  --panel-inset: rgba(255, 255, 255, .03);
  --panel-lift-top: rgba(148, 163, 184, .055);
  --panel-lift: rgba(148, 163, 184, .035);
  --hairline: rgba(148, 163, 184, .08);
  --border: rgba(148, 163, 184, .12);
  --border-strong: rgba(148, 163, 184, .22);
  --fill-soft: rgba(148, 163, 184, .06);
  --fill: rgba(148, 163, 184, .12);
  --fill-strong: rgba(148, 163, 184, .18);

  /* — tinta — */
  --ink-strong: #e2e8f0;
  --ink: #cbd5e1;
  --ink-2: #94a3b8;
  --ink-3: #8a99ad;
  --ink-micro: #7c8ba0;
  --ink-dim: #64748b;

  /* — acento único: estado, nunca enfeite — */
  --accent: #22c55e;
  --accent-hover: #4ade80;
  --accent-glow: rgba(34, 197, 94, .35);
  --accent-line: rgba(34, 197, 94, .45);
  --accent-soft: rgba(34, 197, 94, .12);
  --accent-ink: #05070a;

  /* — sinais — */
  --danger: #fca5a5;
  --danger-line: rgba(248, 113, 113, .45);
  --danger-soft: rgba(248, 113, 113, .12);
  --warn: #fcd34d;
  --warn-line: rgba(251, 191, 36, .45);
  --warn-soft: rgba(251, 191, 36, .12);

  /* — paleta de tipos, mesma ordem que o servidor manda em /api/graph §10.2.
       O servidor é a fonte da verdade; estes valores existem para o esqueleto
       estático e como reserva se a chamada falhar.
       Nenhuma cor daqui pode ser o esmeralda do acento: o acento é estado
       (ouvindo, foco, filtro ligado, seleção). O sexto tipo é turquesa
       #2dd4bf por isso — mesma família fria, mas nunca confundível com o
       verde de estado. — */
  --type-1: #3b82f6;
  --type-2: #e2e8f0;
  --type-3: #f59e0b;
  --type-4: #60a5fa;
  --type-5: #a78bfa;
  --type-6: #2dd4bf;
  --type-7: #f472b6;
  --type-8: #fbbf24;
  --type-9: #fb923c;
  --type-10: #94a3b8;
  --type-11: #64748b;

  /* — espaço: só estes sete degraus — */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  /* — forma — */
  --r-panel: 14px;
  --r-control: 10px;
  --r-pill: 999px;
  --tap: 44px;
  --panel-w: 312px;
  --console-w: 720px;

  /* — letra — */
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Code", "JetBrains Mono", Consolas, monospace;
  --fs-micro: 10px;
  --fs-xs: 11px;
  --fs-sm: 12px;
  --fs-body: 13px;
  --fs-md: 15px;
  --fs-lg: 16px;
  --fs-xl: 20px;
  --track-micro: .18em;
  --track-brand: .32em;

  /* — movimento — */
  --ease: cubic-bezier(.2, .6, .2, 1);
  --dur: 160ms;

  /* — camadas, nomeadas, nunca 9999 — */
  --z-canvas: 0;
  --z-glow: 10;
  --z-panel: 20;
  --z-sheetnav: 30;
  --z-bottom: 40;
  --z-header: 50;
  /* As duas telas que vêm antes do palco. O portão fica por cima do
     assistente: se as duas quisessem a tela ao mesmo tempo, quem manda é a
     senha. Os avisos altos continuam acima das duas, porque é por eles que o
     portão diz que a senha está errada. */
  --z-setup: 55;
  --z-gate: 57;
  --z-toast: 60;
}

@media (max-width: 1200px) {
  :root {
    --panel-w: 264px;
  }
}

/* -----------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

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

html {
  height: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  overscroll-behavior: none;
  font-family: var(--sans);
  font-size: var(--fs-body);
  line-height: 1.5;
  color: var(--ink);
  background-color: var(--bg);
  /* vinheta radial suave: claro no centro, onde vive o grafo */
  background-image: radial-gradient(
    118% 92% at 50% 44%,
    var(--bg-3) 0%,
    var(--bg-2) 48%,
    var(--bg) 100%
  );
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
p,
dl,
dd,
figure,
blockquote,
ol,
ul {
  margin: 0;
  padding: 0;
}

ol,
ul {
  list-style: none;
}

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

canvas {
  display: block;
}

kbd {
  padding: 0 var(--s1);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: .9em;
  white-space: nowrap;
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--s1);
}

::selection {
  background: var(--accent-soft);
  color: var(--ink-strong);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Atalho de teclado. Aparece no foco — foco não é hover: é a única forma de
   servir quem navega por teclado sem poluir a tela de todo mundo. */
.skip {
  position: fixed;
  top: var(--s2);
  left: var(--s2);
  z-index: var(--z-toast);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--s4);
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  background: var(--accent);
  border-radius: var(--r-control);
  transform: translateY(calc(-100% - var(--s5)));
  transition: transform var(--dur) var(--ease);
}

.skip:focus-visible {
  transform: translateY(0);
}

/* -----------------------------------------------------------------------------
   2b. Compasso de espera do idioma
   -------------------------------------------------------------------------- */

/* O esqueleto chega sem uma única frase escrita: quem as escreve é a tabela em
   ui/i18n.js, e o idioma só é certo depois de /api/status. Enquanto isso o
   palco fica invisível — ocupando o mesmo espaço, sem mexer no layout — porque
   um instante de espera é melhor do que um piscar na língua errada.

   Só app.js põe este atributo, e ele tem uma rede de 1,5 s que o tira mesmo se
   o servidor não responder. Sem JavaScript o atributo nunca existe e nada aqui
   chega a valer: a página não fica escondida por causa desta regra. */
:root[data-i18n-state="pending"] #stage,
:root[data-i18n-state="pending"] .veil,
:root[data-i18n-state="pending"] .skip {
  visibility: hidden;
}

/* -----------------------------------------------------------------------------
   3. Palco e grafo
   -------------------------------------------------------------------------- */

#stage {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

#graph {
  position: absolute;
  inset: 0;
  z-index: var(--z-canvas);
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: grab;
}

#graph:active {
  cursor: grabbing;
}

/* O canvas é ponto de tabulação (graph.js põe tabindex="0" e escuta o teclado).
   Como ele é `inset: 0`, ocupa a tela inteira: o anel padrão, que fica 2px POR
   FORA, cai fora do ecrã e quem navega por teclado não vê nada. Aqui o anel vai
   para DENTRO — mesma espessura, mesma cor, agora visível. O acento aqui é
   legítimo: é foco, que é estado. */
#graph:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
}

/* Brilho de estado no rodapé: só acende quando o JARVIS está ouvindo ou
   falando. É estado, não enfeite. Fica invisível enquanto nada acontece. */
#stage::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: var(--z-glow);
  height: 180px;
  pointer-events: none;
  background: radial-gradient(62% 100% at 50% 100%, var(--accent-glow), transparent 72%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

body[data-state="listening"] #stage::after {
  opacity: .5;
}

body[data-state="speaking"] #stage::after {
  opacity: .26;
}

/* -----------------------------------------------------------------------------
   4. Superfície de vidro, compartilhada por painéis e console
   -------------------------------------------------------------------------- */

/* O `brightness` no backdrop é o que torna o contraste previsível: sem ele, um
   nó claro do grafo passando atrás do vidro derruba o texto para 2,5:1. Com
   ele, o pior caso possível ainda fica em 4,55:1 no micro-rótulo e 10,6:1 no
   corpo. O véu de --panel-lift devolve ao painel a aparência de superfície:
   só com a cor do contrato o vidro some por cima de um fundo quase preto. */
.panel,
#bottom {
  background-color: var(--panel);
  background-image: linear-gradient(180deg, var(--panel-lift-top), var(--panel-lift));
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  box-shadow: inset 0 1px 0 var(--panel-inset);
  -webkit-backdrop-filter: blur(14px) brightness(.28) saturate(1.05);
  backdrop-filter: blur(14px) brightness(.28) saturate(1.05);
}

/* Sem backdrop-filter o vidro vira chapa: mais opaco para o texto continuar
   legível por cima do grafo. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .panel,
  #bottom {
    background-color: var(--panel-fallback);
  }
}

#left,
#right {
  position: absolute;
  top: calc(var(--s5) + var(--tap) + var(--s4));
  bottom: var(--s5);
  z-index: var(--z-panel);
  display: flex;
  flex-direction: column;
  width: var(--panel-w);
  overflow: hidden;
}

#left {
  left: var(--s5);
}

#right {
  right: var(--s5);
}

/* Seções dentro de um painel: separadas por fio, nunca por outro cartão. */
.pane {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--s4);
}

.pane + .pane {
  border-top: 1px solid var(--hairline);
}

.pane--grow {
  flex: 1 1 auto;
}

.pane--hubs {
  flex: 0 1 auto;
  max-height: 46%;
}

/* Inspetor vazio não reserva coluna: encolhe até o tamanho do próprio texto e
   entrega o resto aos hubs. Com uma nota aberta ele volta a mandar na altura.
   Em navegador sem :has() nada quebra — fica o comportamento antigo. */
#inspector:has(> #inspectorBody > .empty) {
  flex: 0 0 auto;
}

#inspector:has(> #inspectorBody > .empty) ~ .pane--hubs {
  flex: 1 1 auto;
  max-height: none;
}

/* Núcleo: é a única peça de teatro da tela, e teatro cede lugar a trabalho.
   Antes ele era `flex: 0 0 auto` e comia 43% da altura do painel direito, o
   que empurrava três dos onze tipos e os botões do filtro para fora da vista.
   Agora quem cede é ele: o desenho tem teto de tamanho amarrado à altura da
   janela (ver #reactor), e o bloco é `0 1 auto` para, se um dia o painel ficar
   apertado a ponto de transbordar, ser este o primeiro a ceder — nunca a lista. */
.pane--reactor {
  flex: 0 1 auto;
  min-height: 0;
  align-items: center;
  padding: var(--s3) var(--s3) var(--s4);
}

/* Blindagem: nada que app.js injete pode alargar a casca nem vazar para fora.
   Um caminho de arquivo comprido quebra a linha em vez de empurrar o painel. */
.panel > *,
#bottom > *,
#card > *,
.toast > * {
  min-width: 0;
}

#inspectorBody,
#hubsList,
#filtersList,
#card,
#transcript,
.toast {
  overflow-wrap: break-word;
}

/* -----------------------------------------------------------------------------
   5. Vocabulário de conteúdo (usado pelo esqueleto e por app.js)
   -------------------------------------------------------------------------- */

.micro {
  flex: 0 0 auto;
  margin-bottom: var(--s3);
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
}

.micro .count {
  margin-left: var(--s2);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

.scroll {
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, .28) transparent;
}

/* Estado vazio honesto: diz o que fazer, não “nada aqui”. */
.empty {
  padding: var(--s2) 0 var(--s1);
  color: var(--ink-2);
}

.empty strong {
  display: block;
  margin-bottom: var(--s1);
  color: var(--ink);
  font-size: var(--fs-body);
  font-weight: 600;
  text-wrap: balance;
}

.empty p {
  max-width: 46ch;
  text-wrap: pretty;
}

/* 46ch é a medida certa numa coluna de 360px. Na barra de baixo, que tem mais
   de 800px, ela quebrava a mesma frase em três linhas e inflava o console com
   espaço vazio. Ali a linha pode correr. */
#card .empty p {
  max-width: 72ch;
}

.empty p + p {
  margin-top: var(--s2);
  color: var(--ink-micro);
  font-size: var(--fs-sm);
}

/* Instrução que só existe com rato e teclado. Num telemóvel ela ocupa duas
   linhas para explicar algo que não dá para fazer ali. */
@media (hover: none) and (pointer: coarse) {
  .only-desk {
    display: none;
  }
}

/* Linha de lista: régua, nunca caixa. A linha inteira é o alvo, com 44px de
   altura mínima e um fio de 1px entre uma e outra. Vale tanto para botões
   soltos dentro do container quanto para <li> com o controle dentro. */
.row,
#hubsList > :is(button, a),
#hubsList li > :is(button, a),
#filtersList > :is(button, a, label):not(.btn),
#filtersList li > :is(button, a, label):not(.btn) {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: var(--tap);
  padding: var(--s2) 0;
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-body);
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--r-control);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

/* Os fios. Um por vizinho, seja o próprio controle ou o <li> que o embrulha. */
.row + .row,
#hubsList > :not(:first-child),
#filtersList > :not(:first-child),
#hubsList li + li,
#filtersList li + li {
  border-top: 1px solid var(--hairline);
}

@media (hover: hover) {
  .row:hover,
  #hubsList :is(button, a):hover,
  #filtersList :is(button, a, label):not(.btn):hover {
    color: var(--ink-strong);
    background-color: var(--fill-soft);
  }
}

.row:active,
#hubsList :is(button, a):active,
#filtersList :is(button, a, label):not(.btn):active {
  background-color: var(--fill);
}

/* O anel do foco vai para dentro: a lista rola com overflow escondido e um
   anel para fora seria cortado. Fundo de acento bem discreto, porque num
   telemóvel o foco é a única pista que sobra — hover não existe lá. */
.row:focus-visible,
#hubsList :is(button, a):focus-visible,
#filtersList :is(button, a, label):not(.btn):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  background-color: var(--accent-soft);
}

/* Tipo desligado no filtro: continua legível e clicável, só recua — a tinta
   esmaece e o ponto de cor cai para um quarto. Nunca some. */
.row[aria-pressed="false"],
#filtersList [aria-pressed="false"] {
  color: var(--ink-micro);
}

.row[aria-pressed="false"] .dot,
#filtersList [aria-pressed="false"] .dot {
  opacity: .25;
}

/* Hubs: o nome é a informação, então é ele que fica claro. O acento é só da
   linha em foco — é ela que está aberta no inspetor. */
#hubsList .row-title {
  color: var(--ink-strong);
  font-weight: 500;
}

.row[aria-current="true"],
.row[data-focused="true"],
.row.is-active,
#hubsList [aria-current="true"],
#hubsList [data-focused="true"],
#hubsList .is-active {
  color: var(--accent);
}

#hubsList :is([aria-current="true"], [data-focused="true"], .is-active) :is(.row-title, .num) {
  color: var(--accent);
}

.row-main {
  min-width: 0;
}

.row-title {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.row-sub {
  display: block;
  overflow: hidden;
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Ponto de cor do tipo. app.js define --c com a cor que o servidor mandou. */
.dot {
  width: 8px;
  height: 8px;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  background: var(--c, var(--ink-2));
  opacity: 1;
  transition: opacity var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

/* Números: monoespaçado, alinhados à direita, algarismos de largura fixa. */
.num {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s2);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  letter-spacing: .04em;
  line-height: 22px;
  white-space: nowrap;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}

/* Pares rótulo/valor do inspetor. */
.kv {
  display: grid;
  gap: var(--s2);
  margin-top: var(--s3);
}

.kv > div {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: var(--s3);
  align-items: baseline;
  padding-top: var(--s2);
  border-top: 1px solid var(--hairline);
}

.kv dt {
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
}

.kv dd {
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--ink);
  font-size: var(--fs-sm);
}

.kv dd.path {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--ink-2);
}

.prose {
  max-width: 68ch;
  color: var(--ink);
  font-size: var(--fs-md);
  line-height: 1.5;
  overflow-wrap: break-word;
  text-wrap: pretty;
}

.prose + .prose {
  margin-top: var(--s3);
}

/* Trecho vindo de um arquivo. Sem borda-listra: recuo e letra monoespaçada. */
.quote {
  margin-top: var(--s2);
  padding-left: var(--s3);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

/* -----------------------------------------------------------------------------
   6. Cabeçalho e distintivos
   -------------------------------------------------------------------------- */

#brand {
  position: absolute;
  top: var(--s5);
  left: var(--s5);
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  gap: var(--s3);
  height: var(--tap);
  pointer-events: none;
}

#brand h1 {
  color: var(--ink-strong);
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: var(--track-brand);
  text-transform: uppercase;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  padding: 0 var(--s2);
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-micro);
  line-height: 20px;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}

.badge--live {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.badge--warn {
  color: var(--warn);
  background: var(--warn-soft);
  border-color: var(--warn-line);
}

.badge--danger {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: var(--danger-line);
}

/* -----------------------------------------------------------------------------
   7. Botões
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 var(--s3);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-control);
  cursor: pointer;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

/* `display: inline-flex` acima vence a regra `[hidden] { display: none }` que
   vem do navegador, e um botão escondido continuava à vista — foi assim que o
   "Cancelar" sobreviveu ao fim do trabalho. Aqui a conta fica certa. */
.btn[hidden] {
  display: none;
}

.btn svg {
  flex: 0 0 auto;
}

@media (hover: hover) {
  .btn:hover {
    color: var(--ink-strong);
    background: var(--fill);
    border-color: var(--border-strong);
  }
}

.btn:active {
  background: var(--fill-strong);
}

.btn:disabled {
  cursor: not-allowed;
  opacity: .45;
}

.btn--icon {
  padding: 0;
}

/* O botão que É a ação principal da tela onde está — hoje só um: «Voltar a este
   ponto», na tela de recuperar. Ele pesa mais com o que a régua já tem, tinta
   forte e fio forte, e NÃO com o esmeralda: o acento é estado (a ouvir, em
   foco, filtro ligado, seleção, enviar armado, aviso de sucesso), e uma sexta
   coisa a usá-lo apagaria as outras cinco. Hierarquia por contenção. */
.btn--strong {
  color: var(--ink-strong);
  background: var(--fill);
  border-color: var(--border-strong);
}

@media (hover: hover) {
  .btn--strong:hover {
    background: var(--fill-strong);
  }
}

.btn--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--ink-2);
}

@media (hover: hover) {
  .btn--quiet:hover {
    color: var(--ink-strong);
    background: var(--fill-soft);
    border-color: var(--border);
  }
}

/* Microfone: neutro parado, esmeralda a ouvir. */
#btnMic {
  position: relative;
  color: var(--ink-strong);
  border-color: var(--border-strong);
}

#btnMic::after {
  content: "";
  position: absolute;
  inset: -4px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-control);
  opacity: 0;
  transform: scale(.92);
  transform-origin: center;
  pointer-events: none;
}

body[data-state="listening"] #btnMic,
#btnMic.is-listening {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}

body[data-state="listening"] #btnMic::after,
#btnMic.is-listening::after {
  opacity: 1;
  animation: jv-pulse 1.6s var(--ease) infinite;
}

body[data-state="thinking"] #btnMic,
#btnMic.is-thinking {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

@keyframes jv-pulse {
  0% {
    opacity: .9;
    transform: scale(.92);
  }

  70% {
    opacity: 0;
    transform: scale(1.2);
  }

  100% {
    opacity: 0;
    transform: scale(1.2);
  }
}

/* Som ligado/desligado: dois ícones, um por estado. Nunca depende de hover. */
#btnMute .icon-mute {
  display: none;
}

#btnMute[aria-pressed="true"] .icon-mute,
#btnMute.is-muted .icon-mute {
  display: block;
}

#btnMute[aria-pressed="true"] .icon-sound,
#btnMute.is-muted .icon-sound {
  display: none;
}

#btnMute[aria-pressed="true"],
#btnMute.is-muted {
  color: var(--warn);
  border-color: var(--warn-line);
}

/* -----------------------------------------------------------------------------
   8. Console de baixo: cartão, transcrição e barra de pergunta.
      Uma única superfície de vidro, dividida por fios.
   -------------------------------------------------------------------------- */

/* O console cresce para cima a partir do rodapé. Sem teto, um painel de
   trabalho comprido empurrava o topo dele para fora do ecrã e o senhor perdia
   o cabeçalho do trabalho e os primeiros passos, sem forma de os alcançar.
   O teto reserva a faixa da marca: 24 de margem + 44 do cabeçalho + 16 de
   respiro + 24 da margem de baixo. */
#bottom {
  position: absolute;
  right: calc(var(--panel-w) + var(--s7));
  bottom: var(--s5);
  left: calc(var(--panel-w) + var(--s7));
  z-index: var(--z-bottom);
  display: flex;
  flex-direction: column;
  max-width: var(--console-w);
  max-height: calc(100dvh - (var(--s5) + var(--tap) + var(--s4) + var(--s5)));
  margin-inline: auto;
  overflow: hidden;
}

/* O CARTÃO NÃO TEM TETO PRÓPRIO, e isto é a correção de um defeito medido.
   Ele tinha `max-height: min(38dvh, 420px)`, o que num ecrã de 1440x900 dava
   342px cravados: o Resumo do dia trazia 940px de resposta e escondia 598
   (dois dos sete compromissos à vista), o Restaurar escondia 427, o Ambiente
   515 num telemóvel. O produto respondia bem e tapava a resposta.

   E o teto era inventado: o console flutua sobre um canvas quase vazio, e o
   orçamento de altura de verdade já existia em #bottom, que reserva a faixa
   da marca e mais nada. Dois tetos a disputar o mesmo espaço fizeram nascer
   uma exceção por tela (Restaurar 52dvh, telemóvel 26dvh, paisagem 20dvh):
   cada uma a fugir do teto partilhado. Foram-se todas.

   Agora é um orçamento só, guardado num sítio só. O cartão é do tamanho da
   resposta, e quando o console inteiro estoura o orçamento é ele que cede
   primeiro — nunca a transcrição, nunca a barra de pergunta. O que ficar de
   fora rola, e o rodapé #cardMais diz por escrito que continua. */
#card {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--s4);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, .28) transparent;
}

/* Chão garantido, e SÓ no caso que o pediu: com o painel do trabalho aberto
   por cima, a tela de Ambiente era esmagada até sobrarem duas linhas e os
   botões de Instalar — o motivo de ele ter aberto aquela tela — caíam abaixo
   do corte. Sem painel aberto o cartão não tem chão nenhum, senão uma resposta
   de duas linhas viria com 300px de vidro vazio por baixo.
   O pedido de chave é a única coisa que ganha deste chão: ali o campo é que
   não pode sair do ecrã, e o cartão cede tudo. A exceção está DENTRO do mesmo
   seletor, e não numa regra a seguir: duas regras a disputar por especificidade
   é como o chão acabou a vencer o teto do pedido de chave — medido, 234px de
   cartão onde a regra da chave pedia 180. */
@media (min-width: 900px) {
  #bottom:has(#evolucao:not([hidden])):not(:has(#evolucaoChave:not([hidden])))
    #card:not(:has(> .empty)) {
    min-height: min(26dvh, 240px);
  }
}

#card > * {
  animation: jv-rise var(--dur) var(--ease);
}

@keyframes jv-rise {
  from {
    opacity: .2;
    transform: translateY(4px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* «Continua abaixo». O aviso de que a resposta não acabou onde a vista acaba.

   Uma barra de rolagem fina num vidro escuro não é aviso nenhum, e num
   telemóvel nem existe. Isto é: uma linha escrita, do tamanho do dedo, que
   aparece só quando há mesmo mais para ler e some no fim. Um toque desce uma
   página; chegado ao fim, a mesma linha passa a «Voltar ao início» e sobe.

   Vive FORA do #card de propósito. Dentro dele, mostrar e esconder mudava a
   altura que se estava a medir para decidir mostrar ou esconder. Aqui é mais
   uma seção do console, separada por fio, como manda a régua. */
#cardMais {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  width: 100%;
  min-height: var(--tap);
  padding: var(--s1) var(--s4);
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: var(--fs-xs);
  background: none;
  border: 0;
  border-top: 1px solid var(--hairline);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease);
}

#cardMais[hidden] {
  display: none;
}

@media (hover: hover) {
  #cardMais:hover {
    color: var(--ink-strong);
  }
}

#cardMais:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* A seta acompanha a palavra: para baixo enquanto há mais, para cima quando a
   linha passa a ser o caminho de volta. Quem não vê a seta lê a frase. */
#cardMais svg {
  flex: 0 0 auto;
  transition: transform var(--dur) var(--ease);
}

#cardMais[data-dir="topo"] svg {
  transform: rotate(180deg);
}

/* Rolado para baixo, o topo do cartão foi cortado. O desvanecer de 20px diz
   isso sem ocupar linha nenhuma. É máscara, não fundo: o vidro continua vidro. */
#card[data-rolado="1"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 20px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 20px);
}

.card-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s3);
}

.card-title {
  min-width: 0;
  overflow: hidden;
  color: var(--ink-strong);
  font-size: var(--fs-body);
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-note {
  margin-top: var(--s3);
  padding-top: var(--s3);
  color: var(--ink-micro);
  font-size: var(--fs-sm);
  border-top: 1px solid var(--hairline);
}

/* Aviso: ícone e cor, sem listra lateral. */
.notice {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s3);
  align-items: start;
  margin-top: var(--s3);
  padding-top: var(--s3);
  color: var(--warn);
  font-size: var(--fs-sm);
  border-top: 1px solid var(--hairline);
}

.notice--danger {
  color: var(--danger);
}

/* Estado e frase na MESMA linha: “PARADO · Pronto, senhor.” O rótulo de estado
   é uma coluna fixa à esquerda; a frase corre ao lado e quebra sozinha. */
#transcript {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s2) var(--s4);
  color: var(--ink-2);
  font-size: var(--fs-body);
  border-top: 1px solid var(--hairline);
  overflow-wrap: break-word;
  text-wrap: pretty;
}

/* app.js injeta a própria folha depois desta; por isso o seletor com id. */
#transcript .jv-state {
  display: inline;
  flex: 0 0 auto;
  margin-bottom: 0;
}

#transcript .jv-said {
  flex: 1 1 auto;
  min-width: 0;
}

body[data-state="listening"] #transcript,
body[data-state="speaking"] #transcript {
  color: var(--ink-strong);
}

/* Medidor de nível e dica de estado: também são de app.js e também precisam do
   id repetido para vencer a folha dele. Console, não cartão: tudo rente. */
#bottom #bars {
  height: 12px;
  padding: 0 var(--s4) var(--s1);
}

#bottom #hint {
  padding: 0 var(--s4) var(--s2);
  font-size: var(--fs-xs);
  line-height: 1.4;
}

#askform {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s2) var(--s3) var(--s3);
  border-top: 1px solid var(--hairline);
}

.ask-tools {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
}

/* Os três atalhos continuam do tamanho do dedo, só que calados: sem preencher,
   o campo e o microfone é que mandam na barra. */
.ask-tools .btn {
  padding: 0 var(--s2);
  color: var(--ink-2);
  font-size: var(--fs-xs);
  background: transparent;
}

@media (hover: hover) {
  .ask-tools .btn:hover {
    color: var(--ink-strong);
    background: var(--fill-soft);
  }
}

.ask-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* Campo de uma linha feito com <textarea>, para o Shift+Enter prometido na dica
   funcionar de verdade. Altura travada em 44px: 8 + 28 + 8. O que passar disso
   rola dentro do campo — a barra não cresce sozinha por baixo do dedo. */
#ask {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--tap);
  padding: var(--s2);
  color: var(--ink-strong);
  font-family: var(--sans);
  /* 16px evita o zoom automático do iOS ao focar o campo */
  font-size: var(--fs-lg);
  line-height: 1.75;
  background: transparent;
  border: 0;
  border-radius: var(--r-control);
  resize: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  appearance: none;
}

#ask::-webkit-scrollbar {
  width: 0;
  height: 0;
}

#ask::placeholder {
  color: var(--ink-3);
  opacity: 1;
}

#ask:focus {
  outline: none;
}

#ask:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* O console inteiro acende quando o campo está em foco: isso é estado. */
#bottom:focus-within {
  border-color: var(--accent-line);
}

/* Enviar ganha o acento assim que existe algo escrito. */
#ask:not(:placeholder-shown) + #btnSend {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.ask-sep {
  flex: 0 0 auto;
  width: 1px;
  height: var(--s5);
  background: var(--border);
}

/* Três atalhos de TECLADO, e por isso `.only-desk` no esqueleto: num telemóvel
   comiam duas linhas fixas por cima do campo para ensinar teclas que não
   existem debaixo de um dedo, desenhadas em <kbd> como se fossem teclas de
   verdade. O que não podia sumir era como cortar a fala do JARVIS — e não
   sumiu: quem carrega isso no telemóvel é o #hint, que aparece no instante em
   que ele está a falar e diz «Toque no microfone», o gesto que existe ali. */
.ask-hint {
  margin: 0;
  color: var(--ink-micro);
  font-size: var(--fs-xs);
  line-height: 1.5;
  text-wrap: pretty;
}

/* Três frases, uma por <span>, porque cada uma vem inteira da tabela de idiomas
   e monta as próprias teclas. Continuam a correr na mesma linha. */
.ask-hint span + span {
  margin-left: .35em;
}

/* -----------------------------------------------------------------------------
   9. Núcleo (reator)
   -------------------------------------------------------------------------- */

/* Quadrado e centrado, com teto de largura. À largura toda do painel ele virava
   um disco de 288px que engolia a lista de filtros; agora o teto é o menor
   entre 160px e 12% da altura da janela. Numa tela de 900px isso dá 108px e os
   onze tipos mais os botões cabem sem rolar, com folga; numa tela alta cresce
   até 160px, porque aí sobra altura. É o núcleo que cede, nunca a lista.
   app.js observa o tamanho real e redesenha na resolução da tela. */
#reactor {
  flex: 0 0 auto;
  align-self: center;
  width: 100%;
  max-width: min(176px, 18dvh);
  aspect-ratio: 1 / 1;
  margin-inline: auto;
}

/* O rótulo do núcleo fica centrado com o desenho, não encostado à esquerda. */
.pane--reactor .micro {
  align-self: center;
  margin-bottom: var(--s2);
}

/* -----------------------------------------------------------------------------
   10. Avisos altos (toasts)
   -------------------------------------------------------------------------- */

#toasts {
  position: absolute;
  top: var(--s5);
  left: 50%;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  width: min(420px, calc(100% - var(--s7)));
  pointer-events: none;
  transform: translateX(-50%);
}

.toast {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: start;
  padding: var(--s3) var(--s4);
  color: var(--ink);
  font-size: var(--fs-sm);
  background: var(--panel-fallback);
  border: 1px solid var(--border);
  border-radius: var(--r-control);
  box-shadow: inset 0 1px 0 var(--panel-inset);
  pointer-events: auto;
  animation: jv-toast var(--dur) var(--ease);
}

.toast strong {
  display: flex;
  gap: var(--s2);
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: var(--s1);
  color: var(--ink-strong);
  font-weight: 600;
}

/* Quantas vezes o mesmo aviso aconteceu. Só aparece a partir da segunda: uma
   caixa nova por repetição empilhava até tapar a resposta, e «×2» diz o mesmo
   num canto da linha que já existe. Número, não alarme: nunca leva o acento. */
.jv-toast-times {
  flex: 0 0 auto;
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-micro);
}

.jv-toast-times:empty {
  display: none;
}

/* O botão de fechar do aviso é gerado por app.js. Aqui garantimos o alvo de
   toque mínimo mesmo que ele venha só com um “×” dentro. */
.toast button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-control);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

@media (hover: hover) {
  .toast button:hover {
    color: var(--ink-strong);
    background: var(--fill-soft);
  }
}

.toast--warn {
  border-color: var(--warn-line);
}

.toast--warn strong {
  color: var(--warn);
}

.toast--danger {
  border-color: var(--danger-line);
}

.toast--danger strong {
  color: var(--danger);
}

.toast--ok {
  border-color: var(--accent-line);
}

.toast--ok strong {
  color: var(--accent);
}

@keyframes jv-toast {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* -----------------------------------------------------------------------------
   11. Controle segmentado (só abaixo de 900px)
   -------------------------------------------------------------------------- */

#sheetnav {
  display: none;
}

.seg-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.seg {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 var(--s3);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  border-radius: var(--r-control);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.seg-input:checked + .seg {
  color: var(--accent);
  background: var(--accent-soft);
}

.seg-input:focus-visible + .seg {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* -----------------------------------------------------------------------------
   12. Abaixo de 900px: os painéis viram gaveta e o grafo fica com a tela toda
   -------------------------------------------------------------------------- */

@media (max-width: 899px) {
  /* A faixa de cima fica reservada para a marca: a gaveta cresce até ali e
     para. Nada de nome do produto por cima da lista. */
  #stage {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding-top: calc(env(safe-area-inset-top) + var(--s3) + var(--tap) + var(--s2));
  }

  #brand {
    top: calc(env(safe-area-inset-top) + var(--s3));
    left: max(var(--s3), env(safe-area-inset-left));
    max-width: calc(100% - var(--s5));
  }

  /* Num ecrã de 390px não cabem três pastilhas. Enquanto houver trabalho, as
     duas que só dizem "está tudo normal" saem da frente e deixam passar a que
     muda: o aviso do trabalho. O que AVISA nunca sai — nem o DEMO, que diz que
     tudo na tela é inventado, nem o modelo em falta. */
  #brand:has(.ev-flag:not([hidden])) #modeBadge.badge--live,
  #brand:has(.ev-flag:not([hidden])) #brainBadge:not(.badge--warn) {
    display: none;
  }

  #toasts {
    position: fixed;
    top: calc(env(safe-area-inset-top) + var(--s3) + var(--tap) + var(--s2));
    right: max(var(--s3), env(safe-area-inset-right));
    left: max(var(--s3), env(safe-area-inset-left));
    width: auto;
    transform: none;
  }

  /* AQUI O VIDRO ACABA. Num computador o painel é uma janela estreita na beira
     do ecrã, com o grafo a respirar à volta: ver através dele é o desenho.
     Num telemóvel é uma folha de ponta a ponta por cima do meio do grafo, que
     é justamente onde os nós e os nomes são densos — e o que se via através
     era «Revisão com Cliente» a passar por baixo da palavra «Grafo».
     Medido: nome do nó a #e2e8f0 atravessava as três folhas. Opaco resolve na
     raiz, e ainda poupa três camadas de desfoque ao telemóvel. A borda e o véu
     ficam: sem eles a folha deixa de parecer superfície e vira buraco. */
  #left,
  #right,
  #bottom,
  #sheetnav {
    background-color: var(--panel-solid);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* A gaveta: mesma superfície, agora presa em baixo.
     `position: relative` não desloca nada: serve só para o z-index valer. Sem
     ele, o canvas do grafo (que é absoluto) fica POR CIMA da gaveta, do
     seletor e da barra — e engole o toque. */
  #left,
  #right {
    position: relative;
    inset: auto;
    z-index: var(--z-panel);
    order: 1;
    /* Quando falta altura, é a gaveta que cede — nunca a barra de pergunta. */
    flex: 0 1 auto;
    min-height: 0;
    width: auto;
    max-height: 46dvh;
    margin: 0 max(var(--s3), env(safe-area-inset-right)) var(--s2)
      max(var(--s3), env(safe-area-inset-left));
    animation: jv-sheet var(--dur) var(--ease);
  }

  /* Sem :has() (navegador antigo) as duas gavetas simplesmente ficam à vista:
     mais alto, porém nada some. */
  #stage:has(#tabGrafo:checked) #left,
  #stage:has(#tabGrafo:checked) #right,
  #stage:has(#tabInspetor:checked) #right,
  #stage:has(#tabFiltro:checked) #left {
    display: none;
  }

  #sheetnav {
    position: relative;
    z-index: var(--z-sheetnav);
    order: 2;
    flex: 0 0 auto;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s1);
    margin: 0 max(var(--s3), env(safe-area-inset-right)) var(--s2)
      max(var(--s3), env(safe-area-inset-left));
    padding: var(--s1);
    /* Opaco, e não os 95% de antes: 5% de um nome de nó a #e2e8f0 chegava do
       outro lado e batia com a palavra da própria pastilha. */
    background: var(--panel-solid);
    border: 1px solid var(--border);
    border-radius: var(--r-panel);
  }

  /* O orçamento do console num telemóvel. 76dvh deixa passar uma resposta
     comprida quase inteira — o Ambiente cabia em 219px e escondia 515 — e
     ainda guarda uma faixa de grafo à vista por cima, para o senhor saber onde
     está. `0 1 auto` e não `0 0 auto`: se a gaveta e o console juntos não
     couberem, os dois cedem, em vez de o palco transbordar pelo topo. */
  #bottom {
    position: relative;
    inset: auto;
    z-index: var(--z-bottom);
    order: 3;
    flex: 0 1 auto;
    min-height: 0;
    max-height: 76dvh;
    max-width: none;
    margin: 0 max(var(--s3), env(safe-area-inset-right))
      max(var(--s3), env(safe-area-inset-bottom))
      max(var(--s3), env(safe-area-inset-left));
  }

  /* Com uma gaveta aberta, quem manda é a gaveta: foi para ela que ele tocou.
     Sem isto o console ficava com 511px dos 844 e a lista de tipos era espremida
     a 126px — três dos doze tipos à vista, e os três controles fora do ecrã.
     O cartão continua lá, a rolar, e o «Continua abaixo» diz que continua. */
  #stage:has(#tabInspetor:checked) #bottom,
  #stage:has(#tabFiltro:checked) #bottom {
    max-height: 42dvh;
  }

  .pane--hubs {
    max-height: none;
  }

  /* Na gaveta a altura é curta e disputada, e o núcleo é teatro: sai de cena.
     O estado que ele desenha já está escrito por extenso em #transcript, duas
     linhas abaixo, então nada de informação se perde — a lista de tipos, que é
     o motivo de o senhor ter aberto a gaveta, fica com a altura toda. */
  .pane--reactor {
    display: none;
  }

  #inspector:has(> #inspectorBody > .empty) ~ .pane--hubs {
    flex: 1 1 auto;
  }
}

@keyframes jv-sheet {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Telas muito baixas em paisagem: a gaveta encolhe em vez de comer a tela, e
   o console pode usar quase tudo, porque em 400px de altura não há folga para
   repartir com um grafo que ninguém consegue ler assim. */
@media (max-width: 899px) and (max-height: 520px) {
  #left,
  #right {
    max-height: 40dvh;
  }

  #bottom {
    max-height: 86dvh;
  }
}

/* -----------------------------------------------------------------------------
   13. Autonomia (contrato §9): o painel do trabalho ao vivo, o pedido de chave,
       e as duas telas novas (Conectores e Ambiente).

   Regras herdadas, sem exceção:
   - Régua, não caixa. O painel é mais uma seção do console de vidro, separada
     por fio. Nenhum cartão dentro de cartão, nenhuma listra lateral.
   - O acento marca UMA coisa: o passo que está sendo executado agora. Mais o
     enviar da chave, que arma quando o campo tem texto — dois usos já previstos
     no PRODUCT.md. O distintivo de estado NÃO é esmeralda.
   - Nada aparece só no hover. O botão que revela as linhas cruas é botão de
     verdade, com 44px, visível o tempo todo.
   - prefers-reduced-motion para o relógio e o pulso: o anel do passo em
     execução para de bater e fica aceso, parado.
   -------------------------------------------------------------------------- */

/* `overflow-y: auto` é a rede de segurança: quando o console fica apertado — um
   cartão aberto por cima de um trabalho comprido — o painel rola dentro de si
   em vez de transbordar por cima da transcrição. Com espaço a sobrar, esta
   rolagem nunca chega a existir. */
#evolucao {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--s3) var(--s4);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, .28) transparent;
  border-top: 1px solid var(--hairline);
}

#evolucao[hidden] {
  display: none;
}

/* Primeira seção do console: o fio de cima cairia colado à borda de vidro. */
#bottom > #evolucao:first-child {
  border-top: 0;
}

.ev-top {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  min-height: var(--tap);
}

.ev-top .micro {
  margin-bottom: 0;
}

/* `margin-inline-start: auto` e não um espaçador elástico: quando a linha
   quebra num telemóvel, o espaçador levaria a primeira linha inteira e os dois
   botões cairiam encostados à esquerda. Assim eles ficam à direita nas duas
   situações. */
.ev-top-actions {
  display: flex;
  gap: var(--s2);
  margin-inline-start: auto;
}

#evolucaoTempo {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* O pedido é a frase do senhor, citada: recuo e monoespaçado, como .quote. */
.ev-pedido {
  flex: 0 0 auto;
  margin-top: var(--s1);
  padding-left: var(--s3);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

.ev-pedido:empty {
  display: none;
}

/* A narração. Cresce por baixo, rola sozinha, e é o único bloco daqui que
   cede altura: quando o console fica apertado, é ela que encolhe, nunca o
   pedido de chave nem a barra de pergunta. */
#evolucaoPassos {
  flex: 1 1 auto;
  min-height: 0;
  max-height: min(30dvh, 300px);
  margin-top: var(--s3);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(148, 163, 184, .28) transparent;
}

/* Com uma chave pedida, a narração recua: o que importa naquele instante é o
   campo, e ele não pode ficar empurrado para fora do ecrã. */
#evolucao:has(#evolucaoChave:not([hidden])) #evolucaoPassos {
  max-height: min(18dvh, 180px);
}

/* E o cartão recua com ela. Um pedido de chave é a única coisa que este produto
   pede ao senhor, e o trabalho fica parado até ele responder: com uma tela
   aberta por baixo — Recuperar, Conectores — o console dividia a altura em dois
   e o campo caía abaixo da dobra, o que é o mesmo que não existir. O cartão
   continua na tela, encolhido e a rolar; a chave é que manda no espaço.
   Um seletor só: a altura própria do cartão Recuperar, que este par de regras
   existia para vencer, deixou de existir (seção 8). */
#bottom:has(#evolucaoChave:not([hidden])) #evolucao {
  flex: 1 1 auto;
}

#bottom:has(#evolucaoChave:not([hidden])) #card {
  max-height: min(20dvh, 200px);
}

/* Num telemóvel a altura é toda disputada, e o pedido de chave é a única coisa
   parada à espera do senhor: o cartão cede quase tudo. Continua lá, a rolar. */
@media (max-width: 899px) {
  #bottom:has(#evolucaoChave:not([hidden])) #card {
    max-height: 12dvh;
  }
}

.ev-step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.5;
}

.ev-step + .ev-step {
  border-top: 1px solid var(--hairline);
}

/* O ponto acompanha a PRIMEIRA linha da frase, não o meio do parágrafo. */
.ev-step .dot {
  --c: var(--ink-dim);
  align-self: start;
  margin-top: 6px;
}

.ev-text {
  min-width: 0;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

/* Etiqueta do passo excepcional. Existe para o erro e o aviso não dependerem
   só da cor do ponto: quem não distingue cor lê a palavra. */
.ev-tag {
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  white-space: nowrap;
}

.ev-step--erro .dot {
  --c: var(--danger);
}

.ev-step--erro .ev-tag {
  color: var(--danger);
}

.ev-step--aviso .dot,
.ev-step--revertido .dot,
.ev-step--esperando_chave .dot {
  --c: var(--warn);
}

.ev-step--aviso .ev-tag,
.ev-step--revertido .ev-tag,
.ev-step--esperando_chave .ev-tag {
  color: var(--warn);
}

/* O passo em execução: o único esmeralda da lista. */
.ev-step.is-now {
  color: var(--ink-strong);
}

.ev-step.is-now .dot {
  --c: var(--accent);
  animation: jv-ev-pulse 1.8s var(--ease) infinite;
}

.ev-step.is-now .ev-tag {
  color: var(--accent);
}

@keyframes jv-ev-pulse {
  0% {
    box-shadow: 0 0 0 0 var(--accent-glow);
  }

  70% {
    box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
  }

  100% {
    box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
  }
}

/* Desfecho: pronto, revertido, falhou, cancelado. Sem cor de alarme no
   revertido — a mensagem existe para acalmar, não para assustar. */
#evolucaoDesfecho:empty {
  display: none;
}

.ev-out {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
}

.ev-out strong {
  display: block;
  margin-bottom: var(--s1);
  color: var(--ink-strong);
  font-size: var(--fs-body);
  font-weight: 600;
  text-wrap: balance;
}

.ev-out p {
  max-width: 72ch;
  color: var(--ink);
  font-size: var(--fs-sm);
  text-wrap: pretty;
}

.ev-out p + p {
  margin-top: var(--s2);
}

.ev-out .ev-why {
  color: var(--ink-2);
  font-family: var(--mono);
  overflow-wrap: anywhere;
}

/* -----------------------------------------------------------------------------
   O pedido de chave. É a única coisa que este produto pede ao senhor, então é
   a única coisa da tela que tem direito a espaço.
   -------------------------------------------------------------------------- */

#evolucaoChave {
  flex: 0 0 auto;
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
}

#evolucaoChave[hidden] {
  display: none;
}

.ev-key-title {
  display: block;
  color: var(--accent);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
}

/* O que é uma chave, para quem nunca ouviu a palavra. Vem antes de tudo o
   resto: sem isto, a tela abria com «HIGGSFIELD_API_KEY» e um campo, e um
   estranho não sabia se aquilo era para digitar ou para procurar. */
.ev-key-explain {
  max-width: 72ch;
  margin-top: var(--s2);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

/* O nome da chave é o RÓTULO do campo, e não um enfeite solto por cima dele. */
.ev-key-label {
  display: block;
  margin-top: var(--s3);
  cursor: pointer;
}

.ev-key-lead {
  display: block;
  color: var(--ink);
  font-size: var(--fs-sm);
}

.ev-key-name {
  display: block;
  margin-top: var(--s1);
  color: var(--ink-strong);
  font-family: var(--mono);
  font-size: var(--fs-md);
  overflow-wrap: anywhere;
}

.ev-key-what {
  margin-top: var(--s1);
  max-width: 72ch;
  color: var(--ink);
  text-wrap: pretty;
}

/* Alvo de 44px porque é um link para tocar com o polegar, não uma nota de pé
   de página. Sublinhado, não colorido: o acento aqui seria enfeite. */
.ev-key-where {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.ev-key-where a {
  color: var(--ink-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.ev-key-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s2);
}

.ev-key-input {
  flex: 1 1 240px;
  min-width: 0;
  height: var(--tap);
  padding: 0 var(--s3);
  color: var(--ink-strong);
  font-family: var(--mono);
  /* 16px: menos que isso e o iPhone dá zoom sozinho ao focar o campo. */
  font-size: var(--fs-lg);
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-control);
  appearance: none;
}

.ev-key-input::placeholder {
  color: var(--ink-3);
  opacity: 1;
}

.ev-key-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent-line);
}

.ev-key-input:disabled {
  cursor: not-allowed;
  opacity: .45;
}

/* Enviar arma quando existe algo colado. Mesmo gesto do enviar da pergunta. */
.ev-key-form[data-filled="1"] .ev-key-send {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* A saída. Fica na mesma linha, mas empurrada para a direita e apagada: é o
   caminho para quem não tem a chave, não uma alternativa de igual peso. */
.ev-key-giveup {
  margin-inline-start: auto;
}

.ev-key-note {
  max-width: 72ch;
  margin-top: var(--s2);
  color: var(--ink-micro);
  font-size: var(--fs-xs);
  text-wrap: pretty;
}

.ev-key-done {
  margin-top: var(--s2);
  color: var(--ink);
  font-size: var(--fs-sm);
}

/* As linhas cruas: botão de verdade, sempre visível, nunca hover. */
.ev-raw {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
}

#evolucaoCru {
  flex: 0 1 auto;
  max-height: min(24dvh, 220px);
}

#evolucaoCru[hidden] {
  display: none;
}

/* -----------------------------------------------------------------------------
   O aviso do cabeçalho: enquanto houver trabalho, ele fica à vista mesmo com o
   senhor a navegar no grafo. Casca de 44px em volta da pastilha de 20px.
   -------------------------------------------------------------------------- */

#brand .ev-flag {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--r-pill);
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

#brand .ev-flag[hidden] {
  display: none;
}

.ev-flag-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--accent);
  animation: jv-ev-pulse 1.8s var(--ease) infinite;
}

/* Parado à espera de uma chave não é "trabalhando": o ponto muda de cor e a
   palavra ao lado muda com ele, para não sobrar informação só na cor. */
.ev-flag[data-estado="esperando_chave"] .ev-flag-dot {
  background: var(--warn);
}

.ev-flag-time {
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

@media (hover: hover) {
  .ev-flag:hover .badge {
    color: var(--ink-strong);
    border-color: var(--border-strong);
  }
}

/* -----------------------------------------------------------------------------
   Conectores e Ambiente. Desenham-se com o vocabulário que já existe (.row,
   .row-main, .chip, .btn); só faltavam a coluna de estado e a nota do rodapé.
   -------------------------------------------------------------------------- */

.ev-state {
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

.ev-state--warn {
  color: var(--warn);
}

.ev-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
}

.ev-note {
  max-width: 72ch;
  margin-top: var(--s3);
  color: var(--ink-micro);
  font-size: var(--fs-sm);
  text-wrap: pretty;
}

/* Uma linha com botão dentro não é alvo inteiro: o botão é que é.
   E sem ponto de cor à esquerda: aqui um ponto não diria nada — não há tipo de
   arquivo para colorir — e um enfeite redondo ao lado de cada linha vira
   textura. Duas colunas: o que a linha é, e em que estado está. */
.row.ev-row {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  padding: var(--s3) 0;
  cursor: default;
}

/* Num ecrã estreito a segunda coluna DESCE, e não é preferência: é a conta.
   Medido a 390px na tela das chaves depois de o apagar entrar — três peças na
   coluna da direita («na configuração», «Trocar», «Apagar») e o nome da chave
   ficou com 90px, a rachar em «ELEVENLABS…» com a descrição a passar por baixo
   dos botões. O nome da chave é o que ele tem de ler antes de carregar em
   apagar, e é ele que ganha a largura. Vale para todas as linhas de ação —
   chaves, ambiente, pontos de restauro — porque todas podem crescer uma peça. */
@media (max-width: 599px) {
  .row.ev-row {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--s2);
  }

  .row.ev-row > .ev-actions {
    justify-content: flex-start;
  }
}

/* Com o painel do trabalho aberto, um cartão VAZIO é só espaço ocupado a dizer
   "nada aqui" — e o espaço faz falta ao trabalho. Ele volta no instante em que
   houver uma resposta de verdade para mostrar. */
#stage:has(#evolucao:not([hidden])) #card:has(> .empty) {
  display: none;
}

@media (max-width: 899px) {
  /* Num telemóvel não cabem duas áreas de rolagem dentro da mesma casca. Aqui
     a narração perde a rolagem própria e o painel INTEIRO passa a rolar como um
     bloco só. Antes disto, num ecrã de 390x844 com uma chave pedida, o console
     espremia a lista de passos até zero — a narração desaparecia por completo —
     e o resto do painel transbordava por cima da transcrição. */
  #evolucao {
    max-height: 44dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: rgba(148, 163, 184, .28) transparent;
  }

  #evolucaoPassos,
  #evolucao:has(#evolucaoChave:not([hidden])) #evolucaoPassos {
    flex: 0 0 auto;
    max-height: none;
    overflow: visible;
  }

  #evolucaoCru {
    max-height: none;
  }

  /* Com o painel aberto a altura é disputada: cedem o cartão e a gaveta,
     nunca a barra de pergunta nem o pedido de chave. A chave sai deste
     seletor em vez de o vencer noutra regra: as duas tinham a mesma
     especificidade e ganhava a que estivesse mais abaixo no arquivo — esta —,
     o que deixava os 12dvh do pedido de chave escritos e mortos. */
  #stage:has(#evolucao:not([hidden])):not(:has(#evolucaoChave:not([hidden]))) #card {
    max-height: 16dvh;
  }

  #stage:has(#evolucao:not([hidden])) :is(#left, #right) {
    max-height: 30dvh;
  }
}

/* -----------------------------------------------------------------------------
   13.b O gasto do trabalho, enquanto corre.

   Registo micro, tinta apagada, ao lado do relógio: é informação de rodapé, não
   um alarme. NÃO usa âmbar, NÃO usa esmeralda, não pisca, não cresce. Um pedido
   já custou US$ 8,15 sem ninguém ver; o senhor pediu para ver o número, não
   para ouvir opinião sobre ele.
   -------------------------------------------------------------------------- */

.ev-spend {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s2);
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ev-spend[hidden] {
  display: none;
}

.ev-spend-sep {
  color: var(--ink-dim);
}

/* -----------------------------------------------------------------------------
   13.c Recuperar: os retratos do trabalho do senhor, guardados antes de o
   JARVIS desfazer alguma coisa.

   Régua, não caixa: cada ponto no tempo é uma seção separada por fio. A data é
   a única coisa em tinta forte da tela, porque trazer de volta o retrato errado
   é o único estrago que ela pode causar. Nada de acento: aqui não há estado
   vivo nenhum — é história parada.
   -------------------------------------------------------------------------- */

.bk-point {
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
}

.bk-when {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s3);
}

.bk-when-date {
  color: var(--ink-strong);
  font-family: var(--mono);
  font-size: var(--fs-md);
  line-height: 1.4;
  overflow-wrap: anywhere;
}

.bk-when-ago {
  color: var(--ink-micro);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* O pedido que estava a correr, citado como a fala do senhor no painel. */
.bk-what {
  max-width: 72ch;
  margin-top: var(--s1);
  padding-left: var(--s3);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.55;
  overflow-wrap: anywhere;
}

/* Quando o JARVIS não sabe qual pedido gerou o ponto, a linha diz isso — e diz
   apagada, para não competir com a data e o conteúdo, que são o que É certo.
   Nada de itálico nem de aspas: é uma afirmação, não uma citação. */
.bk-what--unknown {
  color: var(--ink-micro);
  font-family: inherit;
}

/* A explicação por baixo: por que não dá para saber, e o que continua certo. */
.bk-why {
  max-width: 72ch;
  margin-top: var(--s1);
  padding-left: var(--s3);
  color: var(--ink-micro);
  font-size: var(--fs-xs);
  line-height: 1.5;
  text-wrap: pretty;
}

.bk-row .row-title {
  color: var(--ink-strong);
}

.bk-confirm {
  margin-bottom: var(--s3);
  padding-left: var(--s3);
}

.bk-confirm-line {
  max-width: 72ch;
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

.bk-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s2);
}

.bk-done {
  max-width: 72ch;
  margin-bottom: var(--s3);
  padding-left: var(--s3);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

/* Esta tela não é uma resposta, é uma lista para agir, e o botão de trazer de
   volta não pode cair abaixo do corte: é o único caminho de volta ao trabalho
   do senhor. Tinha aqui duas exceções de altura — 52dvh no computador, 58dvh
   no telemóvel — a fugir do teto de 38dvh do cartão. O teto foi-se (seção 8) e
   as exceções com ele: agora esta lista tem o orçamento inteiro do console,
   que é mais do que qualquer uma das duas dava. */

/* =============================================================================
   13f. AS DUAS TELAS QUE VÊM ANTES DO PALCO

   O portão da senha e o assistente de primeiro acesso. São a primeira coisa que
   um estranho vê, e por isso a única parte do produto onde a tela inteira é de
   uma coisa só.

   Três decisões, e as três têm motivo:

   1. OPACAS, não vidro por cima do grafo. Um grafo a espreitar por trás do
      portão promete uma tela que o senhor não pode alcançar; o vidro aqui seria
      enfeite a mentir. A mesma vinheta do corpo, para ser a mesma sala.
   2. UMA COLUNA DE 62ch, centrada, com margem automática. Centrar por margem e
      não por `place-items` é o que deixa a coluna rolar quando o teclado do
      telemóvel come metade do ecrã, em vez de cortar o topo.
   3. SEM ACENTO EM REPOUSO. O esmeralda continua a valer só para estado: o
      passo onde o senhor está, o campo em foco, e o enviar armado porque já há
      texto. Um botão verde parado aqui apagaria o significado dele no resto do
      produto.
   -------------------------------------------------------------------------- */

.veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-setup);
  display: flex;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding:
    calc(var(--s5) + env(safe-area-inset-top))
    calc(var(--s4) + env(safe-area-inset-right))
    calc(var(--s5) + env(safe-area-inset-bottom))
    calc(var(--s4) + env(safe-area-inset-left));
  background-color: var(--bg);
  background-image: radial-gradient(
    118% 92% at 50% 38%,
    var(--bg-3) 0%,
    var(--bg-2) 48%,
    var(--bg) 100%
  );
}

/* `display: flex` acima vence o `[hidden] { display: none }` do navegador, e o
   portão ficava à vista depois de aberto. Mesma armadilha do `.btn`. */
.veil[hidden] {
  display: none;
}

#gate {
  z-index: var(--z-gate);
}

/* `margin: auto` nos dois eixos: centra enquanto cabe, e passa a rolar a partir
   do topo assim que não cabe. `place-items: center` cortava o topo. */
.veil-box {
  width: 100%;
  max-width: 62ch;
  margin: auto;
}

/* A marca é a âncora, não o assunto: pequena, apagada, e nunca traduzida. */
.veil-mark {
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-brand);
  text-transform: uppercase;
}

.veil-title {
  margin-top: var(--s3);
  color: var(--ink-strong);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -.01em;
  text-wrap: balance;
}

@media (min-width: 720px) {
  .veil-title {
    font-size: 26px;
  }
}

/* O porquê. É a frase que decide se o senhor entende o que está a fazer, então
   tem o contraste do corpo e não o do rodapé. */
.veil-why {
  max-width: 68ch;
  margin-top: var(--s3);
  color: var(--ink);
  font-size: var(--fs-md);
  line-height: 1.6;
  text-wrap: pretty;
}

.veil-note {
  max-width: 68ch;
  margin-top: var(--s3);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.6;
  text-wrap: pretty;
}

.veil-foot {
  max-width: 68ch;
  margin-top: var(--s5);
  padding-top: var(--s4);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.6;
  text-wrap: pretty;
  border-top: 1px solid var(--hairline);
}

/* -------------------------------------------------------------- *
 * O campo, partilhado pelo portão e pelo assistente
 * -------------------------------------------------------------- */

.field {
  display: block;
  margin-top: var(--s4);
}

.field-label {
  display: block;
  margin-bottom: var(--s2);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 500;
}

/* O olho de mostrar a senha vive DENTRO da moldura do campo, e a moldura é que
   acende no foco: sem isto o anel de foco cortava o botão ao meio. */
.field-wrap {
  display: flex;
  align-items: center;
  gap: var(--s1);
  padding-right: var(--s1);
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-control);
}

.field-wrap:focus-within {
  border-color: var(--accent-line);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.field-input {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--tap);
  padding: 0 var(--s3);
  color: var(--ink-strong);
  /* 16px é o piso: abaixo disso o iPhone dá zoom sozinho ao focar. */
  font-size: var(--fs-lg);
  background: transparent;
  border: 0;
  appearance: none;
}

.field-input:focus {
  outline: none;
}

.field-input::placeholder {
  color: var(--ink-3);
  opacity: 1;
}

.field-input:disabled {
  cursor: not-allowed;
  opacity: .45;
}

/* Uma senha escrita às cegas num telemóvel é onde o estranho desiste. O olho é
   um botão de verdade, de 44px, sempre visível — nunca escondido atrás de um
   hover que num dedo não existe. */
.field-eye {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  color: var(--ink-2);
  background: transparent;
  border: 0;
  border-radius: var(--r-control);
  cursor: pointer;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

@media (hover: hover) {
  .field-eye:hover {
    color: var(--ink-strong);
    background: var(--fill-soft);
  }
}

.field-eye[aria-pressed="true"] {
  color: var(--ink-strong);
}

.field-eye .icon-off {
  display: none;
}

.field-eye[aria-pressed="true"] .icon-on {
  display: none;
}

.field-eye[aria-pressed="true"] .icon-off {
  display: block;
}

.field-area {
  width: 100%;
  min-height: 132px;
  padding: var(--s3);
  color: var(--ink-strong);
  font-family: var(--sans);
  font-size: var(--fs-lg);
  line-height: 1.55;
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-control);
  resize: vertical;
  appearance: none;
}

.field-area::placeholder {
  color: var(--ink-3);
  opacity: 1;
}

.field-area:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent-line);
}

.field-hint {
  max-width: 68ch;
  margin-top: var(--s2);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

/* O erro fica COLADO no campo que o causou, e leva o sinal escrito além da
   cor: vermelho sozinho não chega a quem não distingue vermelho. */
.field-error {
  display: flex;
  gap: var(--s2);
  max-width: 68ch;
  margin-top: var(--s2);
  color: var(--danger);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

.field-error:empty {
  display: none;
}

.field-error span:first-child {
  flex: 0 0 auto;
  font-weight: 700;
}

/* -------------------------------------------------------------- *
 * O portão
 * -------------------------------------------------------------- */

.gate-form {
  margin-top: var(--s2);
}

.gate-send {
  width: 100%;
  margin-top: var(--s4);
  min-height: 48px;
  font-size: var(--fs-md);
}

@media (min-width: 720px) {
  .gate-send {
    width: auto;
    min-width: 220px;
  }
}

/* Armado porque já há o que enviar. Mesmo gesto do enviar da pergunta e do
   enviar de uma chave: é sempre isto que o esmeralda quer dizer. */
.gate-form[data-filled="1"] .gate-send {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* -------------------------------------------------------------- *
 * O assistente: a régua dos passos
 * -------------------------------------------------------------- */

.setup-rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  margin-top: var(--s4);
}

/* Alvo de 44px em volta de uma pastilha de 32px: o dedo acerta no espaço, o
   olho vê a pastilha. Sem isto, oito alvos de 44px lado a lado pareciam oito
   botões grandes a competir com o passo que está aberto. */
.setup-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  color: var(--ink-2);
  background: transparent;
  border: 0;
  cursor: pointer;
  touch-action: manipulation;
}

.setup-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  background: var(--fill-soft);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

@media (hover: hover) {
  .setup-dot:hover .setup-num {
    color: var(--ink-strong);
    background: var(--fill);
    border-color: var(--border-strong);
  }
}

.setup-dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -4px;
  border-radius: var(--r-pill);
}

/* Feito: tinta forte e um visto. Sem acento — feito é história, não estado. */
.setup-dot[data-state="done"] .setup-num {
  color: var(--ink-strong);
  border-color: var(--border-strong);
}

/* Onde o senhor está. É a seleção atual, e seleção atual é uma das seis coisas
   que o esmeralda tem direito a dizer. */
.setup-dot[aria-current="step"] .setup-num {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* -------------------------------------------------------------- *
 * O assistente: o passo
 * -------------------------------------------------------------- */

.setup-step {
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--hairline);
}

.setup-count {
  color: var(--ink-micro);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
}

.setup-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2) var(--s3);
  margin-top: var(--s2);
}

.setup-title {
  color: var(--ink-strong);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.3;
  text-wrap: balance;
}

/* Obrigatório contra opcional. É a etiqueta que o contrato de primeiro acesso
   exige em cada passo, e ela é PALAVRA, nunca só uma cor. */
.setup-tag {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--s2);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}

.setup-tag--req {
  color: var(--ink-strong);
  border-color: var(--border-strong);
}

/* O que se perde ao pular. Sem isto, «Opcional» é uma palavra sem consequência
   e o senhor pula tudo sem saber o que está a desligar. */
.setup-lost {
  max-width: 68ch;
  margin-top: var(--s3);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
  text-wrap: pretty;
}

.setup-body {
  margin-top: var(--s4);
}

/* A lista de serviços. Régua, não grade de cartões iguais: cada linha é um alvo
   inteiro de 44px, e o nome do serviço é a informação. */
.setup-pick {
  display: flex;
  flex-direction: column;
  margin-top: var(--s3);
}

.setup-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  width: 100%;
  min-height: 52px;
  padding: var(--s2) var(--s2) var(--s2) 0;
  color: var(--ink-strong);
  font-size: var(--fs-md);
  text-align: start;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  cursor: pointer;
  touch-action: manipulation;
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

@media (hover: hover) {
  .setup-opt:hover {
    background: var(--fill-soft);
  }
}

.setup-opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.setup-opt:disabled {
  cursor: not-allowed;
  opacity: .45;
}

.setup-opt-main {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
}

.setup-opt-sub {
  color: var(--ink-2);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.setup-opt-go {
  flex: 0 0 auto;
  color: var(--ink-micro);
}

/* O estado de uma peça (o cérebro a responder, a voz ligada). Palavra e não
   pastilha: já há etiquetas de sobra nesta tela. */
.setup-state {
  display: flex;
  gap: var(--s2);
  max-width: 68ch;
  margin-top: var(--s3);
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: 1.55;
}

.setup-state--warn {
  color: var(--warn);
}

.setup-state--ok {
  color: var(--ink-strong);
}

.setup-state b {
  flex: 0 0 auto;
  font-weight: 700;
}

/* -------------------------------------------------------------- *
 * O assistente: a barra de navegar
 *
 * Colada ao fundo da coluna: com o polegar, «Continuar» tem de estar sempre ao
 * alcance, e não no fim de um passo comprido que obriga a rolar para o achar.
 * -------------------------------------------------------------- */

.setup-nav {
  position: sticky;
  bottom: calc(var(--s5) * -1 - env(safe-area-inset-bottom));
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s5);
  padding:
    var(--s3) 0
    calc(var(--s3) + env(safe-area-inset-bottom));
  background: var(--bg);
  border-top: 1px solid var(--hairline);
}

.setup-nav .setup-go {
  margin-inline-start: auto;
}

.setup-nav .btn {
  min-height: 48px;
}

/* Num telemóvel a ação principal ocupa a linha inteira, e as outras duas
   partilham a de cima: um alvo largo é um alvo que o polegar não erra. */
@media (max-width: 559px) {
  .setup-nav .setup-go {
    flex: 1 1 100%;
    margin-inline-start: 0;
    order: -1;
  }
}

/* A ação de um passo (guardar o texto, enviar a chave). Fica dentro do passo,
   e não na barra de baixo: a barra é para andar, isto é para fazer. */
.setup-nav-inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s3);
}

.setup-nav-inline .btn {
  min-height: 48px;
}

/* -----------------------------------------------------------------------------
   14. Movimento reduzido: mata a animação, mantém o layout idêntico
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }

  /* O anel do microfone continua a marcar o estado, parado. */
  body[data-state="listening"] #btnMic::after,
  #btnMic.is-listening::after {
    opacity: 1;
    transform: scale(1.06);
    animation: none;
  }

  /* O passo em execução e o aviso do cabeçalho continuam acesos, sem bater.
     O relógio do painel também para de correr: app.js deixa de fazer o tique
     e passa a escrever o tempo só quando chega um passo novo. */
  .ev-step.is-now .dot,
  .ev-flag-dot {
    box-shadow: 0 0 0 3px var(--accent-soft);
    animation: none;
  }
}

/* -----------------------------------------------------------------------------
   15. Alto contraste do Windows
   -------------------------------------------------------------------------- */

@media (forced-colors: active) {
  .panel,
  #bottom,
  .toast,
  #sheetnav {
    background: Canvas;
    border: 1px solid CanvasText;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .btn,
  .badge {
    border: 1px solid ButtonText;
  }

  .ev-key-input {
    background: Field;
    border: 1px solid ButtonText;
  }

  .dot {
    forced-color-adjust: none;
  }

  :focus-visible {
    outline: 2px solid Highlight;
  }
}
