/* ============================================================
   CHACAL — A SAGA
   Três cenas (hero 241 · corte 148 · travessia 192) tratadas como
   UMA timeline de 581 frames num único canvas.

   Por que um canvas só, e não três: a emenda entre as cenas é
   frame-exata (o 241 da cena 1 é a mesma pose do 1 da cena 2, e a
   silhueta no fim da 2 coincide com a da 3 — medido). Qualquer troca
   de elemento no meio dessa emenda introduz um repaint que o olho lê
   como corte. Então o canvas é o mesmo do começo ao fim; o que muda
   é só qual plano de texto está por cima.

   Este arquivo carrega DEPOIS de chacal.css e components.css, e
   sobrescreve a altura do hero (que era 100vh) para o curso da saga.
   ============================================================ */

:root {
  /* Curso de scroll de cada capítulo. O JS reescreve estes valores no
     boot (ele é a fonte da verdade — usa os mesmos números para mapear
     scroll → frame). Ficam aqui para o layout nascer certo antes do JS. */
  --ch-cut: 250vh;
  --ch-walk: 300vh;
}

/* ------------------------------------------------------------
   1. O CURSO E O PALCO FIXO
   ------------------------------------------------------------ */

.saga {
  /* 100vh de janela fixa + o curso de cada capítulo */
  height: calc(100vh + var(--ch-cut) + var(--ch-walk));
  background: var(--bg-deep);
}

.saga .hero__stage {
  position: sticky;
  top: 0;
  height: 100vh;
}

/* Safari iOS altera `vh` e `innerHeight` quando sua barra de endereço
   recolhe durante o gesto. `svh` usa a altura estável do menor viewport,
   evitando que o palco e o canvas mudem de escala no meio do scroll. */
@supports (height: 100svh) {
  .saga { height: calc(100svh + var(--ch-cut) + var(--ch-walk)); }
  .saga .hero__stage { height: 100svh; }
}

/* Os três planos ocupam a mesma janela, sobre o mesmo canvas.

   EMPILHAMENTO — os dois planos de texto ficam ACIMA da atmosfera.

   Havia aqui uma regra proibindo z-index em `.plane--walk`: o texto
   da cena 3 usava mix-blend-mode: multiply para desaparecer dentro do
   corpo dele, e blend precisa alcançar o backdrop — um contexto de
   empilhamento no caminho o isolaria do canvas. Esse multiply não
   existe mais: quem recorta o texto no corpo dele hoje é uma MÁSCARA
   (ver §5), e máscara é uma operação local, não precisa de backdrop.
   Então a proibição caiu junto com o motivo dela.

   E precisava cair: sem z-index, a frase da cena 3 pintava debaixo de
   .hero__scene (1), .hero__vignette (2) e .hero__grain (3). Só não
   aparecia porque `--atmo` leva as três a zero no branco — isto é, a
   legibilidade do texto dependia de uma dose, não da ordem. Qualquer
   tratamento de imagem novo escrito nessas camadas voltava a cobrir a
   frase. Agora a ordem é que garante.

   Ordem de pintura resultante:
   canvas → atmosfera (1-3) → plane--walk (4) → hero__type (4)
   → plane--slash (5) → carregamento/dica (9).
   walk e slash nunca estão em cena ao mesmo tempo, então 4 e 5 não
   competem; hero__type empata em 4 e ganha por ordem do DOM, o que é
   indiferente (ele está em opacity 0 durante a travessia). */
.plane {
  position: absolute;
  inset: 0;
}
.plane--walk  { z-index: 4; }
.plane--slash { z-index: 5; }

.saga .hero__load,
.saga .hero__cue,
.saga .hero__skip { z-index: 9; }

/* as imagens estáticas só entram quando o movimento é dispensado */
.plane__still { display: none; }

/* ------------------------------------------------------------
   2. ATMOSFERA — dose por plano
   As camadas do hero (grade em screen, vinheta, varredura, grão)
   foram desenhadas para fundo quase preto. Sobre o branco da cena 3
   a grade em screen não tem o que clarear, então essa sai de cena
   junto com --atmo.

    A VINHETA sai junto com as demais camadas no plano final. Mesmo com
    a cor adaptada, manter parte dela em papel branco criava uma tarja
    escura desconexa. A textura do próprio frame sustenta o branco sem
    precisar de uma moldura de transição.
   ------------------------------------------------------------ */

.saga .hero__scene { opacity: var(--atmo, 1); }

.saga .hero__vignette {
  opacity: var(--atmo, 1);
}

/* O grão precisa MULTIPLICAR a dose calibrada em components.css, não
   substituí-la: `opacity: var(--atmo)` fazia o grão rodar em 1.0 aqui
   (a regra .saga vence por especificidade), ou seja, o dobro da dose
   desenhada — era essa a fonte do chuvisco.

   O grão NÃO acompanha --atmo. Vinheta e varredura sim (escurecem, e
   sobre papel isso é sujeira), mas o grão é soft-light em torno de 128:
   ele clareia e escurece na mesma medida, então funciona sobre branco.
   Zerá-lo junto com o resto deixava a cena clara sem NADA cobrindo o
   upscale de ~3x do frame 1280 — a área lisa virava bloco de WebP puro.
   Dose menor que no escuro: no claro quem faz o trabalho pesado é a
   retícula, e o grão só amacia a aresta da trama.

   O FALLBACK TEM QUE BATER com o de components.css. `--grain` nunca é
   escrita por ninguém — só o valor padrão do var() vale —, então dois
   números diferentes viram duas doses diferentes, e como esta regra
   ganha por especificidade era ESTA que valia na intro inteira.
   Mexer na dose exige mexer nos dois lugares. */
.saga .hero__grain {
  opacity: calc(var(--grain, .28) * (var(--atmo, 1) + (1 - var(--atmo, 1)) * .34));
}

/* RETÍCULA — o espelho do resto da atmosfera: entra à medida que a
   vinheta muda de cor. É o tratamento que assume a textura em vez de
   tentar esconder o upscale.

   Dose baixada de .22 para .12 quando a sombra passou a valer também
   no papel: são dois disfarces para o mesmo defeito, e somados viravam
   tratamento em cima de tratamento. A sombra tira o artefato do limiar
   de percepção, a retícula só dá tooth ao que sobra.

   Com a célula afinada (2px, ver components.css) a trama deixa de ler
   como bolinha e vira grão de papel: a dose pode voltar um pouco para
   .09 sem reavivar o defeito original. */
.saga .hero__screen { opacity: 0; }

/* o texto do hero sai de cena quando o corte começa */
.hero__type { opacity: var(--type-hero, 1); }

/* ------------------------------------------------------------
   3. TINTA QUE INVERTE
   A cena 2 vira o fundo de escuro para branco. Medido por luma na
   própria caixa do texto (x 5-45%, y 46-86% do quadro): estável em
   ~0.09 até o frame 87, cruza o meio no 97, branco puro (0.997) do
   113 em diante. --luma vem do JS com esse número interpolado, e a
   tinta acompanha em vez de dar um salto num limiar.
   ------------------------------------------------------------ */

.ink {
  --ink: var(--fg);
  --ink-soft: var(--muted);
  --ink-line: oklch(1 0 0 / .28);
  --ink-accent: var(--accent-hot);
}
@supports (color: color-mix(in oklab, white 50%, black)) {
  .ink {
    --ink: color-mix(in oklab, var(--l-fg) calc(var(--luma, 0) * 100%), var(--fg));
    --ink-soft: color-mix(in oklab, var(--l-muted) calc(var(--luma, 0) * 100%), var(--muted));
    --ink-line: color-mix(in oklab, var(--l-border) calc(var(--luma, 0) * 100%), oklch(1 0 0 / .28));
    --ink-accent: color-mix(in oklab, var(--l-accent) calc(var(--luma, 0) * 100%), var(--accent-hot));
  }
}

/* ------------------------------------------------------------
   4. CENA 2 — O CORTE
   O bloco fica na metade esquerda, na faixa que a medição diz estar
   livre de personagem nas três cenas. Posição em px vem do JS, em
   coordenadas do QUADRO — não da viewport: em tela recortada
   (cover) a metade esquerda da tela não é a metade esquerda do
   quadro, e o texto encostaria na lâmina.
   ------------------------------------------------------------ */

.sl {
  position: absolute;
  left: var(--sl-x, 8vw);
  width: var(--sl-w, 40vw);
  top: var(--sl-y, 50vh);
  transform: translateY(-50%);
  opacity: var(--sl-in, 0);
  color: var(--ink);
}

.sl__index {
  display: flex;
  align-items: center;
  gap: 13px;
  margin: 0 0 20px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  /* caixa alta pede tracking positivo: sem isso as contrafomas colidem */
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.sl__index b { font-weight: 500; color: var(--ink); }
.sl__index i { flex: none; width: 40px; height: 1px; background: var(--ink-line); }

.sl__head {
  margin: 0;
  max-width: 22ch;
  font-family: var(--font-display);
  font-size: clamp(30px, calc(var(--sl-w, 40vw) / 9.4), 76px);
  line-height: .96;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--ink);
}
.sl__head em {
  font-style: normal;
  color: var(--ink-accent);
}

.sl__note {
  margin: 24px 0 0;
  max-width: 42ch;
  font-size: clamp(14px, 1.05vw, 17px);
  line-height: 1.55;
  color: var(--ink-soft);
}

.sl__go { margin: 26px 0 0; pointer-events: auto; }
.sl__go a {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid var(--ink-line);
  padding-bottom: 4px;
  transition: gap .3s var(--ease), border-color .3s var(--ease);
}
.sl__go a:hover { gap: 16px; border-color: var(--ink-accent); }
.sl__go svg { width: 16px; height: 16px; flex: none; }

/* ------------------------------------------------------------
    5. CENA 3 — A TRAVESSIA
    O texto materializa atrás das costas dele enquanto ele caminha
    para a esquerda. A frente da revelação é a aresta de trás da
    silhueta, medida frame a frame (WALK_BACK no saga.js): 0.875 do
    quadro no frame 1, 0.10 no 174, fora do quadro no 187.

    Revelação por MÁSCARA: em vez de mix-blend-mode multiply + blur
    + glow na tentativa de esconder o texto quando sobreposto ao
    personagem (o que criava artefatos), a máscara faz o trabalho
    real: uma franja vertical com transição suave recorta o texto
    exatamente na aresta da silhueta. Ele anda para a ESQUERDA, então
    o rastro — onde a frase existe — fica à DIREITA da aresta:
    direita = revelado, esquerda = onde ele ainda está = recortado.
    Sem multiply, sem blur, resultado limpo em qualquer dpr.
    ------------------------------------------------------------ */

.wk {
  position: absolute;
  left: var(--wk-x, 8vw);
  width: var(--wk-w, 62vw);
  top: var(--wk-y, 32vh);
  z-index: 1;
}

.wk__head {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(26px, calc(var(--wk-w, 62vw) / 6.2), 120px);
  line-height: .9;
  letter-spacing: -0.025em;
  text-transform: uppercase;
  color: var(--l-fg);
}
.wk__line { display: block; }

/* --- revelação por máscara -----------------------------------------
   Em coordenadas do bloco de texto (px dentro da viewport), a aresta
   do personagem vira uma coordenada x de máscara: `--mask-x` . A
   franja de transição `--mask-f` suaviza a borda sem blur. O valor
   declarado aqui é fallback; o JS escreve ambos a cada tique. */
.wk__vis {
  display: block;
  /* POLARIDADE — transparente ATÉ a aresta (ali ele ainda está, e o
     texto não pode aparecer sobre o corpo), opaco DEPOIS dela (o rastro
     que ele já deixou). Estava invertido: recortava justamente o lado
     onde o saga.js acende os glifos (p = (cxn - back)/lead só é
     positivo à direita da aresta), então máscara e opacidade se
     anulavam — e no fim da caminhada, com back → 0 e --mask-x → 0, a
     máscara antiga apagava a frase inteira no quadro em que ela
     deveria estar completa e legível.

     A franja fica do lado DELE (de --mask-x para trás), não do lado já
     revelado: cai sobre o corpo, onde ninguém a lê. Somada para frente,
     no fim do curso o degradê ficaria dentro da frase pronta e
     desbotaria a primeira letra. */
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent, transparent calc(var(--mask-x, 0px) - var(--mask-f, 18px)),
    #000 var(--mask-x, 0px));
  mask-image: linear-gradient(
    90deg,
    transparent, transparent calc(var(--mask-x, 0px) - var(--mask-f, 18px)),
    #000 var(--mask-x, 0px)
  );
}

.wk__c {
  display: inline-block;
  white-space: pre;
  color: var(--l-fg);
  transform: translate3d(var(--dx, 0px), 0, 0);
  /* sem will-change na cena branca — é um bloco de texto simples
     que não mexe em opacity, filter ou transform a cada tique.
     A revelação quem faz é a máscara, e essa é GPU-friendly. */
}

/* a fonte de luz — facho ciano estreito na posição exata da silhueta,
   visível atrás do personagem enquanto o texto nasce */
.wk__seam {
  position: absolute;
  z-index: 2;
  left: var(--seam-x, -200px);
  top: var(--seam-y, 40%);
  width: 1px;
  height: var(--seam-h, 120px);
  transform: translate(-50%, -50%);
  opacity: var(--seam-o, 0);
  background: linear-gradient(180deg,
    transparent,
    oklch(0.66 0.185 245 / .55),
    transparent);
  box-shadow: 0 0 16px 4px oklch(0.66 0.185 245 / .22);
  pointer-events: none;
}

/* --- vapor ---------------------------------------------------------
   Canvas sobre o bloco para partículas de dissolução. O texto some
   via opacidade (JS) quando a frente da onda cruza o carácter; as
   partículas assumem — são finas (2px de amostra), brancas sobre
   branco com opacidade reduzida, sobem e dissipam. */
.wk__vapor {
  position: absolute;
  z-index: 3;
  left: 0;
  top: 0;
  pointer-events: none;
}

.wk__sig {
  margin: 30px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--l-muted);
  opacity: var(--wk-sig, 0);
}

/* ------------------------------------------------------------
   6. RETRATO / TELA ESTREITA
   Em cover, tela estreita recorta as laterais do quadro e a metade
   esquerda deixa de existir. O JS troca para 'contain' e pinta as
   barras com a cor do fundo do próprio frame (a mesma luma medida),
   então o letterbox não aparece. Aqui só o texto encolhe.
   ------------------------------------------------------------ */

@media (max-width: 900px), (max-aspect-ratio: 1/1) {
  .sl__note { display: none; }
  .sl__head { max-width: 18ch; }
  .wk__sig { margin-top: 18px; }
}

/* Modo retrato (.is-band): o JS mede uma FAIXA FIXA no rodapé da
   viewport para o texto do corte (--sl-y). Na travessia, o texto volta
   ao eixo do personagem (--wk-y), para nascer nas costas também no mobile.
   Com o zoom dinâmico o quadro cobre a tela, então a faixa do corte fica
   SOBRE o frame — daí o scrim abaixo, que é o que garante a leitura.

   A âncora tem de ser o TOPO: o translateY(-50%) do modo paisagem
   centraria o bloco no ponto medido e a primeira linha subiria por cima
   do personagem. O tamanho de tipo passa a ser travado por vw: as
   fórmulas de paisagem derivam da largura do BLOCO, que na faixa é a
   tela inteira, e devolveriam um display de 76px num celular. */
.saga.is-band .sl {
  transform: none;
  max-width: none;
}

/* Scrim — degradê que sobe do rodapé e assenta o texto sobre a imagem.
   A cor vem de --luma (0 = plano escuro, 1 = plano branco), a mesma
   variável que já migra a tinta entre as cenas: no corte ele escurece,
   na travessia ele clareia, sem código de cor novo e sem uma segunda
   fonte de verdade para "que plano é este".

   `mix-blend-mode: normal` e um único stop suave: cartão com borda
   competiria com o frame; o degradê some na imagem. */
.saga.is-band .plane::after {
  content: '';
  position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    /* a faixa subiu de 46% para 62%: a câmera v2 deixa o corpo ocupar
       60-72% da altura, então o scrim precisa cobrir do rodapé até
       metade da tela para o texto nunca encostar no frame */
    height: 62%;
    pointer-events: none;
    z-index: 0;
  background: linear-gradient(
    to top,
    rgb(calc(9 + 246 * var(--luma, 0)) calc(13 + 242 * var(--luma, 0)) calc(17 + 238 * var(--luma, 0)) / .92) 0%,
    rgb(calc(9 + 246 * var(--luma, 0)) calc(13 + 242 * var(--luma, 0)) calc(17 + 238 * var(--luma, 0)) / .72) 38%,
    rgb(calc(9 + 246 * var(--luma, 0)) calc(13 + 242 * var(--luma, 0)) calc(17 + 238 * var(--luma, 0)) / 0) 100%
  );
}

/* O scrim é necessário no corte, onde o texto vive no rodapé. Na
   travessia ele virava uma névoa branca sem função e competia com a
   revelação que acontece na altura do personagem. */
.saga.is-band .plane--walk::after { display: none; }

/* O texto sobe acima do scrim (o scrim é z-index 0 dentro do plano). */
.saga.is-band .sl,
.saga.is-band .wk { z-index: 1; }
.saga.is-band .sl__index { margin-bottom: 12px; }
.saga.is-band .sl__head {
  max-width: 16ch;
  font-size: clamp(26px, 7.4vw, 42px);
  line-height: .98;
}
.saga.is-band .sl__go { margin-top: 18px; }

.saga.is-band .wk__head {
  /* A frase curta sustenta um display grande, equivalente à presença da
     cena ampla no desktop, sem escapar da coluna móvel. */
  font-size: clamp(34px, 10vw, 46px);
  line-height: .86;
  letter-spacing: -0.018em;
}
.saga.is-band .wk__sig {
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 0.16em;
}

/* ------------------------------------------------------------
   7. MOVIMENTO DISPENSADO
   Nada anima: os três planos empilham como blocos normais, cada um
   com um frame estático, e todo o texto fica visível e legível.
   ------------------------------------------------------------ */

[data-motion="reduced"] .saga { height: auto; }
[data-motion="reduced"] .saga .hero__stage {
  position: static;
  height: auto;
  overflow: visible;
}
[data-motion="reduced"] .plane {
  position: relative;
  inset: auto;
  min-height: 100vh;
  display: flex;
  align-items: center;
}
[data-motion="reduced"] .plane--hero { display: block; }
[data-motion="reduced"] .plane--slash { background: var(--l-bg); }
[data-motion="reduced"] .plane--walk { background: var(--l-bg); }
[data-motion="reduced"] .plane__still {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: .85;
}
[data-motion="reduced"] .sl,
[data-motion="reduced"] .wk {
  position: relative;
  left: auto;
  top: auto;
  transform: none;
  width: auto;
  max-width: 46ch;
  margin: 0 var(--gut);
  opacity: 1;
  --luma: 1;
}
[data-motion="reduced"] .wk { max-width: 22ch; }
[data-motion="reduced"] .sl__head { font-size: clamp(30px, 5.4vw, 64px); }
[data-motion="reduced"] .wk__head { font-size: clamp(30px, 7vw, 84px); }
[data-motion="reduced"] .wk__c { opacity: 1 !important; transform: none !important; color: var(--l-fg) !important; }
[data-motion="reduced"] .wk__vis { -webkit-mask-image: none !important; mask-image: none !important; }
[data-motion="reduced"] .wk__vapor,
[data-motion="reduced"] .wk__seam { display: none; }
[data-motion="reduced"] .wk__sig { opacity: 1; }
[data-motion="reduced"] .hero__load { display: none; }
