/* ═══════════════════════════════════════════════════════════════════════════
   layout.css — barra, camada de voo, trilho, palco, poço de mídia, beats,
   marcação de origem, loader, e os dois estados estáticos (.rm e .no-js).
   ═════════════════════════════════════════════════════════════════════════ */

/* ═══ LOCKUP ════════════════════════════════════════════════════════════════
   Um único conjunto de regras serve às DUAS cópias inlinadas do lockup (a da
   barra e a do voo), porque as duas usam as mesmas classes .lockup__mark e
   .lockup__word. Nada aqui alcança id interno por #id — os ids internos estão
   duplicados no documento de propósito (ver o comentário longo no index.html)
   e um seletor #id só pegaria a primeira cópia.

   PROIBIDO PARA SEMPRE, EM QUALQUER PONTO DO VOO: rotação, sombra, brilho,
   filtro ou contorno no símbolo. Manual da marca, e ele ganha de tudo. O
   filter:none abaixo é a trava; a ausência de rotação é responsabilidade do
   handoff.js (config.MARK_ROTATION_ALLOWED = false).
   ═════════════════════════════════════════════════════════════════════════ */
.lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--nav-gap);
}
.lockup__mark, .lockup__word { display: block; }
.lockup__mark > svg {
  display: block;
  height: var(--nav-mark-h);
  width: var(--nav-mark-w);
}
.lockup__word > svg {
  display: block;
  height: var(--nav-word-h);
  width: var(--nav-word-w);
}
.lockup,
.lockup__mark > svg,
.lockup__word > svg {
  filter: none !important;        /* sem sombra, sem brilho, nunca */
  box-shadow: none !important;
  overflow: visible;
}

/* ═══ BARRA ════════════════════════════════════════════════════════════════ */
.nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  background: var(--vl-veil);
  /* Filete por box-shadow e não por border: a barra precisa medir exatamente
     --nav-h, porque esse número entra no cálculo da folga do poço e no
     scroll-padding dos âncoras. */
  box-shadow: inset 0 -1px 0 transparent;
  transition: box-shadow var(--dur-base) var(--ease-out);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  /* backdrop-filter aqui é seguro: a barra é IRMÃ de #brandFlight, nunca
     ancestral dela. Se um dia a camada de voo virar filha da barra, isso
     precisa sair no mesmo commit. */
  .nav {
    background: rgba(244, 241, 236, .68);
    -webkit-backdrop-filter: saturate(1.4) blur(14px);
    backdrop-filter: saturate(1.4) blur(14px);
  }
}
.nav.is-stuck { box-shadow: inset 0 -1px 0 var(--vl-rule); }

.nav__inner {
  width: var(--container-pct);
  max-width: var(--container-max);
  margin-inline: auto;
  height: 100%;
  padding-block: 0;
  padding-left: max(0px, env(safe-area-inset-left));
  padding-right: max(0px, env(safe-area-inset-right));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-6);
}

.nav__brand {
  display: block;
  text-decoration: none;
  flex: none;
}

/* Caixa em px TRAVADA. Largura e altura explícitas: o cabeçalho não pode
   refluir quando o JS esconde o slot para o voo assumir. Por isso a troca é
   por opacity, jamais por display:none — display:none zeraria o rect que o
   handoff.js usa como ponto de partida e derrubaria os <clipPath> que a
   segunda cópia referencia. */
.nav__slot {
  width: var(--nav-slot-w);
  height: var(--nav-mark-h);
  flex: none;
  transition: opacity var(--dur-base) var(--ease-out);
}
.nav__slot.is-flying { opacity: 0; }

.nav__menu {
  display: flex;
  align-items: center;
  gap: var(--s-8);
  min-width: 0;
}
.nav__link {
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 15px;
  letter-spacing: 0;
  color: var(--vl-ink);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-quick) var(--ease-out);
}
.nav__link:hover { color: var(--vl-purple); }

/* A PORTA DO PAINEL NO CELULAR — não existe no computador.
   Acima de 735px há QUATRO portas à vista (os três links de seção e o
   "Experimente"), cada uma com destino próprio; uma quinta genérica seria ruído.
   Abaixo, todas as quatro somem por falta de largura — sem esta, o conteúdo
   longo inteiro ficaria inalcançável no celular. Ver o comentário no index.html:
   é o mesmo blocker que esta página já pagou uma vez. */
.nav__porta { display: none; }

/* O alvo de toque é altura MÍNIMA, não enchimento: --tap-min governa a caixa e
   o enchimento vertical só decide onde o texto fica dentro dela. É isso que
   permite apertar o botão nas telas estreitas sem que ele deixe de ser um alvo
   de 44px — o que encolhe lá é a largura, e a largura não é o alvo. */
.nav__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  font-family: var(--f-display);
  font-weight: 500;
  font-size: 15px;
  line-height: 1.2;
  white-space: nowrap;
  background: var(--vl-purple);
  color: var(--vl-on-purple);
  padding: 10px var(--s-5);
  border-radius: var(--raio);
  text-decoration: none;
  transition: background-color var(--dur-quick) var(--ease-out);
}
.nav__cta:hover { background: var(--vl-ink); }

/* FECHAR — mora na BARRA, e essa é a única posição em que ele não cobre leitura.

   Era position:sticky + float:right DENTRO do rolador do painel. O float reserva
   espaço na posição de fluxo dele; o sticky o tira de lá assim que a rolagem
   começa. Da segunda tela em diante ele virava uma chapa opaca por cima do texto:
   medido a 390x844, varrendo 41 posições de rolagem, 26.8% delas tinham o botão
   sobre linha de texto. Faixa de exclusão dentro do painel seria paliativo — é a
   mesma conclusão que tirou a marcação de origem de dentro do palco (ver o
   comentário no index.html), e pela mesma razão: quem não pode ser coberto não
   mora na camada que rola.

   Aqui ele é filho do menu, numa faixa por onde conteúdo não passa mais (ver o
   inset de .painel__scroll). Com o painel aberto ele TOMA O LUGAR do CTA: nada se
   perde, porque a seção #cta é uma das que o painel carrega e o leitor chega nela
   rolando. Os links de seção seguem na barra — ela continua sendo o menu do
   painel, que é o desenho documentado em painel.js. */
.nav__fechar {
  display: none;                 /* só existe enquanto o painel está aberto */
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  font: 500 15px/1.2 var(--f-body);
  letter-spacing: .01em;
  white-space: nowrap;
  color: var(--vl-ink);
  background: var(--vl-bg);
  border: 1px solid var(--vl-rule);
  border-radius: var(--raio);
  padding: 10px var(--s-5);
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease-out),
              color var(--dur-quick) var(--ease-out);
}
.painel-aberto .nav__cta { display: none; }
.painel-aberto .nav__fechar { display: inline-flex; }
.nav__fechar:hover { background: var(--vl-purple); color: var(--vl-on-purple); border-color: var(--vl-purple); }
.nav__fechar:focus-visible { outline: 2px solid var(--vl-purple); outline-offset: 3px; }

/* ═══ CAMADA DE VOO ════════════════════════════════════════════════════════
   Fixa, do tamanho do viewport, inerte ao ponteiro. Fica escondida por padrão
   e só aparece quando o handoff.js declara .is-live — assim, enquanto o Gate A
   (âncora da palma) não fechar e o handoff.js não existir, a página nunca
   mostra um símbolo solto no canto. ═══════════════════════════════════════ */
.flight {
  position: fixed;
  inset: 0;
  z-index: var(--z-flight);
  pointer-events: none;
  visibility: hidden;
  /* Sem transform, sem filter, sem contain aqui: quem transforma é o lockup
     interno. */
}
.flight.is-live { visibility: visible; }

.flight__lockup {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
/* O logotipo colapsa PARA DENTRO do símbolo até q = config.WORD_COLLAPSE_AT.
   handoff.js escreve transform e opacity inline neste nó. */
.flight__word {
  transform-origin: left center;
  will-change: transform, opacity;
}

/* ═══ TRILHO E PALCO ═══════════════════════════════════════════════════════
   svh em vez de vh: durante o recolhimento da barra de URL do iOS o svh não
   muda, e é isso que impede o símbolo de escorregar da palma no meio do pouso.
   dvh está proibido pelo mesmo motivo, ao contrário. O @supports em
   tokens.css troca --vh-unit para 1vh onde svh não existir. ═══════════════ */
.track {
  position: relative;
  height: calc(var(--track-vh) * var(--vh-unit));
  /* A barra é sticky, logo ocupa espaço no fluxo. Sem esta compensação o palco
     nasceria --nav-h abaixo do topo do viewport e, em p = 0, o rodapé do poço
     ficaria cortado até o usuário rolar essa primeira faixa. Puxando o trilho
     para cima em exatamente a altura da barra, o palco ocupa 100svh cheios
     desde o primeiro quadro e a barra passa a flutuar por cima — que é o
     comportamento que a translucidez pressupõe. A barra continua sticky e
     continua parando em top:0. */
  margin-top: calc(var(--nav-h) * -1);
}

.stage {
  position: sticky;
  top: 0;
  height: calc(100 * var(--vh-unit));
  overflow: clip;
}

.stage__inner {
  width: var(--container-pct);
  max-width: var(--container-max);
  margin-inline: auto;
  height: 100%;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--stage-gap);
}

/* ── Coluna de texto. Grelha de UMA célula com todos os beats empilhados na
   mesma área: a linha se dimensiona pelo beat mais alto e nenhum beat reflui o
   layout quando o texto muda de comprimento. É o que torna a página tolerante
   a tradução sem tocar em nada. --copy-reserve é o piso reservado. ──────── */
.stage__copy {
  display: grid;
  grid-template-areas: 'beat';
  align-content: center;
  min-height: var(--copy-reserve);
  max-width: var(--leitura-max);
  min-width: 0;
}

.beat {
  grid-area: beat;
  align-self: center;
  opacity: 0;
  transform: translate3d(0, var(--beat-rise), 0);
  will-change: opacity, transform;
  /* SEM transition: beats.js interpola opacity e transform a partir de p, a
     cada quadro. Uma transição aqui brigaria com o scrub e atrasaria o texto
     em relação à imagem. A assinatura de movimento entra pela curva que o
     beats.js aplica (config.SIGNATURE), não por CSS. */

  /* PONTEIRO — QUEM MANDA EM QUÊ.
     Os beats estão todos empilhados na mesma célula da grelha, então enquanto o
     CSS aqui dizia `pointer-events:none` era impossível SELECIONAR o texto
     visível: o arrasto atravessava o beat de cima e pegava letras de outro
     bloco, invisível, que por acaso estava por cima na ordem de pintura.

     A divisão passa a ser esta, e ela é a única que funciona com o beats.js que
     já existe: o CSS é o PADRÃO (ligado) e o JS é a SUPRESSÃO (desligado). Ele
     escreve `pointer-events:none` inline em todo beat que não é o dominante
     (config.BEAT_POINTER_MIN) e escreve string vazia no dominante, o que apaga
     o inline e cai exatamente nesta linha. Com `none` aqui, aquela string vazia
     caía em `none` também e NENHUM beat, nem o que está sendo lido, capturava
     nada. O `auto` conserta o dominante sem soltar os outros: inline vence
     folha de estilo, então o `none` do JS continua valendo em cima disto.

     Consequência de propósito: não há mais regra soltando o ponteiro em `.beat
     a` isoladamente. Um link dentro de um beat apagado herda o `none` do pai e
     deixa de ser um alvo invisível; o foco de teclado não passa por
     pointer-events, e .beat:focus-within abaixo devolve a visibilidade. */
  pointer-events: auto;
}
/* O beat hero é o estado correto em p = 0, então ele já nasce visível: se o
   beats.js ainda não rodou, a primeira dobra está certa mesmo assim. */
.beat[data-beat='hero'] { opacity: 1; transform: none; }

/* Foco de teclado força o beat a aparecer: o texto nunca sai da árvore de
   acessibilidade, então o foco também não pode cair em algo invisível. */
.beat:focus-within { opacity: 1 !important; transform: none !important; }

.beat .titulo-xl {
  font-size: var(--t-stage-xl);
  letter-spacing: var(--tk-lg);
  max-width: none;
}

.rebobina {
  font-size: var(--t-caption);
  line-height: var(--lh-caption);
  letter-spacing: var(--tk-caption);
  color: var(--vl-ink-faint);
  max-width: 34ch;
}

/* ── Poço de mídia ────────────────────────────────────────────────────────
   DESKTOP: proporção EXATAMENTE 768/960 (config.NATIVE), então object-fit
   cover é a identidade e nada é cortado — o agachamento do quadro 0 mantém os
   pés e o vestido do quadro 355 mantém a abertura.
   A altura de referência é min(86svh, 980px); a largura sai dela. Quando a
   largura disponível é menor que isso (viewport estreito e alto), quem manda é
   a largura e a altura desce junto, mantendo a proporção intacta. NUNCA
   sangra até a borda.
   A centralização é por grid — não existe translate(-50%,-50%) em lugar
   nenhum deste projeto, porque transform em qualquer ancestral de
   #brandFlight o transformaria em bloco contentor de position:fixed. ────── */
.stage__media {
  --well-w: min(
    calc(var(--container-w) - var(--leitura-max) - var(--stage-gap)),
    calc(var(--well-h-cap) * var(--native-w) / var(--native-h))
  );
  position: relative;
  width: var(--well-w);
  height: calc(var(--well-w) * var(--native-h) / var(--native-w));
  flex: none;
  background: var(--vl-bg);
  border-radius: var(--raio);
  overflow: hidden;
}

.stage__poster,
.stage__poster img,
.stage__canvas,
.stage__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.stage__poster { z-index: var(--z-poster); }
.stage__poster img {
  display: block;
  object-fit: cover;
  object-position: center;
}
/* O canvas nasce transparente, então o pôster aparece por baixo até o
   frames.js pintar o primeiro quadro. Nenhuma classe, nenhuma troca. */
.stage__canvas { z-index: var(--z-canvas); display: block; }
.stage__video  { z-index: var(--z-canvas); object-fit: cover; object-position: center; }

/* ── Marcação de origem ───────────────────────────────────────────────────
   Trava 2 do DNA: nunca removível, em nenhum plano. Placa off-white com tinta
   #1A0A24 — portanto nunca pousa sobre tom variável do vídeo. Acima do canvas
   e abaixo da camada de voo. NUNCA animada abaixo de opacity 1; o !important
   está aqui para que nenhum módulo consiga apagá-la por engano. ─────────── */
.origem {
  position: absolute;
  left: var(--s-4);
  bottom: var(--s-4);
  z-index: var(--z-origem);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  background: var(--vl-bg);
  color: var(--vl-ink);
  font-size: 12px;
  line-height: 1.2;
  letter-spacing: .02em;
  padding: var(--s-2) var(--s-3);
  border-radius: 6px;
  max-width: calc(100% - var(--s-8));
  opacity: 1 !important;
  visibility: visible !important;
}
.origem i {
  display: block;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--vl-purple);
}

/* ═══ TRILHA DE ROLAGEM ════════════════════════════════════════════════════
   Onde a pessoa está dentro da tomada. Fina, quieta, na margem direita.

   INDICADOR, NUNCA CONTROLE. pointer-events:none em todo o subárvore: não
   arrasta, não clica, não promete interação que a página não cumpre.

   POR QUE A DIREITA: a marcação de origem mora embaixo À ESQUERDA do poço, em
   todas as faixas, então a direita é a única margem em que as duas nunca se
   encontram. E começa abaixo da barra, então também não cruza com ela — e o
   z-index fica abaixo do dela, para que a barra ganhe se um dia cruzarem.

   TUDO SAI DE --rail-p, QUE O js/rail.js ESCREVE EM #scrubRail. A posição do
   cursor, a faixa marcada e o esmaecimento do fim são função pura dela: rolar
   para trás desfaz exatamente o que rolar para frente fez, sem estado nenhum
   guardado aqui. Cada uso é var(--rail-p, 0) e não há declaração do token no
   bloco, de propósito: assim funciona tanto se o rail.js escrever no elemento
   quanto se escrever no :root.

   O ESMAECIMENTO: a trilha sai entre --rail-marca-b (0,860 — fim da troca, e
   config.HANDOFF.start) e o fim do trilho. Ela ainda está legível quando a
   linha "role de volta devagar" aparece (config.BEATS.rewind começa em 0,862),
   que é justamente quando ela precisa ser a affordance, e já não existe no
   pouso. ═════════════════════════════════════════════════════════════════ */
.rail {
  position: fixed;
  z-index: var(--z-rail);
  top: calc(var(--nav-h) + var(--s-6));
  bottom: calc(var(--s-6) + env(safe-area-inset-bottom));
  right: max(var(--s-3), env(safe-area-inset-right));
  width: var(--rail-w);
  pointer-events: none;
  background: var(--vl-rule);
  border-radius: var(--rail-w);
  opacity: clamp(0, calc(
    (1 - var(--rail-p, 0)) / (1 - var(--rail-marca-b))
  ), 1);
}
.rail > i {
  position: absolute;
  left: 0;
  right: 0;
  border-radius: inherit;
}
/* A PASSAGEM QUE VALE VOLTAR: a troca de roupa, entre os quadros 280 e 330.
   Sai de --rail-marca-a / --rail-marca-b, que são o espelho de
   config.FRAME_MAP — o CSS e o rail.js leem o mesmo par. */
.rail__marca {
  top: calc(var(--rail-marca-a) * 100%);
  height: calc((var(--rail-marca-b) - var(--rail-marca-a)) * 100%);
  background: var(--vl-ink-faint);
}
/* O cursor. Sem transform e sem transição: transform faria dele bloco contentor
   à toa, e transição atrasaria a posição em relação à imagem — o mesmo motivo
   pelo qual os beats não têm transição. A margem negativa é o que centra o
   cursor no valor de --rail-p em vez de pendurá-lo por cima dele. */
.rail__cursor {
  height: var(--rail-cursor-h);
  top: calc(var(--rail-p, 0) * 100%);
  margin-top: calc(var(--rail-cursor-h) / -2);
  background: var(--vl-purple);
}

/* ═══ LOADER ═══════════════════════════════════════════════════════════════
   Tela cheia no off-white da marca. O SÍMBOLO SE DESENHA: a caneta atravessa a
   silhueta, o losango e o entalhe conforme os quadros chegam, e a tinta entra
   nos dois últimos traços. Abaixo dele, pequenos, o numeral e a contagem.

   loader.js escreve #loaderNum, .loader__count e a fração --loader-p em
   #loader, e dispensa a camada com .is-done. Nada aqui é temporizado: o desenho
   é função pura de --loader-p, portanto não pode adiantar, fingir nem voltar.

   Rede de segurança: se o JS nunca subir, loaderBail apaga a camada em
   --loader-bail (espelho de config.LOAD_TIMEOUT_MS), usando a duração e a
   curva da assinatura. ═══════════════════════════════════════════════════ */
.loader {
  position: fixed;
  inset: 0;
  z-index: var(--z-loader);
  background: var(--vl-bg);
  display: grid;
  place-items: center;
  transition: opacity var(--dur-slow) var(--ease-out),
              visibility 0s linear var(--dur-slow);
  animation: loaderBail var(--dur-slow) var(--ease-out) var(--loader-bail) forwards;
}
.loader.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  animation: none;
}
@keyframes loaderBail {
  to { opacity: 0; visibility: hidden; }
}

.loader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  padding-inline: var(--s-6);
}
.loader__desenho {
  display: block;
  margin-bottom: var(--s-5);   /* o desenho manda; a leitura vem depois de uma pausa */
}

/* ── O DESENHO ────────────────────────────────────────────────────────────
   Uma caneta só, três traços. A ponta anda --loader-len-total * --loader-p e
   cada traço esconde o que a ponta ainda não alcançou, então os três formam um
   movimento contínuo e a divisão entre eles é a proporção dos comprimentos
   medidos (tokens.css), não um corte escolhido.

   Tudo aqui é número puro multiplicado por 1px na hora do uso: dividir
   comprimento por comprimento não é válido em calc(), e é exatamente isso que
   a conta da tinta faria se os tokens carregassem unidade.

   PROIBIDO, E A REGRA NÃO TEM EXCEÇÃO: filtro, brilho, sombra ou rotação sobre
   o símbolo. O que existe aqui é traço e preenchimento — desenho, não
   tratamento. O overflow visível é para a caneta não ser cortada na borda do
   viewBox, e não abre espaço para nada além disso. */
.loader-mark {
  display: block;
  height: var(--loader-mark-h);
  width: calc(var(--loader-mark-h) * var(--mark-aspect));
  overflow: visible;
  filter: none !important;
  box-shadow: none !important;

  /* posição da ponta, em unidades de usuário */
  --pen: calc(var(--loader-len-total) * var(--loader-p, 0));
  /* a tinta sobe ao longo dos dois últimos traços; a caneta desce junto */
  --ink: clamp(0, calc(
    (var(--pen) - var(--loader-len-v)) /
    (var(--loader-len-losango) + var(--loader-len-entalhe))
  ), 1);
}

.loader-mark__traco {
  fill: none;
  stroke: var(--vl-purple);
  stroke-width: calc(var(--loader-stroke-w) * 1px);
  stroke-linecap: round;
  stroke-linejoin: round;
  /* --loader-p chega em degraus de 1%; a assinatura suaviza o degrau sem
     nunca inverter o sentido, porque o valor de origem é monotônico. */
  transition: stroke-dashoffset var(--dur-base) var(--ease-out);
}
.loader-mark__traco--v {
  stroke-dasharray: calc(var(--loader-len-v) * 1px);
  stroke-dashoffset: calc(
    clamp(0, calc(var(--loader-len-v) - var(--pen)), var(--loader-len-v)) * 1px
  );
}
.loader-mark__traco--losango {
  stroke-dasharray: calc(var(--loader-len-losango) * 1px);
  stroke-dashoffset: calc(
    clamp(0, calc(var(--loader-len-v) + var(--loader-len-losango) - var(--pen)),
          var(--loader-len-losango)) * 1px
  );
}
.loader-mark__traco--entalhe {
  stroke-dasharray: calc(var(--loader-len-entalhe) * 1px);
  stroke-dashoffset: calc(
    clamp(0, calc(var(--loader-len-total) - var(--pen)), var(--loader-len-entalhe)) * 1px
  );
}

/* UMA opacidade para o grupo inteiro da tinta, e é ela que mantém a trava do
   limão: o entalhe é composto sobre o losango roxo dentro do grupo e só depois
   o grupo encontra o off-white. Opacidade por elemento colocaria limão direto
   sobre papel (contraste 1,04) em todos os quadros do meio. */
.loader-mark__ink {
  opacity: var(--ink);
  transition: opacity var(--dur-base) var(--ease-out);
}
.loader-mark__pen {
  opacity: calc(1 - var(--ink));
  transition: opacity var(--dur-base) var(--ease-out);
}

/* ── A leitura honesta, subordinada ao desenho ────────────────────────────
   Continua sendo o que sempre foi — inteiro de blobs concluídos / 356 e a
   contagem em quadros — só que agora em tamanho de rótulo e de legenda. */
.loader__num {
  font-family: var(--f-display);
  font-weight: 500;
  font-size: var(--t-label);
  letter-spacing: var(--tk-label);
  line-height: var(--lh-label);
  color: var(--vl-ink);
  font-variant-numeric: tabular-nums;
}
.loader__count {
  font-size: var(--t-caption);
  line-height: var(--lh-caption);
  letter-spacing: var(--tk-caption);
  color: var(--vl-ink-faint);
  font-variant-numeric: tabular-nums;
}

/* ═══ FAIXA INTERMEDIÁRIA — 735 a 935 ══════════════════════════════════════
   As duas colunas seguem valendo até 936px: em 1024x768 elas cabem
   folgadamente (poço ~398x497, coluna de texto nos 450) e preservam a
   composição do desktop. Abaixo disso o poço ficaria estreito demais e o
   empilhamento passa a ser a leitura certa.

   Aqui o poço EMPILHA mas guarda a proporção nativa: em largura total nesta
   faixa o cover cortaria na vertical, e cortar na vertical come os pés no
   quadro 0 e a barra do vestido no 355. A altura entra por aspect-ratio com
   largura automática, então mesmo quando o flex encolhe o poço para caber, a
   proporção continua exata. ═══════════════════════════════════════════════ */
/* Duas colunas, com a coluna de leitura estreitando conforme a tela.

   ESTA FAIXA JÁ FOI EMPILHADA E NÃO PODE VOLTAR A SER. Empilhar aqui é
   aritmeticamente impossível com este conteúdo: em 900x700 sobravam 232px para
   o cartão de texto enquanto o beat mais alto (proof) mede 449px. A primeira
   tentativa deixou a imagem absorver o rombo e o poço colapsou para 82x103 com
   a marcação de origem cortada no meio da palavra; a segunda travou o piso da
   imagem e o rombo foi para o texto, que passou a ser cortado em cima E embaixo
   — em 844x900 a H1 aparecia na tela como "quem vai comprar." e o olho
   "Provador virtual" sumia inteiro. Os dois são falha automática.

   Em duas colunas a mesma tela cabe com folga: o poço fica 297x371 e o texto
   450x449, porque a altura deixa de ser dividida entre os dois. A largura do
   poço já é limitada por (container - coluna de leitura - gap), então basta
   estreitar a leitura para a imagem continuar larga o bastante para a marcação
   de origem (224px) caber dentro dela. ═══════════════════════════════════ */
@media (min-width: 735px) and (max-width: 935px) {
  :root {
    --leitura-max: 380px;
    --stage-gap: var(--s-8);
  }
  /* NÃO colocar min-width aqui. Já foi tentado (260px) e quebrou a proporção:
     a largura USADA era travada mas a altura continuava saindo de --well-w sem
     trava, então a razão saía de 0.800 assim que a trava entrava — em 844x390
     o poço virou 260x254 (razão 1.02) e o cover comeu 21.8% da altura do
     quadro, ou seja, os pés dela no quadro 0. A proporção exata é a restrição
     dura desta página; a marcação de origem quebra em mais linhas e cabe. */
  .beat .titulo-xl {
    font-size: clamp(30px, 4.6vw, 44px);
    letter-spacing: -.006em;
  }
  /* O beat `proof` é o mais alto de todos (o cartão roxo com o número) e é o
     único que ainda estourava a coluna nesta faixa, por 24px. Aperta só ele. */
  .beat .palco--beat { padding: var(--s-4); }
  .beat .dado { font-size: var(--dado-beat-apertado); }
}

@media (min-width: 735px) and (max-width: 820px) {
  :root { --leitura-max: 330px; --stage-gap: var(--s-6); }
}

/* ═══ TELA BAIXA, 936 E ACIMA ══════════════════════════════════════════════
   O mesmo aperto que já existe em outras duas faixas, pelo motivo oposto: em
   735-935 falta LARGURA para o cartão roxo e em 734- falta espaço no cartão de
   texto; aqui falta ALTURA. Acima de 936 tudo volta ao tamanho cheio, e em tela
   baixa a coluna de beats passa dos 100svh do palco — que tem overflow:clip,
   então o beat é comido em cima E embaixo sem nenhuma barra de rolagem que
   denuncie.

   MEDIDO, não estimado. O beat mais alto acima de 936 é o `hero` (rótulo +
   título + leitura + botão), não o `proof`:

     1440x500 antes  hero 520,9px em 500 de palco -> 10,5px comidos de cada lado
                     proof 482,0px               -> passava raspando
     1440x500 depois hero 472,9px                -> 13,5px de folga de cada lado
                     proof 435,5px               -> 32,2px de folga

   O aperto é literalmente o mesmo vocabulário da faixa de celular (as três
   margens entre os blocos do beat) mais o do cartão roxo (enchimento e número),
   e devolve 48px no hero e 46px no proof. Nenhum tamanho de tipo muda: o
   respiro entre os blocos encolhe, a tipografia não.

   O corte em 620 dá cerca de 100px de folga sobre os ~521 em que o hero começa
   a encostar, para uma tradução mais longa caber sem editar isto de novo, e
   ainda assim não alcança laptop nenhum em uso comum — o mais baixo tem 720. */
@media (min-width: 936px) and (max-height: 620px) {
  .beat .rotulo  { margin-bottom: var(--s-3); }
  .beat .corpo   { margin-top: var(--s-4); }
  .beat .legenda { margin-top: var(--s-3); }
  .beat .palco--beat { padding: var(--s-4); }
  .beat .dado { font-size: var(--dado-beat-apertado); }
}

/* ═══ CELULAR — 734 e abaixo ═══════════════════════════════════════════════
   Layout de verdade, não um esmagamento: poço em cima ocupando a largura,
   cartão off-white embaixo com o texto em #1A0A24 sobre #F4F1EC. É o par de
   leitura sancionado pela marca, no lugar de branco sobre imagem ou de um véu
   escuro por cima dela. ═════════════════════════════════════════════════ */
@media (max-width: 734px) {
  /* A barra fica com a marca e uma porta só. Os links de seção saem: nesta
     largura eles estouravam a barra, e a página é uma narrativa contínua em
     que a rolagem já entrega cada seção na ordem. O rodapé também não os
     repete — logo abaixo do pedido final, aquela lista era uma segunda oferta
     competindo com ele.

     O "EXPERIMENTE" FICA, e isso reverte a decisão anterior. Ele saía junto com
     os links de seção, pela lógica de que virara um link como os outros. O dono
     pediu de volta no celular, e a razão é de produto, não de layout: ele é a
     única porta que leva DIRETO ao fecho, onde estão o "Provar em você" e o
     pedido de demonstração. A porta "Sobre" abaixo entra pelo começo do painel;
     quem chega pelo celular querendo experimentar tem de chegar no lugar certo.

     ISSO REABRE O PROBLEMA DE LARGURA, e ele está medido no bloco de telas
     estreitas mais abaixo: a barra volta a ter três itens. O rótulo curto
     ("Experimente" contra os antigos "Provar em você") é o que torna isso
     possível — a medição está lá. */
  .nav__link { display: none; }
  .nav__menu { gap: var(--s-4); }

  /* A porta aparece justamente onde os links somem. Rótulo curto por medida, não
     por gosto: a 320px a barra tem 320 - marca (127) - dois vãos (16) = 177px de
     sobra, e "Sobre" cabe onde "Como funciona" não caberia. */
  .nav__porta {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-min);
    font-family: var(--f-body);
    font-weight: 500;
    font-size: 15px;
    color: var(--vl-ink);
    text-decoration: none;
    white-space: nowrap;
  }
  .nav__porta:hover { color: var(--vl-purple); }
  .nav__porta:focus-visible { outline: 2px solid var(--vl-purple); outline-offset: 3px; }

  /* O PALCO EMPILHA DE VERDADE AQUI, e é preciso dizer isso em display.
     Todo este bloco sempre foi escrito para uma coluna flex — `order: -1`,
     `flex: 0 0 auto`, `justify-content` — mas a declaração que troca a grelha
     de duas colunas do computador por essa coluna nunca esteve escrita, e a
     grelha continuava valendo. Consequência medida em 390x844: as colunas
     resolviam para "390px 0px", o poço ficava com LARGURA ZERO encostado na
     direita (x=390, 0x624) e a página no celular abria sem imagem nenhuma, com
     a marcação de origem espremida numa tira de 16px. Sem esta linha, nada mais
     neste bloco tem efeito. */
  .stage__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    max-width: none;
    padding: 0;
    gap: 0;
  }

  /* A PROPORÇÃO É A RESTRIÇÃO DURA, TAMBÉM AQUI. A largura sai de um min() com
     a mesma forma da regra do computador — o que couber na largura, ou o que
     couber na altura que sobra — e a altura sai DELA. Por construção a razão é
     exatamente 768/960 em qualquer tela, e o cover volta a ser identidade.

     A versão anterior era largura cheia com altura própria, e isso quebrava a
     razão nos dois sentidos: em 320x568 dava 320x372 (razão 0,860 > 0,800), e
     razão maior que a nativa é justamente a que corta na VERTICAL — 7% da
     altura do quadro, ou seja, os pés dela no quadro 0 e a barra do vestido no
     355. Em 390x844 dava 390x624 (0,625), que não corta altura mas também não é
     a proporção. Um poço menor e inteiro vale mais que um poço cheio e cortado.

     Sem min-height: um piso em px brigaria com a razão exata na tela mais baixa
     (em 320x568 o poço mede 166x208) e a razão ganha. A marcação de origem
     continua legível porque ela quebra em mais linhas, não porque o poço é
     grande. */
  .stage__media {
    --well-w: min(
      100%,
      calc((100 * var(--vh-unit) - var(--copy-reserve)) * var(--native-w) / var(--native-h))
    );
    width: var(--well-w);
    max-width: 100%;
    height: calc(var(--well-w) * var(--native-h) / var(--native-w));
    flex: none;
    order: -1;              /* o poço em cima, o cartão embaixo */
    border-radius: var(--raio);
  }

  /* O cartão tem altura reservada, e é o mesmo --copy-reserve que a conta do
     poço desconta — um número só para os dois lados, então poço + cartão dão
     exatamente 100svh e nenhum beat pode ser cortado pelo overflow:clip do
     palco. O piso inclui o inset de baixo porque o enchimento do cartão também
     inclui. */
  .stage__copy {
    flex: 0 0 auto;
    width: 100%;
    max-width: none;
    min-height: var(--copy-reserve);
    background: var(--vl-bg);
    color: var(--vl-ink);
    padding: var(--s-5) var(--s-5) calc(var(--s-5) + env(safe-area-inset-bottom));
    padding-left: max(var(--s-5), env(safe-area-inset-left));
    padding-right: max(var(--s-5), env(safe-area-inset-right));
  }

  .beat .corpo   { margin-top: var(--s-4); }
  .beat .legenda { margin-top: var(--s-3); }
  .beat .rotulo  { margin-bottom: var(--s-3); }
  .beat .titulo-xl {
    font-size: clamp(30px, 9.6vw, 44px);
    letter-spacing: -.003em;
    max-width: none;
  }
  .beat .titulo-lg,
  .beat .titulo-md,
  .beat .titulo-sm { max-width: none; }
  .beat .palco--beat { padding: var(--s-5); }
  .beat .dado { font-size: var(--dado-beat-apertado); }

  .origem {
    left: var(--s-3);
    bottom: var(--s-3);
    /* Stays at the DNA's 12px for this component. It was 11px here, which is both an
       ad-hoc size outside the type scale and the wrong direction of travel: this is a
       mandatory, non-removable provenance mark on a phone, so it should not be the
       smallest text on the page. Only the padding tightens at this width. */
    padding: 6px var(--s-2);
  }
}

/* ═══ ESTADOS ESTÁTICOS — .rm e .no-js ═════════════════════════════════════
   .rm  : main.js aplica quando prefers-reduced-motion está ligado.
   .no-js: está no HTML desde a primeira pintura; main.js REMOVE no boot.
   Os dois compartilham exatamente o mesmo layout, e é de propósito: a página
   sem JavaScript é a mesma página do modo sem movimento. Zero marcação
   alternativa, zero segundo lockup para manter.

   O trilho desaba para height:auto, o palco vira estático, os beats voltam a
   empilhar em fluxo com opacity 1 e transform none, o canvas some e o pôster
   fica, o slot da barra continua visível e a camada de voo sai do documento.
   ═════════════════════════════════════════════════════════════════════════ */
.rm .track,
.no-js .track {
  height: auto;
  margin-top: 0;   /* sem palco fixo não há nada para a barra sobrepor */
}

.rm .stage,
.no-js .stage {
  position: static;
  height: auto;
  overflow: visible;
}

.rm .stage__inner,
.no-js .stage__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-12);
  padding-block: var(--secao-y);
}

.rm .stage__copy,
.no-js .stage__copy {
  /* DISPUTAR position E OS INSETS É OBRIGATÓRIO, e a falta disso era o defeito:
     este bloco foi escrito contra o layout anterior ao full-bleed e nunca foi
     revisitado quando .stage__copy virou position:absolute com inset. Como ele
     simplesmente NÃO DECLARA essas propriedades, especificidade não salva —
     elas continuavam vindo do layout vivo. Medido em 390x844: a cópia ia para
     y = -1113, #track colapsava para 192px, a página não rolava e sobravam 20px
     de estouro horizontal. No desktop o mesmo bug aparecia como o corpo do herói
     esparramado sobre o pôster. */
  position: static;
  inset: auto;
  left: auto; right: auto; top: auto; bottom: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-16);
  min-height: 0;
  max-width: var(--leitura-max);
  width: 100%;
  overflow: visible;
}

.rm .beat,
.no-js .beat {
  opacity: 1 !important;
  transform: none !important;
  will-change: auto;
  pointer-events: auto;
}

/* A SAÍDA DE CONTATO NO PALCO — mesma lição do .stage__copy acima, e ela não foi
   aprendida de graça: é OBRIGATÓRIO disputar position E os insets. Nos dois
   caminhos estáticos o palco deixa de ser sticky (`.rm .stage` logo acima), e um
   absolute aqui passaria a se posicionar contra o bloco contentor inicial — a
   saída iria parar no rodapé da JANELA, por cima do conteúdo, em vez de no fim
   do palco.

   E ela NÃO SOME nestes dois caminhos, ao contrário do convite a descer. É o
   oposto: sem JS e em movimento reduzido esta é a única saída de contato do
   trilho, e quando o modal do provador existir ela será a saída dos estados de
   falha dele — inclusive os estados em que o JS é o que quebrou. Some o convite,
   fica a porta.

   opacity e pointer-events explícitos porque quem os escreveria é o beats.js,
   que nestes caminhos não roda: sem isto a saída herdaria o que o CSS deixou e
   dependeria de um módulo ausente para ficar visível. */
.rm .zap--palco,
.no-js .zap--palco {
  position: static;
  inset: auto;
  left: auto; right: auto; top: auto; bottom: auto;
  margin-inline: auto;
  opacity: 1;
  pointer-events: auto;
}

.rm .stage__media,
.no-js .stage__media {
  order: -1;
  --well-w: min(100%, calc(var(--well-h-cap) * var(--native-w) / var(--native-h)));
  width: var(--well-w);
  height: auto;
  aspect-ratio: var(--native-w) / var(--native-h);
  flex: none;
}

.rm .stage__canvas,
.no-js .stage__canvas { display: none; }

.rm .stage__poster,
.no-js .stage__poster { opacity: 1; }

/* O slot da barra NUNCA some nesses estados: se o voo não existe, a marca
   tem que continuar na barra. */
.rm .nav__slot,
.no-js .nav__slot { opacity: 1 !important; }

.rm .flight,
.no-js .flight { display: none; }

/* Sem trilho não há posição dentro da tomada para indicar, e a trilha viraria
   um enfeite fixo na margem. Sai inteira nos dois estados. */
.rm .rail,
.no-js .rail { display: none; }

/* O loader não faz sentido sem movimento; sem JS ele é dispensado pelo
   loaderBail. */
.rm .loader { display: none; }
@media (prefers-reduced-motion: reduce) {
  .loader { display: none; }
  /* Também aqui, e não só via .rm: cobre a janela entre a primeira pintura e o
     boot do JS, em que a classe ainda não foi aplicada. */
  .rail { display: none; }
}

@media (max-width: 734px) {
  .rm .stage__inner,
  .no-js .stage__inner { width: var(--container-pct); max-width: var(--container-max); }
  .rm .stage__copy,
  .no-js .stage__copy { padding: 0; background: none; }
  .rm .stage__media,
  .no-js .stage__media { border-radius: var(--raio); }
}

/* ═══ TELAS MUITO ESTREITAS (320-380) ══════════════════════════════════════
   Vem por último de propósito: precisa vencer as regras de 734- na cascata.

   HISTÓRICO, porque a forma deste bloco só se explica por ele. A marca é uma
   caixa em px TRAVADA (handoff.js mede esse rect como ponto de partida do voo, e
   o cabeçalho não pode refluir), então tudo que divide a barra com ela é que
   cede. Já foram duas rodadas de aperto: primeiro contra um CTA que não quebrava
   linha (316px num container de 267, com 30px de rolagem horizontal), depois
   contra o terceiro item que a .nav__porta acrescentou (295 num container de
   280). A segunda rodada terminou com o rótulo do CTA quebrando em duas linhas
   abaixo de 345px — a única folga que sobrava.

   NADA DISSO EXISTE MAIS, e não por ajuste fino: quando o "Experimente" virou
   link de seção, ele passou a sair no mesmo ponto que os outros três. A barra do
   celular voltou a ter dois itens (marca + porta), a soma parou de estourar e as
   duas consultas que administravam o estouro — o encolhimento do .nav__cta aqui
   e o bloco inteiro de 344px que quebrava o rótulo — foram removidas em vez de
   mantidas por precaução. Regra que não pode disparar é regra que mente sobre o
   que o arquivo faz, e este arquivo já foi mordido três vezes por regra morta.

   O que fica é o aperto dos VÃOS e o corpo da porta, que ainda valem: eles
   servem à barra de dois itens tanto quanto serviam à de três. O alvo de toque
   continua governado por --tap-min na regra base — este bloco já derrubou o
   toque para 31,6px uma vez apertando enchimento, e não volta a fazê-lo. */
@media (max-width: 380px) {
  .nav__inner { gap: var(--s-2); }
  .nav__menu { gap: var(--s-2); }
  .nav__porta { font-size: 13px; }

  /* O "EXPERIMENTE" VOLTOU E A BARRA TEM TRÊS ITENS DE NOVO, então o aperto que
     tinha saído com ele volta junto. Medido a 334px de viewport, antes deste
     bloco: marca 127 + porta 37 + botão 134 = 298 num container de 280, mais
     dois vãos de 8 — 33px além da borda.

     QUEM CEDE É O BOTÃO, e continua sendo por eliminação: encolher o lockup é
     proibido (o mínimo do DNA é o rect que o handoff mede como partida do voo),
     e esconder a porta "Sobre" devolveria o conteúdo longo inalcançável — foi
     blocker uma vez. Sobra a caixa do rótulo.

     A ALTURA NÃO CEDE: --tap-min continua sendo o piso, então o alvo segue com
     44px e o que encolhe é só a horizontal. */
  .nav__cta {
    font-size: 13px;
    padding: 8px var(--s-2);
    border-radius: 6px;
  }
}

/* ═══ SEM JS ═══════════════════════════════════════════════════════════════
   A REGRA `.no-js .loader { animation-delay: 0s }` SAIU DAQUI, e o motivo é que
   a premissa dela era falsa. Ela dizia ".no-js só existe quando o boot nunca
   aconteceu"; .no-js está no HTML desde a PRIMEIRA PINTURA e só sai lá dentro do
   boot() do main.js, depois do DOMContentLoaded. Ou seja, em TODA carga o
   loaderBail rodava com atraso zero: a camada começava a esmaecer no primeiro
   quadro e, quando o main.js tirava a classe, o atraso voltava para 15s e ela
   ressurgia de estalo. Num boot lento isso é o loader sumindo e voltando na cara
   de quem chegou.

   O atalho agora mora num <noscript><style> no <head> do index.html, que é o
   único predicado exato para "script desligado": ele não tem janela de tempo
   nenhuma, porque nunca chega a valer quando há script.

   O QUE ISSO CUSTA, escrito porque é uma troca e não um ganho puro: com script
   LIGADO e o main.js quebrado (404, erro de sintaxe, módulo que não resolve), a
   classe .no-js também nunca sai — antes esse caso saía na hora, e agora espera
   os --loader-bail. É para isso que o bail existe, e o caso é raro; o defeito
   que ele substitui acontecia em 100% das cargas. */

/* A linha de rebobinar convida a rolar de volta para ver a troca acontecer.
   Nos dois estados estáticos não há troca para ver, então ela sai.

   O convite a DESCER sai pelos mesmos dois motivos, um por estado: em movimento
   reduzido não há tomada para percorrer, e sem JS o beats.js não roda — o ícone
   ficaria pendurado na tela para sempre, sem nunca esmaecer, que é o oposto de
   um convite. */
.rm [data-beat="rewind"],
.no-js [data-beat="rewind"],
.rm .descer,
.no-js .descer { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   FULL-BLEED — a animação ocupa a tela inteira, o conteúdo vem por cima.

   Decisão do dono do projeto, revertendo o poço proporcional. Vem por último no
   arquivo de propósito: sobrescreve as regras de poço/faixa/celular sem
   desmontá-las, então voltar atrás é apagar este bloco.

   O que muda de verdade: o material é 4:5 (768x960) e a tela é ~16:9, então o
   cover corta na vertical. Isso é inerente ao full-bleed, não um defeito da
   implementação. O corte é compensado pelo enquadramento dinâmico (--focus-y),
   escrito por scrub.js, para que o assunto continue enquadrado nas duas pontas
   da tomada.

   NADA aqui pode criar transform/filter/backdrop-filter num ANCESTRAL de
   #brandFlight — isso o transformaria em bloco contentor de position:fixed e
   derrubaria o voo do logo silenciosamente.
   ═════════════════════════════════════════════════════════════════════════ */
.stage { overflow: clip; }

.stage__inner {
  display: block;
  position: static;
  width: 100%;
  max-width: none;
  height: 100%;
  margin: 0;
  padding: 0;
  gap: 0;
}

/* O poço vira a tela. absolute contra .stage (sticky, logo é bloco contentor). */
.stage__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  margin: 0;
  border-radius: 0;
  aspect-ratio: auto;
  z-index: 0;
}

/* Véu de leitura. Off-white da marca, nunca cinza e nunca escuro: o ciclorama
   é claro, então o texto continua sendo #1A0A24 e o que precisamos é garantir
   piso de contraste onde o cabelo e o vestido escurecem o fundo. Gradiente
   lateral no desktop, de baixo no celular. */
.stage__inner::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* MEDIDO, não arbitrado. Amostrando os pixels do vídeo por baixo do texto ao
     longo da tomada, o pior caso (cabelo dela) dá contraste 1.09 com a tinta
     #1A0A24 — ilegível. O alpha mínimo do véu nesse ponto é 0.46 para AA (4.5)
     com a tinta e 0.59 para o corpo em --vl-ink-soft. Daí o platô em .68 sobre
     a coluna de texto: cobre os dois com folga.
     Não baixe isso sem reamostrar. E não suba: a .93 lia como um cartão colado
     por cima do vídeo, que é a leitura de "branco fora da animação". A queda é
     longa de propósito, para o véu não ter borda visível. */
  background: linear-gradient(
    100deg,
    rgb(244 241 236 / .70) 0%,
    rgb(244 241 236 / .68) 30%,
    rgb(244 241 236 / .46) 45%,
    rgb(244 241 236 / .16) 58%,
    rgb(244 241 236 / 0) 72%
  );
}

.stage__copy {
  position: absolute;
  z-index: 2;
  left: var(--coluna-esq);
  top: 0;
  bottom: 0;
  width: min(var(--leitura-max), 42vw);
  max-width: var(--leitura-max);
  min-height: 0;
  display: grid;
  grid-template-areas: 'beat';
  align-content: center;
  overflow: visible;
}

/* A marcação de origem sai de dentro do poço e vai para o canto oposto ao
   texto, para não disputar espaço com ele. Continua sobre chapa off-white com
   tinta #1A0A24 — legível sobre qualquer parte do ciclorama, e nunca esmaecida. */
.origem {
  position: absolute;
  z-index: 3;
  left: auto;
  right: max(24px, env(safe-area-inset-right));
  bottom: max(24px, calc(24px + env(safe-area-inset-bottom)));
}

/* ═══ CONVITE A DESCER ═════════════════════════════════════════════════════
   INDICADOR, NUNCA CONTROLE — a mesma regra da trilha. `pointer-events: none`,
   sem href e sem foco: ele não promete uma interação que a página não honra.
   Substituiu o botão "Ver funcionando" do herói, que repetia o CTA da barra.

   QUEM O DIRIGE É O beats.js, pela janela do herói (data-beat="hero" no
   index.html). Some conforme a rolagem começa e volta se o leitor voltar —
   função pura de p, sem número novo e sem estado.

   SEM ANIMAÇÃO EM LAÇO, de propósito. Uma keyframe infinita mantém o
   compositor acordado, e esta página promete 0% de CPU parada — o mesmo motivo
   pelo qual a trilha não tem transição. O convite é a presença do ícone.

   CENTRALIZADO POR left/right/margin, NUNCA POR transform: o beats.js escreve
   transform neste nó para dar o pequeno levante, e dois donos no mesmo
   transform é defeito que este arquivo já pagou uma vez.

   A ALTURA É ACIMA DA MARCAÇÃO DE ORIGEM, sempre e sem regra por viewport, e
   vem de --faixa-origem em tokens.css — o mesmo token que a saída de WhatsApp
   do fecho usa, para que os dois nunca discordem sobre onde a marcação começa.
   Vale no desktop, onde a marcação nem alcança o centro, e no celular, onde ela
   atravessa o centro inteiro. ══════════════════════════════════════════════ */
.descer {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: calc(var(--faixa-origem) + env(safe-area-inset-bottom));
  margin-inline: auto;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  color: var(--vl-purple);
  border: 1px solid var(--vl-purple);
  border-radius: 50%;
  background: rgb(244 241 236 / .72);
  pointer-events: none;
}

/* ═══ SAÍDA DE CONTATO — WHATSAPP ══════════════════════════════════════════
   Duas cópias no documento, um destino só: o fecho da rolagem e o fecho do
   painel. Mesmo texto, mesma classe, mesmo href — main.js confere no boot.

   SECUNDÁRIA POR CONSTRUÇÃO. "Ver funcionando" é o pedido principal e vai abrir
   o provador de verdade; a ordem natural é ver a ferramenta rodando e só depois
   querer falar com alguém. Então aqui NÃO há preenchimento sólido: é a mesma
   chapa de vidro que o texto corrido já usa, com tinta de leitura. Ao lado do
   .btn--fecho, que é roxo cheio e um degrau maior de tipografia, o peso visual
   fica claramente abaixo — que é o pedido.

   NADA DE VERDE WHATSAPP. O ícone é fill="currentColor" e herda a tinta do
   link, como qualquer glifo da página. Cor de terceiro no meio da paleta da
   marca seria a mesma quebra que o manual proíbe para o lime fora do roxo.

   O ícone é o glifo oficial, CHEIO, e essa é a única concessão à "mesma família
   de traço" do resto da página: redesenhá-lo em contorno alteraria uma marca de
   terceiro e derrubaria justamente o reconhecimento que faz o atalho funcionar.
   O que o alinha ao resto é a tinta e o tamanho óptico, não o estilo de traço.
   ═══════════════════════════════════════════════════════════════════════════ */
.zap {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  min-height: var(--tap-min);
  padding: 10px var(--s-5);
  font: 500 15px/1.2 var(--f-body);
  letter-spacing: .01em;
  color: var(--vl-ink);
  text-decoration: none;
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border-radius: var(--raio);
  box-shadow: 0 1px 2px rgb(26 10 36 / .05);
  transition: color var(--dur-quick) var(--ease-out);
}
.zap:hover { color: var(--vl-purple); }
.zap:focus-visible { outline: 2px solid var(--vl-purple); outline-offset: 3px; }

/* Alinhamento ÓPTICO e não geométrico: a bolha tem a cauda embaixo à esquerda,
   então o centro geométrico do quadro de 24 cai abaixo do centro que o olho lê
   contra a altura-x do rótulo. Meio pixel para cima resolve, e é transform no
   ÍCONE — nunca no <a>, que no palco é nó de beat e tem o transform escrito
   pelo beats.js. Dois donos no mesmo transform já custou caro neste projeto. */
.zap__icone {
  flex: none;
  transform: translateY(-.5px);
}

/* No fecho do painel ele vem abaixo do botão principal, com respiro. */
.fecho .zap { margin-top: var(--s-6); }

/* ── No FECHO DA ROLAGEM ───────────────────────────────────────────────────
   Mesma faixa do convite a descer, pelo mesmo token: acima da marcação de
   origem, centrado no palco. Os dois nunca aparecem juntos — o convite vive na
   janela do herói e esta saída começa em 0.965 —, e ocupar o mesmo lugar é
   deliberado: é onde a página convida a entrar e onde ela oferece a saída.

   Centrado por left/right/margin e NUNCA por transform, pela razão acima.
   pointer-events enquanto invisível é do beats.js, que já apaga o alvo de toque
   de qualquer beat com peso abaixo de BEAT_POINTER_MIN — sem isso haveria um
   link clicável no escuro sobre a palma dela. */
.zap--palco {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: calc(var(--faixa-origem) + env(safe-area-inset-bottom));
  margin-inline: auto;
  width: fit-content;
  max-width: calc(100% - var(--s-8));
}

/* O painel fala, o trilho se cala — inclusive esta saída, que tem a gêmea dela
   dentro do painel a poucos dedos de distância. !important pelo mesmo motivo da
   trilha e do convite a descer: é nó de beat e o beats.js escreve opacity
   inline. */
.painel-aberto .zap--palco { opacity: 0 !important; pointer-events: none !important; }

/* ═══ TROCA DE LOOKS ═══════════════════════════════════════════════════════
   A peça trocada é uma CAMADA sobre o poço, não um quadro injetado no motor.
   Ver o comentário no index.html para o porquê. ═══════════════════════════ */

.troca__frame {
  position: absolute;
  inset: 0;
  /* ACIMA do canvas e do vídeo, que dividem --z-canvas, e abaixo da marcação de
     origem. Nasceu em 1 e o canvas pintava por cima: a camada carregava,
     decodificava, anunciava e reportava opacidade 1 — tudo verde na medição, e
     nada na tela. Foi o olho que pegou. */
  z-index: var(--z-troca);
  width: 100%;
  height: 100%;
  object-fit: cover;              /* object-position é escrito por looks.js, do mesmo
                                     focusYAt() que o canvas e o fallback usam */
  opacity: 0;
  pointer-events: none;
  /* DISSOLVÊNCIA, NUNCA CORTE SECO. Mesma família do resto: a curva de saída da
     marca e a escala de durações do config. Só opacity — nada de filter nem de
     transform, que fariam da camada um bloco contentor e custariam pintura. */
  transition: opacity var(--dur-base) var(--ease-out);
}
.troca__frame.is-visivel { opacity: 1; }

/* ── OS CONTROLES ─────────────────────────────────────────────────────────
   Embaixo à esquerda, alinhados à mesma borda da coluna de leitura
   (--coluna-esq), acima da faixa da marcação de origem e acima da saída de
   contato — a mesma pilha, sem que nenhum dos três invada o outro. */
.troca {
  position: absolute;
  z-index: 2;
  left: var(--coluna-esq);
  bottom: calc(var(--faixa-origem) + 44px + var(--s-5) + env(safe-area-inset-bottom));
  max-width: min(360px, calc(100vw - var(--coluna-esq) - var(--s-5)));
}
.troca__grade {
  display: flex;
  gap: var(--s-3);
}

/* NO CELULAR AS MINIATURAS EMPILHAM. Decisão do dono, com referência visual.
   A faixa é a MESMA que o projeto já chama de celular — a que esconde os
   .nav__link, em max-width: 734px. Não inventei um segundo ponto de corte:
   dois números para a mesma ideia saem do lugar na primeira vez que alguém
   mexer num deles.

   Vertical cabe porque a coluna esquerda está vazia no fecho: o texto do
   trilho já se calou, e o que disputa altura ali embaixo são a saída de
   contato e a marcação de origem, das quais a pilha se afasta pelo mesmo
   ancoramento de sempre (--faixa-origem + a altura da saída). */
.troca__rotulo {
  margin-top: var(--s-3);
  font: 500 15px/1.3 var(--f-body);
  letter-spacing: .01em;
  color: var(--vl-ink);
  /* Mesma chapa de vidro do texto corrido: a legenda cai sobre a tomada e o
     limiar dela é o de texto normal, não o de título. */
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border-radius: var(--raio);
  padding: 10px var(--s-4);
  width: fit-content;
  box-shadow: 0 1px 2px rgb(26 10 36 / .05);
}

/* ── OS TRÊS ESTADOS, POR TRÊS MECANISMOS DIFERENTES ──────────────────────
   Opção C, escolhida pelo dono. Neutro é placa lisa; HOVER eleva a placa e não
   usa anel nenhum; SELECIONADO usa anel roxo. Como os mecanismos são
   diferentes, nunca se confundem entre si — o problema de distinguir dois
   estados que usam a mesma linguagem, só variando espessura, desaparece.

   O ANEL É LEGÍVEL POR CONSTRUÇÃO, e não por espessura. A miniatura tem
   ENCHIMENTO: a imagem fica dentro de uma moldura off-white — mas isso resolve
   a borda de DENTRO do anel, e não a de fora.

   CORREÇÃO DE 2026-08-17: o texto acima dizia "o anel roxo sempre assenta sobre
   off-white e nunca sobre a foto", e isso era falso pela metade. box-shadow sem
   `inset` desenha PARA FORA da caixa: o lado interno do anel encosta na moldura
   off-white da placa, e o externo encosta no que houver atrás — que é a foto.
   Medido: pior caso 2,66 contra o limiar 3,0, em 4 de 615 amostras, todas onde o
   anel cai sobre o cabelo escuro dela. Pequeno, e com o remédio já inventado
   neste mesmo arquivo: o estado de FOCO ganhou halo off-white justamente para o
   roxo não sumir ali, e o SELECIONADO tinha ficado sem. Agora o selecionado tem
   a saia off-white de 2px por fora do roxo, então as duas bordas do anel
   encostam em off-white e o contraste deixa de depender do que está atrás.

   Os anéis são compostos por variáveis para poderem coexistir: selecionado e
   focado ao mesmo tempo é o caso normal de quem navega por teclado, e um
   box-shadow sobrescrevendo o outro apagaria justamente o estado.

   A ORDEM DA LISTA É O RESTO DESSA GARANTIA, e faltava. Em box-shadow a
   PRIMEIRA sombra é a de CIMA. Com --halo escrito primeiro, o halo de foco (6px,
   off-white a 92%) cobria por inteiro o anel de selecionado (3px), que vive
   dentro do raio dele — medido na tela em 2026-08-17: focado e selecionado ao
   mesmo tempo, a lista computada saía com o off-white de 6px no topo e o roxo
   embaixo, invisível. Coexistir na declaração não bastava; era preciso coexistir
   na PINTURA. Agora a lista vai do menor para o maior, e cada anel só cobre o
   que está fora dele: roxo em 0–3, saia off-white em 3–5, halo em 5–6, e o
   outline de foco (3–5, e outline pinta acima de qualquer box-shadow) caindo em
   cima de off-white, que era o ponto dele desde o começo. */
.mini {
  --anel: 0 0 0 0 rgb(0 0 0 / 0);
  --halo: 0 0 0 0 rgb(0 0 0 / 0);
  display: block;
  padding: var(--s-2);
  border: 0;
  border-radius: var(--raio);
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  box-shadow: var(--anel), var(--halo), 0 1px 2px rgb(26 10 36 / .05);
  cursor: pointer;
  /* TRAVA DURA: a peça nunca escapa da placa, em motor nenhum. Não é acabamento
     de canto arredondado — é contenção. Ver o bloco de celular: a imagem chegou
     a vazar 400px para fora da placa no WebKit do iOS. */
  overflow: hidden;
  /* Alvo de toque: a miniatura já passa dos 44px, mas a trava fica escrita. */
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  transition: background-color var(--dur-quick) var(--ease-out),
              box-shadow var(--dur-quick) var(--ease-out);
}
.mini__img {
  display: block;
  width: 104px;
  height: 130px;               /* 4:5, a mesma razão do quadro nativo */
  object-fit: cover;
  border-radius: calc(var(--raio) - 3px);
}

/* HOVER — a placa se levanta. Sem anel, de propósito: o anel é do selecionado. */
.mini:hover {
  background: rgb(255 255 255 / .94);
  /* MESMA ORDEM da regra base, e pelo mesmo motivo — esta declaração reescreve a
     lista inteira, então trocar a ordem só lá em cima deixaria o defeito vivo
     debaixo do cursor, que é onde ele apareceria primeiro. */
  box-shadow: var(--anel), var(--halo), 0 6px 16px rgb(26 10 36 / .16);
}

/* SELECIONADO — anel roxo COM SAIA off-white por fora. As duas sombras moram
   dentro da MESMA variável de propósito: assim a ordem entre elas fica presa
   aqui (o roxo por cima, a saia por baixo e portanto visível só na borda de
   fora) e a declaração de box-shadow lá em cima continua com três entradas, sem
   ganhar uma quarta que todo estado teria de zerar. 5px contra 3px deixa 2px de
   off-white — o bastante para o roxo nunca encostar na foto, e bem dentro dos
   8px de enchimento que a coluna do celular reserva contra o overflow:hidden. */
.mini[aria-pressed="true"] {
  --anel: 0 0 0 3px var(--vl-purple), 0 0 0 5px rgb(244 241 236 / .92);
}

/* FOCO — anel duplo, e é o mais forte dos três porque tem de ser. O halo
   off-white por fora existe para o roxo não sumir quando o controle cair sobre
   o cabelo escuro dela; o outline segue sendo o mesmo idioma de foco do resto
   da página (2px, offset 3px). */
.mini:focus-visible {
  outline: 2px solid var(--vl-purple);
  outline-offset: 3px;
  --halo: 0 0 0 6px rgb(244 241 236 / .92);
}

/* O painel fala, o trilho se cala — inclusive estes controles. !important pelo
   mesmo motivo da trilha: são nós de beat e o beats.js escreve opacity inline. */
/* ── CELULAR ─────────────────────────────────────────────────────────────
   ESTE BLOCO VEM DEPOIS DAS REGRAS BASE DE .mini, e a posição é o conserto.
   Ele nasceu logo abaixo de .troca__grade, ANTES delas: mesma especificidade,
   e no CSS a última escrita vence. Resultado — looks.js publicava --mini-alt
   com 83px, o valor certo, e a imagem continuava 104x130 porque a regra base
   sobrescrevia a do celular. É a terceira vez que este arquivo é mordido pela
   mesma cascata: reset escrito antes da regra que ele deveria resetar.
   ────────────────────────────────────────────────────────────────────── */
@media (max-width: 734px) {
  /* A MARCA NA PALMA É INTOCÁVEL, e é o bloco de controles que se afasta dela.
     Nada aqui muda tamanho ou posição do símbolo — quem faz isso é o handoff, e
     este bloco só lê onde ele terminou.

     --marca-base é a base da marca em px, publicada por looks.js a partir do
     MESMO coverFit e das MESMAS constantes que o pouso usa. Ancorando o topo
     dos controles nela, a pilha nunca invade a mão, em altura nenhuma: quanto
     mais curta a tela, mais as miniaturas encolhem, porque a banda entre a mão
     e a base é que manda. Sem isso eu teria de escolher um tamanho fixo menor,
     que resolveria o aparelho do dono e voltaria a colidir no próximo mais
     baixo — foi exatamente o que aconteceu: minha medição a 390x844 dizia 36px
     de folga e no iPhone dele a miniatura estava por cima da marca.

     O reserva de 44% existe só para o primeiro quadro, antes de o looks.js
     publicar a medida; é o valor de um retrato típico (0.3816 mais folga). */
  .troca {
    /* O ENCHIMENTO NÃO É ESTÉTICO: é a folga que o anel de selecionado e o halo
       de foco precisam para não serem cortados pelo overflow:hidden abaixo. A
       placa encostava na borda do contêiner, e o anel de 3px era recortado do
       lado esquerdo — o dono viu no iPhone, em Safari e em Chrome, e reproduzi
       aqui: folga de 0px para um anel de 3px. `left` e `top` recuam o mesmo
       tanto, então o conteúdo continua exatamente onde estava. */
    left: calc(var(--coluna-esq) - var(--s-2));
    top: calc(var(--marca-base, 44%) + var(--s-5) - var(--s-2));
    padding: var(--s-2);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;   /* a pilha cresce para CIMA a partir da base */
    gap: var(--s-3);
    overflow: hidden;            /* trava dura: nada vaza por cima da mão */
  }
  /* O ESPAÇAMENTO É DO gap, E SÓ DELE. A legenda traz margin-top da regra base,
     e dentro desta coluna isso somava 24px onde a conta do looks.js previa 12.
     O conteúdo passava da banda em 10.8px, a grade encolhia por flex-shrink e a
     segunda miniatura invadia o espaço da legenda — encavalando no Chrome do
     iOS, onde a janela é mais curta, e passando raspando no Safari. Dois donos
     do mesmo espaçamento é a mesma classe de defeito que dois donos do mesmo
     transform. */
  .troca__rotulo { margin-top: 0; }
  .troca__grade {
    flex-direction: column;
    /* align-items é OBRIGATÓRIO aqui, não é acabamento. O padrão do flex é
       `stretch`, e numa coluna isso estica cada botão à largura do contêiner:
       medido, o alvo saiu 218x146 com a imagem de 104px encostada à esquerda e
       o resto chapa vazia. Um botão que responde a toque onde não há nada
       visível é promessa falsa de área clicável — e na referência do dono as
       placas abraçam a imagem. */
    align-items: flex-start;
    /* NÃO ENCOLHE. O tamanho das miniaturas já é calculado para caber na banda
       (looks.js), então encolher aqui só poderia significar uma coisa: a conta
       errou. E quando errou, o resultado foi a grade ficar 10.8px menor que o
       próprio conteúdo e as miniaturas transbordarem para cima da legenda —
       silenciosamente, porque flex-shrink não avisa. Com 0 0 auto o erro
       aparece como transbordo no lugar certo e o overflow do pai o contém. */
    flex: 0 0 auto;
  }
  /* A RAZÃO 4:5 FICA NO BOTÃO, não na imagem, e isso não é preferência.
     Deixando a imagem encolher sozinha, a altura caía com a banda e a largura
     não acompanhava: medido a 375x667, a placa saiu 120x94 e o object-fit:cover
     passou a cortar a peça em vez de reduzi-la. Com a razão no botão, quem
     encolhe é a placa inteira e a peça continua enquadrada igual.

     O piso de 72px é de legibilidade: abaixo disso o maiô e o conjunto deixam de
     ser distinguíveis um do outro, e um controle que não se lê não é controle.
     Ele fica acima dos 44px de alvo de toque, então a trava de toque continua
     satisfeita com folga. */
  /* A MINIATURA É DIMENSIONADA EM PIXEL, por looks.js, e nem a placa nem a
     imagem usam porcentagem ou aspect-ratio. Duas tentativas anteriores
     quebraram no WebKit do iOS passando no Chrome do desktop — o raciocínio
     inteiro está no comentário de publicaAlturaDaMiniatura(). Pixel não tem
     interpretação: a placa cresce do conteúdo, como qualquer bloco.

     Os valores de reserva são o tamanho de repouso, para o primeiro quadro
     antes de o JS medir. */
  .mini {
    flex: 0 0 auto;
    height: auto;
  }
  .mini__img {
    width: calc(var(--mini-alt, 130px) * 0.8);   /* 4:5, a razão do quadro nativo */
    height: var(--mini-alt, 130px);
  }
}

.painel-aberto .troca { opacity: 0 !important; pointer-events: none !important; }

/* ── DEGRADAÇÃO: os controles NÃO APARECEM nos dois caminhos estáticos ────
   Escolha entre as duas que o dono ofereceu, e a razão é que a segunda não se
   sustenta aqui:

   · SEM JS não existe troca nenhuma. Miniaturas de peça ao lado de um quadro
     parado seriam a página prometendo o que não pode cumprir — e o convite a
     descer e a linha de rebobinar já saem por esse mesmo motivo.
   · EM MOVIMENTO REDUZIDO o que está na tela é o PÔSTER, que é o quadro 330; os
     tryons são o 355. Trocar ali saltaria a pose no meio do efeito, e o efeito
     inteiro depende de a pose ser idêntica — foi o que a medição da mão provou.
     Oferecer a troca custaria ainda baixar dois quadros para quem pediu menos.

   Nada de conteúdo se perde: a troca é demonstração, não argumento. */
.rm .troca,
.no-js .troca,
.rm .troca__frame,
.no-js .troca__frame { display: none; }

@media (max-width: 934px) {
  .stage__inner::before {
    /* Véu, não cartão. A .95 isto lia como um bloco off-white colado embaixo da
       imagem — exatamente a leitura de "espaço em branco fora da animação" que
       esta página existe para não ter. A imagem tem de aparecer atravessando. */
    /* Mesmo raciocínio medido do desktop: platô em .68 cobrindo a faixa onde o
       cartão de texto vive (10% a 52% a partir da base), queda longa até 78%. */
    background: linear-gradient(
      to top,
      rgb(244 241 236 / .70) 0%,
      rgb(244 241 236 / .68) 40%,
      rgb(244 241 236 / .44) 56%,
      rgb(244 241 236 / .14) 68%,
      rgb(244 241 236 / 0) 80%
    );
  }
  .stage__copy {
    left: max(20px, env(safe-area-inset-left));
    right: max(20px, env(safe-area-inset-right));
    width: auto;
    max-width: none;

    /* GUARDA DE ALTURA. Antes daqui existia só `top: auto` + `bottom: 84px`, e
       era esse o defeito: a caixa era ancorada na BASE sem nenhum piso, então em
       tela baixa ela crescia para cima, passava por baixo da barra e o palco
       (overflow:clip) cortava o começo do texto. Medido: em 844x390 o olho
       "Provador virtual" ficava 100% coberto e a H1 41%; o cartão `proof` era
       recortado em y = -6.2.

       Isto NÃO é conserto de paisagem de celular. Paisagem foi só o caso que
       expôs — o mesmo mecanismo derruba uma janela de desktop estreita e baixa
       (900x440 reproduz), que é situação comum: tela dividida, laptop com dock e
       barra de abas comendo altura. Por isso a guarda é de ALTURA e vale para
       qualquer largura desta faixa, sem caso especial por viewport. Se algum dia
       for preciso uma regra por tamanho de tela aqui, o conserto está errado.

       Três peças, e as três são necessárias:
         top    piso duro: a caixa não pode começar acima da barra.
         bottom âncora de base, como antes.
         safe   quando o conteúdo NÃO cabe entre os dois, `safe end` desiste do
                alinhamento à base e volta ao início — o navegador prefere
                perder o fim a esconder o começo. Sem o `safe`, `end` sozinho
                empurra o excesso para fora do topo, que é exatamente o defeito.

       O critério em css/layout.css:575 continua valendo palavra por palavra: H1
       truncada e o olho sumindo são falha automática. Agora a página cumpre. */
    top: calc(var(--nav-h) + var(--s-4));
    bottom: calc(84px + env(safe-area-inset-bottom));
    align-content: safe end;
    overflow: visible;
    /* O layout antigo de celular dava um CARTÃO off-white a esta coluna. Em
       full-bleed isso é exatamente a leitura errada: um bloco branco colado por
       cima do vídeo, com borda reta. Quem garante a legibilidade agora é o véu
       em gradiente do ::before, calibrado pelos pixels reais do vídeo. */
    background: none;
    border-radius: 0;
    padding: 0;
    box-shadow: none;
  }
  .origem {
    left: max(20px, env(safe-area-inset-left));
    right: auto;
    bottom: max(20px, calc(20px + env(safe-area-inset-bottom)));
  }
}

/* O poster é o frame 330. Segue o mesmo enquadramento que o canvas usa nesse
   ponto (config.FOCUS_Y), senão o primeiro quadro pintado salta ao trocar para
   o canvas — e no movimento reduzido, que só mostra o poster, a composição
   ficaria diferente da que todo mundo vê. */
.stage__poster img { object-position: 50% 40%; }

/* ═══════════════════════════════════════════════════════════════════════════
   PAINEL — o conteúdo longo, por cima da animação.

   A animação é o fundo permanente da página. Este painel é a única superfície
   de leitura densa, e ele flutua sobre a modelo em vidro fosco em vez de virar
   uma chapa branca abaixo dela.

   backdrop-filter aqui é seguro E deliberado: #painel vive dentro de <main>,
   e #brandFlight é filho direto de <body> nascido antes de <main>, então o
   painel nunca é ancestral da camada de voo. Ver o comentário no index.html.
   ═════════════════════════════════════════════════════════════════════════ */
.painel {
  position: fixed;
  inset: 0;
  z-index: var(--z-painel, 60);
  display: block;
}
.painel[hidden] { display: none; }

.painel__veu {
  position: absolute;
  inset: 0;
  /* Vidro fosco, não parede. A modelo continua visível atrás — é isso que
     diferencia "conteúdo SOBRE a animação" de "conteúdo EM VEZ da animação".
     A .90 ela sumia e o painel lia como uma página branca comum, que é
     exatamente o que esta página existe para não ter.

     O desfoque muda a conta do contraste: ele achata o detalhe de alta
     frequência, então o que importa não é o pixel mais escuro e sim a MÉDIA da
     vizinhança do raio do blur. Medido nessa base, a pior vizinhança dá
     rgb(69,43,27) e a .75 rende 11.1 na tinta e 7.4 no corpo — folga larga
     sobre o AA de 4.5. Foi assim que deu para abrir a imagem sem perder
     legibilidade. Não suba isto sem reamostrar. */
  background: rgb(244 241 236 / .75);
  -webkit-backdrop-filter: saturate(1.15) blur(22px);
  backdrop-filter: saturate(1.15) blur(22px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Sem blur o texto ficaria sobre a imagem crua: compensa com opacidade. */
  /* Sem blur não existe o achatamento acima: aí o pixel escuro isolado volta a
     mandar e o alpha precisa subir de verdade. */
  .painel__veu { background: rgb(244 241 236 / .94); }
}

.painel__scroll {
  position: absolute;
  /* A CAIXA COMEÇA ABAIXO DA BARRA, e isso é LIMITE, não folga.

     Era inset:0 com o recuo feito por padding-top. Padding só afasta o conteúdo na
     posição inicial — rolando, o texto sobe por dentro dele e passa POR BAIXO da
     barra, que é translúcida (alpha .68 com blur). Medido a 390x844, varrendo 41
     posições: 73.2% delas tinham linha de texto sob a barra, até 4 linhas de uma
     vez. Duas leituras sobrepostas, exatamente o que .painel-aberto .stage__copy
     existe para evitar do outro lado.

     Quem impede a rolagem de passar é a borda da caixa, não o enchimento. Com o
     inset abaixo, a faixa de topo deixa de ser alcançável por conteúdo — e é isso
     que também dá ao .nav__fechar um lugar onde ele não cobre ninguém. */
  inset: var(--nav-h) 0 0 0;
  overflow-y: auto;
  overscroll-behavior: contain;   /* a rolagem do painel não vaza para o trilho */
  -webkit-overflow-scrolling: touch;
  padding-top: var(--s-8);
  padding-bottom: calc(var(--s-12) + env(safe-area-inset-bottom));
  scroll-behavior: auto;
}
.painel__scroll:focus { outline: none; }

/* (O botão de fechar mudou de casa: agora é .nav__fechar, na barra. As regras que
   viviam aqui foram embora com ele — regra sem elemento é a mesma classe de defeito
   que .painel-aberto .rail sem !important, e este arquivo já pagou por ela.) */

/* A primeira seção do painel não precisa da folga de topo das seções soltas. */
.painel .secao:first-of-type { padding-top: var(--s-8); }

/* O trilho não rola enquanto o painel está aberto. Trava por classe no <html>,
   preservando a posição — nada de scrollTo, que perderia o lugar da narrativa. */
.painel-aberto { overflow: hidden; }

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

/* ── SEM JS: o painel volta a ser fluxo normal ────────────────────────────
   Os âncoras do menu voltam a funcionar como sempre. Ninguém perde conteúdo
   porque um script não carregou. */
.no-js .painel { position: static; display: block; }
.no-js .painel[hidden] { display: block; }
.no-js .painel__veu { display: none; }
.no-js .painel__scroll { position: static; overflow: visible; padding-top: 0; }
/* .nav__fechar já nasce display:none e só aparece sob .painel-aberto, que é classe
   escrita por JS — sem JS ele nunca aparece, sem precisar de regra própria. */

/* Movimento reduzido: o painel continua sendo camada (é conteúdo sob demanda,
   não animação), mas sem desfoque nem transição. */
.rm .painel__veu {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background: rgb(244 241 236 / .97);
}

/* As seções foram desenhadas para viver sobre a chapa off-white da página, e
   por isso carregam fundo próprio OPACO. Dentro do painel isso cobria a
   animação inteira — o véu ficava por baixo do conteúdo e não adiantava dosar
   alpha nenhum. Aqui quem é superfície de leitura é o véu, e só ele.
   O bloco roxo (.palco) mantém o fundo: ele é peça de marca, não chapa. */
.painel .secao,
.painel .rodape,
.painel .secao--colada,
.painel .secao--fecho { background: transparent; }

/* ═══════════════════════════════════════════════════════════════════════════
   CONTEÚDOS INTERNOS — mesma animação de fundo, rolando junto, SEM VÉU.

   O painel deixa de ser vidro fosco: o conteúdo assenta direto sobre a tomada,
   e a tomada scrubba com a rolagem do texto (main.js troca o p do trilho pelo
   progresso de leitura do painel enquanto ele está aberto).
   ═════════════════════════════════════════════════════════════════════════ */
.painel__veu { display: none; }

/* Sem véu, o texto do TRILHO ficaria visível por baixo do texto do painel —
   duas leituras sobrepostas. O trilho se cala enquanto o painel fala.
   opacity, não display:none: o beat volta sem reflow ao fechar. */
.painel-aberto .stage__copy { opacity: 0; }
.painel-aberto .stage__inner::before { opacity: 0; }
/* !important pelo MESMO motivo da trilha logo abaixo: .descer é um nó de beat, e
   o beats.js escreve opacity inline nele a cada tick. Sem isto a regra seria
   letra morta e o ícone ficaria por cima do texto do painel — que é exatamente o
   defeito que acabou de sair da faixa de topo. (.stage__copy não precisa: ali o
   alvo é o CONTÊINER, e o beats.js escreve nos filhos.) */
.painel-aberto .descer { opacity: 0 !important; }
/* !important porque quem escreve a opacidade da TRILHA é o rail.js, INLINE, a cada
   tick — e de propósito: o cabeçalho dele explica que a escrita inline tem de vencer
   a rampa de CSS, que é o padrão degradado para quando o módulo não carrega. Regra de
   estado não vence estilo inline sem isto, e sem isto esta linha era letra morta:
   medido com o painel aberto, a trilha ficava em opacity 1, inline e computado.
   Mesmo remédio e mesmo motivo da .painel-aberto .origem logo abaixo. O valor inline
   continua sendo escrito e continua correto — ao fechar, a trilha volta sem reflow. */
.painel-aberto .rail { opacity: 0 !important; }

/* A marcação de origem NÃO se cala. Ela é obrigatória em todo plano, e com o
   painel aberto continua havendo imagem gerada na tela. */
.painel-aberto .origem { opacity: 1 !important; }

/* (O botão de fechar já nasce com chapa própria em .nav__fechar — a barra também
   não tem véu atrás. Não há nada a compensar aqui.) */

/* ═══════════════════════════════════════════════════════════════════════════
   CARTÃO DE VIDRO — só atrás do texto corrido, nunca atrás do título.

   Sem véu de tela cheia, medido: 371 amostras abaixo de AA e pior caso 1.00
   (texto invisível). Mas o problema é de TAMANHO, não de superfície: o título
   grande sobrevive sobre a tomada, a linha de leitura não. Então quem ganha
   chapa é o texto corrido, e o título fica solto sobre a imagem — que é onde a
   animação continua aparecendo inteira.

   Branco (--vl-card), não off-white: o cartão é uma superfície que LEVANTA da
   imagem, e o manual reserva o branco exatamente para isso.
   ═════════════════════════════════════════════════════════════════════════ */
.painel .secao .corpo,
.painel .secao .legenda,
.painel .secao .nota,
.painel .rodape__nota {
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border-radius: var(--raio);
  padding: var(--s-4) var(--s-5);
  /* O recuo negativo alinha a PRIMEIRA LINHA do texto com o título que está
     fora do cartão. Sem isso o enchimento empurraria a coluna para a direita e
     o bloco pareceria desalinhado do próprio título. */
  margin-left: calc(var(--s-5) * -1);
  margin-right: calc(var(--s-5) * -1);
  box-shadow: 0 1px 2px rgb(26 10 36 / .05);
}

/* Dentro de bloco roxo o texto já tem chapa própria: dois cartões empilhados
   seria um erro de leitura, e o lime só existe sobre o roxo. */
.painel .palco .corpo,
.painel .palco .legenda,
.painel .palco .nota { background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  padding: 0; margin-left: 0; margin-right: 0; box-shadow: none; }

/* Os itens de lista viram cartões inteiros: título pequeno e legenda andam
   juntos, então a chapa envolve os dois em vez de partir o item ao meio. */
.painel .recurso,
.painel .passo {
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border-radius: var(--raio);
  padding: var(--s-5);
  box-shadow: 0 1px 2px rgb(26 10 36 / .05);
}
.painel .recurso .legenda,
.painel .passo .legenda,
.painel .recurso .corpo,
.painel .passo .corpo {
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  padding: 0; margin-left: 0; margin-right: 0; box-shadow: none;
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Sem desfoque o detalhe fino da imagem volta a atravessar: sobe a opacidade. */
  .painel .secao .corpo,
  .painel .secao .legenda,
  .painel .secao .nota,
  .painel .rodape__nota,
  .painel .recurso,
  .painel .passo { background: rgb(255 255 255 / .94); }
}

/* O rótulo de seção NÃO é título: são 19px em roxo, e sobre a tomada isso é a
   pior combinação possível — medido, 50 de 96 amostras abaixo de AA, pior caso
   1.06 (invisível). Entra como "texto" na regra do cartão, mas como ficha justa
   ao redor da palavra, não como faixa de coluna inteira: ele emparelha com o
   título logo abaixo, que segue solto sobre a imagem. */
.painel .secao .rotulo {
  display: inline-block;
  background: rgb(255 255 255 / .78);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border-radius: var(--raio);
  padding: var(--s-2) var(--s-3);
  margin-left: calc(var(--s-3) * -1);
  box-shadow: 0 1px 2px rgb(26 10 36 / .05);
}
.painel .palco .rotulo {
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  padding: 0; margin-left: 0; box-shadow: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .painel .secao .rotulo { background: rgb(255 255 255 / .94); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   VIDRO MAIS TRANSPARENTE — o mínimo que ainda passa em AA, medido.

   Varredura sobre 68 nós de texto em 17 pontos da leitura, compondo o cartão
   sobre a média da vizinhança do desfoque:

     tinta --vl-ink-soft #3B2B45  ->  alpha mínimo 0.60
     tinta --vl-ink      #1A0A24  ->  alpha mínimo 0.48

   O desfoque quase não muda o limiar (4.70 a 4.76 de 7px a 20px de blur): ele
   decide quanto DETALHE atravessa, não quanto contraste sobra. Então o desfoque
   pode cair para mostrar mais imagem, e quem compra a transparência é a tinta.

   Escolhido: alpha .54 com blur 12px e o corpo na tinta cheia. O .54 fica seis
   pontos acima do mínimo de .48 de propósito — a tomada muda o tempo todo e a
   varredura amostra 17 posições, não infinitas.

   O manual reserva --vl-ink-soft para leitura corrida sobre a chapa off-white.
   Aqui a superfície é vidro sobre imagem, não off-white, e a regra do DNA para
   texto de leitura é a tinta cheia. É ela que paga a transparência.
   ═════════════════════════════════════════════════════════════════════════ */
.painel .secao .corpo,
.painel .secao .legenda,
.painel .secao .nota,
.painel .rodape__nota,
.painel .recurso,
.painel .passo,
.painel .secao .rotulo {
  background: rgb(255 255 255 / .54);
  -webkit-backdrop-filter: blur(12px) saturate(1.08);
  backdrop-filter: blur(12px) saturate(1.08);
}
.painel .secao .corpo,
.painel .secao .legenda,
.painel .secao .nota,
.painel .rodape__nota,
.painel .recurso .legenda,
.painel .passo .legenda,
.painel .recurso .corpo,
.painel .passo .corpo { color: var(--vl-ink); }

/* Os que não têm cartão próprio seguem na regra antiga. */
.painel .palco .corpo,
.painel .palco .legenda { color: var(--vl-on-purple-soft); }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Sem desfoque não há média de vizinhança: o pixel escuro isolado volta a
     mandar e a transparência tem de recuar. */
  .painel .secao .corpo,
  .painel .secao .legenda,
  .painel .secao .nota,
  .painel .rodape__nota,
  .painel .recurso,
  .painel .passo,
  .painel .secao .rotulo { background: rgb(255 255 255 / .90); }
}

/* ── CORREÇÃO DE CASCATA ────────────────────────────────────────────────────
   O bloco roxo tem chapa própria e texto claro. A regra do cartão de vidro,
   escrita depois neste arquivo, tinha a mesma especificidade e passou a vencer
   dentro dele: o texto branco do .palco ganhou um cartão BRANCO de 54% por
   cima. Branco sobre branco, medido em 1.20 — invisível.

   Este reset vem por último E com especificidade maior (.painel .secao .palco),
   para não depender de ordem de escrita. Nada do que vier depois deve baixar
   isso sem reamostrar o contraste dentro do bloco roxo. */
.painel .secao .palco .corpo,
.painel .secao .palco .legenda,
.painel .secao .palco .nota,
.painel .secao .palco .rotulo,
.painel .secao .palco .dado-rotulo {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 0;
  margin-left: 0;
  margin-right: 0;
  box-shadow: 0 0 0 0 transparent;
  border-radius: 0;
}
.painel .secao .palco .rotulo { display: block; }

/* A assinatura do rodapé é texto solto sobre a tomada e tinha ficado sem chapa. */
.painel .rodape__marca {
  display: inline-block;
  background: rgb(255 255 255 / .54);
  -webkit-backdrop-filter: blur(12px) saturate(1.08);
  backdrop-filter: blur(12px) saturate(1.08);
  border-radius: var(--raio);
  padding: var(--s-2) var(--s-3);
  color: var(--vl-ink);
}

/* ── SEGUNDA CORREÇÃO DE CASCATA, mesma causa ───────────────────────────────
   O item de lista JÁ É o cartão. A regra genérica do vidro, escrita depois,
   voltou a alcançar a legenda dentro dele e desenhou um segundo vidro por
   cima do primeiro — visível como um retângulo mais claro em volta da linha.

   Regra geral deste arquivo, aprendida duas vezes: quem tem cartão é o BLOCO,
   e o texto dentro dele nunca reempilha chapa. Especificidade maior e por
   último, para não depender de ordem de escrita. */
.painel .secao .recurso .legenda,
.painel .secao .recurso .corpo,
.painel .secao .passo .legenda,
.painel .secao .passo .corpo,
.painel .secao .recurso .rotulo,
.painel .secao .passo .rotulo {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 0;
  margin-left: 0;
  margin-right: 0;
  box-shadow: 0 0 0 0 transparent;
  border-radius: 0;
  display: block;
}

/* ═══════════════════════════════════════════════════════════════════════════
   BLOCO ROXO EM VIDRO — o mais transparente que o lime permite.

   Pedido: mesma transparência dos outros cartões (.54), em roxo. Não dá, e o
   motivo é a regra dura da marca. Os cartões brancos carregam tinta ESCURA
   sobre chapa clara: quanto mais imagem atravessa, mais escuro fica o fundo e
   MELHOR o contraste. Aqui é o oposto — texto branco e rótulo lime precisam que
   a chapa continue ESCURA, e o ciclorama por trás é claro. Transparência
   trabalha contra a legibilidade neste bloco, não a favor.

   Medido sobre 65 amostras de fundo ao longo do bloco:
     .54 (o pedido)          lime 3.81 · corpo 2.69   -> reprova os dois
     .74 com o corpo atual   lime 6.52 · corpo 4.60   -> passa
     .61 com corpo em BRANCO CHEIO                     -> passa no limite (lime 4.59)

   Escolhido .65: o corpo vai a branco cheio (é ele que compra a transparência,
   como a tinta comprou nos cartões claros) e sobra margem no lime, que é o
   gargalo — 5.12 contra o mínimo de 4.5.

   Descer daqui apaga o lime sobre o roxo, que é exatamente o que o manual
   proíbe. Não baixar sem reamostrar.
   ═════════════════════════════════════════════════════════════════════════ */
.painel .secao .palco {
  background: rgb(74 0 122 / .65);
  -webkit-backdrop-filter: blur(12px) saturate(1.08);
  backdrop-filter: blur(12px) saturate(1.08);
}
/* O corpo dentro do bloco vai a branco cheio: é o que permitiu descer de .74
   para .65 sem perder AA. */
.painel .secao .palco .corpo,
.painel .secao .palco .legenda,
.painel .secao .palco .dado-rotulo { color: var(--vl-on-purple); }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Sem desfoque o pixel claro isolado do ciclorama volta a atravessar e come o
     lime: a chapa fecha. */
  .painel .secao .palco { background: rgb(74 0 122 / .92); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   O LOGOTIPO FICA NA BARRA.

   Só o símbolo voa. O logotipo escrito não some, não encolhe e não se move —
   ele permanece exatamente onde estava enquanto a marca desce para a mão dela.

   Por isso o que se esconde durante o voo é O SÍMBOLO DENTRO DO SLOT, e não o
   slot inteiro: a caixa continua com a mesma largura em px, o logotipo continua
   no mesmo x, e o cabeçalho não reflui em nenhum quadro.

   A cópia do logotipo que vive dentro da camada de voo nunca aparece. Ela fica
   na marcação porque o lockup é um par e removê-la mudaria a caixa que o
   handoff mede — mas quem o leitor vê é sempre o da barra. */
.nav__slot.is-flying { opacity: 1; }
.nav__slot.is-flying .lockup__mark { opacity: 0; }

/* Pelo ID, não por .flight__word: aquela classe NÃO EXISTE na marcação — o nó
   é <span class="lockup__word" id="flightWord">. O seletor errado não casou com
   nada e o logotipo da camada de voo apareceu em tamanho de palma ao lado do
   símbolo, na mão dela. Regra antiga do arquivo com o mesmo defeito logo acima
   (.flight__word { transform-origin }) também nunca fez efeito. */
#flightWord { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   MARCAÇÃO DE ORIGEM — fixa, fora do palco, acima do painel.

   Ver o comentário longo no index.html: `.stage` é sticky e cria contexto de
   empilhamento, então enquanto isto morava lá dentro nenhum z-index alcançava o
   painel. Medido antes: 100% da placa coberta em 390x844.

   Fica acima do painel e abaixo da barra. É a única coisa da página que o
   painel não pode cobrir, porque é garantia de marca e não decoração.
   ═════════════════════════════════════════════════════════════════════════ */
.origem {
  position: fixed;
  z-index: var(--z-origem-fixa);
  left: auto;
  right: max(24px, env(safe-area-inset-right));
  bottom: max(24px, calc(24px + env(safe-area-inset-bottom)));
  opacity: 1 !important;
  visibility: visible !important;
}
@media (max-width: 934px) {
  .origem {
    left: max(20px, env(safe-area-inset-left));
    right: auto;
    bottom: max(20px, calc(20px + env(safe-area-inset-bottom)));
  }
}
/* Nos caminhos estáticos ela continua fixa: "nunca removível, em nenhum plano"
   inclui o plano em que o JS não subiu. */
.rm .origem,
.no-js .origem { position: fixed; opacity: 1 !important; visibility: visible !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   ALTURA CURTA — a segunda metade da guarda.

   O `top` + `safe end` acima garante que o texto nunca comece atrás da barra.
   Mas garantir que o COMEÇO apareça não é garantir que o BLOCO CAIBA: se o
   conteúdo é mais alto que o espaço, o excesso passa a transbordar para baixo e
   o palco (overflow:clip) come o CTA em vez da H1. Trocar qual ponta se perde
   não é conserto.

   Então aqui a composição se ajusta à altura disponível. UMA consulta de altura,
   não uma regra por aparelho: 844x390, 812x375, 740x420 e 900x440 entram todas
   pelo mesmo mecanismo, e uma janela de desktop dividida entra junto.

   A conta que define o limiar: com ~222px úteis (o caso de 844x390), mostrar
   olho + H1 de três linhas + parágrafo + botão obriga a H1 a ~20px, que não é
   mais um título. Sem o parágrafo, a mesma altura comporta a H1 a ~40px. A
   escolha é entre um título ilegível e uma frase a menos, e o título é o que
   carrega a página.

   O parágrafo NÃO é removido da árvore de acessibilidade — ele fica oculto
   visualmente e continua sendo lido e encontrado no Ctrl+F. Quem perde é o olho
   numa janela baixa, não o leitor de tela.
   ═════════════════════════════════════════════════════════════════════════ */
@media (max-width: 934px) and (max-height: 520px) {
  .stage__copy { gap: var(--s-2); }

  .beat .rotulo { font-size: 13px; letter-spacing: .09em; }

  .beat .titulo-xl,
  .beat .titulo-lg,
  .beat .titulo-md {
    font-size: clamp(24px, 8.2vh, 40px);
    line-height: 1.06;
  }

  /* Visualmente oculto, não removido: mesmo padrão de .visualmente-oculto. */
  .beat > .corpo,
  .beat > .legenda {
    position: absolute !important;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%);
    overflow: hidden; white-space: nowrap;
  }

  /* (As três regras .beat .btn saíram com o botão do herói. Só existia UM .btn
     dentro de beat; o outro .btn da página é o do fecho do painel, que não vive
     num beat e nunca foi alcançado por elas. Regra sem elemento é o mesmo
     defeito da .painel-aberto .rail sem !important.) */

  /* O cartão roxo é o beat mais alto e era o outro sintoma do mesmo defeito
     (recortado em y = -6.2). Aperta junto, pelo mesmo motivo. */
  .beat .palco--beat { padding: var(--s-3); }
  .beat .dado { font-size: 36px; }
}
