/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   A INTRO DO SITE — o manifesto e o tour, portados do app
   ═══════════════════════════════════════════════════════════════════════════════════════════════

   Isto é o porte de `ManifestoView.swift` + `TourView.swift`. A ordem, as frases, as cores, os
   tamanhos e a mecânica são de lá; o que muda está marcado como DIVERGÊNCIA no comentário
   correspondente. A regra que vale para o arquivo todo: quando o app e o site discordarem, o app é
   a fonte da verdade e a divergência vai escrita, com o motivo.

   O QUE MUDA NO FIM, e é o pedido: a última porta não é "Criar minha conta" (aqui não há cadastro),
   é BAIXAR NA APP STORE. Ela aponta para `/download`, que é o único lugar do site onde a URL da
   loja existe (netlify.toml) — nenhuma página pode ficar desatualizada.

   AS FONTES QUE NÃO VIERAM. O app usa quatro faces licenciadas; o site já servia uma (Alte Haas) e
   agora serve duas (mais a Henrietta, que é o texto do manifesto e não tem substituto). As outras
   duas caem no sans do sistema, e é uma troca consciente:
     * `tkHeadline` (Circular XX Book) → sans do sistema. Aparece em nomes e títulos de linha.
     * `tkHandle` (Söhne Breit Kräftig) → sans do sistema com tracking aberto, que é a característica
       que se perde primeiro. Vale para os @arrobas.
   Servir quatro arquivos de fonte licenciada de um site público é um problema diferente de embutir
   os mesmos quatro num binário, e duas delas não carregam texto que o visitante vá ler com atenção.
   ═════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── O palco ──────────────────────────────────────────────────────────────────────────────────── */

.intro {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* A TINTA DESTA TELA É FIXA, não é token de tema — igual ao app (ManifestoView.swift:139). O
     gradiente da marca é o mesmo nos dois temas, então a tinta que lê sobre ele também é. */
  --ink: #FAF6EF;
  --deep: #12030D;
  color: var(--ink);
}
.intro[hidden] { display: none !important; }

/* A saída: o calor do gradiente escoa para o preto antes de entregar a tela para a home. É a mesma
   emenda do app (manifesto → splash), e existe pelo mesmo motivo: a página de baixo é `--bg`, então
   terminar no gradiente aceso faria um corte visível na troca. */
/* `--mag` MORA AQUI, no palco, e não na cena — porque duas coisas precisam ler o mesmo número: a
   cena, que se amplia por ele, e a mesa, que reserva a folga para a ampliação caber. Ver o bloco de
   desktop. No aparelho vale 1 e nada acontece. */
.intro { --mag: 1; }
.intro { transition: opacity 0.42s ease-out; }
.intro.leaving { opacity: 0; pointer-events: none; }

/* ── O FUNDO VIVO ─────────────────────────────────────────────────────────────────────────────
   `LivingGradient` (Theme.swift): as paradas do `KasterGradient` — amostradas 1:1 do conceito,
   quase-preto → vermelho profundo → laranja — com três manchas quentes derivando em ciclos de 8,5,
   11 e 14 segundos. OS TRÊS NÚMEROS NÃO SÃO MÚLTIPLOS ENTRE SI, e é o detalhe todo: com períodos
   múltiplos as manchas coincidem e o campo pulsa em bloco, que é o oposto de respirar.

   Sob um texto que se digita isso não é enfeite. Os cinco segundos de uma frase longa aparecendo
   letra por letra são cinco segundos de tela parada se não houver movimento atrás. */
.intro-bg {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    #12030D 0%, #270712 22%, #550A0A 38%, #AC190A 52%, #E62509 64%,
    #F62E0C 74%, #F94213 83%, #FB6A2B 91%, #FF873E 100%);
  pointer-events: none;
}
.intro-bg .blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  will-change: transform, opacity;
}
.intro-bg .b1 {
  width: 520px; height: 520px; left: -12%; top: 34%;
  background: radial-gradient(circle, rgba(255,135,62,0.55), transparent 70%);
  animation: drift1 8.5s ease-in-out infinite alternate;
}
.intro-bg .b2 {
  width: 440px; height: 440px; right: -14%; top: 12%;
  background: radial-gradient(circle, rgba(230,37,9,0.50), transparent 70%);
  animation: drift2 11s ease-in-out infinite alternate;
}
.intro-bg .b3 {
  width: 600px; height: 600px; left: 18%; bottom: -22%;
  background: radial-gradient(circle, rgba(249,66,19,0.45), transparent 70%);
  animation: drift3 14s ease-in-out infinite alternate;
}
@keyframes drift1 { from { transform: translate3d(0,0,0) scale(0.92); opacity: 0.7 } to { transform: translate3d(60px,-40px,0) scale(1.1); opacity: 1 } }
@keyframes drift2 { from { transform: translate3d(0,0,0) scale(1.06); opacity: 1 }   to { transform: translate3d(-50px,60px,0) scale(0.9); opacity: 0.66 } }
@keyframes drift3 { from { transform: translate3d(0,0,0) scale(0.95); opacity: 0.8 } to { transform: translate3d(40px,-56px,0) scale(1.12); opacity: 1 } }

/* ── O TOUR TEM O OUTRO GRADIENTE, E ISSO NÃO É AJUSTE DE GOSTO ─────────────────────────────
   No app o manifesto usa `LivingGradient()` e o tour usa `LivingGradient(subtle: true)` — duas
   variantes que já existem lá, com o motivo escrito: "aqui quem tem de aparecer são as ilustrações,
   e o campo laranja aceso brigaria com o vidro dos cartões e com as capas dos discos."

   Eu tinha portado só a variante forte, e o navegador mostrou exatamente o que o comentário previa:
   os cartões de vidro sumiam no laranja, a placa vermelha dos Picks perdia contraste e as capas de
   disco brigavam com o fundo. As paradas do `subtle` são quase-pretas em cima e o calor só aparece
   embaixo; as manchas ficam na metade de baixo, mais escuras e mais lentas (9,5 · 13 · 16 s). */
.intro.tour-mode .intro-bg {
  background: linear-gradient(to bottom,
    #0A0A0B 0%, #140709 50%, #2C0D0A 72%, #551A0F 88%, #8A2F18 100%);
}
.intro.tour-mode .intro-bg .blob { opacity: 0.5; filter: blur(70px); }
.intro.tour-mode .intro-bg .b1 { top: 58%; animation-duration: 9.5s; background: radial-gradient(circle, rgba(138,47,24,0.6), transparent 70%); }
.intro.tour-mode .intro-bg .b2 { top: 62%; animation-duration: 13s;  background: radial-gradient(circle, rgba(85,26,15,0.6), transparent 70%); }
.intro.tour-mode .intro-bg .b3 { bottom: -28%; animation-duration: 16s; background: radial-gradient(circle, rgba(172,25,10,0.45), transparent 70%); }
/* O poço de legibilidade é do MANIFESTO: ele existe para o texto ler sobre o gradiente aceso. No
   tour o campo já é escuro, e escurecer mais o miolo só apagaria as ilustrações. */
.intro.tour-mode .intro-pool { opacity: 0; }

/* Um poço escuro atrás do texto (`legibilityPool`, ManifestoView.swift:225). O gradiente acende
   embaixo; escurecer o miolo dá contraste ao texto sem apagar a cor da marca nas bordas. */
.intro-pool {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(circle at 50% 40%, rgba(0,0,0,0.44) 0, transparent 72%);
}

/* O grão da marca. NO APP É UM PNG LADRILHADO (`GrainNoise`, 530KB); aqui é `feTurbulence` num SVG
   embutido — mesma leitura, sem download. Foi por isso que o asset não veio junto. */
.grain {
  position: absolute; inset: 0; pointer-events: none;
  opacity: 0.07; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── A coluna de conteúdo ─────────────────────────────────────────────────────────────────────
   `min(430px, …)` e não a largura da janela: o app roda num aparelho de ~390pt e o desenho todo
   (reserva do texto, a mesa das ilustrações, a altura dos botões) foi medido nessa largura. Numa
   tela de 1400px sem trava, a frase do manifesto viraria uma linha só e o tour uma faixa. A coluna
   estreita não é limitação, é o formato.

   E A ALTURA É A DA JANELA VISÍVEL, NÃO A DO ELEMENTO FIXO — esta é a linha que conserta o botão
   cortado no Safari do iPhone.

   `.intro` é `position: fixed; inset: 0`, e num navegador de aparelho isso NÃO dá a altura que o
   olho vê: dá a "large viewport", a janela como se as barras estivessem recolhidas. Num iPhone 14
   Pro são 852pt de elemento fixo contra 765pt de área visível — os outros 87 estão embaixo da barra
   de endereço. A coluna herdava os 852 pelo `flex: 1`, o pé dela caía nos 836, e o botão do tour
   ficava 71px atrás da barra. Era o que o dono via: a cápsula creme aparecia pela metade.

   `env(safe-area-inset-bottom)` não resolve e não é para isso: com a barra na tela ele vale ZERO
   (a barra cobre a faixa do indicador). Ele protege do indicador, não da barra.

   `dvh` é a janela visível AGORA, barras incluídas na conta, e é estável aqui porque a página não
   rola (`body { overflow: hidden }`) — sem rolagem a barra do Safari nunca se recolhe, então a
   medida não muda no meio do caminho. O `100vh` acima é só para navegador sem `dvh`: não conserta,
   mas também não atrapalha.

   E É `max-height` NA COLUNA, não `height` no palco, de propósito: o gradiente continua sangrando
   até a borda de baixo por trás da barra. Travar `.intro` deixaria uma faixa da home aparecendo ali
   quando a barra animasse. */
.intro-stage {
  position: relative;
  z-index: 2;
  flex: 1;
  max-height: 100vh;
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
  width: min(430px, 100%);
  margin: 0 auto;
  padding: max(10px, env(safe-area-inset-top)) 20px calc(16px + env(safe-area-inset-bottom));
}

/* ── Botões ───────────────────────────────────────────────────────────────────────────────────
   Cápsula creme sólida para o principal; cápsula vazada para o secundário. A hierarquia se faz pelo
   PREENCHIMENTO e não pelo tamanho (mesma altura, mesma largura, mesma forma), porque é a única
   distinção que sobrevive num fundo que vai do quase-preto ao laranja — trocar a COR do secundário
   exigiria um tom que funcionasse sobre o gradiente inteiro. É a razão escrita em
   ManifestoView.swift:376.

   E OS RÓTULOS NUNCA VÃO EM CAIXA BAIXA. Vale para o site como vale para o app: as frases do
   manifesto são minúsculas porque a caixa faz parte da citação; botão é o produto falando, e o
   produto fala em sentence case (kaster-tom-de-voz.md §Botões). */
.ibtn {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; height: 54px; border: 0; border-radius: 999px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 17px; font-weight: 600; letter-spacing: 0.005em;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.26s cubic-bezier(0.22,1,0.36,1), background 0.18s, border-color 0.18s;
}
.ibtn:hover { text-decoration: none; }
/* `hidden` NÃO VENCE UMA CLASSE, e este `!important` é o conserto de um defeito que só apareceu no
   navegador: o botão secundário nascia visível na PRIMEIRA etapa do manifesto, onde não deve haver
   escolha nenhuma. O atributo estava certo — o JS marca `hidden` em toda etapa que não é a última —,
   mas `[hidden] { display: none }` mora na folha do navegador e perde para qualquer regra de autor,
   inclusive o `display: flex` do `.ibtn` logo acima. Sem esta linha o `hidden` não faz nada aqui. */
.ibtn[hidden] { display: none !important; }

/* ── A DICA DE TECLADO ─────────────────────────────────────────────────────────────────────────
   NO APARELHO ELA NÃO EXISTE. A cápsula de 54pt é a coisa certa lá: é o alvo que o dedo alcança,
   e é o que o app faz. Um "aperte Enter" num telefone é uma instrução impossível.

   NO DESKTOP ELA SUBSTITUI A CÁPSULA, e a ideia é do dono: num navegador o teclado é o gesto
   natural, e uma cápsula de 54px de altura é vocabulário de telefone plantado numa janela.

   O DETALHE QUE FAZ ISSO NÃO SER UMA REGRESSÃO: o botão continua existindo. Ele não virou texto
   decorativo — é o MESMO `<a role="button" tabindex="0">`, com os dois rótulos dentro, e quem
   escolhe qual aparece é o CSS. Então quem usa leitor de tela continua encontrando um controle;
   quem está no mouse continua podendo clicar; e redimensionar a janela troca o rótulo na hora, sem
   script envolvido. Trocar o texto pelo JS conforme a largura seria a versão frágil disto. */
.lbl-key { display: none; }
.tour-hint { display: none; }
kbd {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 0.82em; line-height: 1;
  padding: 0.34em 0.5em; min-width: 1.9em;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 6px;
  background: rgba(250,246,239,0.10);
  box-shadow: inset 0 0 0 1px rgba(250,246,239,0.34), 0 1px 0 rgba(250,246,239,0.18);
  color: inherit;
}
/* O press-feedback é `PressableStyle(scale: 0.97)` no app: responde no toque, não na soltura. */
.ibtn:active { transform: scale(0.97); }
.ibtn.primary { background: var(--ink); color: var(--deep); }
.ibtn.primary:hover { background: #fff; }
.ibtn.secondary {
  background: rgba(250,246,239,0.10); color: var(--ink);
  border: 1px solid rgba(250,246,239,0.55);
}
.ibtn.secondary:hover { background: rgba(250,246,239,0.18); border-color: var(--ink); }
.ibtn svg { width: 19px; height: 19px; fill: currentColor; }

/* "Pular", no alto à direita. Existe por princípio: seis etapas antes da loja são seis etapas que
   alguém pode não querer, e prender a pessoa na apresentação do próprio produto é o oposto de
   confiança. Some na última etapa, onde a porta principal já é a saída. */
.intro-top { display: flex; align-items: center; justify-content: flex-end; height: 44px; flex: 0 0 auto; }
.intro-skip {
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 15px; font-weight: 600;
  color: rgba(250,246,239,0.72);
  padding: 8px 4px;
  transition: color 0.18s, opacity 0.2s;
}
.intro-skip:hover { color: var(--ink); }
.intro-top.hide .intro-skip { opacity: 0; pointer-events: none; }

/* ═══ 1. O MANIFESTO ══════════════════════════════════════════════════════════════════════════ */

.mf { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* COLUNA, não linha: a logo entra ACIMA da frase na última etapa, e as duas se centram juntas. */
.mf-mid {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 26px;
}

/* A LOGO OFICIAL DO KASTER, a mesma da home. Largura igual à de lá (`min(268px, 66vw)`) para as
   duas telas — que agora são a mesma — não terem duas escalas do mesmo lockup. */
.mf-logo {
  width: min(268px, 66vw); height: auto;
  filter: drop-shadow(0 6px 26px rgba(0,0,0,0.8));
}
.mf-logo[hidden] { display: none !important; }

/* A ALTURA É RESERVADA, e o texto se alinha ao TOPO dela.
   Sem a reserva, cada etapa tem a altura da própria frase e o bloco se recentraliza a cada troca —
   a primeira linha dançaria de etapa em etapa. Com ela, a primeira linha nasce sempre na mesma
   altura e uma frase longa cresce para baixo. É `minHeight: 300` no app. */
.mf-text {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 27px;
  line-height: 1.34;
  text-align: center;
  /* A RESERVA É `min-height` E A CAIXA NÃO ESTICA. `align-self: stretch` estava aqui, e o navegador
     mostrou o que ele faz: a caixa crescia até o pé da área livre (600px numa tela de 932) e a
     primeira linha nascia colada no topo da tela em vez de no meio. No app o bloco de 300pt vive
     entre dois `Spacer`, ou seja: a RESERVA fica centrada verticalmente e o TEXTO fica no topo dela.
     Sem o stretch, o `align-items: center` do pai faz exatamente isso. */
  width: 100%;
  min-height: 300px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
@media (max-width: 380px) { .mf-text { font-size: 24px; min-height: 264px; } }

/* A RAMPA DA FRENTE. Duas letras esmaecidas antes do sólido, e o resto da frase TRANSPARENTE em vez
   de ausente — é o que impede o parágrafo de refluir a cada letra (TypedLine, ManifestoView:619).
   O texto inteiro está no DOM desde o primeiro quadro; o que muda é só a cor. */
.mf-text .off { color: transparent; }
.mf-text .r1 { color: rgba(250,246,239,0.55); }
.mf-text .r2 { color: rgba(250,246,239,0.26); }
/* O cursor também está lá desde o primeiro quadro, transparente enquanto digita: assim o espaço
   dele já está reservado e ele não empurra nada ao acender. Pisca só quando a frase acaba, e é aí
   que ele passa a significar "acabou, é sua vez". */
.mf-text .caret { color: transparent; }
.mf-text.done .caret { color: var(--ink); animation: caret 1.06s steps(1) infinite; }
@keyframes caret { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }

.mf-foot { display: flex; flex-direction: column; gap: 16px; flex: 0 0 auto; }

/* ── A ÚLTIMA ETAPA É A HOME DO KASTER ────────────────────────────────────────────────────────
   Pedido do dono: "essa tela deveria ser a tela que tinha do kaster com os discos passando de fundo,
   onde tem os termos de uso e política de privacidade". Faz sentido de produto — a frase final
   ("redescubra seu próprio gosto no kaster") É a entrega, e entregá-la num campo vazio com dois
   botões joga fora uma tela que já existe e é melhor.

   O MURAL NÃO É PORTADO, É REVELADO. A `.splash` já está montada por baixo desta camada, com as
   colunas de capas rolando e os três véus de legibilidade que fazem texto ler sobre elas. Então na
   última etapa o gradiente vivo do manifesto simplesmente SE APAGA e o que aparece é a home. Nada é
   duplicado: uma cópia do mural aqui seria uma segunda animação de 45 imagens rodando ao mesmo
   tempo, pelo mesmo resultado.

   O que a `.splash` esconde nesse momento é só o próprio conteúdo dela (logo, tagline, botão,
   rodapé) — senão ele apareceria ATRÁS do texto do manifesto, que está por cima. Ver `at-home` no
   body, ligado pelo intro.js. */
/* O GRADIENTE SE APAGA, O POÇO DE LEGIBILIDADE FICA — e essa distinção é medida, não gosto.
   O `.intro-pool` é o poço escuro atrás do texto, portado do `legibilityPool` do app. Eu tinha
   apagado os dois juntos, e aí a frase final passou a ser lida sobre capas de disco. Amostrando o
   quadro: no pior ponto o contraste dava 4,7:1 — passa para texto grande, mas por pouco. E uma
   captura não fecha a conta, porque O MURAL ROLA: a capa que está atrás da frase agora não é a que
   estará em dez segundos, e uma capa clara pode chegar ali a qualquer momento.
   Com o poço, o piso deixa de depender de qual disco está passando. */
.intro.is-home .intro-bg { opacity: 0; }

/* A RESERVA DO TEXTO SAI NA ÚLTIMA ETAPA, e é isso que centra o slogan de verdade.
   Os 240px (300 no aparelho) existem para a primeira linha de cada etapa nascer sempre na mesma
   altura — sem eles, uma frase de duas linhas e outra de quatro fariam o parágrafo dançar a cada
   troca. Na última etapa NÃO HÁ próxima etapa com que alinhar: a reserva vira só um bloco vazio
   empurrando a frase para cima, e foi o que o dono viu ("precisa centralizar bem mais o slogan").
   Sem ela, o grupo — logo, frase, traços, botões, rodapé — se centra como a home se centra. */
.intro.is-home .mf-text { min-height: 0; }

/* E O GRUPO INTEIRO SE COMPACTA, que é o que centra o slogan de verdade.
   `.mf-mid` é `flex: 1`: nas etapas normais ele toma toda a altura livre, e é isso que mantém a
   frase no alto com o pé onde o dedo alcança. Na última etapa isso deixava o slogan flutuando a 27%
   da tela com um vão enorme até os botões — medido, o centro da frase ficava 103px acima do meio.

   Com `flex: 0 0 auto` o miolo passa a medir o próprio conteúdo, e o `justify-content: center`
   centra logo + frase + traços + botões + rodapé como UM bloco. É exatamente a composição da home
   (a `.content` dela é uma coluna centrada), e agora as duas telas são a mesma. */
.intro.is-home .mf { justify-content: center; gap: 34px; }
.intro.is-home .mf-mid { flex: 0 0 auto; }
.intro-bg, .intro-pool { transition: opacity 0.7s ease; }

.mf-home { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.mf-home[hidden] { display: none !important; }   /* `hidden` não vence uma classe; ver `.ibtn[hidden]` */
.mf-soon {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  margin: 2px 0 0; font-size: 13px; color: rgba(250,246,239,0.56);
}
.mf-soon svg { width: 13px; height: 13px; fill: currentColor; opacity: 0.85; }
.mf-legal {
  margin: 0; text-align: center;
  font-size: 12.5px; line-height: 1.9; color: rgba(250,246,239,0.5);
}
.mf-legal a { color: rgba(250,246,239,0.78); text-decoration: none; }
.mf-legal a:hover { color: var(--ink); text-decoration: underline; }

/* Um traço por etapa: onde a pessoa está, quanto falta, e a volta. Só dá para VOLTAR — um traço à
   frente fica desabilitado, porque pular para uma etapa não lida é oferecer um atalho que engole o
   texto. Quem quer o atalho tem o "Pular", que é honesto sobre o que faz. */
.mf-segs { display: flex; gap: 6px; height: 44px; }
.mf-segs button {
  flex: 1; background: none; border: 0; padding: 0; height: 44px;
  display: flex; align-items: center; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.mf-segs button:disabled { cursor: default; }
.mf-segs i {
  display: block; width: 100%; height: 3px; border-radius: 999px;
  background: rgba(250,246,239,0.2);
  transition: background 0.3s ease;
}
.mf-segs button.past i { background: rgba(250,246,239,0.5); }
.mf-segs button.now  i { background: var(--ink); }

/* A troca de frase: quem ENTRA desliza, quem SAI só desaparece.
   Cruzar dois deslizes é mais lento de ler do que um deslize contra um fade, e o sentido do
   movimento é o sentido da leitura — de baixo quando avança, de cima quando volta. */
.mf-text.in-fwd { animation: mfIn 0.34s cubic-bezier(0.22,1,0.36,1); }
.mf-text.in-back { animation: mfInBack 0.34s cubic-bezier(0.22,1,0.36,1); }
@keyframes mfIn     { from { opacity: 0; transform: translateY(22px) } to { opacity: 1; transform: none } }
@keyframes mfInBack { from { opacity: 0; transform: translateY(-22px) } to { opacity: 1; transform: none } }

/* ═══ 2. O TOUR ═══════════════════════════════════════════════════════════════════════════════
   Seis páginas que se arrastam: a ilustração em cima, a frase embaixo dela, as bolinhas no pé.

   A ORDEM VERTICAL É DECISÃO DO DONO e ela já esteve invertida: com a frase em cima, a ilustração
   vira legenda do texto; com a frase embaixo, a imagem trabalha primeiro e o texto diz o que ela é.

   NENHUMA ILUSTRAÇÃO É UM DESENHO. Cada uma remonta os componentes do app — o cartão de memória, o
   botão do Shazam, as linhas da fila, os blocos de um pick, o balão do motto, o cabeçalho de perfil
   — com as medidas, cores e fontes de lá. É a mesma regra do `TourView`, e o custo é o mesmo: mexer
   naqueles componentes obriga a mexer aqui. É o ponto. Uma captura de tela envelheceria em silêncio.

   DIVERGÊNCIA: no app o tour sobe DE BAIXO por cima do manifesto, como camada de tela cheia. Aqui
   ele TROCA com o manifesto no mesmo palco, com um fade curto — a página já é uma camada fixa sobre
   a home, e empilhar uma terceira só para reproduzir o gesto de uma folha custaria mais do que
   entrega. O caminho de volta continua sendo o mesmo por onde se entrou. */

.tour { display: none; flex-direction: column; flex: 1; min-height: 0; }
.tour.on { display: flex; animation: fadeIn 0.3s ease-out; }
.mf.off { display: none; }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }

/* A FITA SANGRA ATÉ A BORDA DA TELA, e é isso que a margem negativa faz.
   A área de rolagem recorta o que sai dela. Com o respiro de 20px por fora, essa borda de recorte
   caía 20px DENTRO da tela e a sombra dos cartões era cortada ali, em reta, dos dois lados.
   Sangrando até a borda, o recorte coincide com a borda do aparelho — que é onde recortar não se lê
   como defeito. */
.tour-deck {
  flex: 1; min-height: 0;
  margin: 0 -20px;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.tour-deck::-webkit-scrollbar { display: none; }

.tpage {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex; flex-direction: column;
  min-width: 0;
  padding: 0 20px;
}

/* O RESPIRO DA SOMBRA, POR DENTRO DO RECORTE. A sombra de uma peça é desenhada FORA dos limites
   dela e chega a uns 34px abaixo; sem esta folga a cena encosta no pé da área de rolagem e o borrão
   termina numa reta. Junto com a trava de largura da mesa, são duas garantias para o mesmo defeito
   — porque uma só já falhou três vezes no app.

   E A MESA RECORTA O QUE NÃO CABE NELA, que é uma rede e não um estilo. O recorte é no limite do
   PADDING, então os 42px de respiro acima ficam DENTRO dele: em toda tela onde a cena cabe, isto não
   faz nada — a sombra continua inteira. Ele só age onde a altura acabou (telefone deitado, ~300px de
   janela útil), e ali a escolha é entre a ilustração aparecendo pela metade ou a ilustração passando
   por cima da legenda. Meia ilustração se lê como enquadramento; texto atravessado por um cartão se
   lê como defeito. */
.tpage-art {
  flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 42px 0;
  overflow: hidden;
}

/* A LEGENDA TEM ALTURA RESERVADA, pelo mesmo motivo do manifesto: sem ela, uma frase de duas linhas
   e outra de três empurram a ilustração a cada arrasto, e o que devia ser um deslize lateral vira
   um solavanco vertical. E ela é HENRIETTA — a face editorial, a mesma do manifesto que vem
   imediatamente antes. Legenda de tela de apresentação é texto, não interface. */
.tpage-cap {
  flex: 0 0 auto;
  min-height: 96px;
  font-family: var(--serif);
  font-weight: 300;
  font-size: 24px;
  line-height: 1.32;
  text-align: center;
  padding: 0 4px;
}
@media (max-width: 380px) { .tpage-cap { font-size: 21px; min-height: 88px; } }

/* As bolinhas. A ATUAL É UMA CÁPSULA, não uma bolinha acesa: a diferença de FORMA se lê de relance
   e sobrevive ao daltonismo e ao brilho no sol, que é mais do que a diferença de opacidade
   consegue. */
.tour-dots { display: flex; align-items: center; justify-content: center; gap: 8px; height: 44px; flex: 0 0 auto; }
.tour-dots button {
  background: none; border: 0; padding: 0; height: 44px; cursor: pointer;
  display: flex; align-items: center;
  -webkit-tap-highlight-color: transparent;
}
.tour-dots i {
  display: block; height: 7px; width: 7px; border-radius: 999px;
  background: rgba(245,245,246,0.26);
  transition: width 0.42s cubic-bezier(0.22,1,0.36,1), background 0.42s ease;
}
.tour-dots button.now i { width: 22px; background: var(--text); }

/* AS SETAS NÃO EXISTEM NO APARELHO: ali a fita se arrasta com o dedo, e dois botões flutuando sobre
   a cena seriam duas coisas a mais na tela para fazer o que o gesto já faz melhor. Elas nascem
   escondidas AQUI, antes do bloco de desktop, porque é lá que elas são ligadas — a ordem importa:
   uma regra igual escrita depois de uma media query vence a media query. */
.tour-arrow { display: none; }

/* O botão só existe na ÚLTIMA página, e é de propósito: a referência não tem botão nenhum no tour,
   e um "Continuar" fixo em todas transformaria um folheto que se folheia numa fila de formulário.
   No fim, porém, tem de haver uma porta. A altura fica reservada nas outras páginas para as
   bolinhas não pularem quando ele aparece. */
.tour-foot { height: 54px; flex: 0 0 auto; }
.tour-foot .ibtn { animation: footIn 0.3s cubic-bezier(0.22,1,0.36,1); }
@keyframes footIn { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }

/* ── TELA CURTA: O TOUR SE APERTA EM VEZ DE TRANSBORDAR ────────────────────────────────────────
   Estes dois cortes são o outro lado do `max-height` da coluna, e sem eles aquele conserto trocaria
   um defeito por outro.

   O TOUR TEM UM PISO DE ALTURA: barra do "Fechar" (44) + a cena mais alta (379) + a legenda (96) +
   as bolinhas (44) + o botão (54) + os respiros = ~675px. No aparelho o app tem a tela inteira e
   isso cabe folgado; NO NAVEGADOR a barra de endereço come ~87pt, e aí um iPhone SE oferece 580 e
   um 13 mini 640. Antes do `max-height` a coluna simplesmente ignorava a janela — media 757px
   sempre — e o pé caía fora da tela; com ele a coluna obedece, e o que sobra apertado é a mesa da
   ilustração: medi 97px de cena para fora da mesa no SE, 51 no 13 mini.

   ENTÃO A FOLGA VEM DA MOLDURA, NÃO DO CONTEÚDO, e nesta ordem: primeiro o respiro da mesa (42px
   em cima e embaixo, que é sombra, não desenho), depois a reserva da legenda, depois a barra do
   topo e as bolinhas. Só quando isso não basta a CENA encolhe, pelo mesmo `--mag` que o desktop usa
   para ampliá-la — um número, dois sentidos.

   `max-width: 899px` porque o desktop é outra composição (duas colunas) e já tem os cortes de
   altura dele mais abaixo; sem a trava, uma janela de laptop baixa cairia aqui.

   OS DEGRAUS SÃO MEDIDOS, não estimados: para cada um eu medi a mesa que sobra e a cena mais alta
   das seis (379px sem ampliação), e o `--mag` é o que faz a segunda caber na primeira. Mesa × cena,
   depois dos cortes, com o pé do tour sempre 16px acima da borda:

     14 Pro Max  430×845  533 × 379  (154px de folga)   mag 1
     14 Pro      393×765  453 × 379  ( 74px)            mag 1
     13 mini     390×640  355 × 349  (  6px)            mag 0,92
     SE / 8 / 7  375×580  320 × 311  (  9px)            mag 0,82
     Android     360×553  280 × 273  (  7px)            mag 0,72 */
@media (max-width: 899px) and (max-height: 720px) {
  .intro { --mag: 0.92; }
  .tpage-art { padding: 18px 0; }
  .tpage-cap { min-height: 74px; font-size: 22px; }
  .intro-top { height: 36px; }
  .tour-dots, .tour-dots button { height: 38px; }
}
/* Segundo degrau — iPhone SE, 8, 7: a moldura já está no osso, então daqui em diante quem cede é a
   cena. 0,82 põe os 379px da mais alta em 311. */
@media (max-width: 899px) and (max-height: 620px) {
  .intro { --mag: 0.82; }
  .tpage-art { padding: 10px 0; }
  .tpage-cap { min-height: 66px; font-size: 20px; }
}
/* Terceiro degrau — Android estreito e qualquer janela mais baixa que isso. */
@media (max-width: 899px) and (max-height: 570px) {
  .intro { --mag: 0.72; }
}
/* Quarto degrau: abaixo disso não há aparelho em pé, é aparelho DEITADO (uma tela de 393 de lado
   sobra ~340 de altura útil). Não vale desenhar uma composição para o retrato deitado; vale a cena
   não invadir a legenda enquanto alguém gira o telefone e gira de volta. */
@media (max-width: 899px) and (max-height: 530px) {
  .intro { --mag: 0.62; }
  .tpage-cap { min-height: 0; font-size: 18px; }
}

/* ── A MOLDURA QUE TRANSFORMA COMPONENTE EM ILUSTRAÇÃO ────────────────────────────────────────
   Três tratamentos, aplicados IGUAIS nas seis páginas — e o "iguais" importa tanto quanto o
   tratamento, porque estilizar cena por cena foi o que produziu, no app, seis páginas
   desalinhadas: halo de cor, perspectiva leve e o grão da marca.

   O HALO É UM `::before` E NÃO UM IRMÃO NO FLUXO, e esta linha já custou três rodadas no app: ele
   tem tamanho fixo e, como irmão, virava o elemento mais largo — a moldura inteira passava a medir
   430 numa tela de 390 e tudo lá dentro era desenhado maior do que a tela. Posicionado
   absolutamente, ele não participa da medida de ninguém. */
.stage {
  position: relative;
  width: 100%;
  max-width: 324px;
  animation: float var(--period, 6s) ease-in-out infinite alternate;
  /* SEM `transform-style: preserve-3d`, e ele estava aqui sem motivo. A perspectiva mora dentro da
     própria transformação da cena (`perspective(900px)` no `@keyframes float`), e nenhuma filha é
     transformada em 3D — então `preserve-3d` não fazia nada além de criar risco: no WebKit,
     `preserve-3d` junto de `backdrop-filter` numa descendente (o vidro dos cartões, o botão do
     Shazam) é combinação conhecida por renderizar errado. Tirado por não pagar nada e custar isso. */
  will-change: transform;
}
.stage::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 430px; height: 360px;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, var(--glow) 0, transparent 62%);
  filter: blur(26px);
  animation: halo calc(var(--period, 6s) * 0.78) ease-in-out infinite alternate;
  pointer-events: none;
  z-index: -1;
}
/* AQUI MORAVA UM SEGUNDO GRÃO, E ELE ERA O "QUADRADO" QUE O DONO VIU EM VOLTA DAS CENAS.
 *
 * Era um `::after` com `inset: 0`, ou seja, exatamente a caixa da cena, com a mesma textura de
 * `feTurbulence` a 7% e `mix-blend-mode: overlay`. Num campo escuro, e sem nada com que misturar
 * dentro do contexto de empilhamento que o `transform` da cena cria, o `overlay` não escurece nada:
 * a textura entra crua e ACLARA o retângulo. Medido numa captura, atravessando a borda de cima da
 * cena: a luminosidade salta de 14,4 para 22,4 em quatro pixels. Uma reta 56% mais clara em volta
 * de cada ilustração — o que ele descreveu como "parece que estão dentro de um quadrado", e o que
 * dava a impressão de a arte estar cortada, porque a borda passa por cima dela.
 *
 * E ELE ERA REDUNDANTE, que é o que torna a remoção fácil: o grão da marca JÁ cobre a tela inteira,
 * na `.grain` lá em cima, sem borda nenhuma porque cobre tudo. No app o grão é por cena (o
 * `TourStage` o aplica como overlay) porque lá não existe um grão de tela cheia; aqui existe, e eu
 * portei os dois. A textura continua na tela; o que sai é a moldura que ela desenhava.
 *
 * NÃO ESTAVA SÓ NO SAFARI: este defeito está no Chromium também, onde eu testei. Eu olhei as
 * capturas procurando composição e não procurando BORDAS, e uma borda de 8 pontos de luminosidade
 * não salta aos olhos numa imagem reduzida — salta na tela de 27 polegadas de quem está usando. */
/* A FLUTUAÇÃO E O BALANÇO. Seis pixels de sobe-e-desce e um grau e meio de giro, em ciclos longos:
   o suficiente para a peça parecer suspensa e pouco o bastante para ninguém apontar o que se mexe.
   Movimento que se nota vira distração. E cada página tem o SEU período (6,4 / 6,1 / 7,3 / 5,5 /
   6,7 / 5,9), sem múltiplos entre si — seis cenas no mesmo compasso pulsam em bloco quando se
   arrasta entre elas, que é o oposto de vida. */
/* A AMPLIAÇÃO DO DESKTOP ENTRA AQUI DENTRO, no `scale`, e não num `zoom` por fora.
   `--mag` vale 1 no aparelho e sobe no bloco de desktop. Como as funções de transformação se
   aplicam da direita para a esquerda, a ordem `translateY → scale → perspective → rotate` escala a
   cena JÁ inclinada, e os 6px de flutuação continuam sendo 6px reais em qualquer ampliação. */
@keyframes float {
  from { transform: translateY(6px)  scale(var(--mag, 1)) perspective(900px) rotateY(calc(var(--tilt, -4deg) + 1.5deg)) rotateX(0.4deg) }
  to   { transform: translateY(-6px) scale(var(--mag, 1)) perspective(900px) rotateY(calc(var(--tilt, -4deg) - 1.5deg)) rotateX(-0.4deg) }
}
@keyframes halo { from { transform: translate(-50%,-50%) scale(0.92); opacity: 0.7 } to { transform: translate(-50%,-50%) scale(1.08); opacity: 1 } }

/* A DERIVA: a micro-animação de UMA peça. A moldura já faz a cena inteira flutuar; isto é a camada
   de baixo — cada peça solta (uma capa atrás do cartão, um disco da vitrine) sobe, desce e balança
   no PRÓPRIO ciclo, com o próprio atraso. É a diferença entre uma folha de papel se mexendo e um
   punhado de coisas soltas numa mesa: se todas subissem juntas, o olho leria um bloco só. */
.drift { animation: drift var(--dp, 5s) ease-in-out infinite alternate; animation-delay: var(--dd, 0s); will-change: transform; }
@keyframes drift {
  from { transform: translateY(5px) rotate(calc(var(--base, 0deg) - var(--sway, 1.1deg))) }
  to   { transform: translateY(-5px) rotate(calc(var(--base, 0deg) + var(--sway, 1.1deg))) }
}

/* A sombra e a inclinação de uma peça — e SÓ isso. Nenhuma das duas entra no layout, então nunca
   empurram nem desalinham ninguém. Posição é assunto do fluxo. */
.piece { box-shadow: 0 12px 22px rgba(0,0,0,0.5); }

/* ── As peças do app, reconstruídas ───────────────────────────────────────────────────────── */

/* A CAPA, NAS DUAS CAMADAS QUE O APP TEM — e é por isso que ela é um `div` com um `img` dentro em
   vez de só um `img`.

   Embaixo, a MALHA GENERATIVA: `CoverArt` (Components.swift:148) desenha o brilho no centro, o
   meio-tom nas bordas e o quase-preto nos cantos, semeada por "título·artista" e sempre
   monocromática — cor é reservada para imagem de verdade. Em cima, a capa REAL, que no app chega do
   resolvedor de artes e aqui já está no disco: as quatro do tour (Blonde, Clube da Esquina,
   Currents, Sobrevivendo no Inferno) estavam entre as 45 que o mural desta página já servia.

   Portar as duas camadas e não só a de cima é o que faz a cena não ter buraco enquanto o JPEG
   carrega — que é exatamente o problema que a malha resolve no aparelho. A malha fica visível se a
   imagem faltar. */
.art {
  position: relative;
  background:
    radial-gradient(circle at 50% 44%, var(--glow2) 0, var(--mid) 46%, var(--deep2) 100%);
  overflow: hidden;
  flex: 0 0 auto;
}
.art > img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.art::after {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background: radial-gradient(circle at 50% 50%, transparent 50%, rgba(0,0,0,0.18) 95%);
}

/* A capa como OBJETO: quadrada, canto pequeno, com o fio de luz na borda que um disco tem quando
   pega claridade. Canto de 8 e não de 14 de propósito — capa de disco não é ícone de app. */
.sleeve { border-radius: 8px; box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.16); }

/* O avatar generativo: gradiente angular semeado, iniciais por cima. `Avatar`,
   Components.swift:210. */
.av {
  position: relative; border-radius: 50%; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.12);
  flex: 0 0 auto;
}
.av span {
  color: #fff; font-family: "AlteHaas", -apple-system, sans-serif;
  letter-spacing: -0.05em; z-index: 1;
}

/* `tkCardGlass` no escuro é Liquid Glass. Aqui é `backdrop-filter` — a aproximação mais honesta que
   a web tem. A regra que veio junto do app e que vale mais que a técnica: TRANSLÚCIDO SÓ SE EMPILHA
   SOBRE OPACO. Dois vidros sobrepostos leem os dois textos ao mesmo tempo. */
.glass {
  background: rgba(255,255,255,0.07);
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  box-shadow: inset 0 0 0 0.75px rgba(255,255,255,0.14);
  border-radius: 22px;
}
.tkcard { background: var(--surface); box-shadow: inset 0 0 0 0.75px var(--stroke); border-radius: 20px; }
.tkcard2 { background: var(--surface2); box-shadow: inset 0 0 0 0.75px var(--stroke); border-radius: 14px; }

.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; white-space: nowrap; }
.pill.gold { color: var(--gold); background: rgba(245,196,81,0.14); padding: 3px 7px; font-size: 10px; font-weight: 700; letter-spacing: 0.2px; }
.pill.red  { color: var(--red);  background: rgba(216,51,64,0.16); padding: 4px 9px; font-size: 11px; font-weight: 600; letter-spacing: 0.2px; }
.pill svg { width: 1em; height: 1em; fill: currentColor; }

/* As estrelas são a exibição de nota do app inteiro, e são douradas (`RatingStars`: "ratings are
   gold app-wide — the sanctioned accent"). */
.stars { display: inline-flex; gap: 1.5px; color: var(--gold); }
.stars svg { width: 1em; height: 1em; fill: currentColor; }

.hand { font-size: 12px; letter-spacing: 0.06em; color: var(--gold); }
.nm { font-size: 17px; font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
.sub { font-size: 13px; color: var(--text2); }
.ser { font-family: var(--serif); font-weight: 300; }

/* ── Cena 0 — o básico: avaliar, resenhar, conversar ─────────────────────────────────────────
   A CENA É CENTRADA NA MESA. Ela é a primeira coisa que se vê do tour, e alinhada à esquerda dava
   leitura de coisa torta (foi assim que ela saiu no aparelho). O eixo é o do meio e as três peças
   se penduram nele, na ordem em que acontecem: a nota, a resenha, a conversa. Ninguém precisa ler a
   legenda para entender a sequência — ela está desenhada. */
.s0 { display: flex; flex-direction: column; align-items: center; gap: 14px; }
/* AS ESTRELAS SÃO GRANDES DE PROPÓSITO. A 13px elas são um detalhe dentro de um cartão; a 30 são o
   GESTO de dar nota, que é a coisa mais simples que o app faz e a primeira que alguém faz. */
.s0 .big-stars { font-size: 30px; transform: rotate(-4deg); filter: drop-shadow(0 8px 14px rgba(0,0,0,0.45)); }
.s0 .rev { display: flex; gap: 12px; width: 100%; padding: 12px; }
.s0 .rev .art { width: 56px; height: 56px; border-radius: 8px; }
.s0 .rev .col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.s0 .rev .t { font-size: 15px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s0 .rev .p { font-size: 13px; color: var(--text2); line-height: 1.35; }
.s0 .rev .when { font-size: 11px; color: var(--text3); flex: 0 0 auto; }
/* AS RESPOSTAS ENTRAM RECUADAS DOS DOIS LADOS. O recuo simétrico mantém o bloco no eixo e continua
   dizendo o que o recuo de um lado dizia no fio de verdade: isto pendura na resenha acima, é
   conversa, não outro post. */
.s0 .thread { width: 100%; padding: 0 22px; display: flex; flex-direction: column; gap: 12px; }
.cmt { display: flex; gap: 10px; align-items: flex-start; }
.cmt .av { width: 30px; height: 30px; }
.cmt .av span { font-size: 12px; }
.cmt .b { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cmt .who { font-size: 13px; font-weight: 700; color: var(--text); }
.cmt .at { font-size: 12px; letter-spacing: 0.06em; color: var(--gold); }
.cmt .tx { font-size: 14px; color: var(--text2); line-height: 1.35; }

/* ── Cena 1 — Memory Lane ────────────────────────────────────────────────────────────────────
   A MARCA VEM PRIMEIRO, e ela não é "memórias": é MEMORY LANE. É a única funcionalidade do kaster
   com marca própria — logo, marca gráfica e cor — e ela se apresenta no app exatamente assim, o
   lockup centralizado no topo (MemoryLaneView.swift:45).

   64px e não 34: a 34 ela legendava o cartão; a 64 ela é maior do que dentro do próprio app (46), e
   é para ser — aqui não se está usando a funcionalidade, está se apresentando o nome dela. SEM HALO
   ATRÁS: a moldura desta página já tem um halo vermelho, e dois borrões da mesma cor sobrepostos não
   dão base à marca, dão neblina. */
.s1 { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.s1 img.ml { height: 64px; width: auto; }
.s1 .well { position: relative; width: 100%; height: 244px; }
/* As capas espiam ATRÁS do cartão, e o deslocamento vem com reserva: a caixa tem 244 de altura para
   o cartão (~214) mais os 30 que as capas sobem. */
.s1 .well .back { position: absolute; inset: 0 0 auto 0; display: flex; justify-content: space-between; }
.s1 .well .ks { position: absolute; left: 0; right: 0; top: 30px; }

/* O CARTÃO DE MEMÓRIA É ESCURO NO TEMA ESCURO. `KeepsakeTokens.card` é
   Color(light: 0xE7E5DB, dark: 0x1F1F1E) e o comentário de lá diz com todas as letras: face escura
   com tinta creme no escuro, papel quente com tinta escura no claro. Uma versão desta cena
   empilhava TRÊS cartões porque eu tinha construído a maquete achando que ele fosse sempre papel
   creme — três faces escuras num campo escuro viraram uma mancha. Um cartão só, e a separação vem
   de duas capas OPACAS por cima dele. */
.ks {
  position: relative;
  display: flex; align-items: center;
  padding: 18px 0 18px 20px;
  background: var(--ks-card);
  border-radius: 22px;
  box-shadow: inset 0 0 0 1px var(--stroke), 0 14px 24px rgba(0,0,0,0.5);
  overflow: hidden;
}
.ks .words { flex: 1; min-width: 0; padding-right: 20px; z-index: 1; }
/* O ano é a única coisa GRAVADA no papel, em caixa alta e na face de display do app.
   48 E NÃO 62, que é o tamanho do app — e a diferença tem conta. O ano divide a linha com o bloco
   de artefatos da direita, que ocupa 177 fixos: num cartão de 324 sobram 107 para as palavras, e
   "2017" a 62px pede uns 135. No aparelho isso se resolve sozinho porque o `EmbossedText` tem
   `minimumScaleFactor(0.5)` e encolhe para caber (o comentário de lá conta que o último dígito já
   foi cortado uma vez). CSS não encolhe texto por conta própria, então o número que caberia é
   escrito aqui — sem isso o selo passa por cima do "7", que é o defeito que o navegador mostrou. */
.ks .yr {
  font-family: "AlteHaas", -apple-system, sans-serif;
  font-size: 48px; line-height: 0.92; letter-spacing: -0.05em;
  color: var(--ks-ink); white-space: nowrap;
}
.ks .cal {
  width: 22px; height: 22px; margin-top: 9px; border-radius: 5px;
  background: #fff; color: var(--ks-chip-ink);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-size: 8px; line-height: 1; font-weight: 700;
}
.ks .cal em { font-style: normal; font-size: 6px; color: #C0392B; text-transform: uppercase; letter-spacing: 0.04em; }
.ks .tt { font-family: "AlteHaas", -apple-system, sans-serif; font-size: 17px; letter-spacing: -0.05em; color: var(--ks-ink); margin-top: 12px; }
.ks .ar { font-family: "AlteHaas", -apple-system, sans-serif; font-size: 12.5px; letter-spacing: -0.05em; color: var(--ks-gray); margin-top: 1px; }
.ks .mo { font-family: Georgia, "Times New Roman", serif; font-style: italic; font-size: 12px; color: rgba(244,239,230,0.74); margin-top: 7px; line-height: 1.3; }
.ks .arts { position: relative; width: 177px; height: 151px; flex: 0 0 auto; }
.ks .arts .disc { position: absolute; left: 50%; top: 50%; width: 111px; height: 111px; border-radius: 4px;
                  transform: translate(-11%, -20%) rotate(7deg); box-shadow: 0 6px 12px rgba(0,0,0,0.3), inset 0 0 0 1px rgba(255,255,255,0.5); }
/* O SELO É PERFURADO DE VERDADE. No app é uma `Shape` com semicírculos ao longo das quatro bordas, e
   as reentrâncias são TRANSPARENTES sobre a capa torta que está atrás — o comentário de lá diz que
   um círculo branco por cima não conseguiria isso, e é verdade aqui também.

   A PRIMEIRA TENTATIVA FOI UMA MÁSCARA CSS DE CINCO CAMADAS com `mask-composite: intersect`, e o
   navegador devolveu um quadrado azul sem moldura nenhuma: o `intersect` não compôs, então sobrou a
   última camada e o papel branco desapareceu. Agora a moldura é um `<path>` de SVG com
   `fill-rule="evenodd"` — o retângulo mais um subcaminho circular por furo, e o evenodd faz de cada
   círculo um buraco de verdade. Uma camada, nada para compor, e o furo é vazado como no app.

   O selo carrega `EraPhoto3`, um dos cinco assets de época que o app embute, e ele NÃO É UMA
   FOTOGRAFIA: o modelo o descreve como imagem abstrata autogerada. */
.ks .arts .stamp {
  position: absolute; left: 50%; top: 50%; width: 118px; height: 118px;
  transform: translate(-77%, -57%) rotate(-6deg);
  filter: drop-shadow(0 3px 8px rgba(0,0,0,0.28));
}
.ks .arts .stamp > svg { position: absolute; inset: 0; width: 100%; height: 100%; fill: #fff; }
.ks .arts .stamp img { position: absolute; inset: 11px; width: calc(100% - 22px); height: calc(100% - 22px); object-fit: cover; border-radius: 2px; }

/* ── Cena 2 — Descobertas ────────────────────────────────────────────────────────────────────
   A cena conta o laço inteiro sem uma palavra: o botão vermelho, a etiqueta dourada de "descobriu
   essa música" AO LADO dele — porque ela é a CONSEQUÊNCIA de apertar, e pousada dentro do cartão
   virava só mais uma linha dourada sobre o texto da primeira faixa — e três linhas da fila. */
.s2 { display: flex; flex-direction: column; gap: 20px; }
.s2 .top { display: flex; align-items: center; gap: 12px; }
.s2 .shz {
  position: relative; width: 84px; height: 84px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  /* O VERMELHO NÃO PODE MORRER NO PRETO, e era o que estava acontecendo.
     No app o botão é vidro TINGIDO: `glassEffect(.regular.tint(TK.red.opacity(0.42)))`. O material
     do iOS CLAREIA o que está atrás antes de receber o tingimento, então os 42% de vermelho pousam
     sobre um cinza médio e o resultado lê como vermelho. Na web o `backdrop-filter` não clareia
     nada: 42% de #D83340 sobre um campo quase preto compõem um marrom — e o comentário do próprio
     `TourView` chama esse botão de "o ponto focal da página". Marrom não é ponto focal.

     As duas camadas abaixo refazem a conta do material: 18% de branco por baixo (o clareamento que
     o vidro daria) e 82% de vermelho por cima. Medido no navegador: #BE3641, a 26 de distância de
     `TK.red` — o escurecimento que sobra é o que qualquer material translúcido faz sobre campo
     escuro, e é o que o app também faz. Antes eram uns 60 de distância, para o lado do marrom. */
  background:
    linear-gradient(rgba(216,51,64,0.82), rgba(216,51,64,0.82)),
    rgba(255,255,255,0.18);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  box-shadow: inset 0 0 0 0.75px rgba(255,255,255,0.22), 0 3px 9px rgba(0,0,0,0.18);
  flex: 0 0 auto;
}
/* `color: #fff` E NÃO SÓ `fill`: o glifo é desenhado com arcos traçados em `currentColor`, e sem
   isto ele herdava a tinta creme da tela em vez do branco que o app usa sobre o vermelho. O `fill`
   fica para o ponto de baixo da onda. */
.s2 .shz svg { width: 42px; height: 42px; color: #fff; fill: #fff; }
/* AS ONDAS DO RECONHECIMENTO, em looping. É a única micro-animação do tour que SIGNIFICA alguma
   coisa em vez de só dar vida: é o desenho universal de "está escutando", e conta o que a página
   inteira tenta explicar antes de a legenda ser lida. Dois anéis defasados por metade do ciclo,
   para nunca haver um instante sem onda nenhuma. Não voltam para dentro — nascem, abrem e somem. */
.s2 .shz i {
  position: absolute; left: 0; top: 0; width: 84px; height: 84px;
  border-radius: 50%; border: 2px solid rgba(216,51,64,0.5);
  animation: ring 2.8s ease-out infinite;
  pointer-events: none;
}
.s2 .shz i:nth-child(2) { animation-delay: 1.4s; }
@keyframes ring { from { transform: scale(0.92); opacity: 0.85 } to { transform: scale(1.75); opacity: 0 } }
/* RESPIRO DE 12 E NÃO DE 16: cada linha da fila gasta uns 190px em coisa de tamanho fixo (capa 52,
   botão de prévia, círculo de ouvida, respiros internos) e o que sobra é a coluna de texto. Com 16,
   "Let It Happen" truncava. */
.s2 .list { padding: 12px; display: flex; flex-direction: column; gap: 10px; }
.qrow { display: flex; align-items: center; gap: 12px; padding: 10px; }
.qrow .art { width: 52px; height: 52px; border-radius: 10px; }
.qrow .col { flex: 1; min-width: 0; }
.qrow .t { font-size: 15px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qrow .a { font-size: 12px; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qrow .play { width: 30px; height: 30px; border-radius: 50%; background: var(--surface3);   /* `Circle().fill(TK.surface3)` = #333335 no app, e não branco a 10%: medido #343436, distância 1,7 */ display: flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.qrow .play svg { width: 12px; height: 12px; fill: var(--text); }
.qrow .chk { width: 27px; height: 27px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--text3); flex: 0 0 auto; }

/* ── Cena 3 — Picks ──────────────────────────────────────────────────────────────────────────
   UM PICK É UM POST LONGO. O app diz isso com todas as letras na folha que apresenta a coisa: "um
   pick é um post mais longo, com músicas, fotos e vídeos", sob o título "a música guia o texto"
   (PickIntroSheet.swift:19). Cada pessoa tem os seus numa página própria com o nome dela, e os
   outros leem, curtem e comentam.

   A PLACA VERMELHA é a chapa de identificação da funcionalidade (PicksListView.swift:56). Ela faz
   aqui o que o lockup faz no Memory Lane: sem ela a cena é um cartão de texto que podia ser de
   qualquer app; com ela, a funcionalidade tem nome na tela. */
.s3 { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.s3 .plate {
  font-family: var(--serif); font-weight: 300; font-size: 18px; letter-spacing: 1.2px;
  color: var(--red); padding: 8px 18px; border-radius: 999px;
  background: rgba(216,51,64,0.13);
  box-shadow: inset 0 0 0 0.75px rgba(216,51,64,0.32);
}
.s3 .body { width: 100%; padding: 12px; }
/* O TEXTO SAI POR BAIXO, E ESSE É O DESENHO DA PALAVRA "LONGO". Os quatro blocos somam mais do que
   a janela: o resto fica de fora e a máscara dissolve os últimos 26% em vez de terminar numa reta.
   Nenhuma quantidade de conteúdo CABENDO diria "isto continua". A máscara é do CONTEÚDO e não do
   cartão — mascarar o cartão apagaria o vidro junto. */
.s3 .clip {
  /* 248 E NÃO 214, medido no navegador. Com 214 o título de duas linhas (55) mais o parágrafo de
     três (74) mais os vãos já comiam a janela e a FAIXA — que é o argumento da página, a música no
     meio do texto — nascia dentro do esfumado, ilegível. 248 deixa a faixa inteira em pé e joga o
     trecho destacado para o corte, que é o bloco certo para se perder: quem lê "…" ali entende que
     o texto continua, que é a única coisa que o corte precisa dizer. */
  height: 248px; overflow: hidden;
  mask-image: linear-gradient(to bottom, #000 0, #000 78%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 78%, transparent 100%);
  display: flex; flex-direction: column; gap: 14px;
}
.s3 h4 { font-family: var(--serif); font-weight: 300; font-size: 24px; line-height: 1.14; margin: 0; color: var(--text); }
.s3 p { font-size: 17px; line-height: 1.45; margin: 0; color: var(--text); }
.s3 .song { display: flex; align-items: center; gap: 12px; padding: 12px; }
.s3 .song .art { width: 54px; height: 54px; border-radius: 10px; position: relative; }
.s3 .song .art i { position: absolute; inset: 0; background: rgba(0,0,0,0.34); border-radius: 10px; display: flex; align-items: center; justify-content: center; z-index: 2; }
.s3 .song .art i svg { width: 15px; height: 15px; fill: #fff; }
.s3 .song .t { font-family: var(--serif); font-weight: 300; font-size: 18px; color: var(--text); }
.s3 .song .a { font-size: 14px; color: var(--text2); }
.s3 .leg { font-size: 13px; color: var(--text2); text-align: center; margin: 7px 0 0; }
.s3 blockquote { display: flex; gap: 14px; margin: 0; }
.s3 blockquote em { display: block; width: 3px; border-radius: 2px; background: var(--text2); flex: 0 0 auto; }
.s3 blockquote span { font-size: 17px; line-height: 1.45; color: rgba(245,245,246,0.92); }

/* ── Cena 4 — Life Motto ─────────────────────────────────────────────────────────────────────
   O QUE FICA CENTRADO É O PAR, NÃO O ROSTO — e isto é conta, não gosto.

   O balão NASCE NO CANTO SUPERIOR ESQUERDO DO ROSTO E ABRE PARA A DIREITA. É assim que ele é
   desenhado, porque no perfil o rosto fica no canto do cabeçalho: as duas bolinhas da cauda saem da
   borda esquerda dele apontando para baixo, para a cara. Centralizá-lo SOBRE o rosto faria a cauda
   apontar para o vazio; centralizar o ROSTO deixa metade da cena vazia (as duas versões erradas que
   o app já teve). Então o rosto começa a 68px da esquerda: o desenho todo mede uns 189px a partir
   da borda esquerda dele, e 162 − 189/2 ≈ 68 põe o centro óptico do conjunto no centro da mesa. O
   nome vai numa coluna da largura do rosto, então fica centrado na cara e não na mesa. */
.s4 { display: flex; flex-direction: column; }
.s4 .reserve { height: 112px; }
.s4 .row { display: flex; padding-left: 68px; }
.s4 .col { width: 96px; display: flex; flex-direction: column; align-items: center; gap: 16px; }
.s4 .face { position: relative; width: 96px; height: 96px; flex: 0 0 auto; }
.s4 .face .av { width: 96px; height: 96px; box-shadow: inset 0 0 0 3px rgba(255,255,255,0.85); }
.s4 .face .av span { font-size: 38px; }
.s4 .who { text-align: center; white-space: nowrap; }
/* O balão sobe do canto do rosto e não ocupa espaço de layout — por isso a reserva de 112 acima,
   sem a qual ele cresceria para cima e sairia pela borda de cima da mesa. */
.bubble {
  position: absolute; left: 0; bottom: 84px;
  width: max-content; max-width: 250px;
  padding: 10px 11px;
  background: var(--surface3); border-radius: 19px;
  text-align: left;
}
.bubble .verse { font-family: var(--serif); font-weight: 300; font-size: 15px; line-height: 1.3; color: var(--text); white-space: pre-line; }
.bubble .cred { display: flex; align-items: center; gap: 4px; margin-top: 5px; font-size: 11px; font-weight: 600; color: var(--text2); }
.bubble .cred svg { width: 9px; height: 9px; fill: var(--red); flex: 0 0 auto; }
.bubble .tail, .bubble .tail2 { position: absolute; border-radius: 50%; background: var(--surface3); }
.bubble .tail  { width: 13px; height: 13px; left: 33px; bottom: -8px; }
.bubble .tail2 { width: 7px;  height: 7px;  left: 18px; bottom: -18px; }

/* ── Cena 5 — o perfil com a sua cara ────────────────────────────────────────────────────────
   A capa do perfil no kaster é uma CAPA DE DISCO — o app oferece "usar como capa do perfil" na
   folha do álbum —, então a faixa de cima é uma capa escurecida, que é como ela aparece de verdade.

   O AVATAR ENTRA DEPOIS DO RECORTE, e a ordem é o defeito todo: dentro do recorte da capa ele era
   cortado junto com ela e sobrava meia cabeça presa na borda de baixo. Por cima, ele atravessa a
   borda — que é o que ele faz no perfil.

   O escurecimento é 0,6 e não 0,88: ele existe para o nome ler sobre a capa no perfil, mas aqui não
   há texto em cima dela; a 0,88 a capa virava um retângulo cinza e a página perdia justamente a
   coisa que está anunciando. */
.s5 { display: flex; flex-direction: column; }
.s5 .cover { position: relative; height: 116px; border-radius: 20px; overflow: hidden; }
.s5 .cover .art { position: absolute; inset: 0; border-radius: 0; }
.s5 .cover .scrim { position: absolute; inset: 0; background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.6)); z-index: 2; }
.s5 .head { position: relative; height: 0; display: flex; justify-content: center; z-index: 3; }
.s5 .head .av { width: 76px; height: 76px; margin-top: -38px; box-shadow: inset 0 0 0 3px rgba(255,255,255,0.9); }
.s5 .head .av span { font-size: 30px; }
.s5 .below { display: flex; flex-direction: column; align-items: center; gap: 12px; padding-top: 44px; }
.s5 .anthem { display: flex; align-items: center; gap: 10px; }
.s5 .anthem .art { width: 42px; height: 42px; border-radius: 10px; position: relative; }
.s5 .anthem .art i { position: absolute; inset: 0; background: rgba(0,0,0,0.34); border-radius: 10px; display: flex; align-items: center; justify-content: center; z-index: 2; }
.s5 .anthem .art i svg { width: 12px; height: 12px; fill: #fff; }
.s5 .anthem .col { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.s5 .anthem .tr { font-size: 12px; color: var(--text2); }
/* A VITRINE RESPIRA UM DISCO POR VEZ. Períodos e atrasos diferentes: três capas subindo juntas
   seriam uma barra subindo, e o que se quer é uma prateleira. */
.s5 .shelf { display: flex; gap: 8px; }
.s5 .shelf .art { width: 76px; height: 76px; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   O DESKTOP É OUTRO DESENHO, NÃO A MESMA TELA MAIOR
   ═══════════════════════════════════════════════════════════════════════════════════════════════

   O que estava aqui antes era o layout do aparelho centralizado numa janela de 1440: uma coluna de
   324px de ilustração, a frase embaixo dela, e dois terços da tela de gradiente vazio dos lados. Não
   estava cortado — estava abandonado, que é pior: lia como uma captura de tela de celular colada no
   meio do navegador. O dono viu e cobrou, com razão.

   AS TRÊS DECISÕES:

   1. **A PÁGINA DO TOUR VIRA DUAS COLUNAS.** A ilustração à esquerda, a frase à direita, as duas
      centradas na vertical. A ordem "vê primeiro, lê depois" — que é decisão do dono e o motivo de a
      legenda ficar embaixo no aparelho — se traduz para o desktop como esquerda/direita, porque numa
      linha horizontal o "primeiro" é o lado por onde se começa a ler.

   2. **A ILUSTRAÇÃO É AMPLIADA, NÃO ALARGADA.** Cada cena foi medida para uma mesa de 324px — a
      coluna de texto do cartão de memória tem 107px calculados, o rosto do Life Motto começa a 68px
      porque 162 − 189/2 dá 68, a janela do pick tem 248 medidos no navegador. Alargar a mesa refaz
      TODAS essas contas e quebra as seis cenas de uma vez. Ampliar não muda conta nenhuma.

      **A AMPLIAÇÃO É `transform: scale`, E JÁ FOI `zoom`.** Eu tinha escolhido `zoom` por um motivo
      real — ele multiplica cada comprimento e o espaço reservado no layout cresce junto, o que
      `scale` não faz. O que eu não podia fazer era TESTAR: esta máquina só tem Chromium, e o dono
      olha no Safari. `zoom` é uma propriedade legada de WebKit com comportamento próprio justamente
      onde estas cenas vivem — filhas posicionadas em absoluto (o selo e o disco do cartão, o balão
      do motto, o avatar do perfil, o halo, os anéis do Shazam), `filter`, `mask` e `backdrop-filter`.
      Ele viu no aparelho dele e o veredito foi "as artes estão todas cagadas".

      `scale` não tem essa dívida: ele é aplicado na pintura, uniformemente, sobre a subárvore
      inteira — nenhum filho é reposicionado, nenhuma medida é refeita. O preço é não reservar
      espaço, e aqui isso não custa nada: no desktop a cena tem uma COLUNA só para ela (492px para
      uma cena de 324 → 470 ampliada, ainda dentro) e a legenda mora na outra, então não há o que
      atropelar. Os cortes por altura mais abaixo cuidam do eixo vertical.

   3. **AS SETAS EXISTEM SÓ AQUI.** No aparelho a fita se arrasta; num computador ninguém arrasta uma
      tela, e um tour que só responde a swipe é um tour que o visitante de desktop não descobre que
      pode virar. O teclado já funcionava; as setas são a versão visível disso.

   O corte é `min-width: 900px`: abaixo disso um tablet em pé continua com o desenho do aparelho, que
   é o certo para uma coluna estreita. E há um segundo corte por ALTURA, porque uma janela de
   1440×720 tem largura de desktop e altura de nada — ampliar 1,45 ali faria a cena encostar na
   legenda. */
@media (min-width: 900px) {
  .intro-stage { width: min(1120px, 94vw); padding-left: 40px; padding-right: 40px; }

  /* O manifesto também cresce: ele é só tipografia, e tipografia pequena numa tela grande não é
     discreta, é pequena. A medida sobe para 720px e o corpo para 40 — mais ou menos a mesma
     quantidade de caracteres por linha que o aparelho tem, que é o que faz uma linha ser confortável
     de ler. A reserva cai de 300 para 240 porque, mais largo, o parágrafo ocupa menos linhas. */
  .mf-text { font-size: 40px; line-height: 1.26; max-width: 720px; margin: 0 auto; min-height: 240px; }
  /* Os traços e os botões param de acompanhar a largura da tela: um botão de 1120px de largura não
     é um botão, é uma barra. A ordem continua a do app — o principal embaixo. */
  /* `align-self: center` E NÃO `margin: 0 auto`, e a diferença apagou os traços.
     `.mf-foot` é uma coluna flex, então o eixo transversal é o HORIZONTAL — e margem automática
     nesse eixo faz o item ENCOLHER até o conteúdo em vez de centralizar uma largura. O conteúdo aqui
     são seis botões cujos traços têm `width: 100%` de um botão sem largura própria: tudo colapsou.
     Medido: a fileira ficou com 30px (só os cinco vãos de 6) e o traço atual com largura ZERO.

     Isto entrou junto com o bloco de desktop e ficou publicado, porque as minhas conferências mediam
     as CENAS e não o rodapé. Está medido agora e entrou na conferência. */
  .mf-segs { width: 100%; max-width: 560px; align-self: center; }
  .mf-foot .ibtn { max-width: 380px; margin: 0 auto; }

  /* A CÁPSULA DE "CONTINUAR" VIRA A DICA DE TECLADO — pedido do dono, e ele está certo: num
     navegador ninguém precisa de um alvo de 54px para passar de página, e a cápsula grande fazia a
     tela parecer um app rodando dentro do site.

     Ela perde o fundo, a altura fixa e a forma, e ganha a serifa do resto da tela — deixa de ser
     controle e passa a ser INSTRUÇÃO, que é o que ela é. O `hover` acende um pouco, para quem
     descobrir que ainda dá para clicar.

     SÓ VALE PARA "CONTINUAR" (a classe `as-key`, que o JS tira na última etapa). Na última etapa o
     botão volta a ser cápsula sólida, porque ali ele não é "avançar", é BAIXAR NA APP STORE — um
     destino, e destino se aperta. "Aperte Enter para baixar" seria esconder a porta da loja. */
  .ibtn.primary.as-key {
    background: none; height: auto; padding: 6px 0;
    font-family: var(--serif); font-weight: 300; font-size: 21px; letter-spacing: 0.01em;
    color: rgba(250,246,239,0.78);
    max-width: none;
  }
  .ibtn.primary.as-key:hover { background: none; color: var(--ink); }
  .ibtn.primary.as-key:active { transform: none; }
  .ibtn.as-key .lbl-tap { display: none; }
  .ibtn.as-key .lbl-key { display: inline-flex; align-items: center; gap: 11px; }

  /* A MESMA DICA NO TOUR, na altura que já estava reservada para o botão da última página. Aqui as
     setas entram junto porque no tour elas fazem algo que o Enter não faz: voltar. */
  .tour-foot:not(.is-last) .tour-hint {
    display: inline-flex; align-items: center; gap: 9px;
    font-family: var(--serif); font-weight: 300; font-size: 18px;
    color: rgba(245,245,246,0.62);
  }
  .tour-hint em { font-style: normal; opacity: 0.45; margin: 0 2px; }

  /* O MANIFESTO TAMBÉM SE CENTRA COMO UM GRUPO, pelo mesmo motivo do tour.
     No aparelho o pé fica no PÉ porque é lá que o dedo alcança — o botão de 54px na borda de baixo é
     ergonomia de telefone. Numa janela de 900 de altura não há dedo, e o que sobra é um buraco: o
     texto no terço de cima e os botões colados na borda, com 450px de nada entre os dois. Aqui os
     três (frase, traços, botões) viram um bloco só, centrado. */
  .mf { justify-content: center; gap: 40px; }
  .mf-mid { flex: 0 0 auto; }

  /* ── A página do tour, em duas colunas ─────────────────────────────────────────────────── */
  .tpage {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    gap: 56px;
    padding: 0 40px;
  }
  /* A RESERVA DA AMPLIAÇÃO, e ela existe porque `scale` NÃO reserva espaço (é o preço de ter
     trocado o `zoom`, e é o único).

     Uma cena escalada cresce em torno do próprio centro, então ela transborda a caixa em
     `altura × (mag − 1) / 2` para cima e o mesmo para baixo. A mais alta das seis mede 392px, então
     a conta abaixo reserva o pior caso — e o `calc` a escreve em vez de eu chutar um número, o que
     mantém a folga certa quando os cortes por altura mudam o `--mag`. Os 24px são o respiro da
     sombra, que já existia.

     Sem isto o pé da cena passava por cima das bolinhas: medido em 135px de invasão a 1440×900. */
  .tpage-art { padding: calc(392px * (var(--mag) - 1) / 2 + 24px) 0; }
  .tpage-cap {
    text-align: left;
    font-size: 34px;
    line-height: 1.28;
    min-height: 0;           /* a reserva existia para o pulo VERTICAL entre páginas; aqui a frase
                                está ao lado da cena e as duas colunas se centram sozinhas. */
    padding: 0;
    max-width: 15em;
  }
  .intro { --mag: 1.45; }

  /* O rodapé segue a coluna da direita, embaixo da frase — centrado sob as duas colunas ele ficaria
     órfão, sem dizer a qual metade pertence. */
  .tour-dots { justify-content: flex-start; width: calc(50% - 28px); margin-left: auto; padding-left: 0; }
  .tour-foot { width: calc(50% - 28px); margin-left: auto; }
  .tour-foot .ibtn { max-width: 320px; }

  /* O GRUPO INTEIRO FICA CENTRADO NA VERTICAL, E A FITA PARA DE ESTICAR.
     No aparelho a fita é `flex: 1` e come toda a altura livre: as bolinhas ficam no pé da tela
     porque no pé da tela é onde o dedo as alcança. Numa janela de 900 de altura isso abriu 300px de
     vazio entre a frase e as bolinhas — elas ficavam penduradas na borda de baixo, longe do
     conteúdo a que se referem. Medido no navegador: a legenda terminava em 486 e as bolinhas
     começavam em 786.

     Com `flex: 0 1 auto` a fita passa a ter a altura da página mais alta e o conjunto
     (fita + bolinhas + botão) se centra como uma coisa só. */
  .tour { justify-content: center; }
  .tour-deck { flex: 0 1 auto; }
  /* As setas acompanham o MEIO DA FITA e não o meio da coluna: com o conjunto centrado, a fita fica
     49px acima do centro — metade dos 98 que as bolinhas (44) e o botão (54) ocupam embaixo dela. */
  .tour-arrow { top: calc(50% - 49px); }

  /* As setas. Ficam na altura da cena, uma de cada lado da fita, e desaparecem nas pontas em vez de
     virarem botão morto. */
  .tour { position: relative; }
  .tour-arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 52px; height: 52px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: rgba(255,255,255,0.07);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(255,255,255,0.16);
    color: var(--text); cursor: pointer; z-index: 4;
    transition: background 0.18s, border-color 0.18s, opacity 0.24s, transform 0.24s cubic-bezier(0.22,1,0.36,1);
  }
  .tour-arrow:hover { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.34); }
  .tour-arrow:active { transform: translateY(-50%) scale(0.94); }
  .tour-arrow svg { width: 20px; height: 20px; fill: currentColor; }
  .tour-arrow.prev { left: -14px; }
  .tour-arrow.next { right: -14px; }
  .tour-arrow[disabled] { opacity: 0; pointer-events: none; }
}
/* Desktop largo e baixo (1440×720, uma janela não maximizada): largura de desktop, altura de nada.
   Ampliar 1,45 ali põe a cena em cima da legenda. */
@media (min-width: 900px) and (max-height: 820px) { .intro { --mag: 1.18; } }
@media (min-width: 900px) and (max-height: 700px) {
  .intro { --mag: 1; }
  .mf-text { font-size: 33px; min-height: 200px; }
  .tpage-cap { font-size: 29px; }
}
/* ── O MEIO DO CAMINHO: estreito mas alto (tablet em pé) ──────────────────────────────────────
   Aqui as duas colunas seriam erradas — 834px de largura divididos em duas dão duas colunas magras
   —, então o desenho do aparelho continua valendo. O que muda são duas coisas que o aparelho não
   precisa e o tablet sim: a cena cresce um pouco (há espaço de sobra) e o grupo do manifesto se
   centra, porque numa tela de 1112 de altura o pé colado na borda de baixo abre o mesmo buraco que
   abria no desktop. Um iPhone (932 de altura) não entra por aqui: lá o botão no pé é o certo. */
@media (max-width: 899px) and (min-width: 620px) and (min-height: 900px) {
  .intro { --mag: 1.2; }
  .mf { justify-content: center; gap: 40px; }
  .mf-mid { flex: 0 0 auto; }
  .mf-text { font-size: 32px; min-height: 260px; }
  .mf-segs { width: 100%; max-width: 460px; align-self: center; }   /* mesmo motivo do bloco de desktop */
  .mf-foot .ibtn { max-width: 460px; margin: 0 auto; }
  .tpage-cap { font-size: 27px; }
}

/* O `@supports not (zoom: 1)` que morava aqui saiu junto com o `zoom`: `transform: scale` não tem
   ressalva de suporte a fazer. */

/* ── Acessibilidade ──────────────────────────────────────────────────────────────────────────
   Reduzir movimento não é "sem retorno nenhum": é a mesma tela sem o que mexe com o vestibular. O
   fundo para de derivar, as cenas param de flutuar, as ondas param de abrir — e o texto passa a
   aparecer inteiro em vez de ser digitado (o JS lê a mesma media query). O que se lê continua
   idêntico. */
@media (prefers-reduced-motion: reduce) {
  .intro-bg .blob,
  .stage, .stage::before, .drift,
  .s2 .shz i,
  .mf-text.in-fwd, .mf-text.in-back,
  .tour.on, .tour-foot .ibtn { animation: none !important; }
  .stage { transform: scale(var(--mag, 1)) perspective(900px) rotateY(var(--tilt, -4deg)); }
  .mf-text.done .caret { animation: none; }
  .tour-deck { scroll-behavior: auto; }
}
@media (prefers-reduced-transparency: reduce) {
  .glass { background: var(--surface); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .s2 .shz { background: #BE323D; backdrop-filter: none; -webkit-backdrop-filter: none; }   /* o mesmo vermelho que as duas camadas compõem, aqui sólido */
}

/* Foco visível. O manifesto e o tour se navegam por teclado (setas, Enter, Tab), então o anel tem
   de existir — e ele é creme sobre o gradiente, não o azul do sistema. */
.intro :focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 6px; }
