/* ============================================================
   AURÉLIE — Cinematic three-act film
   Techniques applied (epic-design catalogue):
     · Scrub timeline + pinned sticky scene (195-frame sequence)
     · 6-layer depth / parallax system
     · Masked line reveal + word rise      (Act I entry)
     · Character stagger fade              (eyebrows)
     · Word-by-word scroll lighting        (Act II)
     · Side converge + clip wipe           (Act III entry)
     · Theatrical enter + exit per act
     · Float loops, breathing glow, vignette + grain FX
   ============================================================ */

:root {
  --ink:        #14100f;
  --ink-soft:   #4a423f;
  --ink-ghost:  rgba(20, 16, 15, 0.16);
  --crimson:    #8e0f22;
  --paper:      #f7f5f3;
  --gold:       #b08d57;

  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* Red velvet, layered: a lit pool up top, deep falloff at the base,
     a broad sheen across it, and a fine woven texture on top. Used by
     the loader, the smoke transition and the collection sections. */
  --velvet-field:
    radial-gradient(ellipse 90% 62% at 50% -6%, rgba(196, 38, 60, 0.62), rgba(196, 38, 60, 0) 62%),
    radial-gradient(ellipse 78% 70% at 50% 112%, rgba(52, 4, 12, 0.92), rgba(52, 4, 12, 0) 64%),
    linear-gradient(180deg, #6a0b19 0%, #470711 52%, #2c040c 100%);
  --velvet-sheen: linear-gradient(105deg,
    rgba(255, 255, 255, 0) 22%, rgba(255, 190, 190, 0.07) 44%,
    rgba(255, 255, 255, 0) 62%);
  /* Alphas are baked low so the weave can sit directly in the background
     stack — a pseudo-element overlay would need positioning, and that
     fights the sticky viewports these surfaces wrap. */
  --velvet-weave: repeating-linear-gradient(90deg,
      rgba(0, 0, 0, 0.05) 0 1px, rgba(255, 255, 255, 0.018) 1px 2px),
    repeating-linear-gradient(0deg,
      rgba(0, 0, 0, 0.04) 0 1px, rgba(255, 255, 255, 0.015) 1px 2px);

  /* Low-frequency fractal noise — the wisp structure inside each plume.
     The softening is baked in here as feGaussianBlur rather than left to
     a CSS filter: this rasterises ONCE into a texture the plumes share,
     where `filter: blur()` would re-blur a ~1200px surface on every
     scroll frame, per plume, while it is being scaled and rotated.
     Same look, none of the per-frame cost.
     numOctaves is 3, not 4 — the fourth is below the blur's floor.
     The gamma pass then lifts the mid alphas so the plume reads as deep
     crimson smoke rather than dusty rose, without flattening the wisps
     into a solid blob. */
  --smoke-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='s' x='-15%25' y='-15%25' width='130%25' height='130%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.013' numOctaves='3' seed='7'/%3E%3CfeGaussianBlur stdDeviation='12'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='gamma' exponent='0.42' amplitude='1.35'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='480' height='480' filter='url(%23s)'/%3E%3C/svg%3E");

  --cream: #ecdcb8;
  --cream-soft: rgba(236, 220, 184, 0.62);
}

/* Reusable velvet surface for full sections */
.velvet {
  position: relative;
  background-color: #4c0611;
  background-image: var(--velvet-weave), var(--velvet-sheen), var(--velvet-field);
}

html {
  -webkit-font-smoothing: antialiased;
  /* `clip`, not `hidden` — `hidden` makes body a scroll container and
     silently kills `position: sticky` on the film viewport. */
  overflow-x: clip;
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: 'Inter', system-ui, sans-serif;
  overflow-x: clip;
}

.font-display { font-family: 'Cormorant Garamond', Georgia, serif; }

/* ---------- Loader — plain red velvet --------------------- */
.loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 1.75rem;
  color: #ecdcb8;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.45);
  background-color: #4c0611;
  background-image: var(--velvet-weave), var(--velvet-sheen), var(--velvet-field);
  /* A fixed, overflow-hidden overlay swallows wheel and touch, so the
     film can't be scrolled out of Act I before it has decoded. */
  overflow: hidden;
}

.loader__bar {
  width: min(240px, 50vw);
  height: 1px;
  background: rgba(236, 220, 184, 0.25);
  overflow: hidden;
}
.loader__fill {
  height: 100%;
  width: 0%;
  background: #d6b676;
  transition: width 0.35s linear;
}

/* ---------- Scene + depth layers -------------------------- */
/* The stage sits above the collection so the collection can slide in and
   pin behind it unseen; the stage then dissolves to reveal it. */
.scene {
  position: relative;
  z-index: 3;
  height: var(--scene-height, 1220svh);
}

.scene__viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.layer > * { pointer-events: auto; }

/* DEPTH 0 — far background plate (the image sequence) */
.depth-0 { z-index: 0; }
#sequence {
  display: block;
  width: 100%;
  height: 100%;
}

/* DEPTH 1 — atmosphere: warm pool under the case, cool pool on the stone */
.depth-1 { z-index: 1; mix-blend-mode: multiply; }
.glow {
  position: absolute;
  left: 50%;
  top: 52%;
  width: 70vmax;
  height: 70vmax;
  transform: translate(-50%, -50%);
  filter: blur(4px);
  animation: breathe 12s ease-in-out infinite;
}
.glow--velvet {
  background: radial-gradient(circle at 50% 50%,
    rgba(190, 30, 45, 0.10) 0%,
    rgba(176, 141, 87, 0.06) 32%,
    rgba(255, 255, 255, 0) 62%);
}
/* Act III — the stone takes over, the pool turns cold */
.glow--ice {
  opacity: 0;
  background: radial-gradient(circle at 50% 46%,
    rgba(120, 150, 190, 0.10) 0%,
    rgba(190, 205, 225, 0.05) 34%,
    rgba(255, 255, 255, 0) 62%);
  animation-duration: 9s;
}

/* DEPTH 2 — mid decorations: hairlines + orbiting accents */
.depth-2 { z-index: 2; }
.rule {
  position: absolute;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(20, 16, 15, 0.18), transparent);
}
.spark {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--gold);
  opacity: 0.5;
  animation: float-loop 9s ease-in-out infinite;
}
.spark:nth-of-type(odd) { animation-duration: 13s; animation-delay: -3s; }
/* Act III sparkle — cold facets throwing light */
.spark--ice { background: #cfe0f2; box-shadow: 0 0 12px 2px rgba(190, 215, 240, 0.7); opacity: 0; }

/* DEPTH 4 — text content, one block per act -------------- */
.depth-4 { z-index: 4; }

.act {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: min(42rem, 100%);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-inline: clamp(1.5rem, 7vw, 8rem);
  padding-bottom: 6vh;
}
/* Acts II & III sit opposite the case, which holds dead centre */
.act--right {
  left: auto;
  right: 0;
  text-align: right;
  align-items: flex-end;
}
.act--right .eyebrow { justify-content: flex-end; }
/* Only Act I is visible on load; the rest are driven by scroll */
.act--later { visibility: hidden; opacity: 0; }

/* Masked line reveal — each line clips its own rising word set */
/* will-change is applied by GSAP during the entry tween and cleared
   afterwards — declaring it here would pin the layer forever. */
.line { display: block; overflow: hidden; padding-bottom: 0.08em; }
.line > span { display: inline-block; }

.eyebrow {
  display: flex;
  flex-wrap: wrap;
  font-size: 0.6875rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--crimson);
  margin-bottom: clamp(1.25rem, 3vw, 2rem);
}
.eyebrow span { display: inline-block; white-space: pre; }

.headline {
  font-size: clamp(2.75rem, 7vw, 6.5rem);
  line-height: 1;
  font-weight: 300;
  letter-spacing: -0.02em;
}
/* Act III carries a spec table and a CTA under it, and the case has
   grown by then — smaller display size keeps both clear of the product. */
.headline--sm { font-size: clamp(2.25rem, 5.2vw, 4.75rem); }

.headline em {
  font-style: italic;
  color: var(--crimson);
}

.subline {
  margin-top: clamp(1.25rem, 3vw, 2rem);
  max-width: 32ch;
  font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
  line-height: 1.7;
  color: var(--ink-soft);
}
.act--right .subline { margin-left: auto; }

/* Word-by-word scroll lighting (Act II) */
.lit {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: clamp(1.75rem, 3.6vw, 3.25rem);
  line-height: 1.28;
  font-weight: 300;
  max-width: 18ch;
  color: var(--ink);
}
.act--right .lit { margin-left: auto; }
.lit span {
  display: inline-block;
  white-space: pre;
  opacity: 0.14;           /* unlit — GSAP raises this word by word */
}

.cta-row {
  margin-top: clamp(2rem, 4vw, 3rem);
  display: flex;
  align-items: center;
  gap: 2rem;
  flex-wrap: wrap;
}

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem 2.25rem;
  font-size: 0.75rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--paper);
  background: var(--ink);
  isolation: isolate;
  overflow: hidden;
  transition: transform 0.5s var(--ease-out-expo);
}
.btn::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--crimson);
  transform: translateY(101%);
  transition: transform 0.6s var(--ease-out-expo);
}
.btn:hover { transform: translateY(-2px); }
.btn:hover::after { transform: translateY(0); }

.btn-ghost {
  font-size: 0.75rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid rgba(20, 16, 15, 0.25);
  padding-bottom: 0.35rem;
  transition: border-color 0.4s ease, color 0.4s ease;
}
.btn-ghost:hover { color: var(--crimson); border-color: var(--crimson); }

/* Spec table (Act III) */
.spec {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem 2.5rem;
  margin-top: clamp(1.75rem, 3.5vw, 2.5rem);
  max-width: 26rem;
}
.spec div { border-top: 1px solid var(--ink-ghost); padding-top: 0.7rem; }
.spec dt {
  font-size: 0.625rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.35rem;
}
.spec dd { font-family: 'Cormorant Garamond', Georgia, serif; font-size: 1.5rem; }

/* Scroll cue */
.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 2.25rem;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  font-size: 0.625rem;
  letter-spacing: 0.35em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.scroll-cue__track {
  width: 1px;
  height: 54px;
  background: rgba(20, 16, 15, 0.15);
  overflow: hidden;
}
.scroll-cue__thumb {
  display: block;
  width: 100%;
  height: 40%;
  background: var(--crimson);
  animation: cue 2.4s var(--ease-out-expo) infinite;
}

/* ---------- Fixed chrome ---------------------------------- */
.masthead {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: clamp(1.25rem, 2.5vw, 2rem) clamp(1.5rem, 7vw, 8rem);
  mix-blend-mode: difference;
  color: #fff;
}
.masthead a { transition: opacity 0.3s ease; }
.masthead a:hover { opacity: 0.55; }

/* Held here rather than in Tailwind utilities on the element. As utilities
   they outranked the narrow-screen overrides below — the type stayed 11px at
   0.28em with a 40px gap whatever the media query asked for, and the row only
   fitted a 360px screen by 14px. */
.masthead__nav {
  display: flex;
  gap: 2.5rem;
  font-size: 11px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
}

/* Act rail — orientation on a 1000vh page */
.rail {
  position: fixed;
  right: clamp(1rem, 2.5vw, 2.25rem);
  top: 50%;
  transform: translateY(-50%);
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
  font-size: 0.625rem;
  letter-spacing: 0.28em;
  color: var(--ink);
  opacity: 0;
  transition: opacity 0.6s ease;
}
.rail.is-visible { opacity: 1; }
.rail span {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  opacity: 0.45;
  transition: opacity 0.5s ease, color 0.5s ease;
}
.rail span::before {
  content: '';
  width: 22px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0.45);
  transform-origin: right center;
  transition: transform 0.5s var(--ease-out-expo);
}
.rail span.is-active { opacity: 1; color: var(--crimson); }
.rail span.is-active::before { transform: scaleX(1); }

/* DEPTH 5 — foreground FX: vignette + grain */
.depth-5 { z-index: 5; }
.vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 45%, transparent 45%, rgba(20, 16, 15, 0.10) 100%);
}
.grain {
  position: absolute;
  inset: -50%;
  opacity: 0.035;
  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");
  animation: grain-shift 0.7s steps(2) infinite;
}

/* ══════════════════════════════════════════════════════════
   SMOKE TRANSITION — DEPTH 6, inside the pinned stage.
   It is a layer of the film, not a section after it: the stage is
   frozen on Act III's last frame and the plumes roll up over that
   frame, so there is no white section to scroll into first.
   ══════════════════════════════════════════════════════════ */
.depth-6 { z-index: 6; }
.smoke-stage { overflow: hidden; }
/* .layer hands pointer-events back to its children — the veil covers the
   whole stage and would eat clicks on the acts underneath. */
.smoke-stage, .smoke-stage * { pointer-events: none; }

.smoke__veil {
  position: absolute;
  inset: 0;
  opacity: 0;
  background-color: #4c0611;
  background-image: var(--velvet-sheen), var(--velvet-field);
}

/* Hidden outright until the beat runs. The plumes live inside the film's
   pinned stage, so without this the browser would carry six large masked
   layers through the whole 1220svh of the film to show them for 220 of
   it. `is-live` is toggled by the smoke's own ScrollTrigger. */
.smoke {
  position: absolute;
  inset: -20%;
  visibility: hidden;
}
.smoke.is-live { visibility: visible; }
/* Promotion, too, is rented rather than owned — only while it is running */
.smoke.is-live .plume { will-change: transform, opacity; }

/* A plain blurred gradient reads as a colour wash, not smoke. Masking
   each plume with low-frequency fractal noise breaks it into wisps;
   the radial gradient underneath keeps the edges soft. Deep crimson
   rather than bright red, so it stays smoke over the white studio
   instead of turning pink. */
.plume {
  position: absolute;
  opacity: 0;
  /* Opaque through the core — a translucent plume just tints whatever
     is behind it (pink over the white studio, mauve over half-faded
     velvet). Solid centres let the noise mask do all the wisping. */
  background: radial-gradient(circle at 50% 50%,
    rgb(124, 11, 29) 0%,
    rgb(104, 8, 24) 38%,
    rgba(78, 6, 18, 0.82) 56%,
    rgba(44, 3, 10, 0) 78%);
  -webkit-mask-image: var(--smoke-noise);
          mask-image: var(--smoke-noise);
  /* 150%, not `cover`: the noise tile and the plumes are both square, so
     `cover` resolved to exactly 100% and every mask-position below was
     a no-op — all six plumes wore the identical crop of the noise.
     Oversizing the mask makes position mean something, and each plume
     now carries genuinely different wisps. */
  -webkit-mask-size: 150%;
          mask-size: 150%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: 50% 50%;
          mask-position: 50% 50%;
}
.plume:nth-child(even) {
  -webkit-mask-position: 10% 80%;
          mask-position: 10% 80%;
}
.plume:nth-child(3n) {
  -webkit-mask-position: 86% 18%;
          mask-position: 86% 18%;
}

.transition__title {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  opacity: 0;
  color: var(--cream);
  text-shadow: 0 4px 30px rgba(0, 0, 0, 0.6);
}
.transition__title span {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: 0.5em;
  text-transform: uppercase;
  color: var(--cream-soft);
  margin-bottom: 1.1rem;
}

/* ══════════════════════════════════════════════════════════
   COLLECTION — four pieces revealed one at a time, each zooming
   in while the frame alternates left / right / left / right.
   ══════════════════════════════════════════════════════════ */
/* --overlap pulls the section up under the film stage: it slides in and
   pins while the stage is still opaque, so by the time the smoke
   dissolves the first piece is already in frame. */
.collection {
  position: relative;
  z-index: 1;
  height: var(--scene-height, 700svh);
  margin-top: calc(-1 * var(--overlap, 0px));
}
.collection__viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.piece {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: center;
  padding: 12vh clamp(1.5rem, 6vw, 7rem) 10vh;
  opacity: 0;
  visibility: hidden;
}
/* Alternating side: even pieces put the plate left, odd ones right */
.piece--right .piece__plate { order: 2; }
.piece--right .piece__copy  { order: 1; text-align: right; }

.piece__plate {
  position: relative;
  overflow: hidden;
  background: var(--paper);
  border: 1px solid rgba(214, 182, 118, 0.4);
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55), 0 4px 14px rgba(0, 0, 0, 0.35);
  aspect-ratio: 960 / 512;
  will-change: transform;
}
.piece__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform-origin: 50% 45%;
  will-change: transform;
}

.piece__copy { color: var(--cream); }
.piece__no {
  font-size: 0.625rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--cream-soft);
  margin-bottom: 1.1rem;
}
.piece__name {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 300;
  font-size: clamp(2rem, 4.6vw, 3.75rem);
  line-height: 1.04;
  letter-spacing: -0.01em;
}
.piece__name em { font-style: italic; color: #e9b9c2; }
.piece__note {
  margin-top: 1.25rem;
  max-width: 34ch;
  line-height: 1.75;
  color: rgba(236, 220, 184, 0.72);
  font-size: clamp(0.875rem, 1.05vw, 1rem);
}
.piece--right .piece__note { margin-left: auto; }
.piece__meta {
  margin-top: 1.75rem;
  display: flex;
  gap: 2rem;
  font-size: 0.625rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--cream-soft);
}
.piece--right .piece__meta { justify-content: flex-end; }

/* ---------- Card grid — the whole collection at once ------ */
.cards {
  position: relative;
  background: #2c040c;
}
/* ScrollTrigger's pin wrapper — never let the paper body show through
   the scroll length it inserts beneath the pinned section. */
.pin-spacer { background: #2c040c; }

/* Exactly one viewport tall: the section gets pinned at 'top top', and
   the finale panel's 100svh box then lines up with the screen it has to
   open across. The velvet lives here rather than on the section so the
   gradient keeps the box it was designed for. */
.cards__stage {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background-color: #4c0611;
  background-image: var(--velvet-weave), var(--velvet-sheen), var(--velvet-field);
}

/* Takes the slack, so head + grid centre in whatever is left once the
   finale's band has taken its share at the foot. */
.cards__inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(2.5rem, 7vh, 5rem) clamp(1.5rem, 6vw, 7rem) clamp(1.5rem, 4vh, 3rem);
}
.cards__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 2rem;
  flex-wrap: wrap;
  margin-bottom: clamp(1.75rem, 4vh, 3rem);
  color: var(--cream);
}
.cards__head p {
  font-size: 0.625rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--cream-soft);
}
.cards__head h2 {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 300;
  font-size: clamp(2rem, 4.4vw, 3.5rem);
}
.cards__grid {
  display: grid;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}

.card {
  display: block;
  color: var(--cream);
  transition: transform 0.6s var(--ease-out-expo);
}
.card__frame {
  position: relative;
  overflow: hidden;
  background: var(--paper);
  border: 1px solid rgba(214, 182, 118, 0.35);
  box-shadow: 0 22px 50px rgba(0, 0, 0, 0.45);
  aspect-ratio: 4 / 5;
  /* Keeps four cards + head + the finale band inside one viewport on a
     laptop. The ratio gives way before the height does; object-fit
     crops the studio white, which is all that is lost. */
  max-height: 52svh;
}
.card__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.9s var(--ease-out-expo);
}
.card:hover { transform: translateY(-6px); }
.card:hover .card__frame img { transform: scale(1.06); }
.card__name {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 1.375rem;
  margin-top: 1.1rem;
}
.card__meta {
  margin-top: 0.35rem;
  font-size: 0.625rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--cream-soft);
}

/* ---------- Footer ---------------------------------------- */
/* Exactly the ground the finale opens onto, so releasing the pin reads
   as the same room continuing rather than a new section arriving. */
.footer {
  position: relative;
  /* Above .finale's panel (2): if the pin ever released without the
     panel dropping back out of fixed, the footer still wins. */
  z-index: 3;
  background: #0a0809;
  color: var(--paper);
  padding: clamp(3.5rem, 11vh, 7rem) clamp(1.5rem, 6vw, 7rem) clamp(1.75rem, 4vh, 2.5rem);
}

.footer__top {
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.85fr);
  align-items: start;
}

.footer__mark {
  font-size: clamp(1.25rem, 2.2vw, 1.75rem);
  font-weight: 300;
  letter-spacing: 0.42em;
  text-transform: uppercase;
}
.footer__line {
  margin-top: 1.15rem;
  max-width: 32ch;
  font-size: 0.875rem;
  line-height: 1.85;
  color: rgba(247, 245, 243, 0.5);
}

.footer__nav {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.footer__nav h3 {
  font-size: 0.625rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 1.15rem;
}
.footer__nav a,
.footer__nav p {
  display: block;
  font-size: 0.875rem;
  line-height: 2.05;
  color: rgba(247, 245, 243, 0.58);
}
.footer__nav a { transition: color 0.4s ease; }
.footer__nav a:hover { color: var(--paper); }

.footer__foot {
  margin-top: clamp(2.5rem, 7vh, 4.5rem);
  padding-top: clamp(1.25rem, 3vh, 2rem);
  border-top: 1px solid rgba(236, 220, 184, 0.12);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1.5rem;
  flex-wrap: wrap;
  font-size: 0.625rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(247, 245, 243, 0.38);
}
.footer__legal { display: flex; gap: clamp(1.25rem, 3vw, 2.25rem); }
.footer__legal a { transition: color 0.4s ease; }
.footer__legal a:hover { color: var(--gold); }

/* ══════════════════════════════════════════════════════════
   FINALE — the button becomes the room.
   A band at the foot of the collection section, no section of its own.
   The panel inside it is ALREADY viewport-sized and anchored to that
   section's bottom edge; a clip-path is the only thing holding it down
   to a pill. Pinning the section and opening the clip therefore grows
   the black up over the card grid in place — nothing scales, nothing
   slides, and the type inside is never distorted.
   ══════════════════════════════════════════════════════════ */
.finale {
  /* JS re-writes these in px so its clip maths and this CSS agree */
  --pill-h: 56px;
  --pill-lift: 3rem;             /* the pill's clearance off the section's foot */
  position: relative;
  z-index: 2;                    /* over the card grid it expands across */
  height: calc(var(--pill-h) + var(--pill-lift) * 2);
}

/* Absolute while it is still just a button in the page's flow, so it
   scrolls with the grid. The moment the open begins the stage is locked
   to the viewport's foot — at which point these two positionings are
   geometrically identical, so .is-live swaps to fixed with no jump and
   the room is guaranteed to fill the screen from there on. */
.finale__panel.is-live {
  position: fixed;
  inset: 0;
  width: auto;
  height: auto;
}

.finale__panel {
  position: absolute;
  left: 0;
  bottom: 0;                     /* == the viewport's foot, once locked */
  width: 100%;
  height: 100svh;
  display: grid;
  background: #0a0809;
  color: var(--paper);
  /* --pad is driven by scroll and overshoots 1 — see buildFinale() */
  padding: calc(var(--pad, 0) * clamp(1.25rem, 4.5vw, 4.5rem));
  /* Held down to the pill, sitting in the band at the section's foot */
  clip-path: inset(
    calc(100% - var(--pill-h) - var(--pill-lift))
    calc(50% - min(36vw, 130px))
    var(--pill-lift)
    calc(50% - min(36vw, 130px))
    round 999px);
  will-change: clip-path;
}

/* Spans the panel, but clip-path clips pointer events too — so only the
   pill is ever clickable, and the label lands centred inside it.
   z-index matters: .finale__frame has opacity < 1, which per spec paints
   it as though it were positioned at z-index 0 — later in the DOM, so
   without this it sits on top of the button and eats the click. */
.finale__seal {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: var(--pill-lift);
  height: var(--pill-h);
  display: grid;
  place-content: center;
  appearance: none;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.6875rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper);
  transition: letter-spacing 0.6s var(--ease-out-expo), color 0.4s ease;
}
.finale__seal:hover { letter-spacing: 0.34em; color: var(--gold); }
.finale__seal:focus-visible { outline: 1px solid var(--gold); outline-offset: 6px; }

/* A grid child, not an absolute one — so the panel's padding is what
   pushes it in, and the 20% overshoot is visible on this hairline.
   Its own box spans the whole panel, so it must stay transparent to
   the pointer: only what is inside it is ever meant to be hit, and
   those are hidden (visibility, via autoAlpha) until the room opens. */
.finale__frame {
  display: grid;
  place-content: center;
  justify-items: center;
  text-align: center;
  border: 1px solid rgba(236, 220, 184, 0.16);
  padding: clamp(1.5rem, 5vw, 4rem);
  opacity: 0;
  pointer-events: none;
}
.finale__frame a { pointer-events: auto; }

.finale__eyebrow {
  font-size: 0.625rem;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: rgba(236, 220, 184, 0.6);
  margin-bottom: clamp(1.25rem, 3vw, 2rem);
}
.finale__title {
  font-weight: 300;
  line-height: 1.05;
  font-size: clamp(2.25rem, 6vw, 5rem);
  letter-spacing: -0.02em;
}
.finale__title em { font-style: italic; color: var(--gold); }
.finale__note {
  margin: clamp(1.25rem, 3vw, 2rem) auto 0;
  max-width: 42ch;
  line-height: 1.75;
  font-size: clamp(0.875rem, 1.05vw, 1rem);
  color: rgba(247, 245, 243, 0.62);
}
.finale__actions {
  margin-top: clamp(2rem, 4vw, 3rem);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2rem;
  flex-wrap: wrap;
}

/* Button variants for black ground */
.btn--light { background: var(--paper); color: var(--ink); }
.btn--light::after { background: var(--gold); }
.btn-ghost--light { color: var(--paper); border-color: rgba(247, 245, 243, 0.28); }
.btn-ghost--light:hover { color: var(--gold); border-color: var(--gold); }

/* ---------- Keyframes ------------------------------------- */
@keyframes breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(1);    opacity: 1; }
  50%      { transform: translate(-50%, -50%) scale(1.12); opacity: 0.75; }
}
@keyframes float-loop {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(6px, -18px, 0); }
}
@keyframes cue {
  0%        { transform: translateY(-100%); }
  60%, 100% { transform: translateY(150%); }
}
@keyframes grain-shift {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-3%, 2%); }
  50%  { transform: translate(2%, -3%); }
  75%  { transform: translate(-2%, -2%); }
  100% { transform: translate(3%, 1%); }
}

/* ---------- Narrow / touch -------------------------------- */
/* The scrim only exists on narrow screens, where copy sits under the
   plate rather than beside it. */
.scrim { display: none; }

@media (max-width: 900px) {
  /* No room beside the case — the plate moves to the upper third
     (see render()) and every act stacks underneath it. */
  .act, .act--right {
    left: 0;
    right: auto;
    width: 100%;
    max-width: none;
    justify-content: flex-end;
    padding-bottom: 9vh;
    text-align: left;
    align-items: flex-start;
  }
  .scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg,
      rgba(247, 245, 243, 0) 38%,
      rgba(247, 245, 243, 0.82) 54%,
      rgba(247, 245, 243, 0.97) 66%);
  }
  /* The act is a column flex with flex-start alignment, so children size
     to their content — without this the CTA row can never wrap and just
     runs off the edge instead. */
  .cta-row, .spec, .subline, .lit { width: 100%; }
  .cta-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.25rem;
  }
  .lit { max-width: none; font-size: clamp(1.5rem, 6.5vw, 2.25rem); }
  .headline--sm { font-size: clamp(2rem, 8.5vw, 3rem); }
  .spec { gap: 0.9rem 1.5rem; margin-top: 1.5rem; }
  .spec dd { font-size: 1.25rem; }

  /* Collection: no room for a side-by-side, so the plate stacks over
     the copy and the left/right alternation falls away. */
  .piece {
    grid-template-columns: minmax(0, 1fr);
    align-content: center;
    gap: 2rem;
    padding: 10vh 1.5rem 8vh;
  }
  .piece--right .piece__plate { order: 0; }
  .piece--right .piece__copy  { order: 0; text-align: left; }
  .piece--right .piece__note  { margin-left: 0; }
  .piece--right .piece__meta  { justify-content: flex-start; }
  .piece__meta { gap: 1.25rem; }
  .act--right .eyebrow { justify-content: flex-start; }
  .act--right .subline, .act--right .lit { margin-left: 0; }
  .rail { display: none; }

  .finale__actions { flex-direction: column; gap: 1.25rem; }

  /* Two-up rather than auto-fit — a single column would make the grid
     three viewports tall and there would be nothing left to pin. */
  .cards__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .card__frame { max-height: 26svh; }
  .card__name { font-size: 1.125rem; margin-top: 0.8rem; }

  .footer__top { grid-template-columns: minmax(0, 1fr); }
  .footer__foot { flex-direction: column; align-items: flex-start; gap: 1rem; }

  /* Wordmark and links stack: side by side they need ~450px and there is no
     room for that beside a 0.42em-tracked wordmark. The bar carries no
     background (mix-blend-mode: difference), so the second row costs only
     the height of the type itself. */
  .masthead {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.4rem;
    padding-top: 1rem;
    padding-bottom: 0.85rem;
  }
  .masthead__nav {
    /* Scales with the screen so the row still fits a 320px phone, where the
       desktop 11px/0.28em/40px measured 321px wide against 312px of usable
       width. */
    gap: clamp(0.85rem, 4.5vw, 2rem);
    font-size: 9px;
    letter-spacing: 0.16em;
  }
}

@media (pointer: coarse) {
  .grain, .spark { display: none; }
  .glow { animation: none; }
}

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

/* Reduced motion: the film collapses into a static still and the three
   acts stack as ordinary prose — no scroll hijack, no content lost. */
body.is-static .scene { height: auto; }
body.is-static .scene__viewport { position: relative; height: auto; overflow: visible; }
body.is-static .layer { position: relative; inset: auto; }
/* Match the plate's own ratio so the still fills its box edge to edge
   instead of letterboxing inside an arbitrary height. */
body.is-static .depth-0 { aspect-ratio: 960 / 512; max-height: 78svh; }
body.is-static .depth-1,
body.is-static .depth-2,
body.is-static .depth-5,
body.is-static .scroll-cue,
body.is-static .rail { display: none; }
body.is-static .act,
body.is-static .act--right {
  position: relative;
  inset: auto;
  width: 100%;
  max-width: none;
  visibility: visible;
  opacity: 1;
  padding-block: clamp(3rem, 8vh, 6rem);
  text-align: left;
  align-items: flex-start;
}
body.is-static .act--right .eyebrow { justify-content: flex-start; }
/* The right-hand acts centre their own blocks with margin-left:auto —
   without this they stay pinned right while their text reads left. */
body.is-static .act--right .subline,
body.is-static .act--right .lit { margin-left: 0; }
body.is-static .lit span { opacity: 1; }

/* The smoke has nothing to say without motion — the layer collapses to a
   plain velvet band announcing the chapter, and the pieces stack as
   ordinary content below it. */
body.is-static .smoke { display: none; }
body.is-static .smoke-stage { overflow: visible; }
body.is-static .smoke__veil { position: absolute; opacity: 1; }
body.is-static .transition__title {
  position: relative;
  inset: auto;
  opacity: 1;
  padding-block: clamp(3rem, 10vh, 6rem);
}

body.is-static .collection { height: auto; margin-top: 0; }
body.is-static .collection__viewport { position: relative; height: auto; overflow: visible; }
body.is-static .piece {
  position: relative;
  inset: auto;
  opacity: 1;
  visibility: visible;
  padding-block: clamp(3rem, 8vh, 5rem);
}

/* The finale has nowhere to open to without scroll — it arrives already
   opened, and the seal it grew out of is redundant. */
body.is-static .cards__stage { min-height: 0; display: block; }
body.is-static .cards__inner { display: block; }
body.is-static .finale { height: auto; }
body.is-static .finale__panel {
  position: relative;
  inset: auto;
  width: auto;
  height: auto;
  clip-path: none;
  --pad: 1;
  min-height: 70svh;
}
body.is-static .finale__seal { display: none; }
body.is-static .finale__frame { opacity: 1; }
