/* ═══════════════════════════════════════════════════════════════════════════
   SCENES. The loud layer.

   The rule that makes reduced motion free instead of a second build:
   THE END STATE IS THE DEFAULT. Everything below renders finished. The start
   states live under .js-motion and nowhere else, so a browser that never runs
   JS, and a visitor who asked for less motion, both get the finished page by
   doing nothing at all.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The sections are transparent and the page colour lives on <body>, so the
   fixed stage can show through from behind while the text still sits in front
   of it. An opaque section here would hide the stage completely; a stage above
   the sections would bury the words. This is the only ordering that gives both. */
.scene {
  position: relative;
  z-index: 3;
  color: var(--ink);
  padding-block: var(--sec-y);
}
.scene--tall { padding-block: calc(var(--sec-y) * 1.35); }

/* the one exception. the dossier is opaque, because it has no stage art and
   is meant to read as a sheet of paper laid over everything else. */
.scene--paper { background: var(--bg); }

.scene__in {
  position: relative;
  display: grid;
  gap: 1.35rem;
  justify-items: start;
  /* so the headline can be sized against its own column instead of the
     viewport. German runs a quarter longer and would otherwise overflow. */
  container-type: inline-size;
}
.scene__in--doc { gap: 1.1rem; }

.scene--hero {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-top: 96px;
}
.scene--hero .scene__in { gap: 1.6rem; }

.cta { display: flex; flex-wrap: wrap; gap: 0.7rem; }

.kicker-link { font-size: var(--fs-small); color: var(--ink-3); }
.kicker-link a {
  /* the underline takes the TEXT's colour at a third strength, not the fixed
     hairline: over the bright cloud bank the dim text all but vanished while
     a --line-2 border stayed crisp, and a floating dash in the middle of the
     sky reads as a rendering bug, not a link */
  border-bottom: 1px solid color-mix(in srgb, currentColor 35%, transparent);
  padding-bottom: 1px;
  transition: color var(--t-nav) var(--ease-out), border-color var(--t-nav) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .kicker-link a:hover { color: var(--accent-ink); border-color: var(--accent); }
}

/* one oversized number, used twice on the page */
.figure { display: flex; align-items: baseline; gap: 0.85rem; flex-wrap: wrap; }
.figure__n {
  font-family: var(--font-display);
  font-size: clamp(3rem, 7vw, 5rem);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--accent-ink);
  font-variant-numeric: tabular-nums;
}
.figure__cap { color: var(--ink-2); max-width: 24ch; font-size: var(--fs-small); }

/* ═══════════════════════════  MOTION START STATES  ═══════════════════════
   Only ever applied when JS ran and the visitor did not ask for less motion.
   ═══════════════════════════════════════════════════════════════════════ */

.js-motion [data-rise] {
  opacity: 0;
  transform: translateY(18px);
}
.js-motion [data-rise].is-in {
  opacity: 1;
  transform: none;
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
}

/* Headline lines uncover from their own box. Percentage translate, so it is
   correct at any font size and in any language. */
[data-lines] .ln { display: block; overflow: hidden; padding-bottom: 0.06em; }
[data-lines] .ln > span { display: block; }
.js-motion [data-lines] .ln > span { transform: translateY(105%); }
.js-motion [data-lines].is-in .ln > span {
  transform: none;
  transition: transform var(--t-line) var(--ease-out);
}
.js-motion [data-lines].is-in .ln:nth-child(2) > span { transition-delay: var(--stag-line); }
.js-motion [data-lines].is-in .ln:nth-child(3) > span { transition-delay: calc(var(--stag-line) * 2); }

/* ═══════════════════════════  MEDIA AND STILLS  ═══════════════════════════
   The stills are what phones, reduced motion and a JS failure see. They are
   not a downgrade: they carry the same information the stage carries, just
   without the camera.
   ═══════════════════════════════════════════════════════════════════════ */

.media { position: relative; overflow: hidden; border-radius: var(--r-md); background: var(--bg-3); }
.media img { width: 100%; height: 100%; object-fit: cover; }
.media--frame { width: min(232px, 42vw); aspect-ratio: 9 / 16; border: 1px solid var(--line-2); }
.media--card  { width: 100%; aspect-ratio: 9 / 16; }

.still { width: 100%; margin-top: 0.75rem; }
.ctx-cinema .still { display: none; }

/* S0 still: two feeds side by side */
.still--feeds { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; max-width: 460px; }
.feedcol figcaption { color: var(--ink-3); margin-bottom: 0.5rem; }
.feedcol__slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; }
.feedcol__slots > i { display: block; aspect-ratio: 9 / 16; border-radius: 3px; }

/* S1 still: the edit tracks under the frame */
.tracks { display: grid; gap: 5px; width: min(300px, 60vw); margin-top: 0.9rem; }
.tracks li { height: 7px; border-radius: 3px; background: var(--surface-2); }
.tracks li:nth-child(1) { width: 100%; background: var(--accent-soft); }
.tracks li:nth-child(2) { width: 72%; }
.tracks li:nth-child(3) { width: 88%; }
.tracks li:nth-child(4) { width: 46%; }

/* S2 still: the workflow node */
.node {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--surface);
  max-width: 420px;
}
.node__port { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); flex: none; }
.node__body { display: grid; gap: 0.2rem; }
.node__name { color: var(--ink); font-weight: 600; letter-spacing: 0.08em; }
.node__meta { color: var(--ink-3); }

/* S3 still: a reduced lattice, plus the number in words */
.still--lattice {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 6px;
  max-width: 520px;
}
.still--lattice i {
  display: block;
  aspect-ratio: 9 / 16;
  border-radius: 3px;
  background: var(--surface-2);
}
.still--lattice i[data-state="posted"] { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent); }

/* S4 still: the schedule lane as a readable list of jobs */
.lane { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; max-width: 560px; }
.lane__job {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 1rem;
  padding: 0.7rem 0.95rem;
  background: var(--bg-2);
  color: var(--ink-3);
}
.lane__job span:first-child { color: var(--ink-2); }
.lane__job span:last-child { color: var(--accent-ink); }

/* S5 still: the phone */
.phone {
  width: min(228px, 56vw);
  padding: 9px;
  border: 1px solid var(--line-2);
  border-radius: 30px;
  background: var(--bg-2);
  box-shadow: var(--shadow-s);
}
.phone__screen { border-radius: 22px; overflow: hidden; background: var(--bg-3); }
.phone__screen .media video { width: 100%; height: 100%; object-fit: cover; display: block; }
.phone__acts { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; padding: 0.7rem; }
.pill {
  display: flex; align-items: center; justify-content: center; gap: 0.3rem;
  padding: 0.55rem 0;
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
}
.pill svg {
  width: 11px; height: 11px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}
.pill--no  { background: var(--no-soft); box-shadow: inset 0 0 0 1px var(--no); color: var(--no-ink); }
.pill--yes { background: var(--yes); color: #08150E; }

/* S6 still: the reasons */
.reasons { display: flex; flex-wrap: wrap; gap: 0.5rem; max-width: 520px; }

/* S7 still: the rules, with real text on every bar */
.rules { display: grid; gap: 0.5rem; max-width: 640px; width: 100%; }
.rule {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr) auto;
  align-items: center;
  gap: 0.9rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--line);
  border-left: 2px solid var(--accent);
  border-radius: var(--r-sm);
  background: var(--surface);
  font-size: var(--fs-small);
}
.rule__from { color: var(--ink-3); text-decoration: line-through; text-decoration-color: var(--ink-3); }
.rule__to { color: var(--ink); font-weight: 500; }
.rule__w { color: var(--accent-ink); white-space: nowrap; }
.still--rules { display: grid; gap: 1.4rem; }

/* S8 still: day one against month six, compared by size and nothing else */
.still--months { display: flex; align-items: flex-end; gap: 2.5rem; }
.mgroup { display: grid; gap: 0.6rem; justify-items: center; }
.mgroup__label { color: var(--ink-3); }
.mgroup__box { display: block; border-radius: var(--r-sm); border: 1px solid var(--line-2); }
.mgroup__box--small { width: 46px; height: 46px; background: var(--surface); }
.mgroup__box--big { width: 156px; height: 156px; background: var(--accent-soft); border-color: var(--accent); }

/* ═══════════════════════════  S9 · THE DOSSIER  ═══════════════════════════
   The only light section and the only one that never moves. After eight
   scenes of camera, stillness is the device.
   ═══════════════════════════════════════════════════════════════════════ */

.scene--paper .h2 { max-width: 22ch; }

.doc {
  width: 100%;
  margin-top: 0.75rem;
  border-top: 1px solid var(--line);
}
.doc__row {
  display: grid;
  grid-template-columns: minmax(190px, 0.42fr) 1fr;
  gap: 1.5rem;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--line);
}
.doc__row dt {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-2);
  padding-top: 0.2rem;
}
.doc__row dd { color: var(--ink); max-width: 62ch; }

.doc__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 1.25rem; margin-top: 1.75rem; }
.doc__back {
  font-size: var(--fs-small);
  color: var(--ink-2);
  border-bottom: 1px solid var(--line-2);
  padding-bottom: 1px;
}
@media (hover: hover) and (pointer: fine) {
  .doc__back:hover { color: var(--ink); }
}

@media (max-width: 720px) {
  .doc__row { grid-template-columns: 1fr; gap: 0.4rem; }
}

/* ═══════════════════════════  S10 · QUIZ, FORK, CLOSE  ═══════════════════ */

.quiz { display: grid; gap: 1.4rem; width: 100%; margin-top: 0.6rem; }
.quiz__q { display: grid; gap: 0.65rem; }
.quiz__q legend {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--ink);
  margin-bottom: 0.15rem;
}
.quiz__opts { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* only on the question that takes several answers, so its presence is the
   signal. A hint under every question would say nothing. */
.quiz__hint {
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  margin-top: -0.3rem;
}

.fork {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  width: 100%;
  margin-top: 1.5rem;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.fork__side { padding: 1.5rem 1.4rem; background: var(--bg-2); display: grid; gap: 0.7rem; align-content: start; }
.fork__side--loud { background: var(--bg-3); }
.fork__label { color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.12em; }
.fork__side--loud .fork__label { color: var(--accent-ink); }
.fork__line { font-size: 1.0625rem; color: var(--ink-2); text-wrap: pretty; }
.fork__side--loud .fork__line { color: var(--ink); font-weight: 500; }
.fork__side--loud .fork__line b { color: var(--accent-ink); font-weight: 600; }

@media (max-width: 640px) { .fork { grid-template-columns: 1fr; } }

.window {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 1.1rem;
  width: 100%;
  margin-top: 2rem;
  padding: 0.9rem 1.2rem;
  border: 1px solid var(--accent);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
}
.window__line { font-size: var(--fs-small); font-weight: 500; }
.window__clock { color: var(--accent-ink); font-weight: 600; margin-left: auto; }

.plans {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  width: 100%;
  margin-top: 1.5rem;
}
.plan {
  display: grid;
  gap: 0.6rem;
  align-content: start;
  padding: 1.6rem 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  position: relative;
  transition: border-color var(--t-panel) var(--ease-out), background-color var(--t-panel) var(--ease-out);
}
.plan--hero { border-color: var(--line-2); background: var(--bg-2); }
.plan.is-picked { border-color: var(--accent); background: var(--accent-soft); }
.plan__badge {
  position: absolute;
  top: -0.7rem; left: 1.4rem;
  padding: 0.24rem 0.7rem;
  border-radius: var(--r-pill);
  /* the lit-surface recipe, same as the primary button: a gradient down the
     face, a specular hairline along the top edge, a bloom underneath. One
     flat hex on a lit pill is the tell this site does not allow itself. */
  background: linear-gradient(180deg, var(--accent-2), var(--accent) 55%, color-mix(in srgb, var(--accent) 86%, #000));
  color: var(--on-accent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(0, 0, 0, 0.18),
    0 4px 14px rgba(0, 0, 0, 0.40),
    0 0 0 0.5px rgba(0, 0, 0, 0.25);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.plan__name { color: var(--ink); }
.plan__price { display: flex; align-items: baseline; gap: 0.2rem; }
.plan__n {
  font-family: var(--font-display);
  font-size: 2.6rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.plan__cur { font-family: var(--font-display); font-size: 1.4rem; font-weight: 600; }
.plan__per { color: var(--ink-3); font-size: var(--fs-small); margin-left: 0.35rem; }
.plan__line { color: var(--ink-2); font-size: var(--fs-small); min-height: 2.8em; }
.plan .btn { margin-top: 0.4rem; }

@media (max-width: 640px) { .plans { grid-template-columns: 1fr; } }

.faq {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.4rem 2.5rem;
  width: 100%;
  margin-top: 2.75rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
}
.qa { display: grid; gap: 0.4rem; align-content: start; }
.qa h3 { font-size: 1rem; }
.qa p { color: var(--ink-2); font-size: var(--fs-small); }

@media (max-width: 720px) { .faq { grid-template-columns: 1fr; } }

.close {
  margin-top: 2.5rem;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.4vw, 1.75rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.25;
  max-width: 30ch;
  text-wrap: balance;
}

/* ═══════════════════════════  THE PROGRESS RAIL  ═══════════════════════════
   A plain progress bar for the first third of the page. At the schedule scene
   it quietly acquires tick marks and becomes the publishing calendar, and the
   ticks behind the visitor read as published. Two elements, one clipped fill.
   ═══════════════════════════════════════════════════════════════════════════ */

.rail {
  position: fixed;
  top: 0; right: 0;
  z-index: 115;
  width: 2px; height: 100%;
  background: var(--line);
  opacity: 0;
  transition: opacity var(--t-panel) var(--ease-out);
}
.ctx-cinema .rail, .ctx-panels .rail { opacity: 1; }
.rail__fill {
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform-origin: top;
  transform: scaleY(0);
}
.rail__ticks {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 500ms var(--ease-out);
}
.rail.is-calendar .rail__ticks { opacity: 1; }
.rail__ticks i {
  position: absolute;
  right: 0;
  width: 7px; height: 2px;
  background: var(--ink-3);
  transform: translateX(2px);
}

/* ═══════════════════════════  TELEMETRY  ═══════════════════════════════════
   Arrives with the schedule scene and stays. It reads out the numbers the
   story is generating, and the last one is the only number on the page that
   goes down.
   ═══════════════════════════════════════════════════════════════════════════ */

.tele {
  position: fixed;
  right: calc(var(--gutter) + 14px);
  bottom: 26px;
  z-index: 116;
  width: 224px;
  padding: 0.85rem 1rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--glass);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  backdrop-filter: blur(16px) saturate(160%);
  box-shadow: var(--shadow-s);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--t-panel) var(--ease-out), transform var(--t-panel) var(--ease-out);
}
.tele.is-in { opacity: 1; transform: none; }
.tele__jump {
  display: block;
  margin-bottom: 0.6rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent-ink);
}
/* one quiet line that says whom the widget is for, so a normal reader knows
   they may ignore it instead of wondering what "Peak : off peak" wants */
.tele__cap {
  font-size: 0.5625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0.15rem 0 0.45rem;
}
.tele__list { display: grid; gap: 0.38rem; }
.tele__row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.tele__row dt { font-size: 0.6875rem; color: var(--ink-3); }
.tele__row dd { color: var(--ink); font-weight: 500; }
.tele__row--down dd { color: var(--accent-ink); }

/* Under panels the rail becomes a strip at the bottom with one metric.
   Never on a timer: it changes when the story changes. */
@media (max-width: 1023px), (max-height: 619px) {
  .tele {
    right: 0; left: 0; bottom: 0;
    width: 100%;
    border-radius: 0;
    border-left: 0; border-right: 0; border-bottom: 0;
    padding: 0.5rem var(--gutter);
    display: flex; align-items: center; gap: 1rem;
  }
  .tele__jump { margin: 0; padding: 0; border: 0; }
  .tele__list { display: flex; margin-left: auto; }
  .tele__row:not(.is-shown) { display: none; }
}

/* ═══════════════════════════  THE STAGE  ═══════════════════════════════════
   One fixed layer. The protagonist never leaves it, which is why the page
   reads as a single camera move rather than eleven separate sections. There
   are no GSAP pins anywhere on this site: native scrolling is untouched, so
   there is no pin seam to jump and no momentum wrapper to fight iOS with.
   ═══════════════════════════════════════════════════════════════════════════ */

.stage {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  display: none;
  contain: layout paint;
}
.ctx-cinema .stage { display: block; }

/* Painted over the acts, under the text. It guarantees the reading column
   stays legible no matter what the stage is doing behind it, and it gives the
   whole thing depth: the machine is always further away than the words. */
.stage__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(97deg,
    var(--bg) 0%,
    var(--bg) 32%,
    color-mix(in srgb, var(--bg) 62%, transparent) 46%,
    transparent 60%);
}

/* No transform here on purpose. GSAP owns it through xPercent and yPercent, and
   a transform declared in CSS as well would be parsed and fought over. */
/* visibility as well as opacity: a prop at opacity 0 still exists for the
   compositor, and on some GPUs a big masked, 3D-transformed, will-changed
   layer shows its tile seams as a faint grid even at zero - the rack did
   exactly that over the "Freeze it" scene. Hidden props paint nothing.
   The timelines drive autoAlpha, which flips visibility at exactly 0. */
.act {
  position: absolute;
  top: 50%; left: calc(50% + 16vw);
  opacity: 0;
  visibility: hidden;
  /* NO standing will-change here, and this line is load-bearing: a promoted
     layer keeps a GPU texture even while hidden, and on some GPUs that
     texture leaks through as a ghost - the rack showed as a faint grid over
     the "Freeze it" scene with opacity 0 AND visibility hidden. GSAP promotes
     layers by itself for the duration of a tween, which is the only time a
     prop needs one. */
}

/* The protagonist. No fill: at the start it is an empty slot, and an empty
   slot with a background is just a panel. */
.act-frame {
  width: min(21vw, 290px);
  aspect-ratio: 9 / 16;
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  overflow: hidden;
}
.act-frame__media { position: absolute; inset: 0; }
.act-frame__media img,
.act-frame__media video { width: 100%; height: 100%; object-fit: cover; }

/* ── the capture ───────────────────────────────────────────────────────────
   A bar runs down the clip and a wash follows it, clipped from the top, so
   above the line the picture is drained and below it the video is still
   playing. That split is the whole reason the scene reads as a capture
   instead of as a dissolve: for a second and a half you can see both states
   of the same frame at once.
   ───────────────────────────────────────────────────────────────────────── */
.act-frame__wash {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--bg) 78%, transparent),
    color-mix(in srgb, var(--bg) 62%, transparent));
  -webkit-backdrop-filter: grayscale(1) contrast(0.8);
  backdrop-filter: grayscale(1) contrast(0.8);
  clip-path: inset(0 0 100% 0);
}
.act-frame__scan {
  position: absolute;
  left: -4%; right: -4%;
  top: -2%;
  height: 2px;
  opacity: 0;
  background: linear-gradient(90deg, transparent, var(--accent-2), transparent);
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* ── the emitter ───────────────────────────────────────────────────────────
   A wire out of the port, a charge along it, three finished clips stacking up
   at the far end. Positioned against the same centre point as the node, so it
   grows out of the node's own right edge whatever the node's width resolves
   to.
   ───────────────────────────────────────────────────────────────────────── */
.act-out {
  /* Positioned here rather than by inheriting .act, because .act centres its
     children on a point and this one has to start at the node's right edge
     and grow rightwards. The vertical centring is a GSAP yPercent, set in
     build(), so it composes with the tweens instead of being overwritten by
     the first one that touches transform. */
  position: absolute;
  top: 50%;
  left: calc(50% + 16vw + min(13.5vw, 183px) + 14px);
  width: 210px;
  opacity: 0;
  visibility: hidden;
  /* same ghost-layer rule as .act: no standing will-change */
}
.act-out__wire {
  position: absolute;
  left: 0; top: 50%;
  width: 116px; height: 1px;
  margin-top: -0.5px;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 25%, transparent));
  transform-origin: left center;
}
.act-out__spark {
  position: absolute;
  left: 0; top: 50%;
  width: 6px; height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--accent-2);
  box-shadow: 0 0 12px 3px color-mix(in srgb, var(--accent) 70%, transparent);
}
.act-out__stack { position: absolute; left: 116px; top: 50%; }
.act-out__card {
  position: absolute;
  top: 0; left: calc(var(--i) * 22px);
  width: 54px;
  aspect-ratio: 9 / 14;
  transform: translateY(-50%) translateY(calc(var(--i) * -9px));
  border-radius: 4px;
  overflow: hidden;
  background: var(--bg-3);
  box-shadow: 0 0 0 1px var(--line-2), 0 12px 26px rgba(0, 0, 0, 0.6);
  z-index: calc(3 - var(--i));
}
.act-out__card img { width: 100%; height: 100%; object-fit: cover; opacity: 0.85; }


/* ── the wall of output ────────────────────────────────────────────────────
   A bounded panel, not a full bleed. Six columns and five rows on the stage's
   own half, so it never passes under the reading column and never ends in a
   ragged half tile at the screen edge.

   Every cell is identical. The version before this lit one tile in nine with
   an accent outline and a brighter image, and that single exception was enough
   to turn a quantity into a collage: the eye stops reading "a lot" and starts
   counting the special ones.

   The mask is what keeps it from looking cropped. The block is taller than a
   short laptop viewport, and a hard cut at the top and bottom row reads as a
   mistake, while a fade reads as more of the same continuing past the frame.
   ───────────────────────────────────────────────────────────────────────── */
.act-lattice {
  width: min(34vw, 470px);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  padding: 26px;
  /* the light behind the rack. The grid gaps let it through, which is what
     makes the tiles read as standing in front of something. */
  background: radial-gradient(58% 44% at 42% 40%,
    color-mix(in srgb, var(--accent) 16%, transparent) 0%, transparent 72%);
  -webkit-mask-image: radial-gradient(78% 62% at 42% 46%, #000 52%, transparent 100%);
  mask-image: radial-gradient(78% 62% at 42% 46%, #000 52%, transparent 100%);
}
.act-lattice i {
  display: block;
  position: relative;
  aspect-ratio: 9 / 14;
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg-3);
  /* a gold hairline along the top edge only, the way light lands on a shelf */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, var(--accent) 42%, transparent),
    inset 0 0 0 1px var(--line),
    0 10px 24px rgba(0, 0, 0, 0.55);
  opacity: 0;
  /* no standing will-change on twenty tiles: GSAP promotes them while the
     group tweens actually run, and twenty permanent layers under a mask is
     exactly the stack that leaves seams on some GPUs */
}
.act-lattice i img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* held back, but not to the point of being grey mush. At 0.42 this was a
     contact sheet of thumbnails nobody could make out. */
  opacity: 0.66;
}

/* the competitors, drifting behind the hero */
.act-feeds {
  left: 50%;
  width: 104vw;
  display: grid;
  grid-template-columns: repeat(22, 1fr);
  gap: 6px;
}
.act-feeds i {
  display: block;
  aspect-ratio: 9 / 16;
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px var(--line);
}
.act-feeds i[data-full="1"] { background: var(--surface-2); }

/* ══════════════════════════  S4 · THE CALENDAR  ═════════════════════════
   Drawn as the application it would be, not as a row of chips in space.

   What was here before carried the same information and looked like a debug
   view: a bare track of day columns on a hairline, no frame, no controls, and
   one colour for everything, so four different workflows were
   indistinguishable from each other.

   Three changes, and all three were asked for. A window, so it reads as
   software. Controls, so it reads as software somebody uses: a view switcher,
   two arrows, Today, and a primary action. None of them do anything, none of
   them are focusable, and that is deliberate. And four colours, one per
   workflow, because telling them apart at a glance is the entire point of
   colour-coding a calendar and it cannot be done in four shades of one gold.
   ═══════════════════════════════════════════════════════════════════════ */

/* Bigger, and every type size up with it. The window was drawn at the size a
   real one would be on a laptop and then shown at half that: correct, and
   unreadable. Nothing here is scaled down any more, so what is on screen is
   what was drawn. */
.act-cal {
  width: min(58vw, 840px);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: linear-gradient(180deg, var(--bg-2), var(--bg));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.05),
    0 0 0 1px var(--line-2),
    var(--shadow);
}

/* ── the title bar ── */
.act-cal__bar {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.72rem 0.95rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.act-cal__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  flex: none;
}
.act-cal__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
}

.act-cal__seg {
  display: flex;
  gap: 1px;
  margin-left: 0.4rem;
  padding: 2px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
}
.act-cal__segb {
  padding: 0.26rem 0.72rem;
  border-radius: var(--r-pill);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--ink-3);
}
.act-cal__segb.is-on { background: var(--bg-3); color: var(--ink); box-shadow: 0 1px 3px rgba(0,0,0,.4); }

.act-cal__ctrl { display: flex; align-items: center; gap: 0.35rem; margin-left: auto; }
.act-cal__nav { display: flex; gap: 2px; }
/* two chevrons, drawn with one rotated corner each. No icon font, no svg. */
.act-cal__nav i {
  display: grid;
  place-items: center;
  width: 26px; height: 26px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
}
.act-cal__nav i::before {
  content: "";
  width: 5px; height: 5px;
  border-left: 1.4px solid var(--ink-2);
  border-bottom: 1.4px solid var(--ink-2);
  transform: rotate(45deg);
  margin-left: 2px;
}
.act-cal__nav i:last-child::before { transform: rotate(-135deg); margin: 0 2px 0 0; }

.act-cal__btn {
  padding: 0.34rem 0.75rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--ink-2);
}
.act-cal__btn--cta {
  border: 0;
  background: var(--metal);
  color: var(--on-accent);
  box-shadow: inset 0 1px 0 rgba(255, 252, 240, 0.5), 0 3px 10px rgba(0, 0, 0, 0.4);
}

/* ── the grid ── */
.act-cal__body { overflow: hidden; padding: 0.55rem 0 0.7rem; }
/* no standing will-change: a promoted child inside a hidden prop still owns a
   GPU texture, and that is the ghost-grid recipe (see the .act comment) */
.act-cal__track { display: flex; align-items: stretch; }
.act-cal__day {
  flex: none;
  width: 158px;
  display: grid;
  gap: 5px;
  align-content: start;
  padding: 0.6rem 0.55rem 0.85rem;
  border-left: 1px solid var(--line);
}
.act-cal__day[data-weekend="1"] { background: rgba(255, 255, 255, 0.014); }
.act-cal__head { display: flex; align-items: baseline; gap: 0.35rem; margin-bottom: 0.25rem; }
.act-cal__dow { color: var(--ink-3); letter-spacing: 0.14em; font-size: 0.625rem; }
.act-cal__num { font-family: var(--font-display); font-weight: 600; font-size: 0.9375rem; color: var(--ink-2); }
.act-cal__day[data-peak="1"] .act-cal__num { color: var(--ink); }

/* the event. A colour bar down the leading edge, the tint of the same colour
   behind it, and the time in that colour: three uses of one hue is what makes
   a chip read as belonging to a category rather than as being decorated. */
.act-cal__ev {
  position: relative;
  display: grid;
  gap: 0.1rem;
  padding: 0.4rem 0.55rem 0.44rem 0.72rem;
  border-radius: 6px;
  background: color-mix(in srgb, var(--c) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 30%, transparent);
}
.act-cal__ev::before {
  content: "";
  position: absolute;
  left: 0; top: 4px; bottom: 4px;
  width: 2px;
  border-radius: 2px;
  background: var(--c);
}
.act-cal__ev[data-c="1"] { --c: var(--cal-1); }
.act-cal__ev[data-c="2"] { --c: var(--cal-2); }
.act-cal__ev[data-c="3"] { --c: var(--cal-3); }
.act-cal__ev[data-c="4"] { --c: var(--cal-4); }
.act-cal__evt { color: var(--c); font-size: 0.625rem; font-weight: 600; }
.act-cal__evn { color: var(--ink-2); font-size: 0.6875rem; font-family: var(--font-mono); }

/* ── the phone ─────────────────────────────────────────────────────────────
   Built to look like the app, not like a picture of a phone. Every element
   here is one a real approval screen would need, which is why it reads as
   something you could actually tap.
   ───────────────────────────────────────────────────────────────────────── */
.act-phone { width: min(20vw, 268px); }
.act-phone__shell {
  position: relative;
  display: grid;
  gap: 0;
  padding: 10px 10px 12px;
  border-radius: 34px;
  background: #060607;
  box-shadow:
    0 0 0 1.5px rgba(255, 240, 246, 0.16),
    0 0 0 7px rgba(255, 240, 246, 0.035),
    0 40px 90px rgba(0, 0, 0, 0.72);
}
.act-phone__island {
  position: absolute;
  top: 15px; left: 50%;
  width: 62px; height: 17px;
  transform: translateX(-50%);
  border-radius: var(--r-pill);
  background: #000;
  z-index: 3;
}
.act-phone__bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.45rem 0.7rem 0.5rem;
  color: var(--ink-2);
  font-size: 0.5625rem;
}
.act-phone__sig { display: flex; align-items: flex-end; gap: 2px; }
.act-phone__sig i { display: block; width: 3px; background: var(--ink-2); border-radius: 1px; }
.act-phone__sig i:nth-child(1) { height: 4px; }
.act-phone__sig i:nth-child(2) { height: 6px; }
.act-phone__sig i:nth-child(3) { height: 8px; }

.act-phone__head {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.15rem 0.6rem 0.6rem;
}
.act-phone__app {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.8125rem;
  letter-spacing: -0.02em;
}
.act-phone__sub { display: block; color: var(--ink-3); font-size: 0.5625rem; }
.act-phone__badge {
  margin-left: auto;
  min-width: 20px;
  padding: 0.12rem 0.35rem;
  border-radius: var(--r-pill);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 0.5625rem;
  font-weight: 700;
  text-align: center;
}

.act-phone__screen {
  position: relative;
  border-radius: 18px;
  overflow: hidden;
  background: var(--bg-3);
  aspect-ratio: 9 / 13;
}
.act-card { position: absolute; inset: 0; }
.act-card img,
.act-card video { width: 100%; height: 100%; object-fit: cover; display: block; }
.act-card__meta {
  position: absolute; inset: auto 0 0 0;
  padding: 1.6rem 0.6rem 0.5rem;
  background: linear-gradient(to top, rgba(4, 4, 5, 0.9), transparent);
  color: var(--ink-2);
  font-size: 0.5625rem;
}
.act-stamp {
  position: absolute; top: 8px; right: 8px;
  padding: 0.24rem 0.5rem;
  border-radius: var(--r-pill);
  background: rgba(10, 10, 12, 0.78);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #FFFFFF;
  font-size: 0.5625rem; font-weight: 600; line-height: 1.25;
  white-space: nowrap;
  opacity: 0;
}

/* ── the verdict ───────────────────────────────────────────────────────────
   Two actions, and the visitor must be able to tell which is which without
   reading. Red rejects, green approves, each with a glyph, and both filled
   rather than one filled and one outlined: an outlined button next to a solid
   one reads as "secondary", and rejecting is not the secondary action here,
   it is half the product.

   These two are the only saturated colours on the site, which is exactly why
   they work: on a black and champagne page a green and a red are unmistakably
   controls and could not be mistaken for decoration.
   ───────────────────────────────────────────────────────────────────────── */
.act-verdict { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; padding: 0.65rem 0 0; }
.act-verdict__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.32rem;
  padding: 0.58rem 0;
  border-radius: var(--r-pill);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  cursor: default;
}
.act-verdict__btn svg {
  width: 11px; height: 11px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.act-verdict__btn--no {
  background: var(--no-soft);
  box-shadow: inset 0 0 0 1px var(--no);
  color: var(--no-ink);
}
.act-verdict__btn--yes { background: var(--yes); color: #08150E; }

.act-ask {
  position: absolute;
  inset: auto 10px 12px 10px;
  padding: 0.7rem 0.7rem 0.75rem;
  border-radius: 18px;
  background: #0F0F11;
  box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--line);
}
.act-ask__q { font-size: 0.6875rem; font-weight: 600; color: var(--ink); margin-bottom: 0.45rem; }
.act-ask__chips { display: flex; flex-wrap: wrap; gap: 5px; }
.act-ask__chip {
  padding: 0.28rem 0.5rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  font-size: 0.5625rem;
  line-height: 1.3;
  color: var(--ink-2);
}
.act-ask__chip[data-picked="1"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--ink);
}

/* ── the workflow node ─────────────────────────────────────────────────────
   The hinge of the page. It is a saved object in a tool, so it is drawn like
   one: a name, a live state, what it runs on, where it publishes, and the
   line that says a human still has to say yes.
   ───────────────────────────────────────────────────────────────────────── */
.act-node {
  width: min(27vw, 366px);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: linear-gradient(180deg, var(--bg-2), var(--bg));
  box-shadow: 0 0 0 1px rgba(255, 95, 61, 0.14), var(--shadow);
  /* overflow stays visible so the caption can hang below the card; the
     head and foot carry their own corner radii instead */
}
.act-node__head {
  border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.8rem 0.95rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.act-node__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
  flex: none;
}
.act-node__name { color: var(--ink); font-weight: 600; letter-spacing: 0.09em; font-size: 0.6875rem; }
.act-node__state {
  margin-left: auto;
  padding: 0.14rem 0.45rem;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: 0.5625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.act-node__body { display: grid; gap: 0.42rem; padding: 0.85rem 0.95rem; }
.act-node__row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.act-node__row > span:first-child { color: var(--ink-3); font-size: 0.75rem; }
.act-node__row > span:last-child { color: var(--ink); font-size: 0.6875rem; }
.act-node__foot {
  border-radius: 0 0 calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px);
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.6rem 0.95rem 0.75rem;
  border-top: 1px solid var(--line);
}
.act-node__chan { display: flex; gap: 4px; }
.act-node__chan span {
  padding: 0.16rem 0.34rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-3);
  font-size: 0.5625rem;
}
.act-node__gate { margin-left: auto; color: var(--accent-ink); font-size: 0.5625rem; }
.act-node__port {
  position: absolute;
  top: 50%; right: -5px;
  width: 10px; height: 10px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--accent);
  /* the soft ring every other accent dot carries; the ports were the odd ones out */
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* the memory */
.act-rules { width: min(32vw, 430px); display: grid; gap: 7px; }
.act-rules .rule {
  grid-template-columns: 1fr;
  gap: 0.28rem;
  padding: 0.7rem 0.85rem;
  font-size: 0.8125rem;
  background: var(--bg-2);
  box-shadow: var(--shadow-s);
}
.act-rules .rule__w { justify-self: end; margin-top: 0.1rem; }

/* the months */
.act-months { display: flex; align-items: flex-end; gap: 2rem; }

/* Below the widest desktop the reading column takes a larger share of the
   screen, so the cast moves further out of its way. Measured, not guessed:
   at 1024 the memory stack and the hero frame were the two that touched. */
@media (max-width: 1279px) {
  .act { left: calc(50% + 19vw); }
  /* the lane is the one act that is still full bleed, so it stays centred.
     The wall is a bounded panel now and travels with the rest of the cast. */
  .act-feeds { left: 50%; }
  .act-out { left: calc(50% + 19vw + min(13.5vw, 183px) + 14px); }
  .act-cal { width: min(60vw, 620px); }
  .act-cal__day { width: 138px; }
  .act-lattice { width: min(38vw, 400px); gap: 10px; padding: 18px; }
}

/* The question the phone asks after a rejection. Sits in the same screen the
   card just left, so the beat lands where the copy says it does. */
.act-ask {
  position: absolute;
  inset: auto 0 0 0;
  padding: 1rem 0.9rem 4.2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  background: linear-gradient(to top, var(--bg-2) 62%, transparent);
}
.act-ask__q {
  width: 100%;
  margin-bottom: 0.35rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}
.act-ask__chip {
  padding: 0.34rem 0.62rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  font-size: 0.6875rem;
  line-height: 1.3;
  color: var(--ink-2);
}
.act-ask__chip[data-picked="1"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--ink);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE BACKDROP, THE 3D DECK, AND THE GALLERY
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the backdrop ──────────────────────────────────────────────────────────
   A CSS gradient is what everyone gets. The canvas on top of it only becomes
   visible once WebGL has actually compiled and drawn, so a failed context or a
   blocked GPU leaves the page looking finished rather than black.
   ───────────────────────────────────────────────────────────────────────── */
.bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* what a browser without WebGL gets: the same composition, still. Two
     clouds and a core, no stars, because a static starfield is a texture. */
  background:
    radial-gradient(72% 52% at 78% 22%, #1B1830 0%, transparent 62%),
    radial-gradient(110% 80% at 96% 76%, #101020 0%, transparent 64%),
    radial-gradient(60% 40% at 20% 90%, #0A0A12 0%, transparent 70%),
    var(--bg);
}
.bg__gl {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  transition: opacity 900ms var(--ease-out);
}
.bg__gl.is-live { opacity: 1; }

/* Grain over everything. It is what stops a big smooth gradient from banding
   on an 8 bit display, and it is most of why the page does not read as flat. */
.bg__grain {
  position: absolute;
  inset: -50%;
  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='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* the paper section is opaque, so it needs its own grain or it looks plastic */
.scene--paper { position: relative; }
.scene--paper::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  opacity: 0.035;
  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.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── the label chip ────────────────────────────────────────────────────────
   "AI VIDEO, ON A SCHEDULE" and "IN DEVELOPMENT" used to be solid white
   blocks with two detached white bars trailing them. On a deep blue page a
   filled white rectangle is the loudest thing on the screen, and it was being
   spent on the smallest piece of text there: it read as a sticker somebody
   forgot to take off.

   What it is now is what a status marker actually is: a hairline capsule, the
   text in the accent, and one small dot that breathes. The dot is the only
   animation, it runs at four seconds a cycle, and it is what makes the label
   read as live rather than as printed.
   ───────────────────────────────────────────────────────────────────────── */
/* The outer element is only a wrapper. The pill styling in chrome.css belongs
   to the quiz and the format chips; without this reset the label capsule ends
   up inside a second pill. */
.chip:has(> .chip__box) {
  display: inline-flex;
  padding: 0;
  border: 0;
  background: none;
}
.chip__box {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.85rem 0.4rem 0.7rem;
  border-radius: var(--r-pill);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  white-space: nowrap;
}
.chip__box::before {
  content: "";
  width: 5px; height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--accent-2);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 70%, transparent);
}
.js-motion .chip__box::before { animation: chipPulse 4s var(--ease-out) infinite; }
@keyframes chipPulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 62%, transparent); opacity: 1; }
  45%      { box-shadow: 0 0 0 5px transparent; opacity: 0.55; }
}

/* the two trailing bars are gone with the block they trailed */
.chip > i { display: none; }

/* ── the hero ─────────────────────────────────────────────────────────────── */

.hero__grid { display: grid; gap: 2.5rem; align-items: center; }
.hero__copy { max-width: 100%; }

/* ── the 3D deck ───────────────────────────────────────────────────────────
   Real perspective, not a fake one: a perspective root, a preserve-3d space,
   and three planes at three different Z depths. The space leans with the
   pointer, so the parallax between the planes is a consequence of actual
   depth rather than three elements being moved by different amounts.
   ───────────────────────────────────────────────────────────────────────── */
.deck {
  perspective: 1100px;
  perspective-origin: 52% 50%;
}
.deck__space {
  position: relative;
  height: min(66vh, 600px);
  transform-style: preserve-3d;
  /* set directly by js/depth.js, never through a custom property: a variable on
     the parent would invalidate every child's style on every frame */
  transform: rotateX(0deg) rotateY(0deg);
  will-change: transform;
}

.plane {
  position: absolute;
  top: 50%; left: 50%;
  width: clamp(150px, 15vw, 208px);
  transform-style: preserve-3d;
  transform: translate3d(calc(-50% + var(--x, 0px)), calc(-50% + var(--y, 0px)), var(--z, 0px)) rotateY(var(--ry, 0deg));
}
.plane--front { width: clamp(178px, 17.5vw, 244px); }
/* was clamp(128px, 13vw, 178px), and at z -430 that rendered as a hundred
   pixel sliver hiding behind the product plane */
.plane--far   { width: clamp(158px, 16.5vw, 238px); }

.plane__screen {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--bg-3);
  box-shadow: var(--shadow);
  aspect-ratio: var(--ar, 9 / 16);
  /* the bright top edge is light catching the material. it is the difference
     between a rectangle and an object. */
  box-shadow: 0 0 0 1px var(--line-2), 0 30px 70px rgba(0, 0, 0, 0.6);
}
.plane__screen video,
.plane__screen img { width: 100%; height: 100%; object-fit: cover; display: block; }

.plane__cap {
  display: block;
  margin-top: 0.6rem;
  color: var(--ink-2);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

/* the slot that fills itself the moment a real file lands next to it */
.plane__screen--slot[data-empty] img { display: none; }
.plane__screen--slot[data-empty] {
  background:
    repeating-linear-gradient(-45deg, var(--surface) 0 8px, transparent 8px 16px),
    var(--bg-2);
}
.plane__screen--slot[data-empty] .ai-tag { display: none; }
.slot-note { display: none; }
.plane__screen--slot[data-empty] .slot-note {
  display: block;
  position: absolute;
  inset: auto 0 0 0;
  padding: 0.5rem;
  text-align: center;
  color: var(--ink-3);
  background: var(--bg-2);
}

/* ── the gallery ──────────────────────────────────────────────────────────── */

.scene--gallery { padding-block: calc(var(--sec-y) * 1.1); }

.formats { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-top: 0.3rem; }
.formats .chip { display: inline-flex; }

/* The deck bleeds by design, so the section clips rather than the page growing
   a horizontal scrollbar. `clip` and not `hidden`: hidden would make the hero a
   scroll container, and a stray focus could then scroll it sideways. */
.scene--hero, .scene--gallery { overflow-x: clip; }

/* On a phone the perspective is not worth the squeeze. The three planes lie
   down into a plain row, which shows the same three results without any of the
   depth arithmetic having to fit into 390 pixels. */
@media (max-width: 900px) {
  .deck { perspective: none; }
  .deck__space {
    height: auto;
    transform: none !important;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.55rem;
    align-items: start;
  }
  .plane {
    position: static;
    width: 100% !important;
    transform: none !important;
  }
  .plane .plane__screen { aspect-ratio: 9 / 16; }
  .plane__cap { font-size: 0.625rem; }
}


/* ═══════════════════════════  THE GALLERY  ═══════════════════════════════
   Two shapes for the same twelve elements. The grid is the default and the
   fallback; js/ring.js swaps in the cylinder when there is room for it.
   ═══════════════════════════════════════════════════════════════════════ */

.gal {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
  width: 100%;
  margin-top: 1.6rem;
}
.gal__ring { display: contents; }

.card { display: grid; gap: 0.55rem; align-content: start; }
.card__screen {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--bg-3);
  /* The box takes the shape of the film, so object-fit never has anything to
     crop. A 1:1 clip forced into a 9:14 hole is what "zoomed in and cheap"
     actually looks like. */
  aspect-ratio: var(--ar, 9 / 14);
  box-shadow: 0 0 0 1px var(--line), 0 22px 50px rgba(0, 0, 0, 0.5);
}
.card__screen video,
.card__screen img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card__cap { color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.14em; }

.gal__hint { display: none; }

@media (max-width: 900px) { .gal { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 560px) { .gal { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; } }

/* ── the cylinder ──────────────────────────────────────────────────────────
   The stage owns the perspective, the ring owns the rotation, and each card is
   placed once with rotateY + translateZ. After that exactly one element is
   transformed per frame, which is why twelve videos on a spinning cylinder
   still hold sixty frames.
   ───────────────────────────────────────────────────────────────────────── */
.gal.is-ring {
  display: block;
  position: relative;
  height: clamp(340px, 48vh, 470px);
  margin-top: 2.75rem;
  perspective: 1500px;
  perspective-origin: 50% 46%;
  cursor: grab;
  overflow-x: clip;
}
.gal.is-ring.is-grabbed { cursor: grabbing; }
.gal.is-ring .gal__ring {
  display: block;
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  will-change: transform;
}
.gal.is-ring .card {
  --cw: clamp(150px, 13.5vw, 200px);
  position: absolute;
  top: 50%; left: 50%;
  width: var(--cw);
  margin: 0 0 0 calc(var(--cw) * -0.5);
  translate: 0 -50%;
  transform-style: preserve-3d;
  /* the far side of the ring is further away, so it is dimmer. without this it
     reads as a flat fan of cards instead of one solid object. */
  opacity: 0.3;
  transition: opacity 420ms var(--ease-out);
  pointer-events: none;
}
.gal.is-ring .card.is-facing { opacity: 1; }
.gal.is-ring .card__cap { opacity: 0; transition: opacity 320ms var(--ease-out); }
.gal.is-ring .card.is-facing .card__cap { opacity: 1; }

.gal.is-ring ~ .gal__hint {
  display: block;
  margin-top: 0.9rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

/* v15: the pool of light under the ring is GONE. It existed so the cylinder
   would not float in a void - the void is now the night's cloud bank, which
   grounds it better than a glowing bar ever did, and over the photograph the
   old accent pool read as a leftover from another site. */

/* ═══════════════════════════  BILLING, PLANS, WAITLIST  ══════════════════ */

.billing {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.9rem;
  margin-top: 1.75rem;
}
.seg {
  position: relative;
  display: flex;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}
.seg__pill {
  position: absolute; top: 3px; left: 3px;
  width: 92px; height: calc(100% - 6px);
  border-radius: var(--r-pill);
  background: var(--surface-2);
  transition: transform var(--t-lang) var(--ease-out);
}
.seg[data-on="year"] .seg__pill { transform: translateX(92px); }
.seg__btn {
  position: relative;
  width: 92px;
  padding-block: 0.42rem;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink-3);
  transition: color var(--t-lang) var(--ease-out);
}
.seg__btn.is-on { color: var(--ink); }
.seg__btn:active { transform: scale(0.97); }
.billing__save { color: var(--accent-ink); text-transform: uppercase; letter-spacing: 0.12em; }

.plans { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.plan__list { display: grid; gap: 0.34rem; margin-top: 0.2rem; }
.plan__list li {
  position: relative;
  padding-left: 1.05rem;
  color: var(--ink-2);
  font-size: var(--fs-small);
}
.plan__list li::before {
  content: "";
  position: absolute; left: 0; top: 0.62em;
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
}
.plan__n { font-variant-numeric: tabular-nums; transition: opacity var(--t-nav) var(--ease-out); }
.plan__n.is-swapping { opacity: 0; }

@media (max-width: 900px) { .plans { grid-template-columns: 1fr; } }

/* ── the waitlist ─────────────────────────────────────────────────────────── */

.wait {
  width: 100%;
  margin-top: 2.75rem;
  padding: 2rem 1.9rem 2.1rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--bg-2);
  display: grid;
  gap: 0.75rem;
  justify-items: start;
}
.wait__title {
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 2.4vw, 2rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.08;
}
.wait__body { color: var(--ink-2); max-width: 52ch; }
.wait__form { display: flex; flex-wrap: wrap; gap: 0.55rem; width: 100%; margin-top: 0.5rem; }
.wait__input {
  flex: 1 1 260px;
  min-width: 0;
  padding: 0.85rem 1.15rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: var(--bg);
  color: var(--ink);
  transition: border-color var(--t-nav) var(--ease-out);
}
.wait__input::placeholder { color: var(--ink-3); }
.wait__input:focus-visible { border-color: var(--accent); outline-offset: 1px; }
.wait__input[aria-invalid="true"] { border-color: var(--accent); }
.wait__note { color: var(--ink-3); font-size: var(--fs-small); }
.wait__note[data-state="ok"] { color: var(--accent-ink); font-weight: 500; }

@media (max-width: 560px) {
  .wait { padding: 1.5rem 1.25rem 1.6rem; }
  .wait__form .btn { width: 100%; }
}

/* ═══════════════════════════  COOKIE NOTICE  ═════════════════════════════
   Not a wall. The site sets nothing but its own language and scroll position
   until someone says otherwise, so the honest shape is a notice with a real
   choice, sitting out of the way at the bottom.
   ═══════════════════════════════════════════════════════════════════════ */

.cookie {
  position: fixed;
  left: 50%; bottom: 18px;
  z-index: 210;
  width: min(680px, calc(100vw - 2 * var(--gutter)));
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.1rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  backdrop-filter: blur(20px) saturate(160%);
  box-shadow: var(--shadow);
  transform: translate(-50%, 0);
  transition: transform 400ms var(--ease-out), opacity 400ms var(--ease-out);
}
@starting-style { .cookie { transform: translate(-50%, 140%); opacity: 0; } }
.cookie.is-out { transform: translate(-50%, 140%); opacity: 0; }
.cookie__text { flex: 1 1 260px; font-size: var(--fs-small); color: var(--ink-2); }
.cookie__text a { color: var(--ink); border-bottom: 1px solid var(--line-2); }
.cookie__acts { display: flex; gap: 0.5rem; margin-left: auto; }
.cookie .btn { --btn-y: 0.55rem; padding-inline: 1.1rem; font-size: 0.8125rem; }

/* ═══════════════════════════  LEGAL PAGES  ═══════════════════════════════ */

.legal { max-width: 74ch; }
.legal h2 {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin-top: 3rem;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line);
}
.legal h3 { margin-top: 1.6rem; font-size: 1.0625rem; }
.legal p, .legal li { color: var(--ink-2); margin-top: 0.7rem; }
.legal ul { margin-top: 0.4rem; }
.legal li { position: relative; padding-left: 1.1rem; }
.legal li::before { content: ""; position: absolute; left: 0; top: 0.68em; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
.legal .todo {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  border: 1px dashed var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: 0.75rem;
}
.legal__updated { color: var(--ink-3); font-size: var(--fs-small); margin-top: 1.2rem; }

/* The story is over at the dossier. Everything the stage was carrying leaves
   with it, so nothing floats over the prices. */
.stage.is-struck { opacity: 0; transition: opacity 420ms var(--ease-out); }

/* The rate table. Two lines per plan, because "what does it cost" is two
   numbers and any more than that is a pricing page pretending to be a menu. */
.rate {
  display: grid;
  gap: 0.4rem;
  margin: 0.35rem 0 0.15rem;
  padding: 0.75rem 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.rate > div { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.rate dt { color: var(--ink-3); font-size: var(--fs-small); }
.rate dd { color: var(--ink); font-weight: 500; white-space: nowrap; }
.rate dd span:last-child { color: var(--ink-3); font-weight: 400; }

.plan__cur { align-self: flex-start; margin-top: 0.25rem; }
.plans__note { margin-top: 1.1rem; max-width: 66ch; color: var(--ink-3); font-size: var(--fs-small); }

/* ── the square product films ──────────────────────────────────────────────
   Their own block, because a 1:1 clip standing in a cylinder of 9:16 ones
   makes the whole ring look badly built. Here the square is the format, not
   an error.

   Three columns, six cells. The label is the first cell rather than a line
   floating above the grid, so five films and one piece of type fill the block
   exactly and there is no ragged tail. The previous version put all five in a
   single row: two hundred pixels a tile, five near-identical dark boxes
   shoulder to shoulder, and a stray paragraph wedged inside the second figure
   that pushed its caption a line and a half below every other one. Fewer per
   row and more air is the whole fix.
   ───────────────────────────────────────────────────────────────────────── */
.strip { width: 100%; margin-top: 4rem; }
.strip__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2.25rem 1.75rem;
  align-items: start;
}

/* the label is a cell of the grid, so it shares the rhythm instead of
   hovering over it */
.strip__note {
  display: grid;
  gap: 0.75rem;
  /* stretch first, or the cell is only as tall as its two lines and there is
     nothing for align-content to centre them against */
  align-self: stretch;
  align-content: center;
  padding-right: 1rem;
  padding-bottom: 1.5rem;
}
.strip__label { color: var(--accent-ink); text-transform: uppercase; letter-spacing: 0.14em; }
/* No character cap. The grid column and the gutter already set the measure,
   and a ch limit on top of them broke the line after the second word. */
.strip__line { color: var(--ink-2); font-size: var(--fs-small); text-wrap: pretty; }

.shot { display: grid; gap: 0.7rem; align-content: start; }

/* ── the hover lift ────────────────────────────────────────────────────────
   Transform only. The previous version transitioned box-shadow alongside it,
   and a 40px blur on a 379px tile has to be re-drawn on the CPU every single
   frame of the move, five tiles at a time, while five videos are decoding
   behind them. That is what the stutter was: not the distance, the shadow.

   translate3d rather than translateY, so the tile is already on its own
   compositor layer before the pointer arrives and the browser has nothing to
   promote mid-animation. The deeper shadow moved to the wrapper, where it can
   cross-fade as opacity without ever being repainted.
   ───────────────────────────────────────────────────────────────────────── */
.shot__screen {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  border-radius: var(--r-md);
  background: var(--bg-3);
  box-shadow: 0 0 0 1px var(--line), 0 16px 40px rgba(0, 0, 0, 0.45);
  transform: translate3d(0, 0, 0);
  transition: transform 340ms var(--ease-out);
  will-change: transform;
}
.shot__screen video { width: 100%; height: 100%; object-fit: cover; display: block; }
.shot__cap {
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  transition: color 340ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .shot:hover .shot__screen { transform: translate3d(0, -7px, 0); }
  .shot:hover .shot__cap { color: var(--accent-ink); }
}

/* Two columns from here down, with the label across the top: three tiles on a
   tablet is the same squeeze the desktop row had. */
@media (max-width: 1023px) {
  .strip__row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.75rem 1.25rem; }
  .strip__note { grid-column: 1 / -1; padding: 0 0 0.25rem; align-content: start; }
  .strip__line { max-width: 52ch; }
}
@media (max-width: 560px) { .strip__row { gap: 1.25rem 0.7rem; } }

/* the plain-language captions under the recipe card and the output stack.
   They live inside their props, so the props' own alpha carries them. */
.act-cap {
  position: absolute;
  top: calc(100% + 12px);
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}
.act-cap--out { left: 140px; }

/* the memory panel's own headline: says what the panel is before the rows
   start talking */
.rules__head {
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-2);
  padding-bottom: 0.3rem;
  border-bottom: 1px solid var(--line);
}
