/* ============================================================
   Psyke — psyke.site
   Tokens espelhados de design-system/colors.ts, typography.ts e tokens.ts.
   Ao mudar a paleta da app, actualizar :root abaixo em paralelo.
   ============================================================ */

/* Fontes auto-alojadas (subset latin — cobre todos os diacríticos PT-PT).
   Antes vinham do Google Fonts CDN, o que mandava IP dos visitantes para a
   Google num site que aloja a própria Privacy Policy. DM Sans é variável no
   Google Fonts: os 5 pesos pedidos (300–700) resolvem para o mesmo ficheiro
   .woff2, por isso um único ficheiro cobre todos os @font-face abaixo —
   não é duplicação por engano. */
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/barlow-700.woff2') format('woff2');
}
@font-face {
  font-family: 'Barlow Condensed';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url('/assets/fonts/barlow-900.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/assets/fonts/dmsans-variable.woff2') format('woff2');
}

:root {
  /* palette.* — design-system/colors.ts */
  --bg:            #0D1311;
  --surface:       #1A2420;
  --surface-2:     #161E1B;
  --card-selected: #1F3028;
  --accent:        #2D6A4F;
  --accent-bright: #52B788;
  --text:          #F0EDE8;
  --text-dim:      #8A8A7E;

  /* Derivados — hairlines e véus. Sem gradientes (SPEC §7.2). */
  --rule:        rgba(138, 138, 126, 0.20);
  --rule-strong: rgba(138, 138, 126, 0.38);
  --veil:        rgba(240, 237, 232, 0.035);
  --veil-edge:   rgba(240, 237, 232, 0.07);

  /* fonts.* — design-system/typography.ts */
  --display: 'Barlow Condensed', 'Haettenschweiler', 'Arial Narrow', sans-serif;
  --body:    'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;

  /* radius.* — design-system/tokens.ts */
  --r-card:   10px;
  --r-ticket: 14px;
  --r-button: 12px;
  --r-pill:   999px;
  /* Bezel exterior do bilhete — raio interior + padding, para curvas concêntricas. */
  --r-shell:  22px;

  /* ── Motion ────────────────────────────────────────────────
     Curvas fortes; as built-in do CSS são fracas demais para
     movimento deliberado. ease-in nunca é usado em UI: começa
     lento exactamente no instante em que se está a olhar. */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

  --d-press:  160ms;  /* feedback de toque */
  --d-hover:  260ms;  /* hover e cor */
  --d-reveal: 820ms;  /* entrada em scroll — página de marketing, pode respirar */
  --stagger:  55ms;   /* atraso entre irmãos */

  --page-max: 1080px;
  --gutter:   clamp(20px, 5vw, 56px);
}

/* ── Reset ───────────────────────────────────────────────── */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }
h1, h2, h3, p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }

/* ── Grão de filme ───────────────────────────────────────────
   feTurbulence como data URI, deslocado em passos discretos —
   película real treme a cada fotograma. Camada fixa e pequena
   (inset negativo curto) para não custar memória de composição;
   só transform, nunca repaint. */

body::after {
  content: '';
  position: fixed;
  inset: -14px;
  z-index: 100;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  will-change: transform;
  animation: grain 0.72s steps(1, end) infinite;
}

@keyframes grain {
  0%   { transform: translate(0, 0); }
  12%  { transform: translate(-5px, 3px); }
  25%  { transform: translate(4px, -6px); }
  37%  { transform: translate(-6px, -3px); }
  50%  { transform: translate(5px, 5px); }
  62%  { transform: translate(-3px, 6px); }
  75%  { transform: translate(6px, -4px); }
  87%  { transform: translate(-4px, -5px); }
  100% { transform: translate(0, 0); }
}

/* ── Layout ──────────────────────────────────────────────── */

.shell {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.rule {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 0;
}

/* ── Micro-labels — créditos de filme ────────────────────── */

.eyebrow {
  font-family: var(--body);
  font-weight: 700;
  font-size: 11px;
  line-height: 1.2;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.eyebrow--accent { color: var(--accent-bright); }

/* ── Cabeçalho ───────────────────────────────────────────── */

[data-scroll-sentinel] { position: absolute; top: 0; height: 0; width: 1px; }

.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  /* center, não baseline — baseline alinhava bem o wordmark com os
     links de texto (mesma linha de base), mas o hamburger não tem
     baseline própria (é só spans/box), ficava a boiar mais baixo que
     o resto. padding simétrico (era 26px/22px) — a distância ao
     topo/fundo da barra tinha de ser igual. */
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding-block: 24px;
  background: transparent;
  transition: background var(--d-hover) var(--ease-out), box-shadow var(--d-hover) var(--ease-out);
  /* .masthead também é .shell (max-width:1080px) para o CONTEÚDO ficar
     alinhado com o resto da página — mas isso limitava o próprio FUNDO
     da barra à mesma largura, deixando o resto do viewport (ecrãs largos)
     sem cor nenhuma atrás do header. max-width:none + padding calculado
     substitui o comportamento do .shell só aqui: a caixa passa a ocupar
     o viewport inteiro (fundo edge-to-edge), o padding-inline empurra o
     conteúdo para a mesma posição que .shell dava. */
  max-width: none;
  width: 100%;
  padding-inline: max(var(--gutter), calc((100% - var(--page-max)) / 2));
}

/* Fundo sólido de base — a barra transparente lia-se "fraca" mesmo
   parada no topo. backdrop-filter é só reforço onde há suporte. */
.masthead.is-stuck {
  background: rgba(13, 19, 17, 0.92);
  box-shadow: 0 1px 0 var(--rule-strong);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .masthead.is-stuck {
    background: rgba(13, 19, 17, 0.7);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
  }
}

.wordmark {
  font-family: var(--display);
  font-weight: 900;
  font-size: 20px;
  line-height: 1;
  letter-spacing: 0.28em;
  color: var(--text);
  text-decoration: none;
  transition: color var(--d-hover) var(--ease-out);
}

.masthead__nav {
  display: flex;
  align-items: center;
  gap: 26px;
  font-size: 13.5px;
}

.masthead__nav a {
  color: rgba(240, 237, 232, 0.68);
  text-decoration: none;
  transition: color var(--d-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .masthead__nav a:hover { color: var(--accent-bright); }
}

/* Hamburger — escondido por omissão, só aparece ≤560px (ver media
   query mais abaixo), onde substitui o link "How it works" que a
   essa largura já não cabe ao lado do wordmark. */
.masthead__menu { display: none; }

.masthead__menu-btn {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  width: 22px;
  height: 22px;
  cursor: pointer;
  list-style: none;
}

.masthead__menu-btn::-webkit-details-marker { display: none; }
.masthead__menu-btn::marker { display: none; }

/* Anel de foco genérico (outline 2px + offset 3px) num ícone de 22px
   dava uma caixa desproporcionada/torta — fundo redondo em vez de
   contorno, sem matemática de offset a acertar. */
.masthead__menu-btn:focus-visible {
  outline: none;
  background: var(--card-selected);
  border-radius: 6px;
}

.masthead__menu-btn span {
  height: 2px;
  border-radius: 1px;
  background: var(--text);
}

.masthead__menu[open] .masthead__menu-btn span:nth-child(2) { opacity: 0; }

.masthead__menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 12px);
  min-width: 168px;
  padding: 10px;
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: 0 18px 34px -14px rgba(0, 0, 0, 0.8);
}

.masthead__menu-panel a {
  display: block;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--text);
  text-decoration: none;
}

.masthead__menu-panel a:active { background: var(--card-selected); }

/* ══ SISTEMA DE REVELAÇÃO ════════════════════════════════════
   Estado por omissão = visível. A classe .js (posta no <head>,
   antes do primeiro paint) é que arma os estados escondidos —
   sem JS, a página lê-se na mesma. motion.js marca .is-in via
   IntersectionObserver; nunca um listener de scroll. */

/* Fade + subida + desfoque. Blur < 20px (acima disso é caro,
   sobretudo em Safari). */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  filter: blur(7px);
  transition:
    opacity   var(--d-reveal) var(--ease-out),
    transform var(--d-reveal) var(--ease-out),
    filter    var(--d-reveal) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  filter: none;
}

/* Revelação por linha — a linha sobe de trás de uma máscara.
   O padding/margin compensados evitam que o overflow corte
   acentos e descidas. */
.line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.09em;
  margin-bottom: -0.09em;
}

.line__inner {
  display: block;
  transition: transform 980ms var(--ease-out);
  transition-delay: calc(var(--i, 0) * 90ms);
}

.js .line__inner { transform: translateY(112%); }
.js .is-in .line__inner { transform: none; }

/* ══ HERO ════════════════════════════════════════════════════
   Split: aparelho à esquerda a sangrar pela aresta inferior,
   bloco de copy à direita. Duas vozes tipográficas — Barlow 900
   é a voz dos arquétipos (wordmark, nomes), DM Sans é a voz do
   narrador (headline, corpo). */

.hero {
  position: relative;
  /* overflow:hidden removido — só existia para cortar o sangramento do
     .hero__stage (margin-bottom:-13%, já removido) e a sombra grande
     do telemóvel (0 70px 130px blur) ficava cortada a direito nesse
     limite, lida como uma linha horizontal a mudar de cor com força.
     Sem bleed nenhum para esconder, overflow:hidden deixou de ter função. */
}

/* Glow atmosférico — única excepção "sem gradientes" do site, espelhando
   a mesma excepção que o sharecard já tem no app (dois RadialGradient
   Skia, cor do arquétipo). Aqui é o accent genérico da marca (o hero é
   pré-quiz, sem arquétipo ainda), geometria e opacidades a ecoar o
   sharecard: canto superior-esquerdo grande e forte, inferior-direito
   pequeno e fraco. Só camada de fundo — nunca em botões, cards ou texto. */
/* ellipse com raios explícitos, nunca circle. Num `circle` o raio em
   percentagem resolve contra sqrt((W²+H²)/2), por isso cresce com a
   LARGURA do ecrã: num hero largo e baixo (ou alto e estreito em
   mobile) o glow chegava com alpha ainda não-nula à aresta de baixo e
   cortava a seco — uma linha horizontal a atravessar a página. Com
   ellipse o raio vertical é uma percentagem só da altura, logo o
   alcance é centro+raio e não depende da largura nenhuma:
   0.20+0.58 = 78% e 0.72+0.22 = 94% da altura. Ambos fecham dentro. */
.hero__glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* rgba(...,0) no fim, nunca a keyword "transparent" — "transparent" é
     rgba(0,0,0,0) (preto transparente), e o browser interpola RGB e
     alpha juntos. A meio do fade isso passa por um verde-preto turvo
     antes de chegar a alpha 0, lido como uma linha a mudar de cor com
     força. Manter o mesmo RGB do início ao fim, só o alpha desce,
     elimina a banda. */
  background-image:
    radial-gradient(ellipse 62% 58% at 14% 20%, rgba(82, 183, 136, 0.20), rgba(82, 183, 136, 0)),
    radial-gradient(ellipse 52% 22% at 92% 72%, rgba(82, 183, 136, 0.10), rgba(82, 183, 136, 0));
}

/* Respiração lenta do glow — só opacity, nunca o gradiente em si (repintar
   um radial-gradient a cada frame é caro; opacity é grátis no compositor).
   Única coisa que se move no hero além da entrada — telemóvel fica
   parado (pedido do dono), por isso a respiração tem de ser muito
   subtil: range estreito (1 → 0.88) e devagar (12s). */
@media (prefers-reduced-motion: no-preference) {
  .hero__glow { animation: glow-breathe 12s ease-in-out infinite; }
}

@keyframes glow-breathe {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.88; }
}

.hero__grid {
  /* Positioned + z-index explícito: .hero__glow é absolute com z-index:0,
     e um elemento normal-flow não-posicionado pinta ANTES de um
     posicionado com z-index:auto/0 (ordem de pintura CSS2.1) — sem isto
     o glow ficaria por cima do conteúdo, não atrás. */
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  /* "stage" ocupa as duas linhas da coluna esquerda (nome repetido em
     bloco rectangular) — head/body ficam na direita, exactamente a
     posição visual que tinham dentro do antigo .hero__copy único. */
  grid-template-areas:
    "stage head"
    "stage body";
  align-items: center;
  /* align-content:center — sem isto, as duas rows implícitas (head/body)
     esticam (comportamento default 'stretch') para preencher o
     min-height sempre que "stage" (o telemóvel) não o preenche sozinho.
     Com o telemóvel mais baixo (rácio 9/15, sessão actual) sobrava
     folga a mais, e ia toda para dentro de head/body — abria um vão
     enorme entre a headline e o lede. center mantém as rows no tamanho
     do conteúdo e distribui a folga como margem à volta do bloco
     inteiro, não como espaço dentro dele. */
  align-content: center;
  /* column-gap/row-gap separados (era só "gap", valor único para os
     dois) — o mesmo clamp(28px,5vw,76px) controlava a distância
     horizontal telemóvel↔texto E a vertical headline↔lede ao mesmo
     tempo. A horizontal precisa desse alcance; a vertical com 76px de
     row-gap + as margens próprias do headline/lede (22px+22px) somava
     ~100px entre título e parágrafo, lido como desalinhado. */
  column-gap: clamp(28px, 5vw, 76px);
  row-gap: clamp(6px, 1vw, 16px);
  /* Era 78dvh (chegava a 860px) — dimensionado para o telemóvel alto de
     antes, que por si só quase enchia essa altura. Com o telemóvel mais
     baixo (rácio 9/15) e align-content:center a compactar o texto, 78dvh
     virou espaço morto a mais por baixo do CTA, crescendo ainda mais em
     monitores altos (dvh escala com a altura do ecrã, o conteúdo não).
     58dvh aproxima o hero da altura real do conteúdo. */
  min-height: clamp(460px, 58dvh, 680px);
  padding-block: clamp(32px, 6vh, 64px) 0;
}

.hero__stage {
  grid-area: stage;
  position: relative;
  align-self: end;
  display: flex;
  justify-content: center;
  /* Sem sangria (era margin-bottom:-13%, cortava o queixo do telemóvel
     no overflow:hidden do .hero) — pedido do dono para o aparelho
     aparecer inteiro em desktop, como já acontecia em mobile. */
}

/* Leque de bilhetes à volta do aparelho — um por arquétipo, cor real
   da marca (não é gradiente: cor sólida + sombra, como o resto do
   site). Só desktop, ver max-width:900px mais abaixo. Cada bilhete é
   posicionado por --x/--y (percentagem da caixa do .hero__stage,
   não da sua própria — por isso usa left/top, nunca translate% que
   resolveria contra o tamanho do próprio bilhete). */
.ticket-spill {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.ticket-frag {
  position: absolute;
  left: var(--x);
  top: var(--y);
  z-index: var(--z);
  display: flex;
  align-items: center;
  gap: 8px;
  width: 132px;
  padding: 10px 14px 10px 12px;
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: 0 14px 28px -12px rgba(0, 0, 0, 0.65);
  font-family: var(--display);
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--tint);
  white-space: nowrap;
  opacity: 0;
  animation: frag-deal 620ms var(--ease-out) calc(300ms + var(--i, 0) * var(--stagger)) both;
}

.ticket-frag::before {
  content: '';
  align-self: stretch;
  width: 3px;
  flex: none;
  background: var(--tint);
  border-radius: 2px;
}

/* Entra fechado no centro (leque a abrir-se) até à posição/rotação
   final — nunca scale(0) puro, mantém-se a 0.5 para não "aparecer do
   nada" (mesma regra do .ticket-shell). */
@keyframes frag-deal {
  from { opacity: 0; transform: translate(-50%, -50%) rotate(0deg) scale(0.5); }
  to   { opacity: 1; transform: translate(-50%, -50%) rotate(var(--r)) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .ticket-frag {
    animation: none;
    opacity: 1;
    transform: translate(-50%, -50%) rotate(var(--r));
  }
}

/* Abaixo de 1281px não há sítio para os bilhetes. A única banda livre
   é à esquerda do aparelho e mede: 105px a 900, 116px a 1000, 133px a
   1100, 170px a 1200 — contra 132px de chip. Só a partir de ~1280 (que
   dá 209px) é que cabem com folga, porque é aí que a margem exterior
   ao shell (max-width 1080) começa a abrir. */
@media (max-width: 1280px) {
  .ticket-spill { display: none; }
}

/* position+z-index explícito: os .ticket-frag vão até --z:6 e são
   absolute — sem isto, o texto (não-posicionado) perde sempre contra
   qualquer bilhete que se espalhe até à coluna da copy. Head e body
   eram um único .hero__copy; separados para o telemóvel (.hero__stage)
   poder entrar ENTRE os dois em mobile via grid-template-areas, sem
   tocar no layout desktop (áreas mantêm a mesma posição visual). */
.hero__head {
  grid-area: head;
  position: relative;
  z-index: 10;
}

.hero__body {
  grid-area: body;
  position: relative;
  z-index: 10;
  padding-bottom: clamp(40px, 8vh, 88px);
}

/* Barlow 900 em caixa alta, não DM Sans. A headline em fonte de
   corpo com tracking negativo é o that-look de landing page de SaaS;
   a voz de cartaz já existe na marca (wordmark, nomes de arquétipo,
   códigos de bilhete) e é ela que faz isto ler como cinema.
   Conta de largura: 0.47em por maiúscula, linha mais longa = "WHO YOU
   ARE." (12 caracteres). A 78px pede ~440px da coluna de copy, que
   mede ~490px no ecrã mais largo. Mexer nos cortes de linha obriga a
   refazer isto — as quebras são à mão, não há wrapping a salvar. */
.hero__headline {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(34px, 5.4vw, 78px);
  line-height: 0.88;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  margin-block: 22px 0;
}

.hero__headline em {
  font-style: normal;
  color: var(--accent-bright);
}

.hero__lede {
  max-width: 42ch;
  margin-top: 22px;
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--text-dim);
}

.hero__lede strong {
  color: var(--text);
  font-weight: 500;
}

/* ── Badge da App Store ──────────────────────────────────────
   Asset oficial da Apple (variante preta com borda #a6a6a6, a
   preferida), embebido sem alterações. As Apple Identity Guidelines
   proíbem modificar, angular ou animar o badge — daí não haver aqui
   nenhuma transform, e o grupo .actions revelar-se sem desfoque.
   Altura mínima permitida: 40px. Clear space: ¼ da altura. */

.appstore {
  display: inline-flex;
  height: 50px;              /* alinha com a altura do .btn ao lado */
  flex: none;
  cursor: default;           /* ainda sem href — não finge ser clicável */
}

.appstore img {
  display: block;
  height: 100%;
  width: auto;
}

/* Passa a fazer sentido quando o badge ganhar href. */
a.appstore { cursor: pointer; }

/* ══ APARELHO ════════════════════════════════════════════════
   iPhone em CSS. O rail metálico é feito de anéis sólidos
   sobrepostos (box-shadow) em vez de gradiente — dá o bisel
   maquinado sem violar SPEC §7.2. */

.device {
  /* Positioned + z-index explícito: os bilhetes do .ticket-spill à
     volta dele também são absolute, e um elemento não-posicionado
     (o que o .device era até agora) pinta sempre ANTES de conteúdo
     posicionado com z-index:auto/0 — ficaria sempre atrás dos
     bilhetes, nunca à frente de nenhum. Isto dá controlo explícito. */
  position: relative;
  z-index: 5;
  width: clamp(230px, 30vw, 340px);
  /* 9/16.5, não 9/19.5 (iPhone real) — o screenshot real (hero.png) tem
     rácio ~9:16, nunca preenchia um mockup 19.5:9 sem cortar texto na
     margem ou sobrar ~25% de ecrã vazio por baixo (medido). 16.5 é o
     ponto onde o conteúdo cobre ~95% do ecrã do mockup, ainda lê como
     telemóvel (mais alto que largo, cantos redondos, ilha dinâmica). */
  aspect-ratio: 9 / 15;
  /* Sem 3D (era perspective+rotateY/X/Z) — bitmap rodado sempre perde
     nitidez no resample do browser, pedido do dono para ficar direito
     em vez de inclinado. */
  transform: none;
}

.device__frame {
  position: relative;
  height: 100%;
  padding: 10px;
  border-radius: 46px;
  background: #14181A;
  box-shadow:
    0 0 0 1px rgba(240, 237, 232, 0.18),   /* aresta viva do rail */
    0 0 0 4px #0A0D0C,                      /* corpo do rail */
    0 0 0 5px rgba(240, 237, 232, 0.05),    /* segundo bisel */
    0 70px 130px -40px rgba(0, 0, 0, 0.95); /* sombra projectada */
}

/* Botões laterais — só a aresta esquerda é visível nesta rotação. */
.device__key {
  position: absolute;
  left: -6px;
  width: 3px;
  border-radius: 2px;
  background: #10181A;
  box-shadow: -1px 0 0 rgba(240, 237, 232, 0.14);
}

.device__key--mute   { top: 17%; height: 4%; }
.device__key--up     { top: 24%; height: 8%; }
.device__key--down   { top: 34%; height: 8%; }

.device__screen {
  position: relative;
  height: 100%;
  border-radius: 36px;
  background: var(--bg);
  overflow: hidden;
}

/* Ilha dinâmica — detalhe pequeno que faz o objecto ler como iPhone.
   z-index:1 obrigatório — .device__shot também é absolute agora, e vem
   depois no DOM; sem isto o screenshot pinta por cima e tapa a ilha. */
.device__island {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  width: 32%;
  height: 3.4%;
  border-radius: var(--r-pill);
  background: #050807;
  z-index: 1;
}

/* display:contents — sem isto o <picture> (wrapper webp+fallback) vira o
   item de grid em vez do <img>, perde o width/height:100% directo e o
   screenshot deixa de preencher o ecrã do device. */
.device__screen picture {
  display: contents;
}

/* cover + object-position:top. hero.png tem conteúdo real só até ~y=1600,
   depois é tudo cor --bg até 2300 — um buffer generoso de propósito. O
   rácio do .device__screen NÃO é constante: o padding fixo de 10px do
   .device__frame belisca mais o ecrã interior em telemóvel (~0.434, device
   190px) do que em desktop (~0.4615, device 280px+), porque padding fixo
   não escala com o box. cover+top garante preenchimento total em qualquer
   rácio, e o crop cai sempre dentro do buffer vazio — nunca no conteúdo,
   porque o buffer foi dimensionado para o pior caso (ecrã mais "quadrado").
   position:absolute;inset:0 em vez de width/height:100% — dentro do
   .device__screen (só position:relative, já não é grid), height:100%
   resolvia contra uma altura indefinida e encolhia a imagem. Absolute
   resolve contra a padding-box do ecrã, que tem altura definida. */
.device__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}

/* Entrada com massa: o aparelho é pesado, sobe mais devagar do que
   o texto e assenta na rotação final. */
.js .device[data-reveal] {
  transform: translateY(48px);
}

.js .device[data-reveal].is-in {
  transform: none;
  transition-duration: 1150ms;
}

/* Deriva em scroll conduzida pelo compositor — sem listener de
   scroll e sem trabalho na main thread. Browsers sem suporte
   simplesmente não recebem a deriva. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero__stage {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 100% exit 100%;
    }
  }
}

@keyframes drift {
  from { transform: translateY(0); }
  to   { transform: translateY(-52px); }
}

/* ── Índice dos 12 arquétipos ────────────────────────────── */

.index { padding-block: clamp(56px, 10vh, 104px); }

.index__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding-bottom: 22px;
}

.index__count {
  font-family: var(--display);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.12em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Carrossel horizontal em vez de grelha. Os 12 cards empilhados
   davam ~1800px de scroll em mobile e ~800px em desktop; assim a
   secção ocupa a altura de um card e os restantes ficam de lado.
   A largura do card é propositadamente menor que o contentor, para
   o card seguinte espreitar sempre pela direita — é essa fatia que
   diz que há mais, sem precisar de setas nem de texto a explicar. */

.index__grid {
  display: grid;
  grid-auto-flow: column;
  /* Sem goteira: os fotogramas encostam uns aos outros e a separação
     é o filete escuro do próprio fotograma, como numa fita a sério. */
  grid-auto-columns: clamp(150px, 42vw, 200px);
  gap: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* Sem snap enquanto há deriva automática: o snap é aplicado quando
     o scroll assenta, e com a deriva a escrever scrollLeft a cada
     frame o browser andaria a puxar a posição de volta contra ela.
     Quem tem movimento reduzido não leva deriva e aí o snap entra. */
  scroll-snap-type: none;
  /* Explícito: o html tem scroll-behavior:smooth e, se algum motor o
     deixar chegar aqui, cada escrita de scrollLeft por frame vira uma
     animação suave que a frame seguinte interrompe — a fita ficaria a
     tremer no sítio em vez de derivar. */
  scroll-behavior: auto;
  /* Barra escondida: a fita anda sozinha, por isso já se percebe que
     é uma faixa contínua sem precisar da barra a dizê-lo. O scroll à
     mão continua a funcionar (dedo, roda, teclado). */
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.index__grid::-webkit-scrollbar { display: none; }

/* ── Rolo ─────────────────────────────────────────────────────
   Visto de lado, com o eixo na vertical — não de frente.

   A película enrola-se com a largura da fita ao longo do eixo. Aqui
   vê-se a face dos fotogramas e a fita corre na horizontal, logo a
   largura dela é vertical e o eixo do rolo também tem de ser. Um
   círculo desenhado de frente tem o eixo apontado ao observador: dele
   a película sairia de perfil, a mostrar a espessura, e ficavam dois
   objectos lado a lado virados para planos diferentes.

   Visto com um ligeiro ângulo de cima, e não a direito. De lado
   exacto um cilindro é um rectângulo e nenhuma linha o salva; basta
   inclinar um pouco o ponto de vista para a aba de cima abrir numa
   elipse, aparecer o furo do veio e a base curvar — e aí lê-se como
   cilindro. O eixo continua vertical, o plano continua coerente.

   O corpo tem a altura da fita. As linhas verticais adensam-se para
   as bordas, onde a superfície foge de nós, e descem mais ao centro
   para acompanhar a curva da base.

   A rotação não é um rotate() da silhueta — num cilindro de eixo
   vertical isso poria a coisa toda a rodopiar. O que gira é a
   superfície: linhas a atravessar a faixa do meio, à mesma
   velocidade da fita, porque o rolo não patina. Nas bordas as
   linhas ficam quietas, que é onde definem a forma. */

.filmstrip {
  position: relative;
  /* A altura manda: o corpo do cilindro tem de medir o mesmo que a
     fita (a altura do rolo É a largura da película). A largura vem
     daí, pela proporção do viewBox, para não haver distorção nem
     barras com o background-size:contain. */
  --reel-h: 245px;
  --reel-w: calc(var(--reel-h) * 190 / 232);
}

.filmstrip__reel {
  position: absolute;
  top: 50%;
  /* A maior parte fica fora da aresta: a fita tem de nascer da borda
     direita do rolo, e essa borda tem de estar antes do início da
     faixa. O rolo continua para lá da margem da página. */
  left: calc(var(--reel-w) * -0.55);
  z-index: 2;
  width: var(--reel-w);
  height: var(--reel-h);
  transform: translateY(-50%);
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 190 232'%3E%3Cpath d='M15 24 L15 204 A80 13 0 0 0 175 204 L175 24 Z' fill='%231A2420' stroke='%238A8A7E' stroke-opacity='.30' stroke-width='2'/%3E%3Cellipse cx='95' cy='24' rx='80' ry='13' fill='%23161E1B' stroke='%238A8A7E' stroke-opacity='.42' stroke-width='2'/%3E%3Cellipse cx='95' cy='24' rx='16' ry='2.6' fill='%230D1311' stroke='%238A8A7E' stroke-opacity='.34' stroke-width='1.5'/%3E%3C/svg%3E");
  background-size: contain;
  background-repeat: no-repeat;
}

/* ── Superfície a girar ───────────────────────────────────────
   Linhas a sério em 3D à volta de um eixo vertical, não uma tira
   plana a deslizar — essa lia-se como tapete rolante, porque numa
   tira todas as linhas andam à mesma velocidade e com o mesmo
   espaçamento. Num cilindro não: a posição de cada linha é R·sin(θ),
   por isso adensam-se sozinhas para as bordas, abrandam ali até
   parar, e viram de perfil (largura zero) exactamente na silhueta.

   Sem `perspective`, de propósito: o CSS sem perspectiva projecta
   ortograficamente, que é como o SVG por baixo está desenhado. Com
   perspectiva as linhas deixavam de bater com a silhueta.

   O recorte segue a silhueta do corpo, não um rectângulo: o topo é o
   arco de baixo da aba (elipse) e a base faz barriga. Com um corte
   recto as linhas móveis acabavam todas à mesma altura enquanto as
   estáticas seguiam a curva, e via-se a costura entre umas e outras.

   As coordenadas do path são em px e dependem de --reel-h ser 245px:
   vêm do viewBox 190x232 multiplicado por 245/232. Mudar a altura do
   rolo obriga a recalcular isto. */

.filmstrip__spin {
  position: absolute;
  inset: 0;
  clip-path: path("M16 25.3 A84.5 13.7 0 0 0 185 25.3 L185 215.4 A84.5 13.7 0 0 1 16 215.4 Z");
}

.filmstrip__drum {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* --drum-period vem do JS: é a circunferência a dividir pela
   velocidade da fita, para a superfície nunca patinar na película. */
.filmstrip.is-rolling .filmstrip__drum {
  animation: drum var(--drum-period, 20s) linear infinite;
}

.filmstrip.is-paused .filmstrip__drum { animation-play-state: paused; }

/* Sentido a par da fita (index.html, recycle()) — a fita passou a
   correr para a esquerda (sessão 14), o rolo tem de girar no sentido
   oposto ao anterior para continuar a alimentá-la, não a travá-la. */
@keyframes drum {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(360deg); }
}

.filmstrip__drum i {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1.5px;
  margin-left: -0.75px;
  background: rgba(138, 138, 126, 0.2);
  /* Esconde as linhas da metade de trás do cilindro, que de outra
     forma se viam a passar ao contrário por dentro dele. */
  backface-visibility: hidden;
}

/* Cada arquétipo é agora um bilhete real (fundo --surface, cantos do
   .ticket) em vez de linha plana sobre o fundo da página — a mesma
   identidade do /m/[code] e do perfil, não um efeito à parte só do
   hero. Sem rotação/wobble por card: isto é um índice para ler
   depressa, não uma colagem — a versão anterior (gerada à parte)
   torcia cada card num ângulo diferente e ficava difícil de escanear. */
/* Fotograma de película. Sem cantos redondos (a fita não os tem) e
   com um filete escuro à direita a marcar a divisão para o seguinte.
   O padding vertical tem de dar folga às duas bandas de perfuração,
   que ocupam 22px em cima e em baixo. */
.archetype {
  position: relative;
  scroll-snap-align: start;
  background: var(--surface);
  border-right: 1px solid var(--bg);
  padding: 32px 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  min-height: 190px;
  transition: background var(--d-hover) var(--ease-out);
}

/* Perfurações. O furo vem de um SVG data URI (mesma técnica do grão
   de película) em vez de gradiente — fica uma forma sólida, com canto
   redondo, e não viola a regra dos gradientes.

   background-size com a largura em 25% dá exactamente 4 furos por
   fotograma, como no 35mm real, e garante que o padrão fecha certo na
   aresta de cada fotograma. Com um tile em px fixos os furos
   desalinhavam na junta sempre que a largura do fotograma não fosse
   múltipla do tile — e a largura vem de um clamp(), nunca é redonda.
   O preserveAspectRatio por omissão mantém o furo com o mesmo tamanho
   em qualquer largura; o que muda é o espaço entre furos. */
.archetype::before,
.archetype::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 22px;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 22'%3E%3Crect x='11' y='5.5' width='18' height='11' rx='3' fill='%230D1311'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-size: 25% 22px;
}

.archetype::before { top: 0; }
.archetype::after  { bottom: 0; }

.archetype__no {
  font-family: var(--body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Conta de largura: 0.47em por maiúscula em Barlow 900. O nome mais
   longo é "THE VISIONARY" (13). A 26px pede ~159px do interior de
   172px no fotograma grande; a 17px pede ~104px de 122px no pequeno. */
.archetype__name {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(17px, 2.4vw, 26px);
  line-height: 1.02;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  /* --tint definido inline por arquétipo (displaySafe de colors.ts) */
  color: var(--tint, var(--accent-bright));
}

/* ── Divisória de barras ─────────────────────────────────── */

.bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  max-width: var(--page-max);
  height: 100px;
  margin: clamp(48px, 8vh, 88px) auto 0;
  padding-inline: var(--gutter);
}

.bars span {
  flex: 1;
  height: var(--h);
  border-radius: 6px 6px 2px 2px;
  background: var(--tint);
  transform-origin: bottom;
}

.js .bars[data-reveal] { transform: translateY(14px); }
.js .bars[data-reveal].is-in { transform: none; }

/* scaleY com transform-origin:bottom, nunca translateY — pedido do dono:
   as bases ficam todas alinhadas na mesma linha (como no resto do
   design, align-items:flex-end), só o topo de cada barra sobe/desce,
   como um equalizador. nth-child dá a cada uma um período/fase
   diferentes; :nth-child(even) inverte a direcção para metade crescer
   enquanto a outra encolhe, nunca todas em fase. */
@media (prefers-reduced-motion: no-preference) {
  .bars span { animation: bar-bob 4.6s ease-in-out infinite; }
  .bars span:nth-child(even) { animation-direction: reverse; }
  .bars span:nth-child(3n) { animation-duration: 5.4s; }
  .bars span:nth-child(4n) { animation-delay: -1.4s; }
  .bars span:nth-child(5n) { animation-delay: -2.8s; }
}

@keyframes bar-bob {
  0%, 100% { transform: scaleY(1); }
  50% { transform: scaleY(0.8); }
}

/* ── Como funciona ────────────────────────────────────────── */

.steps { padding-block: clamp(64px, 10vh, 104px) 0; }

.steps__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  margin-top: 30px;
}

/* Fundo do hover vive num ::before absolute (inset:-18px), não em
   padding/margin do próprio .steps__step — um trade-off padding/margin
   negativo (para não empurrar os vizinhos) desalinhava a coluna 1 com
   o eyebrow/heading acima (medido: 18px à esquerda do sítio certo).
   Absolute não participa no grid, zero risco de desalinhar nada. */
.steps__step {
  position: relative;
  transition: transform var(--d-hover) var(--ease-out);
}

.steps__step::before {
  content: '';
  position: absolute;
  inset: -18px;
  z-index: -1;
  border-radius: var(--r-card);
  background: transparent;
  transition: background var(--d-hover) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .steps__step:hover { transform: translateY(-6px); }
  .steps__step:hover::before { background: var(--surface); }
  .steps__step:hover .steps__no { color: var(--accent-bright); }
}

/* Reveal próprio, mais pronunciado que o genérico (translateY 26px) —
   pedido do dono: quer sentir cada passo a "aparecer" ao dar scroll
   (01 entra, mais scroll, 02 entra, mais scroll, 03 entra), não um
   fade subtil igual ao resto da página. Empilham-se um por um em
   mobile (grid 1fr), por isso o IntersectionObserver já os dispara
   individualmente — só faltava o movimento ser grande o suficiente
   para se notar como entrada, não como afinação de opacidade. */
.js .steps__step[data-reveal] {
  transform: translateY(64px) scale(0.9);
  filter: blur(10px);
  transition-duration: 720ms;
}

.js .steps__step[data-reveal].is-in {
  transform: none;
  filter: none;
}

/* Cor própria em custom property (--step-tint, inline no HTML) em vez
   de style="color:..." — assim o :hover consegue sobrepor sem
   !important (uma inline style de `color` venceria sempre um selector,
   uma custom property não). */
.steps__no {
  margin: 0;
  font-family: var(--display);
  font-weight: 900;
  font-size: 56px;
  line-height: 0.85;
  color: var(--step-tint);
  transition: color var(--d-hover) var(--ease-out);
}

/* Brilho contínuo, sem depender de hover — a mudança anterior (hover
   lift) só se via com o rato por cima; a pedido do dono, isto tem de
   ser visível sem interacção nenhuma. Atraso próprio por passo
   (nth-child) para os três não acenderem em coro. */
@media (prefers-reduced-motion: no-preference) {
  .steps__no { animation: step-glow 3.4s ease-in-out infinite; }
  .steps__step:nth-child(2) .steps__no { animation-delay: 0.5s; }
  .steps__step:nth-child(3) .steps__no { animation-delay: 1s; }
}

@keyframes step-glow {
  0%, 100% { text-shadow: 0 0 0 transparent; }
  50%      { text-shadow: 0 0 22px var(--step-tint); }
}

.steps__title {
  margin: 14px 0 0;
  font-family: var(--display);
  font-weight: 900;
  font-size: 24px;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.steps__body {
  margin: 9px 0 0;
  max-width: 30ch;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

/* ── Compatibilidade ──────────────────────────────────────── */

.compat { padding-block: clamp(72px, 11vh, 112px) 0; }

.compat__card {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: 48px;
  align-items: center;
  background: var(--surface);
  border-radius: 22px;
  padding: clamp(32px, 5vw, 56px);
}

.compat__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(38px, 4.4vw, 62px);
  line-height: 0.92;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin-block: 18px 0;
}

.compat__body {
  margin-top: 18px;
  max-width: 40ch;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--text-dim);
}

.compat__form { margin-top: 28px; }

.compat__form label { display: block; margin-bottom: 10px; }

.compat__input-row {
  display: flex;
  max-width: 420px;
  background: var(--text);
  border-radius: var(--r-pill);
  overflow: hidden;
}

.compat__input-row input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--bg);
  font-family: var(--display);
  font-weight: 900;
  font-size: 22px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 14px 6px 14px 22px;
}

.compat__input-row input::placeholder { color: rgba(13, 19, 17, 0.32); }

.compat__input-row .btn { margin: 6px; min-height: 40px; }

.compat__hint {
  margin: 10px 0 0;
  min-height: 1.4em;
  font-size: 12.5px;
  color: var(--text-dim);
}

.compat__example {
  display: flex;
  align-items: center;
}

/* Bilhete duplo — mesma linguagem do .ticket real (/m/[code]: strip
   accent + costura perfurada), não bilhetes rodados com sombra pesada
   e uma pill de % a flutuar por cima (aspecto genérico, corrigido a
   pedido do dono). Lê-se como UM bilhete cortado a meio entre pessoas,
   não dois cartões separados a competir por espaço. */
/* --card-selected, não --surface — o cartão que envolve isto
   (.compat__card) já usa --surface; com a mesma cor o bilhete ficava
   invisível, sem fronteira nenhuma com o fundo. */
.ticket-duo {
  width: 100%;
  max-width: 360px;
  margin-inline: auto;
  background: var(--card-selected);
  border-radius: var(--r-ticket);
  overflow: hidden;
}

.ticket-duo__half {
  padding: 22px 24px;
  border-left: 4px solid var(--tint);
}

.ticket-duo__label {
  margin: 0;
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.ticket-duo__name {
  margin: 6px 0 0;
  font-family: var(--display);
  font-weight: 900;
  font-size: 26px;
  line-height: 0.95;
  text-transform: uppercase;
  color: var(--tint);
}

.ticket-duo__code {
  margin: 8px 0 0;
  font-family: var(--display);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.14em;
  color: var(--text-dim);
}

/* Costura partilhada — furos nas pontas da linha tracejada (cor do
   fundo da PÁGINA sobre o fundo do bilhete, como um furo a sério).
   A % fica pousada em cima da linha, não a flutuar sozinha por cima
   dos bilhetes. */
.ticket-duo__seam {
  position: relative;
  margin: 0 22px;
  border-top: 1px dashed var(--rule-strong);
}

.ticket-duo__seam::before,
.ticket-duo__seam::after {
  content: '';
  position: absolute;
  top: 0;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--bg);
  transform: translateY(-50%);
}

.ticket-duo__seam::before { left: -7px; }
.ticket-duo__seam::after  { right: -7px; }

.ticket-duo__pct {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--surface);
  padding: 4px 14px;
  font-family: var(--display);
  font-weight: 900;
  font-size: 20px;
  color: var(--accent-bright);
}

/* ── Fecho ────────────────────────────────────────────────── */

.closing {
  padding-block: clamp(72px, 11vh, 112px) clamp(72px, 11vh, 100px);
  text-align: center;
}

.closing__title {
  margin: 0 auto;
  max-width: 20ch;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(38px, 5.6vw, 76px);
  line-height: 0.9;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.closing__body {
  margin: 18px auto 0;
  max-width: 40ch;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--text-dim);
}

.closing__actions {
  display: flex;
  justify-content: center;
  margin-top: 30px;
}

@media (max-width: 900px) {
  .steps__grid { grid-template-columns: 1fr; gap: 26px; }
  .compat__card { grid-template-columns: 1fr; }
  /* Coluna 1fr em single-column dava ao .compat__example a largura toda do
     cartão (~600-750px em tablet) — o .ticket-duo tem max-width próprio
     (360px) mas sem isto o :flex do pai ainda o deixava a boiar longe
     do texto, num cartão muito mais largo do que alto. */
  .compat__example { margin: 12px auto 0; width: 100%; max-width: 340px; }
}

/* ── Bilhete de match ────────────────────────────────────── */

.match { padding-block: clamp(48px, 10vh, 108px) clamp(56px, 11vh, 116px); }

.match__intro { max-width: 44ch; }

.match__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(40px, 8vw, 76px);
  line-height: 0.92;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin-block: 22px 0;
}

.match__body {
  margin-top: 20px;
  font-size: 16px;
  line-height: 1.6;
  color: var(--text-dim);
}

/* Bezel duplo — casca exterior com hairline, núcleo dentro. Os
   raios são concêntricos (shell 22 = ticket 14 + padding 8). */
.ticket-shell {
  max-width: 476px;
  margin-top: 42px;
  padding: 8px;
  border-radius: var(--r-shell);
  background: var(--veil);
  border: 1px solid var(--veil-edge);
}

/* Entrada com massa: aproxima-se em vez de se materializar.
   Nunca scale(0) — nada no mundo real aparece do nada. */
.js .ticket-shell[data-reveal] { transform: translateY(26px) scale(0.96); }
.js .ticket-shell[data-reveal].is-in { transform: none; }

.ticket {
  position: relative;
  background: var(--surface);
  border-radius: var(--r-ticket);
  overflow: hidden;
}

.ticket__main {
  display: flex;
  padding: 26px 24px 24px 0;
}

.ticket__strip {
  width: 5px;
  align-self: stretch;
  background: var(--accent-bright);
  margin-right: 22px;
  flex: none;
}

.ticket__content {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.ticket__code {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(44px, 11vw, 62px);
  line-height: 0.9;
  letter-spacing: 0.06em;
  color: var(--accent-bright);
  font-variant-numeric: tabular-nums;
  word-break: break-all;
}

/* Linha perfurada + entalhes — o entalhe é um círculo da cor do
   fundo do bilhete (aqui o véu da casca, não o fundo da página). */
.ticket__perf {
  position: relative;
  height: 22px;
  display: flex;
  align-items: center;
}

.ticket__perf::before,
.ticket__perf::after {
  content: '';
  position: absolute;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--bg);
}

.ticket__perf::before { left: -11px; }
.ticket__perf::after  { right: -11px; }

.ticket__dashes {
  flex: 1;
  margin-inline: 11px;
  border-top: 1px dashed var(--rule-strong);
}

.ticket__stub {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px 22px;
}

.ticket__brand {
  font-family: var(--display);
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 0.32em;
  color: rgba(138, 138, 126, 0.5);
}

/* Alternância de estado do /m/ — o estado genérico é o de arranque,
   para que a página continue legível se o JS não correr. */
[data-state='generic'] .state--code    { display: none; }
[data-state='code']    .state--generic { display: none; }

.copy {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-pill);
  color: var(--text-dim);
  font-family: var(--body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  min-height: 40px;
  padding: 8px 16px;
  cursor: pointer;
  transition:
    color        var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out),
    transform    var(--d-press) var(--ease-out);
}

.copy:active { transform: scale(0.96); }

.copy.is-done {
  color: var(--accent-bright);
  border-color: var(--accent-bright);
}

/* ── Acções ──────────────────────────────────────────────── */

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* 16px cobre o clear space exigido à volta do badge da App Store
     (¼ da altura = 12.5px a 50px). */
  gap: 16px;
  margin-top: 36px;
}

/* A Apple proíbe distorcer o badge. Este grupo revela-se só com
   opacidade e deslocação — sem a passagem por blur. */
.js .actions[data-reveal],
.js .actions[data-reveal].is-in { filter: none; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 44px;
  padding: 12px 12px 12px 24px;
  border-radius: var(--r-pill);
  border: 1px solid transparent;
  font-family: var(--body);
  font-weight: 600;
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition:
    background   var(--d-hover) var(--ease-out),
    border-color var(--d-hover) var(--ease-out),
    color        var(--d-hover) var(--ease-out),
    transform    var(--d-press) var(--ease-out);
}

/* Feedback de toque — a resposta do sistema estala, não se arrasta. */
.btn:active { transform: scale(0.97); }

.btn--primary {
  background: var(--accent-bright);
  color: var(--bg);
}

.btn--ghost {
  border-color: var(--rule-strong);
  color: var(--text);
  background: transparent;
}

/* A seta nunca fica nua ao lado do texto — vive no seu próprio
   disco, encostada ao padding interior direito. */
.btn__icon {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 12px;
  line-height: 1;
  flex: none;
  transition: transform var(--d-hover) var(--ease-out);
}

.btn--primary .btn__icon { background: rgba(13, 19, 17, 0.15); }
.btn--ghost   .btn__icon { background: rgba(240, 237, 232, 0.08); }

/* ── Documento longo (Privacy) ───────────────────────────── */

.doc { padding-block: clamp(52px, 9vh, 92px) clamp(64px, 11vh, 116px); }

.doc__title {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(44px, 9vw, 88px);
  line-height: 0.92;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin-block: 22px 0;
}

.doc__meta {
  margin-top: 18px;
  font-size: 13px;
  color: var(--text-dim);
}

.notice {
  margin-top: 30px;
  padding: 18px 20px;
  background: var(--surface);
  border-left: 3px solid var(--accent-bright);
  border-radius: 0 var(--r-card) var(--r-card) 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-dim);
  max-width: 68ch;
}

.doc__body {
  max-width: 68ch;
  margin-top: 8px;
  counter-reset: sec;
}

.doc__section {
  padding-top: 46px;
  counter-increment: sec;
}

.doc__section h2 {
  font-family: var(--display);
  font-weight: 900;
  font-size: 27px;
  line-height: 1.08;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-bottom: 16px;
  display: flex;
  align-items: baseline;
  gap: 14px;
}

.doc__section h2::before {
  content: counter(sec, decimal-leading-zero);
  font-family: var(--body);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--accent-bright);
  flex: none;
}

.doc__section h3 {
  font-family: var(--body);
  font-weight: 600;
  font-size: 15px;
  margin-top: 24px;
  margin-bottom: 6px;
  color: var(--text);
}

.doc__section p {
  margin-bottom: 14px;
  font-size: 15.5px;
  color: var(--text-dim);
}

.doc__section p:last-child { margin-bottom: 0; }

.doc__section strong { color: var(--text); font-weight: 500; }

.doc__section a {
  color: var(--accent-bright);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.doc__list { margin-bottom: 14px; }

.doc__list li {
  position: relative;
  padding-left: 20px;
  margin-bottom: 9px;
  font-size: 15.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

.doc__list li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 11px;
  width: 6px;
  height: 1px;
  background: var(--accent-bright);
}

/* ── Rodapé ──────────────────────────────────────────────── */

/* Faixa das 12 cores — ecoa "in twelve shades" da tagline, full-bleed
   (fora do .shell de propósito). Estático: já há deriva a mais no
   resto da página, o rodapé é o único sítio que fica quieto. */
.colophon__spectrum {
  display: flex;
  height: 3px;
}

.colophon__spectrum span {
  flex: 1;
  background: var(--tint);
}

.colophon {
  padding-block: clamp(56px, 9vh, 88px) clamp(40px, 6vh, 56px);
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  align-items: start;
  gap: 32px;
}

/* Wordmark com o mesmo peso do masthead (era 13px @ 50% opacidade —
   ilegível a maior, lia-se "pobre" por contraste, não por tamanho). */
.colophon__brand {
  font-family: var(--display);
  font-weight: 900;
  font-size: 22px;
  letter-spacing: 0.28em;
  color: var(--text);
}

.colophon__tagline {
  margin: 12px 0 0;
  max-width: 30ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-dim);
}

.colophon__links {
  display: flex;
  flex-direction: column;
  gap: 13px;
}

.colophon__links a {
  font-size: 14px;
  color: var(--text-dim);
  text-decoration: none;
  transition: color var(--d-hover) var(--ease-out);
}

/* ── Foco visível ────────────────────────────────────────── */

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

/* ══ HOVER ═══════════════════════════════════════════════════
   Fechado atrás de hover+pointer:fine. Em touch o browser dispara
   um hover falso no tap e o estado ficaria colado depois do toque. */

@media (hover: hover) and (pointer: fine) {
  .wordmark:hover { color: var(--accent-bright); }

  /* O fotograma acende como se apanhasse mais luz — as perfurações
     ficam quietas, senão a fita parecia a deformar-se. */
  .archetype:hover { background: var(--card-selected); }

  .btn--primary:hover { background: #6BC79C; }

  .btn--ghost:hover {
    border-color: var(--accent-bright);
    color: var(--accent-bright);
  }

  /* Tensão cinética interna — o disco puxa na diagonal do próprio ↗. */
  .btn:hover .btn__icon { transform: translate(2px, -2px) scale(1.06); }

  .copy:hover {
    color: var(--accent-bright);
    border-color: var(--accent-bright);
  }

  .colophon__links a:hover { color: var(--accent-bright); }
}

/* ══ MOVIMENTO REDUZIDO ══════════════════════════════════════
   Menos movimento e mais suave, não zero: o que ajuda a perceber
   mantém-se (opacidade, cor), o que desloca desaparece. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  body::after { animation: none; }

  .js [data-reveal] {
    transform: none;
    filter: none;
    transition: opacity 260ms var(--ease-out);
    transition-delay: 0ms;
  }

  .js .ticket-shell[data-reveal],
  .js .ticket-shell[data-reveal].is-in { transform: none; }

  .js .device[data-reveal],
  .js .device[data-reveal].is-in {
    transform: none;
  }

  .js .line__inner,
  .js .is-in .line__inner {
    transform: none;
    transition: none;
  }

  .btn:hover .btn__icon,
  .btn:active,
  .copy:active { transform: none; }

  /* Sem deriva automática (o JS não arranca), por isso o snap já não
     tem contra quem lutar e ajuda a percorrer o carrossel à mão. */
  .index__grid { scroll-snap-type: x proximity; }
}

/* ── Ecrãs estreitos ─────────────────────────────────────── */

/* Abaixo de 900px o split desfaz-se: copy primeiro, aparelho por
   baixo, centrado, discreto. Nota: o aparelho é 9:19.5 — largura em
   vw cresce a altura 2.17×. A tentativa anterior (78vw) dava um
   aparelho de 630-740px de alto, dominando o ecrã inteiro. Aqui a
   largura fica contida para o objecto continuar a ler-se como
   detalhe, não como o ecrã todo. */
@media (max-width: 900px) {
  /* Em mobile já não há nada a sangrar — .hero__stage perdeu a margem
     negativa e o leque de bilhetes está escondido (ver a regra dos
     1280px) — mas o clip ficava a cortar a sombra projectada do
     aparelho (0 70px 130px -40px chega ~95px abaixo dele) exactamente
     na aresta do hero. */
  .hero { overflow: visible; }

  /* head → stage → body: o telemóvel sobe para logo depois da headline
     (gancho visual mais cedo), texto+CTAs ficam por baixo — pedido do
     dono, a ordem antiga (texto todo, depois telemóvel no fundo)
     empurrava o aparelho vazio (splash, sem screenshot real) para
     longe do resto, com espaço morto a mais. */
  .hero__grid {
    grid-template-columns: 1fr;
    grid-template-areas:
      "head"
      "stage"
      "body";
    gap: clamp(20px, 5vw, 36px);
    min-height: 0;
    padding-block: clamp(28px, 5vh, 52px) 0;
  }

  /* margin-top negativo — a sombra do .device (0 70px 130px, ~95px
     abaixo do aparelho, ver comentário em .device mais abaixo) não
     ocupa layout (é só shadow, não box), mas o gap do grid via row
     "stage"→"body" soma-se a esse espaço vazio visual e o texto ficava
     a ~140px do telemóvel, muito mais do que o espaço head→stage. */
  .hero__body { padding-bottom: 0; margin-top: -13px; }

  /* margin-top — sem isto o device (rotate -2.5deg, ligeiramente mais
     alto que a linha da grid) chegava a tocar/sobrepor 6px a última
     linha da headline (medido: head.bottom=253, device.top=247). */
  .hero__stage { margin-bottom: 0; margin-top: 28px; }

  /* Nada de 3D aqui. rotateY/rotateX com transform-origin a 38% do
     topo (herdado da regra base) empurra o fundo do aparelho para
     fora da caixa não-transformada — e é essa caixa, não a rodada,
     que o overflow:hidden do .hero usa para recortar. Resultado:
     o queixo do telemóvel desaparecia, mesmo sem margin negativo.
     2D simples elimina o problema por definição — a caixa rodada
     em Z nunca sai da sua própria área.
     Sem rotação nenhuma (era -2.5deg) — qualquer rotação de bitmap
     obriga o browser a reamostrar o conteúdo (resample), e o
     screenshot dentro do ecrã perdia nitidez visivelmente a este
     tamanho. Em mobile o tilt quase não se notava; ficou só o preço
     em nitidez, sem o ganho visual. Desktop mantém o 3D (ver regra
     base) — ecrã maior, efeito mais intencional, menos notado. */
  .device {
    width: clamp(215px, 52vw, 280px);
    transform: none;
  }

  .js .device[data-reveal] {
    transform: translateY(40px);
  }

  .js .device[data-reveal].is-in,
  .js .device[data-reveal].is-in:not(:hover) {
    transform: none;
  }

  /* Coluna única: a copy passa a ter a largura toda do shell, por
     isso a headline pode crescer bastante mais do que no split. */
  .hero__headline { font-size: clamp(34px, 9vw, 72px); }
}

@media (max-width: 560px) {
  body { font-size: 16px; }

  /* Todos os links normais escondidos — só o hamburger fica na barra
     (pedido do dono: "ficar só as barras mesmo"), os 3 vivem dentro
     do painel do menu. */
  .masthead__nav-link { display: none; }
  .masthead__nav { gap: 16px; font-size: 12.5px; }
  .masthead__menu { display: block; position: relative; }

  .actions {
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
  }

  /* O rolo tem largura fixa (a altura tem de casar com a da fita, e
     a proporção com ela), por isso em ecrã estreito esconde-se mais
     para não comer a faixa toda. */
  .filmstrip__reel { left: calc(var(--reel-w) * -0.7); }

  /* Lado a lado não cabe em ecrã pequeno — o código PSY-4K2M a 22px
     Barlow precisa de mais de 100px que sobram depois do botão.
     Empilha: input a toda a largura, botão a toda a largura. */
  .compat__input-row {
    flex-direction: column;
    max-width: none;
    background: transparent;
    border-radius: 0;
    gap: 10px;
  }

  .compat__input-row input {
    background: var(--text);
    border-radius: var(--r-pill);
    padding: 16px 20px;
  }

  .compat__input-row .btn {
    margin: 0;
    width: 100%;
  }

  /* :active substitui o :hover como feedback de toque — resposta
     imediata ao dedo, não à intenção do rato. As doze cores já não
     dependem de estado nenhum: o nome do arquétipo é a cor. */
  .archetype:active { background: var(--card-selected); }

  .colophon { grid-template-columns: 1fr; gap: 28px; }
}
