/* CurtainCall homepage, assets/home.css
   Colour, type, space and depth all come from assets/tokens.css, which every page on the site links first.
   Nothing in this file may write a raw colour or a raw font size: if a value has no token, the fix is to add
   the role to tokens.css, not to write the number here. Everything below is homepage-only.
   Motion: transform and opacity only. */

/* ---------- Fonts, all self-hosted (SIL OFL, licences in assets/fonts/). Instrument Serif (one weight, 400) for
   headings, Inter variable 400..700 for body and UI. Dekko, for the comic "Book a demo" button only, is declared in
   assets/comic.css. ---------- */
/* font-weight here is literal on purpose: @font-face descriptors cannot take var(). Round 6 put var() here, the
   browser dropped the descriptor and registered the faces at weight "normal" (found and fixed 2026-10-04). */
@font-face { font-family: "Instrument Serif"; src: url(/assets/fonts/instrument-serif-regular.woff2) format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Inter"; src: url(/assets/fonts/inter-latin.woff2) format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* No grey flash over a tapped control on iOS and Android: every control has its own pressed state (audit S3). */
html { -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; }
html:not(.lenis) { scroll-behavior: smooth; }
html.rm, html.cap { scroll-behavior: auto; }
body {
  font-family: var(--font-body); font-size: var(--text-md); line-height: var(--leading-loose);
  background: var(--surface-page); color: var(--text-primary);
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}
img, svg, video, canvas { display: block; max-width: 100%; }
a { color: inherit; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
::selection { background: var(--accent); color: var(--text-on-accent); }
/* Icons: every line icon draws the site's one 1.8px stroke at whatever size it is set (review B8.4). They are 24-unit
   Lucide paths, so stroke-width 1.8 thinned to 1.2px on a 16px arrow and 1.5px on the Proof handle; non-scaling-stroke
   measures it in CSS pixels instead. The logo mark keeps its own stroke (Q23 left it). */
:is(.work-cta svg, .proof-cta svg, .proof-handle svg, .proof-play svg, .faq-icon) path { vector-effect: non-scaling-stroke; }
:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }
/* The ring follows each control's own corners, so a pill stays round and the comic button stays square. Until
   2026-10-12 this rule also gave whatever took focus a 4px radius, which rounded the comic button's inked corners
   under the ring (review B8.1). Text links paint no box, so a radius cannot change how they look; their ring alone
   takes the small corner. */
:is(.logo, .nav-link, .menu-link, .intro-skip, .faq-head a, .wl-status a, .proof-cta):focus-visible { border-radius: var(--radius-s); }

/* ---------- Interaction states ----------
   One vocabulary for the whole site, so the four states can never be mistaken for one another:
     hover    warms and lifts. Pointer only, so it never fires on a phone tap.
     focus    draws a ring and changes nothing else. It is the keyboard's state and it must not
              borrow hover's lift, or a keyboard user cannot tell focus from a passing cursor.
     pressed  scales down. It is the only state with negative motion.
     selected a cyan mark that persists: a bar, a dot, or a filled face, with aria-current or
              aria-selected beside it so it is announced and not only seen.
     disabled dimmed and inert.
   Every state carries a second signal besides colour, because colour alone is not a signal for
   everyone. Hover rules sit inside the pointer query so touch devices never get a stuck state. */
:where(a, button, [role="button"], [role="slider"], [tabindex]):disabled,
:where(a, button, [role="button"], [role="slider"], [tabindex])[aria-disabled="true"] {
  opacity: var(--disabled-opacity); pointer-events: none; cursor: not-allowed;
}
@media (hover: hover) and (pointer: fine) {
  /* Every link warms toward cyan, to the one hover colour links have (--accent-bright; review B8.6, Q24): until
     2026-10-12 the FAQ's link cooled from cyan to white while the credits' warmed from white to cyan. The light cyan
     rather than --accent because it holds 5.9:1 over the scrolled nav with white art beneath, where cyan gave 4.1. */
  .menu-link:hover, .faq-head a:hover, .wl-status a:hover, .proof-cta:hover { color: var(--accent-bright); }
  .menu-link:hover { transform: translateX(6px); }
  .nav-hamburger:hover, .menu-close:hover { background: var(--surface-glass); color: var(--text-primary); }
  .logo:hover .logo-icon { transform: rotate(-8deg); }
  .proof-handle:hover { transform: translate(-50%, -50%) scale(1.08); }
}
/* Pressed. A control with no transform of its own presses through transform. The glass plates (the Work ring cards;
   the Pillars cards were plates too until the film replaced them on 2026-10-13) already carry one, their tilt
   (perspective, lift and two rotations, written by initTilt in home.js), and a second transform declaration replaces
   the first rather than composing with it: until 2026-10-12 this comment said they composed, and the ring's hover
   transform had been hiding the round-3 tilt all along (review B3.2). So the plates press through the separate scale
   property, which the browser applies on top of transform. The logo had no pressed state (review B6.3); Skip intro
   already had one, the glass pills' .98 below. */
.nav-link:active, .menu-link:active, .nav-hamburger:active,
.menu-close:active, .step:active, .logo:active { transform: scale(var(--state-pressed-scale)); }
.work-link:active { scale: var(--state-pressed-scale); }
/* A press on a ring card that becomes a drag (6 px, home.js) lets the card go, as a highlight does when a scroll starts on
   iOS. Until 2026-10-16 the ring captured the pointer on the press, so a card never showed its press at all. */
html.work-dragging .work-link:active { scale: none; }
.proof-handle:active { transform: translate(-50%, -50%) scale(.94); }
.logo-icon { transition: transform var(--duration-base) var(--ease-out); }
/* Selected. The nav marks the section you are in with a solid cyan underline, which the hover
   underline cannot be confused with because hover's is currentColor and only appears on hover. */
.nav-link[aria-current] { color: var(--text-primary); }
.nav-link[aria-current]::before { content: ""; position: absolute; left: 0; right: 0; bottom: 10px; height: 2px; background: var(--state-selected); border-radius: 1px; }
/* The phone menu marks it too (2026-10-12, review B8.8), with the same 2px cyan rule, drawn as the name's underline so
   it is as wide as the word, not the row. The How steps carry aria-current="step" beside their dot (home.js). */
.menu-link[aria-current] { text-decoration-line: underline; text-decoration-color: var(--state-selected); text-decoration-thickness: 2px; text-underline-offset: .2em; }

.skip-link { position: fixed; top: var(--space-2); left: var(--space-2); z-index: var(--z-skip); padding: var(--space-3) var(--space-4); background: var(--accent); color: var(--text-on-accent); font-weight: var(--weight-bold); border-radius: var(--radius-pill); transform: translateY(-200%); transition: transform var(--duration-fast) var(--ease-out); }
.skip-link:focus-visible { transform: none; }
/* Focus targets that are not controls: #main after Skip intro (home.js finishIntro) and the Geometry Lock title after
   its skip cue (glock.js dropHint). Focus is handed to them so the next Tab continues from there; a ring round a whole
   region or a heading would read as something to press. */
#main:focus, #gl-title:focus { outline: none; }

#grain { position: fixed; inset: 0; z-index: var(--z-grain); pointer-events: none; opacity: 0.028; width: 100%; height: 100%; }
/* dust (assets/dust.js): a page layer under the content and one over the hero stage, a sibling placed after it
   (.hero isolates; stage and dust both sit at z 0 so DOM order stacks the dust above the scene, .hero-inner at z 1 above both) */
.dust { pointer-events: none; opacity: .85; transition: opacity 1s ease; }
.dust--page { position: fixed; inset: 0; z-index: 0; }
.dust--hero { position: absolute; inset: 0; z-index: 0; }

.container { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }

/* ---------- Type ---------- */
.eyebrow { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-widest); text-transform: uppercase; color: var(--accent); }
/* Display type: one rule sets the face for every heading (Instrument Serif since 2026-10-05; see the TYPE tokens).
   The small numerals .work-num and .step-num are labels, so they stay Inter. No bold exists in this face, and
   font-synthesis is off so nothing can fake one. */
html { font-synthesis-weight: none; }
.hero-title, .section-title, .work-title, .step-title, .menu-link, .slate-amount { font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); }
.section-title { line-height: var(--leading-tight); font-size: var(--display-serif-m); max-width: 21ch; margin-top: var(--space-4); }
.section-sub { color: var(--text-secondary); font-size: var(--text-lg); max-width: 52ch; margin-top: var(--space-4); }
/* No one-word last lines (audit V11, P04: "studies.", "here.", "call.", "ANIMATED"): headings and eyebrows balance
   their lines, leads and card copy avoid an orphan. The hero title is left alone; its two words are set as two lines. */
.section-title, .eyebrow, .work-title, .step-title { text-wrap: balance; }
.section-sub, .hero-sub, .step-desc, .work-line, .proof-caption, .gl-subhead, .gl-body { text-wrap: pretty; }

/* ---------- Buttons: GlassSurface since 2026-10-04 (Eric asked for React Bits' <GlassSurface/> on every glass button).
   Chromium (html.gs-svg): clear glass, the page behind it bent at the rim with a red, green and blue fringe by a
   per-size SVG filter built in assets/glass.js, lit only by soft inner white glows. Safari, Firefox and ?gs=fallback:
   the component's own fallback, a bright frosted blur with a light rim. Primary buttons and the selected proof tab
   keep the brand gradient inside the glass, inset by --gs-cta-inset so a clear ring still bends the page around it.
   Layer rule, in three parts (from round 4, and it matters more with a filter this strong):
   1. The root paints nothing visible: no backdrop-filter, blend mode or overflow clip (a child's backdrop-filter
      reads the nearest ancestor that is its own render surface, and any of those would make the button that surface,
      so the layers would see the button instead of the page), and no fill either, because the root's own background,
      border and inset shadows paint BEFORE its negative z-index children and so land inside the layers' backdrop,
      where the filter drags them inward (a displaced copy of the border once showed as a bright star inside every
      pill). The border stays for layout and for the border-color rules elsewhere, but its paint is hidden by a
      transparent border-image; ::before re-draws it through border-color: inherit in the fallback.
   2. Every backdrop operation lives in two absolutely positioned child spans at negative z-index (the root keeps a
      stacking context so they stay inside it; a plain stacking context is not a render surface): .glass-refract
      carries the fallback's frosted blur, .glass-bend carries the Chromium filter (var(--gs-filter), set on each pill
      by glass.js) and the hover sweep in its ::after. In Chromium the refract span is off, so the glass is clear.
   3. The face (frost or brand gradient, rim, glows) is ::before, above both layers and under the bare text. ---------- */
html { --gs-fill: var(--gs-fb-fill); }
html.gs-svg { --gs-fill: var(--gs-clear-fill); }
.btn-cta, .glass {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  padding: var(--space-2) 20px; border-radius: var(--radius-pill); font-size: var(--text-sm); font-weight: var(--weight-semibold); text-decoration: none;
  white-space: nowrap; min-height: 40px; color: var(--text-primary);
  background: transparent;
  border: 1px solid var(--gs-fb-border); border-image: linear-gradient(transparent, transparent) 1;
  box-shadow: var(--shadow-pill), var(--shadow-gs-outer);
  transition: transform var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out), color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}
.glass-refract, .glass-bend { position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }
.glass-refract { z-index: -3; -webkit-backdrop-filter: var(--gs-fb-filter); backdrop-filter: var(--gs-fb-filter); }
.glass-bend { z-index: -2; overflow: hidden; }
html.gs-svg :is(.btn-cta, .glass) > .glass-refract { display: none; }
/* The empty fallback in var(--gs-filter,) leaves saturate() alone until glass.js has measured the pill. */
html.gs-svg :is(.btn-cta, .glass) > .glass-bend { backdrop-filter: var(--gs-filter,) saturate(var(--gs-sat, 1)); }
.btn-cta::before, .glass::before { content: ""; position: absolute; inset: -1px; z-index: -1; border-radius: inherit; border: 1px solid; border-color: inherit; pointer-events: none;
  background: var(--gs-fill);
  box-shadow: inset 0 1px 0 var(--gs-fb-rim-top), inset 0 -1px 0 var(--gs-fb-rim-bottom);
  transition: box-shadow var(--duration-base) var(--ease-out), background-color var(--duration-fast) ease; }
html.gs-svg :is(.btn-cta, .glass)::before { border-color: transparent; box-shadow: var(--shadow-gs-inset); }
.btn-cta::before { background: var(--accent-gradient-glass); }
html.gs-svg :is(.btn-cta, .proof-tab.is-active)::before { padding: var(--gs-cta-inset); background-clip: content-box; box-shadow: var(--shadow-gs-inset), inset 0 0 12px 2px var(--accent-halo); }
.btn-cta .glass-bend::after, .glass .glass-bend::after { content: ""; position: absolute; top: -70%; left: -30%; width: 45%; height: 240%; background: linear-gradient(100deg, var(--glass-bulge-out) 20%, var(--glass-sweep) 50%, var(--glass-bulge-out) 80%); transform: translateX(-160%) rotate(10deg); transition: transform 1s var(--ease-out); }
.btn-cta:active, .glass:active { transform: translateY(0) scale(.98); }
.btn-cta { color: var(--text-on-bright); font-weight: var(--weight-bold); border-color: var(--border-lit); box-shadow: 0 12px 32px -12px var(--accent-halo); }
/* Hover warms and lifts, and only where a pointer can hover, so a tap never leaves a pill stuck in it. */
@media (hover: hover) and (pointer: fine) {
  .btn-cta:hover .glass-bend::after, .glass:hover .glass-bend::after { transform: translateX(420%) rotate(10deg); }
  .btn-cta:hover, .glass:hover { transform: translateY(var(--state-hover-lift)); border-color: var(--border-lit); }
  html.gs-svg :is(.btn-cta, .glass):hover::before { box-shadow: var(--shadow-gs-inset-hover); }
  html.gs-svg .proof-tab.is-active:hover::before { box-shadow: var(--shadow-gs-inset-hover), inset 0 0 12px 2px var(--accent-halo); }
  .btn-cta:hover { box-shadow: 0 14px 40px -10px var(--accent-glow-strong), 0 0 0 3px var(--accent-ring); }
  /* In Chromium the hover ring and glow move to the face: anything the root paints lands in the glass layer's
     backdrop, and the filter dragged the ring into the pill as a faint diamond (seen 2026-10-04). */
  html.gs-svg .btn-cta:hover { box-shadow: 0 12px 32px -12px var(--accent-halo); }
  html.gs-svg .btn-cta:hover::before { box-shadow: var(--shadow-gs-inset-hover), inset 0 0 12px 2px var(--accent-halo), 0 14px 40px -10px var(--accent-glow-strong), 0 0 0 3px var(--accent-ring); }
}

/* The comic "Book a demo" button (.btn-comic, its Dekko face, keyframes and burst) lives in assets/comic.css since
   2026-10-11, shared with the seven other pages; that file is linked before this one. */

/* ---------- Glass plate: the material of the Work ring links, where initWork adds the class and the blur span only
   (the ring links are their own surface because of will-change, so a bend layer there would see nothing, verified
   2026-09-20). The four Pillars cards were plates too, with a second span that bent the page through the SVG filter
   #cc-bend-card under the pointer; the span, the filter and its rules went with the cards on 2026-10-13, when the
   film replaced the section. Same layer rule as the buttons: the root paints nothing visible (transparent fill,
   border hidden by the transparent border-image, only the outer drop shadow) and clips nothing (the media clips
   itself; the span clips through border-radius: inherit); the span carries the blur (--glass-blur-card); ::before
   draws the rim (border-color: inherit, so the hover border colour still applies), the inset highlights, the tint
   and the light bulge over the content at z 2, next to the cursor specular (::after, no blend mode now, so --spec
   sits at .16).
   Hover rules on the roots must not add inset shadows: they would paint into the backdrop and come back displaced.
   The plate tilts through --rx/--ry, written by initTilt() in home.js. Flat under reduced motion and capture,
   solid under reduced transparency. The How panel is not a plate. ---------- */
.glass-plate { position: relative; isolation: isolate; --mx: 50%; --my: 50%; --rx: 0deg; --ry: 0deg; --ty: 0px;
  background: transparent;
  border: 1px solid var(--border-strong); border-image: linear-gradient(transparent, transparent) 1;
  box-shadow: var(--shadow-card);
  transform: perspective(1000px) translateY(var(--ty)) rotateX(var(--rx)) rotateY(var(--ry));
  transition: transform var(--duration-slow) var(--ease-out), box-shadow var(--duration-slow) var(--ease-out), border-color .25s ease, scale var(--duration-fast) var(--ease-out); }
.glass-plate.is-tilting { transition-duration: .12s, var(--duration-slow), .25s, var(--duration-fast); }
.glass-plate > .glass-refract { -webkit-backdrop-filter: blur(var(--glass-blur-card, 9px)) saturate(1.45); backdrop-filter: blur(var(--glass-blur-card, 9px)) saturate(1.45); }
.glass-plate::before { content: ""; position: absolute; inset: -1px; z-index: 2; border-radius: inherit; border: 1px solid; border-color: inherit; pointer-events: none;
  background: radial-gradient(120% 70% at 50% -20%, var(--glass-plate-bulge), var(--glass-plate-bulge-mid) 45%, var(--glass-bulge-out) 70%), var(--surface-glass);
  box-shadow: inset 0 1px 0 var(--glass-plate-rim-top), inset 0 -1px 0 var(--glass-plate-rim-bottom), inset 1px 0 0 var(--glass-plate-rim-side), inset -1px 0 0 var(--glass-plate-rim-side), var(--shadow-inset-card); }
.glass-plate::after { content: ""; position: absolute; inset: 0; z-index: 2; border-radius: inherit; pointer-events: none; opacity: 0; background: radial-gradient(260px circle at var(--mx) var(--my), var(--glass-spec), var(--glass-bulge-out) 60%); transition: opacity var(--duration-base) ease; }
@media (hover: hover) and (pointer: fine) { .glass-plate:hover::after { opacity: 1; } }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ---------- Logo (em-based so the intro lockup and the nav logo share one shape) ---------- */
.logo { display: inline-flex; align-items: center; min-height: var(--tap-min); gap: .5556em; font-size: var(--text-lg); font-weight: var(--weight-bold); text-decoration: none; line-height: var(--leading-none); }
.logo-icon { width: 1.5556em; height: 1.5556em; flex: none; stroke: var(--accent); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.logo-text { background: var(--accent-gradient-text); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; white-space: nowrap; }

/* ---------- Nav ---------- */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-nav); height: var(--nav-h);
  display: flex; align-items: center;
  transition: transform .5s var(--ease-out), opacity .4s ease, background-color var(--duration-base) ease, border-color var(--duration-base) ease, box-shadow var(--duration-base) ease;
  border-bottom: 1px solid transparent;
}
/* The tint is .70 so the links hold 4.5:1 over anything that scrolls under the bar: composited over pure white art,
   76% white text measures 5.2:1 at .70 and 3.6:1 at the old .58 (a Ninja High School balloon under "Work" already
   measured 4.53:1 at .58, audit S15). */
.nav.is-scrolled {
  background: var(--surface-nav);
  -webkit-backdrop-filter: blur(18px) saturate(1.5); backdrop-filter: blur(18px) saturate(1.5);
  border-bottom-color: var(--border-subtle);
  box-shadow: inset 0 1px 0 var(--border-faint), var(--shadow-nav);
}
.nav.is-hidden { transform: translateY(-100%); }
.nav:focus-within { transform: none; }
.nav-inner { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--space-5); }
.nav-links { display: flex; gap: var(--space-5); align-items: center; justify-content: center; }
/* 76% white at weight 500: at 48% the links fell to 4.02:1 over the scrolled bar with art beneath (audit S15). */
.nav-link { position: relative; display: inline-flex; align-items: center; min-height: var(--tap-min); color: var(--text-body); font-size: var(--text-sm); font-weight: var(--weight-medium); text-decoration: none; transition: color var(--duration-fast) ease, transform var(--duration-fast) var(--ease-out); }
.nav-link::after { content: ""; position: absolute; left: 0; right: 0; bottom: 10px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .4s var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .nav-link:hover { color: var(--accent-bright); }
  .nav-link:hover::after { transform: scaleX(1); }
}
.nav-ctas { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); }

.nav-hamburger { display: none; width: var(--tap-min); height: var(--tap-min); align-items: center; justify-content: center; border-radius: var(--radius-pill); }
.nav-hamburger svg { width: 24px; height: 24px; color: var(--text-primary); transition: transform var(--duration-base) var(--ease-out); }
.nav-hamburger[aria-expanded="true"] svg { transform: rotate(90deg); }

/* ---------- Mobile menu ---------- */
.menu { position: fixed; inset: 0; z-index: var(--z-menu); background: var(--surface-sheet); -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); display: grid; grid-template-rows: auto 1fr auto; padding: 0 var(--gutter) max(28px, env(safe-area-inset-bottom)); overflow-y: auto; overscroll-behavior: contain; opacity: 0; transition: opacity .25s ease; }
.menu.is-open { opacity: 1; }
.menu[hidden] { display: none; }
.menu-top { height: var(--nav-h); display: flex; align-items: center; justify-content: space-between; }
.menu-close { width: var(--tap-min); height: var(--tap-min); display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius-pill); }
.menu-close svg { width: 22px; height: 22px; color: var(--text-primary); }
.menu-links { display: flex; flex-direction: column; justify-content: center; gap: var(--space-2); }
.menu-link { font-size: var(--display-serif-m); text-decoration: none; padding: var(--space-2) 0; color: var(--text-primary); opacity: 0; transform: translateY(12px); transition: opacity .5s var(--ease-out), transform .5s var(--ease-out); }
.menu.is-open .menu-link { opacity: 1; transform: none; }
.menu.is-open .menu-link:nth-child(2) { transition-delay: .04s; }
.menu.is-open .menu-link:nth-child(3) { transition-delay: .08s; }
.menu.is-open .menu-link:nth-child(4) { transition-delay: .12s; }
.menu.is-open .menu-link:nth-child(5) { transition-delay: .16s; }
.menu.is-open .menu-link:nth-child(6) { transition-delay: .20s; }
.menu-bottom { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
/* Lenis ships .lenis-stopped { overflow: clip } at a higher specificity, so the intended lock never applied and
   the page was held still only as a side effect. Say it where it wins. */
html.menu-open, html.menu-open body { overflow: hidden; }
.menu-bottom .btn-comic { flex: 1 1 auto; }

/* ---------- Intro ---------- */
.intro-stage { position: fixed; inset: 0; z-index: var(--z-intro); height: 100dvh; }
.intro-curtain { position: absolute; inset: 0; background: var(--surface-sunken); }
.intro-center { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* The lockup is the nav logo (--text-lg) four times over, and on a phone seven thirds over. Until 2026-10-12 these were
   72px and 42px written as fallbacks of custom properties that nothing set, out of sight of the font-size check. */
.intro-lockup { font-size: calc(var(--text-lg) * 4); }
.intro-icon .clap-arm { transform-box: view-box; transform-origin: 3px 11px; }
.intro-icon path { stroke-dasharray: 1 2; stroke-dashoffset: 1; }
/* The electric slate (assets/electric.js, React Bits' ElectricLogo): two lightning canvases twice the icon's size,
   centred on it, so the glow and the arcs have room around the lines. Their raster spans the icon's view box widened
   to -12..36, which puts the clapper's hinge (3, 11) at 31.25% 47.92% of the arm layer: it rotates there with the svg
   arm. While the lightning is the slate the line icon is hidden; it fades back in as the lockup flies to the nav. */
.intro-mark { position: relative; display: inline-flex; flex: none; }
.intro-electric { position: absolute; inset: -50%; pointer-events: none; }
.intro-electric-arm { transform-origin: 31.25% 47.92%; }
.intro-stage.is-electric .intro-icon { opacity: 0; }
.intro-word { display: inline-block; overflow: hidden; width: 0; }
.intro-caption { position: absolute; left: 50%; top: calc(50% + 78px); transform: translateX(-50%); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-widest); text-transform: uppercase; color: var(--text-body); white-space: nowrap; opacity: 0; }
.intro-skip { position: absolute; right: var(--space-5); bottom: var(--space-5); padding: var(--space-2) var(--space-3); min-height: 36px; font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); }
@media (hover: hover) and (pointer: fine) { .intro-skip:hover { color: var(--text-primary); } }
/* A full tap target on any touch screen, not only under 900px: an iPad held sideways got 38px (review B6.2). */
@media (pointer: coarse) { .intro-skip { min-height: var(--tap-min); } }
@media (max-width: 640px) { .intro-lockup { font-size: calc(var(--text-lg) * 7 / 3); } .intro-caption { top: calc(50% + 54px); font-size: var(--text-2xs); } }

/* ---------- Hero ---------- */
.hero { position: relative; min-height: 100svh; display: flex; align-items: center; padding: calc(var(--nav-h) + 48px) 0 72px; overflow: clip; isolation: isolate; }
.hero-stage { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
/* The light the hero shows if neither the poster nor the scene can paint (both cover it otherwise). */
.hero-light { position: absolute; inset: -10% -10% -10% 30%; background: radial-gradient(ellipse at 62% 42%, var(--accent-wash), transparent 42%), radial-gradient(ellipse at 78% 70%, var(--purple-soft), transparent 40%); }
.hero-poster { position: absolute; inset: 0; opacity: 1; transition: opacity .6s ease; }
.hero-poster img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
html.gl-ready .hero-poster, .hero-poster.is-missing { opacity: 0; }
.hero-canvas { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity .6s ease; }
html.gl-ready .hero-canvas { opacity: 1; }
/* The scene's own box after the poster (clapper.js), so React's first commit leaves the poster in place. A lost WebGL
   context (html.gl-lost) shows the poster again at once, with no fade, over the blank canvas. */
.hero-gl { position: absolute; inset: 0; }
html.gl-lost .hero-poster:not(.is-missing) { opacity: 1; transition: none; }
html.gl-lost .hero-canvas { opacity: 0; transition: none; }
.hero-inner { position: relative; z-index: 1; }
/* The board's place (2026-10-12, review B2.1 and B2.2). .hero-board is a zero-height marker at the board's anchor, as
   wide as the board. clapper.js hands its box to the 3D scene, which centres the board there (re-read on every resize,
   so a turned tablet re-places it), and the poster's board image sits in it too, so the still and the scene agree at
   every size. Wide layouts keep Eric's composition, 75% across and 56% down the first screen, 36% of the width up to
   540 px (times the tuned .96), with one new cap: the arm's top (.82 of the board's width above the anchor with the
   arm lifted by a hover) stays a --space-5 under the nav, which on a 1280 x 650 laptop takes the board from 442 to
   337 px and leaves 1440 x 900 as it was. Portrait phones and tablets centre it in the room the copy leaves under the
   nav (.hero-room, the grid's 1fr row), 60% of the width up to 420 px (times .96) and never taller than that room less
   a --space-4 at each end: the board stands 1.2 of its width tall and its middle is .15 of its width above the anchor.
   The room is at least 2 x --space-7, so on a short phone the board gets smaller; under 615 px tall the short-screen
   rule after the portrait one takes over, and below 526 px even that hero runs past the screen. The ratios are the
   scene's own, printed by tools/hero-poster.py. */
.hero-room { position: absolute; inset: 0; z-index: 0; pointer-events: none; container-type: size; }
.hero-board { --hero-board-w: min(34.56cqw, 518.4px, calc((min(56cqh, 56svh) - var(--nav-h) - var(--space-5)) / .82)); position: absolute; left: 75%; top: min(56cqh, 56svh); width: var(--hero-board-w); height: 0; }
/* The poster's board, cut from the same export as the gradient under it (tools/hero-poster.py prints these numbers),
   placed by its offset from the anchor in percent of the board's width (margin-top percentages are of the width too). */
.hero-board > .hero-poster { inset: auto; left: -55.42%; top: 0; width: 109.58%; margin-top: -73.35%; }
.hero-board > .hero-poster img { height: auto; }
/* The hero fades into the page instead of ending in a cut with a hairline on its brightest edge (review B2.8). Above
   the scene and the poster, under the copy. */
.hero::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--space-8); background: linear-gradient(180deg, var(--scrim-none), var(--surface-page)); pointer-events: none; }
#hero + .section { border-top: 0; }
.hero-eyebrow { margin-bottom: var(--space-5); }
.hero-title { line-height: var(--leading-tight); font-size: var(--display-serif-l); }
.ai-line { display: block; white-space: nowrap; }
.ai-unfold { white-space: nowrap; }
.ai-dot { display: inline-block; color: var(--accent); }
/* Sized to the serif: its caps stand .72em tall (Syne's were .68) and its stems are thin, so the caret keeps the
   same 1.2 ratio to the capitals at a hairline width. */
.ai-caret { display: inline-block; width: .05em; height: .87em; margin-left: .04em; vertical-align: -.02em; background: var(--accent); animation: caretBlink .9s steps(2, start) infinite; }
@keyframes caretBlink { to { opacity: 0; } }
.hero-sub { margin-top: var(--space-5); max-width: 50ch; font-size: var(--text-lg); color: var(--text-body); } /* --text-body, not secondary: 48% white over the moving gradient measured 4.36:1 at 1440 (2026-10-11) */
.hero-ctas { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-6); }

/* ---------- Sections ---------- */
.section { position: relative; padding: var(--space-8) 0; border-top: 1px solid var(--border-subtle); }
.chip { display: inline-flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-pill); border: 1px solid var(--border-accent); background: var(--accent-veil); font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--text-primary); white-space: nowrap; }
.sprite { position: absolute; pointer-events: none; z-index: 3; height: auto; }

/* ---------- Team band: opening credits (2026-10-12, Eric's pick) ----------
   Between the hero and the work: where the team comes from, what it worked on, the programmes CurtainCall is part of.
   Composed like an opening-credits card: centred, the marks the subject, the labels quiet in the Pricing slate's printed
   type. The marks keep their owners' colours (Eric's decision; the distributor logos on the Pillars film's slate have
   done the same since 2026-10-13); the colour is in the image files only, so nothing here is a colour. Height is a
   base times --k, an optical factor per mark (the distributor logos' --logo-k idea; the factors are from the contact
   sheets in temporary screenshots/fix-2026-10-11/team-logos/): a bold wordmark like Meta's at the same height as the
   DreamWorks moon would shout. Width follows from each image's own ratio, which its width and height attributes carry before it loads.
   Desktop: the first group on one line, the other two side by side under it. Phone: the groups stack, the first in two
   pairs, on a smaller base. */
.section.team { padding: var(--space-7) 0; --team-base: clamp(30px, 2.9vw, 42px); }
.team-card { display: grid; justify-items: center; row-gap: var(--space-6); text-align: center; }
.team-row { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-6) var(--space-8); }
/* auto 1fr: in the second row the flex line stretches the shorter group to the taller one's height, and the spare
   height belongs to the marks' track, where they centre, so both groups' marks share one centre line and both labels
   stay one line tall. */
.team-group { display: grid; grid-template-rows: auto 1fr; justify-items: center; row-gap: var(--space-4); }
/* The slate's label type. The padding gives back the trailing tracking after the last letter, so the label centres on
   its letters rather than on the letters plus one space of tracking. */
.team-label { padding-left: var(--tracking-wider); font-size: var(--text-xs); font-weight: var(--weight-semibold); line-height: var(--leading-normal); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); text-wrap: balance; }
.team-marks { list-style: none; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-5) var(--space-6); }
.team-mark { position: relative; flex: none; transition: transform var(--duration-fast) var(--ease-out); }
.team-mark img { display: block; width: auto; max-width: none; height: calc(var(--team-base) * var(--k, 1)); transition: filter var(--duration-fast) ease; }
.team-mark--meta { --k: .7; }
.team-mark--dreamworks { --k: 1.4; }
.team-mark--paramount { --k: 1.25; }
.team-mark--usc { --k: 1; }
.team-mark--rogue-one { --k: .9; }
.team-mark--forbidden-kingdom { --k: .9; }
.team-mark--google { --k: .65; }
.team-mark--nvidia { --k: 1.1; }
.team-fine { font-size: var(--text-xs); line-height: var(--leading-normal); color: var(--text-secondary); }
/* Hover: the mark under a mouse lifts and brightens a little. The marks are not links, so there is no focus or pressed
   state to pair it with. */
@media (hover: hover) and (pointer: fine) {
  .team-mark:hover { transform: translateY(-3px); }
  .team-mark:hover .team-logo { filter: brightness(1.15); }
}
html.rm .team-mark:hover { transform: none; }
/* The light-up (home.js initTeam), only where it runs: never under reduced motion, in capture or without scripts, which
   see the authored state, every mark lit. home.js puts a grey copy of each mark (.team-dim, alt="" and aria-hidden)
   under it and arms the band; .is-dim is the unlit card, all eight marks there in dim grey and the labels faint. Taking
   .is-dim off lights the card: per mark, the colour copy fades in over the grey one, which fades out, as the mark rises
   a few pixels, each starting at its own --d (110 ms apart in reading order, a label one step before its group).
   Opacity, translate and transform only; translate carries the rise so it never fights the hover's transform. The rise
   takes the page's expo ease-out, the light a plain ease-out: on the expo curve a mark was at .64 opacity 90 ms into its
   600 ms and read as switched on rather than lit, while this one warms over about a third of a second.
   .is-instant switches the transitions off for a reset (the band dims again, unseen, once the visitor has scrolled back
   above it). */
.team-dim { position: absolute; inset: 0; filter: grayscale(1); opacity: 0; pointer-events: none; }
.team.is-armed .team-mark { transition: translate .6s var(--ease-out) var(--d, 0s), transform var(--duration-fast) var(--ease-out); }
.team.is-armed .team-logo { transition: opacity .6s ease-out var(--d, 0s), filter var(--duration-fast) ease; }
.team.is-armed .team-dim { transition: opacity .6s ease-out var(--d, 0s); }
.team.is-armed .team-label { transition: opacity .5s ease var(--d, 0s), translate .5s var(--ease-out) var(--d, 0s); }
.team.is-dim .team-mark { translate: 0 8px; }
.team.is-dim .team-logo { opacity: 0; }
.team.is-dim .team-dim { opacity: .32; }
.team.is-dim .team-label { opacity: .4; translate: 0 4px; }
.team.is-instant .team-mark, .team.is-instant .team-mark img, .team.is-instant .team-label { transition: none; }

@media (max-width: 640px) {
  .section.team { padding: var(--space-6) 0; }
  .team-card { row-gap: var(--space-5); }
  .team-row { row-gap: var(--space-5); }
  .team-group { row-gap: var(--space-3); }
  .team-marks { gap: var(--space-4) var(--space-5); }
}
/* Too narrow for the first group's four on one line: two centred lines of two, not three and one. The break is an empty
   full-width flex item ordered between the pairs (the marks keep their reading order); its own line adds a second row
   gap, so the gap is halved here to keep the pairs 16 px apart. */
@media (max-width: 480px) {
  .team-group--from .team-marks { row-gap: calc(var(--space-4) / 2); }
  .team-group--from .team-marks::before { content: ""; flex-basis: 100%; height: 0; order: 1; }
  .team-group--from .team-mark:nth-child(n + 3) { order: 2; }
}

/* ---------- Work showcase ---------- */
.section-work { padding-bottom: var(--space-6); overflow: clip; }
.work-head { margin-bottom: var(--space-6); }
.work-viewport { position: relative; }
.work-track { list-style: none; display: flex; align-items: flex-start; gap: var(--space-5); width: max-content; padding: 0 max(var(--gutter), calc((100vw - var(--container)) / 2 + var(--gutter))); will-change: transform; }
.work-card { flex: none; width: clamp(300px, min(56vw, calc((100vh - 420px) * 16 / 9)), 800px); }
.work-link { display: block; text-decoration: none; color: inherit; border-radius: var(--radius-l); outline-offset: 6px; }
.work-media { position: relative; aspect-ratio: 16 / 9; margin: 0; border-radius: var(--radius-l); overflow: hidden; background: var(--surface-raised); border: 1px solid var(--border-subtle); box-shadow: var(--shadow-card); transition: transform .7s var(--ease-out), box-shadow .7s var(--ease-out), opacity .5s ease; }
.work-media img, .work-media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; -webkit-user-drag: none; }
.work-media video { opacity: 0; transition: opacity .5s ease; }
.work-card.is-playing .work-media video { opacity: 1; }
.work-media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, var(--scrim-weak), var(--scrim-none) 45%); pointer-events: none; }
.work-card:not(.is-active) .work-media { transform: scale(.965); opacity: .72; }
@media (hover: hover) and (pointer: fine) { .work-link:hover .work-media { transform: translateY(-4px) scale(1); opacity: 1; box-shadow: var(--shadow-glow), var(--shadow-card); } }
.work-meta { display: grid; grid-template-columns: auto 1fr; column-gap: var(--space-4); padding: 20px var(--space-2) 0; transition: opacity .4s ease; }
.work-card:not(.is-active) .work-meta { opacity: .55; }
@media (hover: hover) and (pointer: fine) { .work-link:hover .work-meta { opacity: 1; } }
/* The card's number is How it works' step number (review B8.5): one numeral, Inter bold at --text-lg in even-width
   figures, so "01" is as wide as "02". It was 14px with .12em tracking here, 18px there. At this size its capitals start
   where the serif title's do, with no padding. */
.work-num { font-family: var(--font-body); font-weight: var(--weight-bold); color: var(--accent); font-size: var(--text-lg); font-variant-numeric: tabular-nums; }
.work-title { font-size: var(--display-serif-s); line-height: var(--leading-snug); }
.work-more { grid-column: 2; }
.work-line { grid-column: 2; color: var(--text-body); margin-top: var(--space-2); }
.work-facts { grid-column: 2; display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: var(--space-3) 0 0; opacity: 0; transform: translateY(6px); transition: opacity .4s ease, transform .5s var(--ease-out); }
.work-card.is-active .work-facts, .work-link:focus-visible .work-facts { opacity: 1; transform: none; }
@media (hover: hover) and (pointer: fine) { .work-link:hover .work-facts { opacity: 1; transform: none; } }
/* The site's label, as on the slate, the form and the credits (review B8.5: it was the one label at 11px, weight 400). */
.work-facts dt { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); }
.work-facts dd { font-size: var(--text-sm); color: var(--text-primary); }
/* The in-card link (review B8.6, Q24; the Pillars cards shared it until the film replaced them, and the Jancroon
   caption under the Proof stage has used it since 2026-10-16, .proof-cta): Inter 14 semibold in sentence case, cyan,
   the arrow nudging on hover. It was 12px bold capitals here. */
.work-cta, .proof-cta { grid-column: 2; display: inline-flex; align-items: center; gap: var(--space-2); margin-top: var(--space-3); color: var(--accent); font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.work-cta svg, .proof-cta svg { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.8; fill: none; stroke-linecap: round; stroke-linejoin: round; transition: transform var(--duration-base) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .work-link:hover .work-cta svg, .proof-cta:hover svg { transform: translateX(4px); } }
/* position indicator: one segment per card, wrap-aware by construction */
.work-bar { margin-top: var(--space-6); display: flex; gap: var(--space-2); height: 2px; }
.work-seg { display: block; width: 24px; height: 100%; border-radius: 1px; background: var(--border-medium); transition: background-color .3s ease, transform .3s var(--ease-out); }
.work-seg.is-on { background: var(--accent-gradient-text); transform: scaleY(1.5); }
/* Ring (fine pointer, >= 900px): an endless strip that drifts on its own and moves with sideways wheel, drag or arrows.
   Cards are glass plates; the meta sits inside the frame. The drift is the page's one carousel. */
html.work-ring .work-viewport { touch-action: pan-y; cursor: grab; user-select: none; -webkit-user-select: none; }
html.work-dragging .work-viewport, html.work-dragging .work-link { cursor: grabbing; }
html.work-ring .work-head .section-title { max-width: 26ch; }
html.work-ring .work-card { width: clamp(320px, 42vw, 640px); will-change: transform; }
html.work-ring .work-link { position: relative; padding: 6px; border-radius: var(--radius-l); will-change: transform; }
html.work-ring .work-media { border-radius: calc(var(--radius-l) - 6px); border: 0; box-shadow: none; transform: translate(var(--px, 0px), var(--py, 0px)) scale(var(--ms, 1)); transition: transform .5s var(--ease-out), opacity .5s ease; }
html.work-ring .work-card:not(.is-active) .work-media { --ms: .985; opacity: .8; }
@media (hover: hover) and (pointer: fine) {
  html.work-ring .work-link:hover .work-media { --ms: 1; opacity: 1; box-shadow: none; }
  /* No transform here: it would replace the plate's tilt, which already lifts the card 4px (--ty, initTilt). */
  html.work-ring .work-link:hover { border-color: var(--border-bright); box-shadow: var(--shadow-glow), var(--shadow-card); }
}
html.work-ring .work-media::after { background: linear-gradient(to top, var(--scrim-strong) 0%, var(--scrim-shade) 34%, var(--scrim-none) 62%); }
html.work-ring .work-meta { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; padding: 0 28px var(--space-5); color: var(--text-primary); pointer-events: none; }
/* The unfolded copy's own shade (review Must 1, 2026-10-12). Hover or focus unfolds the line and the facts, and the
   title rises to mid-card, above the reach of the scrim at rest: the copy measured 2.5 to 4.2:1 on bare art and the
   Sulver's Journey title sat on its logotype again. This layer is tied to the copy rather than to the card, so it
   covers exactly the unfolded block (whatever the card's size or a fact's wrap) and fades out 64px above the title.
   It sits under the words and over the art and the glass face, trimmed to the picture's edges (the link's 6px padding)
   and moved with the picture's parallax so its edges stay on the art. It fades in with the unfold; on the way out it
   waits until the words have mostly gone. */
html.work-ring .work-meta::before { content: ""; position: absolute; z-index: -1; top: calc(-1 * var(--space-7)); left: 6px; right: 6px; bottom: 6px;
  border-radius: 0 0 calc(var(--radius-l) - 6px) calc(var(--radius-l) - 6px);
  background: linear-gradient(to top, var(--scrim-strong), var(--scrim-medium) calc(100% - var(--space-7)), var(--scrim-none));
  transform: translate(var(--px, 0px), var(--py, 0px)); opacity: 0; transition: opacity .4s ease .15s; pointer-events: none; }
html.work-ring .work-card .work-link:focus-visible .work-meta::before { opacity: 1; transition: opacity .5s var(--ease-out); }
html.work-ring .work-facts dt { color: var(--text-body); }
html.work-ring .work-bar { margin-top: 28px; }
/* In the ring the line and facts exist only while a card is hovered or focused: they type in on enter and fade on leave, so nothing sits over the art at rest.
   At rest their rows also fold to nothing (0fr), so the title sits just above the call to action, in the dark of the
   card's bottom scrim, and the art keeps the top of the card. While the folded rows still took their full height, the
   title floated at mid-card, over the art's busiest part: on Sulver's Journey it printed across the key art's own
   logotype (audit S16, V08, P05). Hover or focus unfolds them and the title rises as the facts type in. */
html.work-ring .work-more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .5s var(--ease-out); }
html.work-ring .work-more-in { min-height: 0; overflow: hidden; }
html.work-ring .work-card .work-line, html.work-ring .work-card .work-facts { opacity: 0; transform: translateY(6px); transition: opacity var(--duration-base) ease, transform .5s var(--ease-out); }
html.work-ring .work-card .work-link:focus-visible .work-more { grid-template-rows: 1fr; }
html.work-ring .work-card .work-link:focus-visible .work-line, html.work-ring .work-card .work-link:focus-visible .work-facts { opacity: 1; transform: none; }
@media (hover: hover) and (pointer: fine) {
  html.work-ring .work-card .work-link:hover .work-more { grid-template-rows: 1fr; }
  html.work-ring .work-card .work-link:hover .work-line, html.work-ring .work-card .work-link:hover .work-facts { opacity: 1; transform: none; }
  html.work-ring .work-card .work-link:hover .work-meta::before { opacity: 1; transition: opacity .5s var(--ease-out); }
}
/* A card away from the front keeps its meta at full strength (its "02" measured 3.91:1 at .55 on the phone strip,
   audit V16, and 3.6 to 3.7:1 for the ring's number and link, review B3.1); the art alone dims. */
html.work-swipe .work-card:not(.is-active) .work-meta, html.work-ring .work-card:not(.is-active) .work-meta { opacity: 1; }
/* Keyboard focus on the list itself, which takes the arrow keys: the global ring wrapped the whole track, about 3,400px
   wide, and showed as two cyan lines across the page (review B3.6). The front card wears the ring instead, and it moves
   with the arrow keys. */
html:is(.work-ring, .work-swipe) .work-track:focus-visible { outline: none; }
html:is(.work-ring, .work-swipe) .work-track:focus-visible .work-card.is-active .work-link { outline: var(--focus-ring-width) solid var(--focus-ring); }
html.work-swipe .work-viewport { overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
html.work-swipe .work-viewport::-webkit-scrollbar { display: none; }
html.work-swipe .work-card { scroll-snap-align: center; width: min(82vw, 520px); }
html.work-grid .work-track { display: grid; grid-template-columns: repeat(2, 1fr); width: auto; padding: 0 var(--gutter); max-width: var(--container); margin: 0 auto; }
html.work-grid .work-card { width: auto; }
html.work-grid .work-card:not(.is-active) .work-media, html.work-grid .work-card:not(.is-active) .work-meta { transform: none; opacity: 1; }
@media (hover: hover) and (pointer: fine) { html.work-grid .work-link:hover .work-media { transform: translateY(-4px); } }
html.work-grid .work-facts { opacity: 1; transform: none; }
html.work-grid .work-bar { display: none; }

/* ---------- Media frames: one corner and one depth (2026-10-12, review B8.9, Q26) ----------
   A picture that stands on the page by itself (the Proof stage, the Geometry Lock still, How it works' screen and its
   phone pictures) has the large corner, a hairline rim and --shadow-glow; at 640px and under its corner steps down to
   --radius-m, so a 350px frame does not wear the curve of a 1,000px one. Until then the radius went by component (the
   still and the phone pictures at 14px, the stage and the screen at 24px, the same size a few screens apart on a phone)
   and there were four depth treatments. Pictures inside a card keep the card's nesting (the ring's plate; the Pillars
   cards' media did too, until 2026-10-13); the Work cards' pictures keep the card shadow their hover adds the glow to,
   and take the phone corner. */
.proof-stage, .gl-mesh, .how-media, .step-media { border-radius: var(--radius-l); border: 1px solid var(--border-subtle); box-shadow: var(--shadow-glow); }
@media (max-width: 640px) { .proof-stage, .gl-mesh, .how-media, .step-media, .work-media { border-radius: var(--radius-m); } }

/* ---------- Proof slider ---------- */
.proof-head { max-width: 780px; }
.proof-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-6); }
.proof-tab { font-size: var(--text-sm); color: var(--text-secondary); }
@media (hover: hover) and (pointer: fine) { .proof-tab:hover { color: var(--text-primary); } }
.proof-tab.is-active { color: var(--text-on-bright); border-color: var(--border-lit); }
/* The fill goes on the glass face (::before), never the root: see the layer rule above the buttons. */
.proof-tab.is-active::before { background: var(--accent-gradient-selected); }
.proof-stage { position: relative; margin-top: var(--space-5); aspect-ratio: 16 / 9; overflow: hidden; background: var(--surface-sunken); }
/* On a short window the stage narrows so the tabs and the whole comparison fit under the nav together (at 1280 x 650
   it was 1152 px wide and ran 146 px past the fold, review B2.5); it stays on the grid's left edge (Q8). The stage is a
   drag surface, so no text selection and no iOS long-press menu on its pictures (review B6.4). */
.proof-stage { width: min(100%, calc((100svh - var(--nav-h) - var(--space-7) - var(--space-6)) * 16 / 9)); -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.proof-panel { position: absolute; inset: 0; --split: 50; }
.proof-panel[hidden] { display: none; }
/* A true before and after (audit S13, 2026-10-11). Each source is cropped to the frame its clip was animated from, at
   the clip's own 16:9 framing, so the two layers share one box, one fit and one position, and the divide only clips
   the animated layer on top. Until then each medium was contained in its own half at its own scale (a full page beside
   a letterboxed frame 160px lower), so nothing lined up. Keep the fit and position identical on both, or the crops
   drift apart. */
.proof-before, .proof-after { position: absolute; inset: 0; background: var(--surface-sunken); }
.proof-after { clip-path: inset(0 0 0 calc(var(--split) * 1%)); }
.proof-before img, .proof-after video { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; object-position: 50% 50%; display: block; }
.proof-line { position: absolute; top: 0; bottom: 0; left: calc(var(--split) * 1%); width: 2px; margin-left: -1px; background: var(--accent); box-shadow: 0 0 18px var(--accent-glow-strong); }
.proof-handle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 46px; height: 46px; border-radius: 50%; background: var(--accent); display: flex; align-items: center; justify-content: center; cursor: ew-resize; box-shadow: 0 6px 24px var(--accent-halo); touch-action: pinch-zoom; } /* one finger drags the line; two zoom the page (review Must 4) */
.proof-handle svg { width: 20px; height: 20px; stroke: var(--text-on-bright); stroke-width: 1.8; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.proof-label, .proof-play { position: absolute; border-radius: var(--radius-pill); background: var(--scrim-medium); border: 1px solid var(--border-subtle); color: var(--text-primary); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
/* Each label rides on its own layer and fades once the line reaches it (home.js), so a name never sits on the wrong side. */
/* Tags on pictures, one recipe (review B8.5): the Proof name tags, Geometry Lock's STATUS pill and its skip cue. Until
   2026-10-12 the three shared their type but not their face: fills at ink .72, .8 and .78, three border tokens, two
   leadings and three paddings. Each keeps its own place, colour and blur. */
.proof-label, .gl-status, .gl-skip span { padding: var(--space-2) var(--space-3); border-radius: var(--radius-pill); background: var(--scrim-medium); border: 1px solid var(--border-strong); font-size: var(--text-2xs); font-weight: var(--weight-semibold); line-height: var(--leading-snug); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
.proof-label { top: var(--space-3); pointer-events: none; transition: opacity var(--duration-fast) ease; }
.proof-label.is-covered { opacity: 0; }
.proof-label-before { left: var(--space-3); }
.proof-label-after { right: var(--space-3); }
/* Reduced motion: nothing plays on its own, and the browser's own controls would be clipped by the divide (their play
   button sits under the source half), so the stage carries one play and pause button on the animated side. */
.proof-play { display: none; right: var(--space-3); bottom: var(--space-3); z-index: 2; width: var(--tap-min); height: var(--tap-min); padding: 0; align-items: center; justify-content: center; cursor: pointer; transition: transform var(--duration-fast) var(--ease-out), border-color var(--duration-fast) ease; }
html.rm .proof-play { display: inline-flex; }
.proof-play svg { width: 18px; height: 18px; fill: currentColor; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.proof-play .i-pause { display: none; fill: none; }
.proof-play.is-playing .i-play { display: none; }
.proof-play.is-playing .i-pause { display: inline; }
@media (hover: hover) and (pointer: fine) { .proof-play:hover { border-color: var(--border-bright); } }
.proof-play:active { transform: scale(var(--state-pressed-scale)); }
.proof-captions { margin-top: var(--space-4); min-height: 3em; }
/* 42ch, which in Inter holds 52 to 54 characters a line (review B1.5, Q2; the 52ch set earlier on 2026-10-12 held about
   67, because 1ch is the width of a zero, wider than the average letter). The FAQ answers and the rate line use the same. */
.proof-caption { display: none; color: var(--text-body); font-size: var(--text-md); max-width: 42ch; }
.proof-caption strong { color: var(--text-primary); }
.proof-caption.is-active { display: block; }
/* The Jancroon caption ends in the in-card link (above) to its case study (Eric, 2026-10-16); the other two samples have
   none. It sits inside its caption, so it shows, hides and leaves the Tab order with it. Its own line, no underline: the
   arrow is the mark. On a touch screen the box grows to 47.8 px (--space-3 above and below the 23.8 px line) while the
   margins give the space back, so the link sits where it does under a mouse and nothing below it moves. */
.proof-cta { display: flex; width: fit-content; text-decoration: none; transition: color var(--duration-fast) ease; }
@media (pointer: coarse) { .proof-cta { margin-top: 0; padding-block: var(--space-3); margin-bottom: calc(-1 * var(--space-3)); } }

/* ---------- Geometry Lock ---------- */
/* Full-bleed stage (assets/glock.js). The section is one screen that pins for five viewports, and the pin's progress
   drives every layer: a screen of dots gathers into Foxman, the alley fades in around him, the wheel scrubs the shot
   frame by frame, it holds at 6.5 s while the dots form the titan, and the punch lands on a white flash that throws
   the dots outward and hands over to the title card.
   Layer order inside the stage, every one absolute inset 0: poster (frame 0), clip, flash, dots canvas, card.
   Poster and clip are explicit cover boxes rather than object-fit alone, so glock.js can pan them with a transform
   (object-position does not run on the compositor) and map the dot targets through the same numbers.
   The card hides with opacity alone, never visibility or display: a screen reader cannot drive a pin, so its heading,
   body and chips stay in the accessibility tree the whole way through.
   html.glock-arrival (no anime.js: the same sequence on a clock; every phone until 2026-10-14, when phones began to
   pin as desktops do) keeps the stage in flow at one screen; the static set
   (html.glock-static, reduced motion, capture, no JS, saved data; matched on the gate classes so nothing flashes
   before glock.js runs) shows the dimmed frame behind a scrim with the card at rest, and no canvas, clip or flash. */
#geometry-lock { padding: 0; border-top: 0; }
/* Pin mode (assets/glock.js): the section is as tall as the sequence plus one screen and the stage sticks to its top,
   so the browser holds it while the page scrolls through. anime.js reads that scroll and drives the sequence. Sticky
   is the browser's own pin: no spacer element, no switch to position: fixed, and so no jump where the pin begins
   and ends, which the GSAP pin it replaced (2026-10-03) could not promise. --gl-pin is the length in screens, set
   from the tuner. Arrival and static modes keep the section one screen tall.
   Two viewport heights, both constant, never dvh (2026-10-14, Eric on his iPhone: "The dark strip under the film
   bothers me"). A phone's browser shows its toolbar, then slides it away as the page scrolls down, so the screen is
   100svh tall at first and 100lvh once the bar has gone (80 to 100 px more on an iPhone). A 100svh stage left that
   strip of page showing under the film; a 100lvh one put its lowest words under the bar while it shows. So the stage
   is 100lvh and holds the picture (clip, poster, dots, flash, scrim), which reaches the screen's bottom edge in both
   states, and the words (the opening title, the card) sit in a frame 100svh tall at its top, which is always on screen.
   dvh would follow the bar, so the stage would resize, and glock.js resample, in the middle of a scrub. The section is
   pin screens of 100svh plus the stage, so the sticky stage travels pin * 100svh, exactly the range anime.js scrubs
   (it measures the window as 100lvh, so its range is the section less 100lvh). Where the two units agree (a desktop, a
   tablet, any emulator) this is the stylesheet it replaced, to the pixel. */
html.glock-pin #geometry-lock { height: calc(var(--gl-pin, 5) * 100svh + 100lvh); }
html.glock-pin .gl-stage { position: sticky; top: 0; height: 100lvh; }
.gl-stage { position: relative; width: 100%; height: 100svh; overflow: clip; background: var(--surface-page); }
.gl-poster, .gl-clip, .gl-flash, .gl-dots, .gl-card { position: absolute; inset: 0; }
html.glock-pin :is(.gl-card, .gl-opening) { bottom: auto; height: 100svh; } /* the words' frame (above) */
/* The cover box is sized by glock.js (sample()), which needs the same numbers to place the dots: a CSS max() against
   svh disagreed with it on phones, where it collapsed to the viewport width and left a bare strip beside the film.
   These values are what the static modes get, and what is on screen until the script has measured. */
.gl-poster img, .gl-clip { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; background: var(--surface-sunken); }
.gl-poster, .gl-clip { opacity: 0; }
.gl-dots { pointer-events: none; max-width: none; } /* max-width: the base reset clamps canvases to 100% */
/* The opening title (2026-10-12, review B4.1, Eric's choice over starting the alley earlier): the film opened on
   three quarters of a screen of near-black, which read as a stalled load. The section's name now holds that stretch,
   in the eyebrow's type at the centre of the stage, and glock.js fades it out as the dots gather (seg().open, from
   lead to the alley's first light). The animated modes only: the static set, no JS and capture keep their layout. */
.gl-opening { position: absolute; inset: 0; z-index: 1; display: none; place-items: center; margin: 0; pointer-events: none; }
html:is(.glock-pin, .glock-arrival) .gl-opening { display: grid; }
/* The punch: a white-hot core on the contact point falling to cyan and then to nothing, so it reads as a blast out of
   the fist with the frame still visible around it. A full-screen wash looked like a pastel fade, not a hit. No blend
   mode: a stacking context here would cost a paint for nothing. */
.gl-flash { pointer-events: none; opacity: 0; background: radial-gradient(ellipse 58% 62% at var(--gl-ix, 50%) var(--gl-iy, 50%), var(--flash-core) 0%, var(--flash-mid) 20%, var(--flash-edge) 46%, transparent 76%); }
/* The words take what the still leaves; the still's column is exactly the still, so the space between them is the
   grid's gap and nothing else (it was centred in half the row until 2026-10-12, leaving 75 px idle either side). */
.gl-card { z-index: 2; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-7) var(--space-8); align-items: center; padding-top: calc(var(--nav-h) + var(--space-2)); padding-bottom: var(--space-5); pointer-events: none; }
/* Hidden until the film brings it in (pin and arrival write its opacity inline). Not in the static set: glock.js
   stamps glock-static for saved data, 2g and a mask that fails to load too, and until 2026-10-12 this rule (0,4,1)
   outranked the static set's opacity: 1 (0,2,1), so a Save-Data visitor saw the dimmed frame with no card. */
html.js:not(.rm):not(.cap):not(.glock-static) .gl-card { opacity: 0; }
/* Static: the frame dimmed and desaturated under a left-weighted scrim, so the copy clears 4.5:1 over the alley. */
html:is(.glock-static, .rm, .cap, :not(.js)) .gl-poster { opacity: 1; }
html:is(.glock-static, .rm, .cap, :not(.js)) .gl-poster img { filter: saturate(.7) brightness(.5); }
html:is(.glock-static, .rm, .cap, :not(.js)) :is(.gl-dots, .gl-clip, .gl-flash) { display: none; }
html:is(.glock-static, .rm, .cap, :not(.js)) .gl-stage::before, html:is(.glock-pin, .glock-arrival) .gl-stage::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: linear-gradient(90deg, var(--scrim-strong), var(--scrim-medium) 45%, var(--scrim-weak)), radial-gradient(circle at 50% 28%, var(--accent-wash), transparent 60%); }
/* The animated modes end on the same picture (2026-10-12): after the punch the opening frame comes back dim under this
   scrim, both driven by --gl-end from glock.js, so the film does not drain to an empty page behind the title card. */
html:is(.glock-pin, .glock-arrival) .gl-stage::before { opacity: var(--gl-end, 0); }
html:is(.glock-static, .rm, .cap, :not(.js)) .gl-card { opacity: 1; pointer-events: auto; }
/* One dot field on screen at a time: the page dust parks and fades as soon as the stage arrives (CC.dust.pause clears
   its inline opacity so this rule can take over), and the nav steps out of the film while the stage is pinned.
   :not(:focus-within) so tabbing into the nav still brings it back, as .nav:focus-within does elsewhere. */
html.glock-near .dust { opacity: 0; }
/* will-change only while the stage is actually in play. These are three layers the size of the viewport, and left
   on permanently they stay promoted after the section is far behind you, so the compositor pays for them on every
   frame of the rest of the page. Measured 2026-09-25: scrolling past Geometry Lock dropped the page from 60fps to
   25, and clearing will-change alone restored it. glock-near is added and removed by an IntersectionObserver on
   the stage in glock.js, which is exactly the window in which these do animate. */
html.glock-near :is(.gl-poster img, .gl-clip) { will-change: transform, opacity; }
html.glock-near .gl-flash, html.glock-near .gl-stage::before { will-change: opacity; }
html.glock-pinned .nav:not(:focus-within) { transform: translateY(-100%); opacity: 0; }
/* Arrival mode (html.glock-arrival, the sequence on a clock: a browser without anime.js, and every phone until
   2026-10-14): the words come in over the film after the first lock rather
   than at the end (audit S6), so they bring their own scrim, faded in with them through --gl-cc, which glock.js sets.
   On one column the still takes the top of the card (its place is held from the start, so nothing moves when it
   appears at the end) and the words sit under it, low on the screen, while the film plays above them. The scrim is
   the words' own box at --scrim-strong with a soft top edge: the body copy is --text-secondary, which needs the .92
   to clear 4.5:1 over the brightest frames (Foxman's white suit); a .78 band measured 4.25:1 there. The wide
   two-column layout (arrival without anime.js on a desktop) keeps a left-weighted scrim over the whole card. */
html.glock-arrival .gl-card::before { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: var(--gl-cc, 0); background: linear-gradient(90deg, var(--scrim-strong), var(--scrim-medium) 40%, var(--scrim-none) 72%); }
@media (max-width: 1024px) {
  html.glock-arrival .gl-card { align-content: end; }
  html.glock-arrival .gl-card::before { content: none; }
  html.glock-arrival .gl-visual { order: -1; }
  html.glock-arrival .gl-copy { position: relative; }
  html.glock-arrival .gl-copy::before { content: ""; position: absolute; inset: -96px calc(-1 * var(--gutter)) -24px; z-index: -1; pointer-events: none; opacity: var(--gl-cc, 0); background: linear-gradient(to top, var(--scrim-strong) calc(100% - 96px), var(--scrim-none)); }
}
/* The skip cue on phones (glock.js tapToFinish, final verify 2026-10-11): the still's STATUS pill in a quieter voice,
   top right and clear of the nav. The button is the 44 px target; the pill inside it is what shows, and carries the
   focus ring. glock.js makes it only in arrival mode and removes it when the run ends, so static mode (reduced motion,
   capture, no JS) never has one. */
.gl-skip { position: absolute; z-index: 3; top: calc(var(--nav-h) + var(--space-2)); right: calc(var(--gutter) - var(--space-2)); min-width: var(--tap-min); min-height: var(--tap-min); padding: 0 var(--space-2); display: inline-flex; align-items: center; justify-content: center; }
.gl-skip span { display: block; color: var(--text-body); transition: color var(--duration-fast) ease, transform var(--duration-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .gl-skip:hover span { color: var(--text-primary); } }
.gl-skip:focus-visible { outline: none; }
.gl-skip:focus-visible span { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: 2px; }
.gl-skip:active span { transform: scale(var(--state-pressed-scale)); }
.gl-subhead { margin-top: var(--space-4); font-size: var(--text-lg); color: var(--text-primary); }
.gl-body { margin-top: var(--space-4); color: var(--text-body); max-width: 56ch; }
.gl-body strong { color: var(--text-primary); }
/* The still: as tall as the screen allows under the nav, so Foxman reads at a glance (380 x 570 on a 1440 x 900
   screen until 2026-10-12). It leans toward the cursor once the card is in (glock.js lockTilt writes --rx and --ry;
   the glass plates' recipe: quick while the cursor moves, an ease-out home on leave). The 3D transform is only on
   while the section is near, so its layer is not kept for the rest of the page. */
.gl-mesh { position: relative; aspect-ratio: 400 / 600; height: min(780px, calc(100svh - var(--nav-h) - 2 * var(--space-5))); width: auto; margin: 0 auto; overflow: hidden; background: var(--surface-sunken);
  --rx: 0deg; --ry: 0deg; transition: transform var(--duration-slow) var(--ease-out); }
html.glock-near .gl-mesh { transform: perspective(1000px) rotateX(var(--rx)) rotateY(var(--ry)); }
.gl-mesh.is-tilting { transition-duration: .12s; }
/* The still's arrival out of the punch (glock.js cardFrame) animates its transform, opacity and blur; will-change
   only for the few hundred px of scroll it takes. */
.gl-visual.is-arriving { will-change: transform, opacity, filter; }
.gl-mesh-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: saturate(.8) brightness(.8); }
.gl-mesh::before { content: ""; position: absolute; inset: 0; z-index: 1; background: linear-gradient(to top, var(--scrim-medium), transparent 55%), radial-gradient(circle at 50% 28%, var(--accent-wash), transparent 60%); }
/* The lock (glock.js, 2026-10-12): the dots trace Foxman on .gl-mesh-dots while the pill reads TRACING over a
   pulsing dot; at the close Foxman lights (his own pixels of the still at full strength, above the scrim, so he
   comes out of the frame while the alley stays down) and the pill flips to LOCKED with one ping. Without the
   is-tracing class, which glock.js sets only in its animated modes, this is the end state: static, capture and
   no JS show Foxman lit and LOCKED. */
.gl-mesh-lit { position: absolute; inset: 0; z-index: 2; width: 100%; height: 100%; object-fit: cover; pointer-events: none; transition: opacity .7s var(--ease-out); }
.gl-mesh.is-tracing .gl-mesh-lit { opacity: 0; transition-duration: var(--duration-fast); }
.gl-mesh-dots { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; max-width: none; pointer-events: none; }
/* The still is the held frame and the STATUS · LOCKED pill. The cyan pose skeleton and the four corner ticks read as
   AI vision software right after the film had made its point with no chrome at all (launch audit, 2026-10-11,
   approved by Eric): the ticks are gone, and the skeleton shows only while its anchors are authored (?anchors=1,
   which stamps html.gl-anchors in glock.js). */
.gl-mesh-svg { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; display: none; }
html.gl-anchors .gl-mesh-svg { display: block; }
.gl-mesh-anchors circle { animation: glPulse 2.4s ease-in-out infinite; }
.gl-mesh-anchors circle:nth-child(2n) { animation-delay: .4s; }
.gl-mesh-anchors circle:nth-child(3n) { animation-delay: .8s; }
@keyframes glPulse { 0%, 100% { opacity: .65; } 50% { opacity: 1; } }
.gl-status { position: absolute; top: var(--space-3); right: var(--space-3); z-index: 4; display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text-primary); white-space: nowrap; } /* nowrap: a short phone's still is about 190 px wide, where STATUS · TRACING broke in two */
.gl-status-dot { position: relative; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); }
/* Both words share one grid cell, so the pill keeps its width when TRACING turns to LOCKED. The leaving word goes
   first and the arriving one follows, so the two never overlap half drawn in the cell. */
.gl-status-word { display: inline-grid; }
.gl-status-word > span { grid-area: 1 / 1; transition: opacity .12s ease; }
.gl-mesh:not(.is-tracing) .gl-status-lock, .gl-mesh.is-tracing .gl-status-trace { transition: opacity var(--duration-fast) ease .12s; }
.gl-status-trace, .gl-mesh.is-tracing .gl-status-lock { opacity: 0; }
.gl-mesh.is-tracing .gl-status-trace { opacity: 1; }
.gl-mesh.is-tracing .gl-status-dot { animation: glTracing .9s ease-in-out infinite; }
.gl-mesh.is-locked .gl-status-dot::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--accent); opacity: 0; animation: glPing .8s var(--ease-out); }
@keyframes glTracing { 0%, 100% { opacity: .35; transform: scale(.7); } 50% { opacity: 1; transform: scale(1); } }
@keyframes glPing { from { opacity: .9; transform: scale(1); } to { opacity: 0; transform: scale(3.2); } }

/* ---------- Pillars film (assets/film.js, 2026-10-13) ---------- */
/* A film the scroll plays, Geometry Lock's stage and engine (the block above), that ends in the hero's own 3D scene:
   "Our promise to you." in white over the sunken city, the Comics words over it, the city dissolves into the crowd
   (Concept art), the dragon lunges and screams (Sound), and the picture fades into the hero's clapperboard, drawn live
   (film.js mounts it in div.pf-gl), which claps shut while the camera moves in to its slate, where the five
   distributors' logos come up with the Distribution words. It replaced the four-card works wheel (assets/wheel.js,
   2026-10-05 to 2026-10-13), whose rules went with it.
   Layers inside the stage, each absolute inset 0, in DOM order: poster (frame 0), clip, the live canvas (while near the
   finale), the scrim, the title, the words. The words' layer, .pf-copy, also holds the end image (.pf-end) first, the
   live finale's last frame rendered from the same scene, under the slate when the live scene cannot run. Poster and
   clip are cover boxes sized by film.js (layout()), the end image too (layoutEnd()). Everything that moves is written
   inline by film.js as opacity and transform. The words hide with opacity alone, so a screen reader, which cannot drive
   a pin, keeps all four headings the whole way through.
   The end image's boxes, as fractions of each render (the blank rows of the slate's body, then its whole face), from
   the render's printout (temporary screenshots/film-2026-10-13/film-live-finale.md): --pf-l-* for the 16:9 image (the
   live framing at 16:9: the body .7 of the width, the face centred), --pf-p-* for the 9:16 (the face across the width
   less two gutters, the body centred). film.js lays the slate out on them for the stand-in, and the static layout below
   lays the logos on them. The 16:9 render is the refracting glass of a desktop, the 9:16 the standard glass every
   touch screen draws (the hero's tier rule, clapper.js), so a phone's live board takes over from the picture unchanged. */
#pillars { padding: 0; border-top: 0; }
/* svh and lvh as Geometry Lock's stage (its block above has the why): the stage is 100lvh, so the picture reaches the
   screen's bottom edge with a phone's toolbar shown or gone, and the words sit in .pf-copy, a frame 100svh tall at its
   top. film.js lays the finale out in that frame and gives the live canvas a box that reaches the stage's bottom too. */
html.film-pin #pillars { height: calc(var(--pf-pin, 6) * 100svh + 100lvh); }
html.film-pin .pf-stage { position: sticky; top: 0; height: 100lvh; }
html.film-pin .pf-copy { bottom: auto; height: 100svh; }
html.film-pin .pf-title { top: 50svh; }
.pf-stage { position: relative; width: 100%; height: 100svh; overflow: clip; background: var(--surface-page);
  --pf-l-bx: .1501; --pf-l-by: .4711; --pf-l-bw: .6998; --pf-l-bh: .4178; --pf-l-fx: .1071; --pf-l-fy: .0375; --pf-l-fw: .7858; --pf-l-fh: .925;
  --pf-p-bx: .0943; --pf-p-by: .4234; --pf-p-bw: .8115; --pf-p-bh: .1533; --pf-p-fx: .0444; --pf-p-fy: .2643; --pf-p-fw: .9111; --pf-p-fh: .3394; }
.pf-poster, .pf-clip, .pf-end, .pf-scrim, .pf-copy, .pf-gl { position: absolute; inset: 0; }
.pf-poster img, .pf-clip, .pf-end img { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; background: var(--surface-sunken); }
.pf-clip, .pf-end, .pf-gl { opacity: 0; }
.pf-gl { pointer-events: none; }
/* The end image as a stand-in (film.js): where it is drawn shorter or narrower than the stage, so that the slate's face
   is not cut (a phone narrower than 9:16 is short of it top and bottom), those two edges fade into the page rather than
   end in a line. The colour is only the mask's alpha. A picture short on both axes cannot happen: it is drawn at least
   as large as the stage on one. */
.pf-end.is-banded-y img { -webkit-mask-image: linear-gradient(to bottom, transparent, var(--surface-page) 9%, var(--surface-page) 91%, transparent); mask-image: linear-gradient(to bottom, transparent, var(--surface-page) 9%, var(--surface-page) 91%, transparent); }
.pf-end.is-banded-x img { -webkit-mask-image: linear-gradient(to right, transparent, var(--surface-page) 9%, var(--surface-page) 91%, transparent); mask-image: linear-gradient(to right, transparent, var(--surface-page) 9%, var(--surface-page) 91%, transparent); }
/* Under the white words only (film.js writes --pf-scrim as the brightest of them). Low and to the left, where the words
   sit; the title carries its own, and the slate's dark body carries the Distribution words. */
.pf-scrim { z-index: 1; pointer-events: none; opacity: var(--pf-scrim, 0); background: linear-gradient(to top, var(--scrim-strong), var(--scrim-medium) 30%, var(--scrim-none) 64%), linear-gradient(90deg, var(--scrim-medium), var(--scrim-none) 58%); }
/* Pinned, the scrim covers the 100lvh stage but its fall is measured from the bottom of the words' 100svh frame (the
   same stops as above, as lengths), so the words keep the contrast they were checked at, and the strip below the
   frame, behind a phone's toolbar or bare once it has gone, carries the scrim's full strength. Equal units: the rule
   above. */
html.film-pin .pf-scrim { background: linear-gradient(to top, var(--scrim-strong) calc(100lvh - 100svh), var(--scrim-medium) calc(100lvh - 70svh), var(--scrim-none) calc(100lvh - 36svh)), linear-gradient(90deg, var(--scrim-medium), var(--scrim-none) 58%); }
/* The film's first words (Eric, 2026-10-13: "Our promise to you."), large and centred in the page's white over the
   city's first frame, on a soft dark field of their own (::before), so the light falling from the top of the frame
   never thins them. film.js lifts the title 40 px as it fades, as the drift gets going; the field leaves with it. */
.pf-title { position: absolute; left: 0; right: 0; top: 50%; translate: 0 -50%; z-index: 2; margin: 0; padding: 0 var(--gutter); pointer-events: none;
  font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display);
  font-size: var(--display-serif-l); line-height: var(--leading-tight); color: var(--text-primary); text-align: center; text-wrap: balance; }
.pf-title::before { content: ''; position: absolute; z-index: -1; inset: -70% 4%; background: radial-gradient(closest-side, var(--scrim-medium), var(--scrim-weak) 55%, var(--scrim-none)); }
.pf-copy { z-index: 3; pointer-events: none; }
/* The white words, one at a time, low on the left edge of the page's container. */
.pf-text { position: absolute; left: max(var(--gutter), calc((100% - var(--container)) / 2 + var(--gutter))); bottom: var(--space-7); width: min(36rem, calc(100% - 2 * var(--gutter))); }
.pf-h { margin-top: var(--space-4); font-family: var(--font-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); font-size: var(--display-serif-m); line-height: var(--leading-tight); color: var(--text-primary); text-wrap: balance; }
/* Each line is one short paragraph (about 530 px at 18 px), so it runs the column's width and holds one line on a
   desktop; a 34ch measure broke them a word after the first sentence ("You / get isolated stems."). */
.pf-p { margin-top: var(--space-4); font-size: var(--text-lg); line-height: var(--leading-normal); color: var(--text-primary); text-wrap: pretty; }
/* Hidden until film.js has run (it then writes every one of them inline). Not in the static set, which shows them. */
html.js:not(.film-static) :is(.pf-text, .pf-logos li, .pf-note) { opacity: 0; }
/* The slate: film.js places it on the clapperboard's blank body (left, top, width and min-height in px), as the live
   scene reports it at the end of the move, and moves it with the body while the move settles (a transform); these are
   the box's numbers for a 16:9 stage, until it has. The logos come first on the slate, the fine print under them and
   the words under that; the words lead in the markup, so the order is set here. */
.pf-slate { position: absolute; left: 15%; top: 47.11%; width: 70%; min-height: 41.79%; z-index: 3; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: var(--space-4); padding-block: var(--space-4); text-align: center; }
/* Portrait, when the body is too short for the words (film.js placeSlateOn, by measurement): the slate starts at the
   body's top, the logos fill the body's box, and the fine print and the words sit under the slate's face, --pf-below
   lower. The logos scale with the body's width (a phone's 315 px body keeps the 18 px floor). */
.pf-slate.is-split { display: grid; grid-template-rows: var(--pf-box-h) var(--pf-below) auto auto; row-gap: 0; justify-items: center; align-items: start; padding-block: 0; }
.pf-slate.is-split .pf-logos { grid-row: 1; align-self: center; max-width: var(--pf-box-w); --logo-base: clamp(18px, calc(var(--pf-box-w) * .034), 30px); }
.pf-slate.is-split .pf-note { grid-row: 3; }
.pf-slate.is-split .pf-text { grid-row: 4; }
/* film.js stamps is-tight when the words need more height than the blank body has: the chip goes (the heading says the
   same) and the heading takes the card-title step, so nothing runs over the slate's printed rules. */
.pf-slate.is-tight:not(.is-side) .pf-chip { display: none; }
.pf-slate.is-tight .pf-h { margin-top: 0; font-size: var(--display-serif-s); }
/* The last step, for a split slate only (film.js): when even that leaves the words below a phone's screen, the
   paragraph takes the body copy's size, which holds it to two lines on a 360 px phone instead of three. */
.pf-slate.is-tighter .pf-p { font-size: var(--text-md); }
.pf-slate .pf-text { position: static; width: auto; order: 1; margin-top: var(--space-4); }
.pf-slate .pf-p { max-width: 27rem; margin-inline: auto; } /* breaks after the first sentence on a desktop */
/* The row is sized by the viewport, with the slate's body as a ceiling (film.js fitRow and stackDown, --pf-logo-max): a
   wide window 560 to 660 px tall frames the board by its height, so its body is narrower than 2.1vw of logos would be. */
.pf-logos { order: -1; list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-5) var(--space-6); --logo-base: min(clamp(18px, 2.1vw, 30px), var(--pf-logo-max, 30px)); }
/* In their own colours (Eric, 2026-10-13), as images of the files in Brand_Assets/web/logos/, which hold the colour, so
   no stylesheet does. --logo-k evens their optical weight, as on the distribution card before. */
.pf-logo { display: block; width: auto; max-width: none; height: calc(var(--logo-base) * var(--logo-k, 1)); }
.pf-logo--disney { --logo-k: 1.5; }
.pf-logo--netflix { --logo-k: .95; }
.pf-logo--alibaba { --logo-k: .78; }
.pf-logo--iqiyi { --logo-k: .8; }
.pf-logo--tencent { --logo-k: .76; }
.pf-note { max-width: 44ch; margin: 0 auto; font-size: var(--text-xs); line-height: var(--leading-normal); color: var(--text-secondary); }
/* Short landscape screens (film.js, SIDE_MQ: landscape, 540 px tall or less; Eric, 2026-10-14): the end side by side.
   The slate's box is the board's blank body and holds the logos alone, three over two (the max-width lets three of
   them in a row and not four), .06 of the body's width (14 to 30 px, the stacked slate's own ceiling), so the two rows
   fill about three quarters of it on every screen, as on a desktop's slate; the Distribution words and the fine print leave
   its flow for a column on the right, which film.js places (--pf-side-x, -w, -y, -ny, from the slate's corner) and
   centres down the screen. Tightened, the heading takes the card-title step and keeps its chip, then the paragraph the
   body copy's size (is-tighter, above), and last the chip goes (is-tightest). The stand-in, the end image, is drawn as
   the static layout's card in the board's place (is-card). */
.pf-slate.is-side { padding-block: var(--space-3); }
.pf-slate.is-side .pf-logos { --logo-base: clamp(14px, calc(var(--pf-box-w) * .06), 30px); gap: var(--space-4) var(--space-5); max-width: calc(var(--logo-base) * 16.37 + 3 * var(--space-5) - 1px); }
.pf-slate.is-side :is(.pf-text, .pf-note) { position: absolute; left: var(--pf-side-x); bottom: auto; width: var(--pf-side-w); margin: 0; text-align: left; } /* bottom: .pf-text's own, for the words over the film */
.pf-slate.is-side .pf-text { top: var(--pf-side-y); }
/* The fine print sits on the bare gradient here, not the slate's dark body: at --text-secondary it measured 3.3 to 3.8 to
   1 there, at --text-body 5.7 and up. */
.pf-slate.is-side .pf-note { top: var(--pf-side-ny); max-width: none; color: var(--text-body); text-wrap: pretty; }
.pf-slate.is-side .pf-p { max-width: none; margin-inline: 0; }
.pf-slate.is-side.is-tight .pf-h { margin-top: var(--space-4); }
.pf-slate.is-side.is-tightest .pf-chip { display: none; }
.pf-slate.is-side.is-tightest .pf-h { margin-top: 0; }
.pf-end.is-card img { border-radius: var(--radius-l); }
/* will-change only while the stage is near (film-near, an IntersectionObserver in film.js), the window in which these
   animate: left on, they would stay promoted far below (Geometry Lock's lesson of 2026-09-25). The scrim is the one
   screen-sized layer; the title's box is its lines, the clip and the live canvas are layers of their own anyway. */
html.film-near :is(.pf-title, .pf-text, .pf-logos li) { will-change: transform, opacity; }
html.film-near .pf-scrim { will-change: opacity; }
html.film-near .dust { opacity: 0; }
html.film-pinned .nav:not(:focus-within) { transform: translateY(-100%); opacity: 0; }
/* The skip on phones (film.js tapToFinish): Geometry Lock's .gl-skip, top right and clear of the nav. */
.pf-skip { position: absolute; z-index: 4; top: calc(var(--nav-h) + var(--space-2)); right: calc(var(--gutter) - var(--space-2)); min-width: var(--tap-min); min-height: var(--tap-min); padding: 0 var(--space-2); display: inline-flex; align-items: center; justify-content: center; }
.pf-skip span { display: block; color: var(--text-body); transition: color var(--duration-fast) ease, transform var(--duration-fast) var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .pf-skip:hover span { color: var(--text-primary); } }
.pf-skip:focus-visible { outline: none; }
.pf-skip:focus-visible span { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: 2px; }
.pf-skip:active span { transform: scale(var(--state-pressed-scale)); }
@media (max-width: 700px) {
  .pf-text { bottom: var(--space-6); }
}
/* Phone lines (2026-10-14, Eric's copy, temporary screenshots/film-2026-10-13/phone-copy.md). Since phones pin both films,
   each beat gets a swipe or so on screen, not the time a desktop reader has: up to 700 px wide each heading carries one
   short line (.phone-line, data-phone-line names the beat) in place of its desktop line (.wide-line), and Geometry Lock's
   one line takes the subhead's place, its paragraph going. Both versions are in the markup, and the hidden one is
   display: none, so only the one on screen is in the accessibility tree. The fine print keeps its full text. */
.phone-line { display: none; }
@media (max-width: 700px) {
  .phone-line { display: inline; }
  .wide-line, .gl-body { display: none; }
  /* The fine print keeps its full text on a phone (Eric, 2026-10-14), so it is fitted instead: two balanced lines, never
     wider than the split slate, which runs gutter to gutter (its 44ch is 324 px, the slate's width at 360; a narrower
     phone would have pushed it into the gutters), and at --text-body, because under the face it sits on the bare
     gradient: --text-secondary measured 3.9 to 4.0 to 1 where the gradient's blue band runs behind it at 360 to 390
     wide, --text-body 7.3 and up (the side-by-side layout's own finding, above). Phones only: the 834 px tablet's split
     slate is as it was. */
  .pf-slate.is-split .pf-note { max-width: min(44ch, 100%); color: var(--text-body); text-wrap: balance; }
}
/* Arrival mode (no anime.js; every phone until 2026-10-14): the stage is not pinned, so it sits wherever the scroll
   leaves it, often partly off screen (64 px
   down after a /#pillars landing, its bottom 64 px below the screen). film.js measures the part on screen on every
   scroll and moves what carries words into it, by translations only: the white words and their scrim rise by the strip
   below the screen (--pf-rise), the title stays centred on the part on screen (--pf-mid), the skip drops by the strip
   above it (--pf-drop), and the end, the board with its slate, rises as far as its last words need (--pf-lift). The
   reasons and the limits are in film.js ("the part of the stage that is on screen"). */
html.film-arrival :is(.pf-scrim, .pf-copy > .pf-text) { translate: 0 calc(-1 * var(--pf-rise, 0px)); }
html.film-arrival .pf-title { translate: 0 calc(-50% + var(--pf-mid, 0px)); }
html.film-arrival .pf-skip { translate: 0 var(--pf-drop, 0px); }
html.film-arrival :is(.pf-gl, .pf-end, .pf-slate) { translate: 0 calc(-1 * var(--pf-lift, 0px)); }
/* Side by side, the words beside the board are centred on the part of the stage on screen (--pf-side-mid), not lifted. */
html.film-arrival .pf-slate.is-side :is(.pf-text, .pf-note) { translate: 0 calc(var(--pf-lift, 0px) + var(--pf-side-mid, 0px)); }
/* Static (reduced motion, capture, no JS, saved data, ?pillars=static; matched on the gate classes so nothing flashes
   before film.js runs, and never under ?pillars=<p>, which draws the film): no pin, no film, no 3D scene and nothing
   fetched but one picture. The title, the four texts in a row (two by two on a tablet, one column on a phone), then the
   end image, the clapperboard's slate, with the five logos laid on its blank body (an absolutely positioned grid item,
   whose containing block is its grid area, the picture's own), and the fine print under it. On a portrait screen the
   9:16 render is cropped to a square around the slate's face (object-position), and its body box follows the crop. */
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-stage { height: auto; display: flex; flex-direction: column; gap: var(--space-6); padding: calc(var(--nav-h) + var(--space-7)) 0 var(--space-8); }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) :is(.pf-poster, .pf-clip, .pf-scrim, .pf-gl) { display: none; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-title { position: relative; left: auto; right: auto; top: auto; translate: none; width: 100%; max-width: var(--container); margin: 0 auto; text-align: left; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-title::before { display: none; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-copy { position: relative; inset: auto; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-areas: "comics concept sound dist" "fig fig fig fig" "note note note note"; gap: var(--space-6); width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); pointer-events: auto; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-slate { display: contents; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-text { position: static; width: auto; margin: 0; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-text--comics { grid-area: comics; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-text--concept { grid-area: concept; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-text--sound { grid-area: sound; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-text--distribution { grid-area: dist; text-align: left; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-slate .pf-p { margin-inline: 0; }
/* Four columns of about 260 px: the card-title step, two lines at most. */
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-h { font-size: var(--display-serif-s); }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-p { font-size: var(--text-md); }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-end { grid-area: fig; position: relative; inset: auto; display: block; opacity: 1; margin-top: var(--space-4); border-radius: var(--radius-l); overflow: clip; aspect-ratio: 16 / 9; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-end img { position: absolute; inset: 0; width: 100%; height: 100%; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-logos { grid-area: fig; position: absolute; margin-top: var(--space-4); left: calc(var(--pf-l-bx) * 100%); width: calc(var(--pf-l-bw) * 100%); top: calc(var(--pf-l-by) * (100% - var(--space-4))); height: calc(var(--pf-l-bh) * (100% - var(--space-4)));
  container-type: inline-size; padding: 0 4%; gap: var(--space-4) var(--space-5); --logo-base: clamp(14px, 3.3cqi, 34px); }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-note { grid-area: note; margin: 0; max-width: none; text-align: left; }
html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) :is(.pf-text, .pf-logos li, .pf-note) { opacity: 1; }
@media (max-width: 1024px) {
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-copy { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-areas: "comics concept" "sound dist" "fig fig" "note note"; }
}
@media (max-width: 700px) {
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-stage { padding-bottom: var(--space-7); }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-copy { grid-template-columns: minmax(0, 1fr); grid-template-areas: "comics" "concept" "sound" "dist" "fig" "note"; }
}
/* Portrait: the 9:16 render, cropped to a square centred on the slate's face (--pf-p-fy + --pf-p-fh / 2 of the
   picture's height, which is 16 / 9 of the square's side), so the logos' box moves up by the crop: its top is
   (by - face centre) x 16 / 9 + .5 of the square, its height bh x 16 / 9. */
@media (orientation: portrait) {
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-end { aspect-ratio: 1; }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-end img { object-position: 50% calc((var(--pf-p-fy) + var(--pf-p-fh) / 2 - .28125) / .4375 * 100%); }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-logos { left: calc(var(--pf-p-bx) * 100%); width: calc(var(--pf-p-bw) * 100%);
    top: calc(((var(--pf-p-by) - var(--pf-p-fy) - var(--pf-p-fh) / 2) * 16 / 9 + .5) * (100% - var(--space-4))); height: calc(var(--pf-p-bh) * 16 / 9 * (100% - var(--space-4))); }
}
/* Short landscape screens (film.js's SIDE_MQ, the same query): the end side by side, as the film ends there. The three
   texts first (in a row from 701 px wide, one under another below it), then the end image as a card with the logos on
   its body, and beside it the Distribution words and the fine print, centred down it: the slate becomes the words'
   column (a flex column, stretched to the row, the card centred in the row), and the logos, taken out of its flow, are
   laid on the card from that column. Their offsets are percentages of the column's width (the card is two columns and a
   gap wide, or 1.5 columns below 701 px), the vertical one a margin, whose percentages are of the width too, so it can
   carry the card's height (9/16 of its width); the box's height comes from its aspect ratio. At 14 px (the floor the
   static logos reach on every one of these screens) they are three over two, as on the film's board: the padding
   leaves room for three in a row and not four. */
@media (orientation: landscape) and (max-height: 540px) {
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-copy { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-areas: "comics concept sound" "fig fig words"; --pf-card-w: calc(200% + var(--space-6)); }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-end { margin-top: 0; align-self: center; }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-slate { grid-area: words; position: relative; inset: auto; width: auto; min-height: 0; padding: 0; display: flex; flex-direction: column; justify-content: center; align-items: stretch; gap: var(--space-5); text-align: left; }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-slate .pf-note { order: 2; text-wrap: pretty; }
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-logos { top: 50%; height: auto; align-content: center; --logo-base: 14px;
    left: calc(-1 * var(--space-6) - (1 - var(--pf-l-bx)) * var(--pf-card-w)); width: calc(var(--pf-l-bw) * var(--pf-card-w));
    margin-top: calc((var(--pf-l-by) - .5) * .5625 * var(--pf-card-w)); aspect-ratio: calc(var(--pf-l-bw) * 16) / calc(var(--pf-l-bh) * 9);
    padding: 0 max(var(--space-3), calc((var(--pf-l-bw) * var(--pf-card-w) - var(--logo-base) * 16.37 - 3 * var(--space-5) + 1px) / 2)); }
}
@media (orientation: landscape) and (max-height: 540px) and (max-width: 700px) {
  html:is(.film-static, .rm, .cap, :not(.js)):not(.film-fixed) .pf-copy { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); grid-template-areas: "comics comics" "concept concept" "sound sound" "fig words"; --pf-card-w: 150%; }
}

/* ---------- How it works ---------- */
.how-head { max-width: 720px; }
/* How it works: numbered steps on the left, one media panel on the right showing the active step
   (hover, tap or focus). Under 900px the panel goes away and each step carries its own picture. */
.how-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 48px; margin-top: var(--space-6); }
.steps { list-style: none; display: flex; flex-direction: column; position: relative; --rail: 0; }
/* the rail draws down the steps as the section scrolls in (GSAP writes --rail); the dot marks the step the panel shows */
.steps::before { content: ""; position: absolute; left: 46px; top: 0; bottom: 0; width: 2px; background: var(--accent-gradient-bar); opacity: .55; transform: scaleY(var(--rail)); transform-origin: top; }
/* The dot is centred on the number's capitals: they start 24px into the padding box (both trimmed, below) and are
   13px tall at --text-lg, so the centre is at 30.5px and the 8px dot starts 4px above it. */
.step.is-active::before { content: ""; position: absolute; left: 43px; top: 26.5px; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 14px var(--accent); }
.step { position: relative; padding: var(--space-5) 0 var(--space-5) var(--space-7); border-top: 1px solid var(--border-subtle); transition: opacity .3s ease, transform .4s var(--ease-out); }
/* Hover nudges the words only. Moving the whole step slid its dot off the rail and its hairline with it, and focus
   borrowed the same nudge (review B5.2); focus now draws the ring and nothing else. */
.step > :not(.step-num) { transition: translate .4s var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .step:hover > :not(.step-num) { translate: 4px 0; } }
.step:focus-visible { outline-offset: 8px; }
.step:last-child { border-bottom: 1px solid var(--border-subtle); }
.step:not(.is-active) { opacity: .6; }
.step:focus-within { opacity: 1; }
@media (hover: hover) and (pointer: fine) { .step:hover { opacity: 1; } }
/* Number and title share a cap line (review B5.1): the title takes the card titles' leading instead of the body's 1.7,
   and both are trimmed to their capitals and baseline, the way the FAQ sets its questions, so top: 24px puts the
   number's capitals level with the title's. Under 900px the number sits above the title and only the leading applies.
   The trimmed title ends at its baseline, so the description sits 16px under it (about 24px to its capitals). */
.step-num { position: absolute; left: 0; top: var(--space-5); font-family: var(--font-body); font-weight: var(--weight-bold); font-size: var(--text-lg); font-variant-numeric: tabular-nums; color: var(--accent); line-height: var(--leading-none); text-box: trim-both cap alphabetic; }
.step-title { font-size: var(--display-serif-s); line-height: var(--leading-snug); text-box: trim-both cap alphabetic; }
.step-desc { color: var(--text-body); font-size: var(--text-md); margin-top: var(--space-4); max-width: 46ch; }
.step-fact { margin-top: var(--space-4); }
.step-media { display: none; }
.step-media--second { margin-top: var(--space-2); }
/* The screen and the man waving over it stick as one: when only the screen stuck, he scrolled away with the page and the
   crop at his thighs hung in mid-air. The top leaves room for his overhang, so his head stops under the nav. He sits
   behind the screen, sunk 18px below its top edge, so the frame hides the crop. On a short window the pair sticks
   higher, so the whole screen stays in view: 62.5cqi is the 16:10 screen's height, read from the column's width. */
.how-media-wrap { position: relative; container-type: inline-size; }
.how-stage { --wave-w: 170px; --wave-sink: 18px; position: sticky; top: calc(var(--nav-h) + var(--space-5) + var(--wave-w) - var(--wave-sink));
  top: max(calc(var(--nav-h) + var(--space-5)), min(calc(var(--nav-h) + var(--space-5) + var(--wave-w) - var(--wave-sink)), calc(100svh - 62.5cqi - var(--space-5)))); }
.how-media { position: relative; z-index: 1; aspect-ratio: 16 / 10; overflow: hidden; background: var(--surface-sunken); --dir: 1; }
.how-media img, .how-media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; opacity: 0; transform: translateX(calc(var(--dir) * 6%)) scale(1.04); transition: opacity .45s ease, transform .6s var(--ease-out); }
.how-media .is-active { opacity: 1; transform: none; }
/* The outgoing picture leaves the way the new one travels, so the two read as one move (review B5.4, Eric's Q19). */
.how-media .is-leaving { opacity: 0; transform: translateX(calc(var(--dir) * -4%)) scale(.98); transition-duration: .3s, .3s; }
.how-media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, var(--scrim-shade), var(--scrim-none) 45%); pointer-events: none; }
.sprite-wave { bottom: calc(100% - var(--wave-sink)); right: 8%; width: var(--wave-w); z-index: 0; }

/* ---------- Pricing slate (2026-10-11, Eric's pick: the tiers as a production slate, not three cards with ticks) ----------
   Printed the way the hero's clapperboard prints its slate (tools/hero-bundle/src/hero.jsx, slateTexture): small tracked
   Inter labels, Instrument Serif values, hairline rules, on a dark face. Polish pass the same night (Eric: "too many words
   and it seems flat"; measured, 137 of 184 words were one style, serif 20px, and the price only matched the heading):
   - one short value per cell, with an Inter caption under it where it needs one (a second <dd>);
   - the price is the largest type in the section. The scale has no serif step between -m and -l, and at -l "$10,000" is
     about 330px wide at 1440 against a tier column's 290px, so on a wide screen the price is -m and this section's title
     steps down to -s; on a phone a tier is a slate of its own and the price takes -l, the title keeping the site's -m;
   - depth: the raised face, lit faintly from the top, and a layered shadow tinted purple and cyan over the black one, so
     the slate sits above the page instead of on it;
   - across the top, the clapperboard arm's stripes (light cyan, dark, near white, dark, leaning like the hero board's)
     instead of the gradient rule. The stripe colours are aliases of existing roles, scoped to the slate; if Eric wants
     them reused, they belong in tokens.css as roles of their own.
   Grid: a label column and three tiers; rows head, length, delivery, formats, includes, revisions, action. Revisions are
   the same on every tier, so they are printed once across the three (row 6), the way ROLL runs across the slate, and the
   buttons come last: the facts, then the call to action. Each tier also carries its own Revisions row, for screen readers
   here and printed on a phone (.slate-row--shared, below). The label column spans rows 1 to 5 and each tier rows 1 to 7
   with `grid-template-rows: subgrid`, so a row lines up across the tiers however its cells wrap; a tier's row 6 is empty
   and the spanning row, opaque and stacked above the tiers, covers their column rules there. Episode stands out
   the slate's way, its label in cyan like the slate's DIR. line, not with a badge. */
/* The cell insets live on the section so the lines under the slate can line up with its text on a phone. */
#pricing { --slate-pad-x: var(--space-5); --slate-pad-y: var(--space-4); }
.pricing-head { max-width: 720px; }
.slate { --slate-band: var(--space-4);
  --slate-stripe-a: var(--accent-bright); --slate-stripe-b: var(--text-primary); --slate-stripe-gap: var(--surface-sunken);
  --slate-stripes: repeating-linear-gradient(120deg, var(--slate-stripe-a) 0 32px, var(--slate-stripe-gap) 32px 64px, var(--slate-stripe-b) 64px 96px, var(--slate-stripe-gap) 96px 128px);
  --slate-face: linear-gradient(180deg, var(--accent-veil), var(--scrim-none) 45%), var(--surface-raised);
  --slate-shadow: 0 1px 2px var(--scrim-weak), 0 24px 56px -28px var(--purple-wash), 0 60px 140px -50px var(--accent-ring), var(--shadow-card);
  position: relative; isolation: isolate; display: grid; margin-top: var(--space-6); padding-top: var(--slate-band);
  grid-template-columns: minmax(128px, .45fr) repeat(3, minmax(0, 1fr)); grid-template-rows: repeat(7, auto);
  background: var(--slate-face); border: 1px solid var(--border-subtle); border-radius: var(--radius-m);
  box-shadow: var(--slate-shadow); overflow: clip; }
/* The arm's stripes, a thin band across the top, with a hinge line under it. */
.slate::before, .slate-tier::before { content: ""; position: absolute; inset: 0 0 auto; height: var(--slate-band); background: var(--slate-stripes); border-bottom: 1px solid var(--border-strong); pointer-events: none; }
.slate-tier::before { content: none; }
.slate-labels { grid-column: 1; grid-row: 1 / 6; display: grid; grid-template-rows: subgrid; }
.slate-label:nth-child(1) { grid-row: 2; }
.slate-label:nth-child(2) { grid-row: 3; }
.slate-label:nth-child(3) { grid-row: 4; }
.slate-label:nth-child(4) { grid-row: 5; }
.slate-tier { position: relative; grid-row: 1 / 8; display: grid; grid-template-rows: subgrid; border-left: 1px solid var(--border-subtle); }
/* Placed by hand: auto-placement would put the tiers in the label column's rows. */
.slate-tier:nth-of-type(1) { grid-column: 2; }
.slate-tier:nth-of-type(2) { grid-column: 3; }
.slate-tier:nth-of-type(3) { grid-column: 4; }
.slate-head { grid-row: 1; padding: var(--space-5) var(--slate-pad-x) var(--space-5); }
.slate-rows { grid-row: 2 / 6; display: grid; grid-template-rows: subgrid; }
.slate-action { grid-row: 7; padding: var(--space-5) var(--slate-pad-x) var(--space-5); border-top: 1px solid var(--border-subtle); }
.slate-shared { grid-column: 1 / -1; grid-row: 6; position: relative; z-index: 1; display: grid; grid-template-columns: subgrid; background: var(--surface-raised); border-top: 1px solid var(--border-subtle); }
.slate-shared .slate-row { display: contents; }
.slate-shared .slate-k, .slate-shared .slate-v { padding: var(--slate-pad-y) var(--slate-pad-x); }
.slate-shared .slate-v { grid-column: 2 / -1; border-left: 1px solid var(--border-subtle); }
/* Cells. A label's line box is as tall as the first line of the value beside it, so the two share a centre line. */
.slate-label, .slate-row { padding: var(--slate-pad-y) var(--slate-pad-x); border-top: 1px solid var(--border-subtle); }
.slate-label, .slate-k, .slate-name { font-family: var(--font-body); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); }
.slate-label, .slate-shared .slate-k { line-height: calc(var(--text-xl) * var(--leading-snug)); }
.slate-v { font-family: var(--font-display); font-weight: var(--weight-display); font-size: var(--text-xl); line-height: var(--leading-snug); color: var(--text-primary); text-wrap: balance; }
.slate-note { margin-top: var(--space-1); font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--text-secondary); text-wrap: balance; }
.slate-name { line-height: var(--leading-normal); }
.slate-tier--lead .slate-name { color: var(--accent); }
.slate-price { display: flex; align-items: baseline; gap: var(--space-2); margin-top: var(--space-3); }
.slate-from { font-size: var(--text-sm); color: var(--text-secondary); }
.slate-amount { font-size: var(--display-serif-m); line-height: var(--leading-none); color: var(--text-primary); }
.slate-rate { max-width: 42ch; margin-top: var(--space-5); font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--text-secondary); text-wrap: pretty; }
.slate-rate-k { color: var(--text-primary); font-weight: var(--weight-medium); }
/* A column warms when its button is hovered: a cyan light falling from the stripes, on a layer that only fades. The
   column itself is not a control, so it no longer lights under a passing pointer (2026-10-12, review B8.10, Q27); the
   light now says which tier the button books. */
.slate-tier::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, var(--accent-soft), var(--accent-veil) 40%, var(--scrim-none)); opacity: 0; transition: opacity var(--duration-base) var(--ease-out); pointer-events: none; }
@media (hover: hover) and (pointer: fine) {
  .slate-tier:has(.slate-btn:hover)::after { opacity: 1; }
}
/* The title steps down only where the price is set larger than it: from 1101px. Between 761 and 1100 the price is at
   its 36px floor, and the title at -s (25.6px) was smaller there than on a phone (review B1.3). */
@media (min-width: 1101px) {
  .pricing-head .section-title { font-size: var(--display-serif-s); }
}
@media (min-width: 761px) {
  /* The tier's own labels: read by a screen reader here, where the label column prints them. The same for each tier's
     own Revisions row: the spanning row prints it once for the eye and is hidden from screen readers. */
  .slate-rows .slate-k, .slate-row--shared { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}
@media (max-width: 1100px) and (min-width: 761px) {
  #pricing { --slate-pad-x: var(--space-4); }
}
/* A phone, and anything too narrow for four columns: one slate per tier, each with its own stripes and its own Revisions
   row, then the rate line once below. Printed once under the three slates, Revisions read as a Series term, more than
   1,000px below Pilot (review Must 2). Every phone-read line is at least --text-md (the token file's floor). */
@media (max-width: 760px) {
  .slate { display: flex; flex-direction: column; gap: var(--space-5); padding-top: 0; background: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; isolation: auto; }
  .slate::before { content: none; }
  .slate-labels { display: none; }
  .slate-tier { display: block; isolation: isolate; padding-top: var(--slate-band); background: var(--slate-face); border: 1px solid var(--border-subtle); border-radius: var(--radius-m); box-shadow: var(--slate-shadow); overflow: clip; }
  .slate-tier::before { content: ""; }
  .slate-head { padding: var(--space-5) var(--slate-pad-x) var(--space-4); }
  .slate-amount { font-size: var(--display-serif-l); }
  .slate-from, .slate-note, .slate-rate { font-size: var(--text-md); }
  .slate-rows { display: block; }
  /* Label beside value, the slate's printed form: a label column of its own inside each tier. */
  .slate-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); column-gap: var(--space-3); align-items: baseline; }
  .slate-row .slate-k { line-height: calc(var(--text-xl) * var(--leading-snug)); }
  .slate-row .slate-v, .slate-row .slate-note { grid-column: 2; }
  .slate-action { padding: var(--space-4) var(--slate-pad-x) var(--space-5); }
  .slate-action .btn-comic { width: 100%; }
  .slate-shared { display: none; }
  .slate-rate { margin-top: var(--space-4); padding: 0 var(--slate-pad-x); }
}

/* ---------- FAQ: questions on the page, answers on demand (2026-10-12, Eric's /polish) ----------
   Eric found the two-column script of 2026-10-11 (every answer on the page, about 180 words) wordy and not interactive.
   Each question is now a full-width button in its heading, and its answer opens under it, one at a time (home.js
   initFaq), so until the visitor asks, the section is a heading, a lead and six serif questions. Kept from the script:
   the questions in the display serif at --display-serif-s, the answers in Inter at --text-body, hairlines between rows.
   Rest, hover, open: a closed question sits at --text-body and warms to --text-primary under a mouse, the way a row
   lights when the cursor reaches it; an open one stays at --text-primary, and its icon has turned into a close mark.
   Motion: the answer opens by grid-template-rows 0fr to 1fr, the one layout animation .impeccable.md accepts (its
   2026-09-19 kept list), with the text fading in and rising 8px a beat behind it. Transitions, not keyframes, so a
   second click halfway reverses from where the row is, and nothing waits on a timer. Closing is quicker than opening,
   because the visitor is done with the answer. Pressed dims the row (--state-pressed-opacity) instantly and lets go
   slower. Under html.rm (reduced motion) a row opens and closes at once and the icon turns without moving (only the
   question's colour change on hover still eases); html.cap switches every transition off.
   Spacing: 24px from the hairline to the question's caps and from its baseline to the button's edge, so closed rows are
   even; an open answer has 40px under it, so it reads with its question rather than with the next row.
   The icon (Lucide plus, 1.8px line however large it is drawn) sits on the question's first baseline and is one cap
   high (1cap), so it spans the capitals of the first line at every size and line height without a hand-set offset; the
   viewBox is cropped so the cross is two thirds of the cap height. It turns 45 degrees into a close mark.
   Both texts are trimmed to cap height and baseline (`text-box`), so the paddings are the gaps the eye sees.
   No scripts (no html.js): every answer shows and the icons hide, since the buttons cannot do anything. */
.faq-head { max-width: 720px; }
.faq-head a { color: var(--accent); }
/* Pressed: an inline link cannot scale, so it dims (the site's inline-link press). */
.faq-head a:active, .wl-status a:active, .proof-cta:active { opacity: var(--state-pressed-opacity); }
.faq-list { margin-top: var(--space-7); border-bottom: 1px solid var(--border-subtle); }
.faq-item { border-top: 1px solid var(--border-subtle); }
.faq-h { margin: 0; font-size: inherit; font-weight: inherit; }
.faq-q { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-5); width: calc(100% + 2 * var(--space-3)); min-height: var(--tap-min); margin: 0 calc(-1 * var(--space-3)); padding: var(--space-5) var(--space-3); text-align: left; font-family: var(--font-display); font-weight: var(--weight-display); font-size: var(--display-serif-s); line-height: var(--leading-snug); letter-spacing: var(--tracking-display); color: var(--text-body); transition: color var(--duration-fast) var(--ease-out), opacity var(--duration-fast) var(--ease-out); }
.faq-q-text { text-wrap: balance; text-box: trim-both cap alphabetic; }
.faq-icon { flex: none; width: 1cap; height: 1cap; fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; transition: rotate var(--duration-base) var(--ease-out), stroke var(--duration-fast) var(--ease-out); }
.faq-item.is-open .faq-q { color: var(--text-primary); }
.faq-item.is-open .faq-icon { rotate: 45deg; }
@media (hover: hover) and (pointer: fine) {
  .faq-q:hover { color: var(--text-primary); }
  .faq-q:hover .faq-icon { stroke: var(--accent-bright); }
}
.faq-q:active { opacity: var(--state-pressed-opacity); transition-duration: var(--duration-fast), 0s; }
/* Focus: the global ring, drawn 4px inside the button instead of 4px outside it, so it clears the hairline above and the
   open answer below. The button reaches 12px past the text on each side (into the gutter) to make room for it. */
.faq-q:focus-visible { outline-offset: calc(-1 * var(--space-1)); border-radius: var(--radius-m); }
.faq-a-clip { min-height: 0; overflow: hidden; }
.faq-a-text { max-width: 42ch; padding: var(--space-1) 0 var(--space-6); font-size: var(--text-md); line-height: var(--leading-loose); color: var(--text-body); text-wrap: pretty; text-box: trim-both cap alphabetic; }
/* A hyphenated compound stays on one line: at 60ch "15-minute" broke after the hyphen. So do a number and its unit and
   a range ("7 to 14 days", review B1.4): a span rather than &nbsp;, so the text stays equal to the FAQPage JSON-LD. */
.faq-nb { white-space: nowrap; }
html.js .faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .25s var(--ease-out); }
html.js .faq-item.is-open .faq-a { grid-template-rows: 1fr; transition-duration: var(--duration-base); }
html.js .faq-a-text { opacity: 0; translate: 0 var(--space-2); transition: opacity .15s var(--ease-out), translate .25s var(--ease-out); }
html.js .faq-item.is-open .faq-a-text { opacity: 1; translate: none; transition: opacity .3s var(--ease-out) .05s, translate var(--duration-base) var(--ease-out) .05s; }
html.rm .faq-icon, html.rm .faq-a, html.rm .faq-a-text { transition: none; }
html:not(.js) .faq-q { color: var(--text-primary); cursor: default; }
html:not(.js) .faq-icon { display: none; }
@media (max-width: 900px) {
  .faq-list { margin-top: var(--space-6); }
  .faq-q { gap: var(--space-4); }
}

/* ---------- The close as an end card (2026-10-11, Eric's pick): no box and no glow ----------
   The title is centred like a film's last card. Under it a hairline, and on it sits the sprite (the product's own
   output, not decoration). Under the line, two paths split at the centre the way the slate splits its columns: the
   studio now on the left, set against the centre line, and the self-serve waitlist on the right, set from it. */
.endcard-title { text-align: center; }
.endcard-title .section-title { margin: 0 auto; max-width: 16ch; }
.endcard-title .section-sub { margin: var(--space-4) auto 0; text-wrap: balance; }
.endcard-rule { position: relative; height: 1px; margin-top: calc(var(--space-7) + var(--space-6)); background: var(--border-strong); }
.endcard-rule .sprite-sit { right: 7%; bottom: -14px; width: 170px; z-index: 1; }
.endcard-paths { display: grid; grid-template-columns: 1fr 1fr; }
.endcard-path { padding: var(--space-6) var(--space-7) 0; }
.endcard-path:first-child { text-align: right; }
.endcard-path + .endcard-path { border-left: 1px solid var(--border-subtle); }
.endcard-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); }
.endcard-path:first-child .endcard-label { color: var(--accent); }
.endcard-text { margin-top: var(--space-3); font-size: var(--text-md); color: var(--text-body); text-wrap: pretty; }
.endcard-path:first-child .endcard-text { margin-left: auto; max-width: 34ch; }
.endcard-action { margin-top: var(--space-5); }
/* Two columns: Book a demo drops by the waitlist's "Email" label (its line plus its gap) so it sits on one line with the
   email field and Join, the two doors level across the centre rule. Stacked on a phone, it keeps the plain gap. */
@media (min-width: 761px) { .endcard-path:first-child .endcard-action { margin-top: calc(var(--space-5) + var(--text-xs) * var(--leading-loose) + var(--space-2)); } }

/* The waitlist form (audit M1, Appendix B). Its states are text and colour only, so nothing moves under reduced motion. */
.wl-form { margin-top: var(--space-5); }
.wl-label { display: block; margin-bottom: var(--space-2); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-secondary); }
.wl-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.wl-input { flex: 1 1 220px; min-width: 0; min-height: 48px; padding: 0 var(--space-5); font: inherit; font-size: var(--text-md); line-height: var(--leading-snug); color: var(--text-primary); background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--radius-pill); transition: border-color var(--duration-fast) ease; }
.wl-input::placeholder { color: var(--text-secondary); opacity: 1; }
.wl-input[aria-invalid="true"] { border-color: var(--feedback-error); }
@media (hover: hover) and (pointer: fine) { .wl-input:hover:not(:focus) { border-color: var(--border-bright); } }
.wl-btn { flex: none; min-height: 48px; padding: 0 var(--space-5); font-size: var(--text-md); line-height: var(--leading-none); }
.wl-status { margin-top: var(--space-3); min-height: calc(var(--text-sm) * var(--leading-normal)); font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--text-secondary); text-wrap: pretty; }
.wl-status:empty { margin-top: 0; }
.wl-status a { color: inherit; text-underline-offset: 3px; }
.wl-status.is-error { color: var(--feedback-error); }
.wl-status.is-success { color: var(--feedback-success); font-size: var(--text-md); }
.wl-status:focus { outline: none; }
.wl-status:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }
.wl-form.is-done .wl-fields { display: none; }
.wl-form.is-done .wl-status { margin-top: 0; }

@media (max-width: 760px) {
  .endcard-rule { margin-top: calc(var(--space-6) + var(--space-7)); }
  .endcard-rule .sprite-sit { display: block; right: 4%; bottom: -10px; width: 124px; }
  .endcard-paths { grid-template-columns: 1fr; }
  .endcard-path { padding: var(--space-6) 0 0; }
  /* Both paths read from the left under the rule, like the form's field and its "Email" label (review B6.6). The
     studio path stayed centred above the left-aligned form until 2026-10-12, two alignments in one column. The title
     above the rule stays centred, as the film's last card. */
  .endcard-path:first-child { text-align: left; }
  .endcard-path + .endcard-path { margin-top: var(--space-6); border-left: 0; border-top: 1px solid var(--border-subtle); }
  .endcard-path:first-child .endcard-text { margin: var(--space-3) 0 0; max-width: none; }
  .endcard-action .btn-comic, .wl-btn, .wl-input { flex: 1 1 100%; width: 100%; }
  /* The message goes between the field and the button, where it is seen with the field it is about: under the button
     it started 72px below the field, likely under the keyboard (review B6.1). Only the order on screen changes: the
     status line stays where it is in the document, a polite live region after the form's fields. */
  .wl-form { display: grid; }
  .wl-fields, .wl-row { display: contents; }
  .wl-input { order: 1; }
  .wl-status { order: 2; min-height: 0; }
  .wl-btn { order: 3; margin-top: var(--space-3); }
}


@media (max-width: 1024px) {
  .gl-card { grid-template-columns: 1fr; gap: var(--space-5); align-content: center; }
  /* 42svh fills what the copy leaves on a 390 x 844 phone (35svh left 150 px idle above the still, 2026-10-12). */
  .gl-mesh { height: min(42svh, 420px); }
}
/* A short phone screen cannot hold the copy and the still at once, so the still goes and the words stay. The line
   is 600 px (740 until 2026-10-12, which hid the still on most real iPhones, about 664 px tall in Safari, though the
   still, min(42svh, 420px), and the copy fit from about 605 px at 390 px wide: review 2026-10-12, B2.3). At 375 px
   wide the copy is a line taller: on a 375 x 667 screen the still's top 34 px sit under the nav while the nav shows. */
@media (max-width: 1024px) and (max-height: 600px) {
  .gl-visual { display: none; }
}
@media (max-width: 900px) {
  html.work-grid .work-track { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .sprite { display: none; }
  /* One row of tabs that scrolls sideways and runs to the screen edge, instead of a second row with one name on it.
     The block padding keeps the focus ring and the pills' glow inside the scroller. */
  .proof-tabs { flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; margin: calc(var(--space-6) - var(--space-2)) calc(-1 * var(--gutter)) calc(-1 * var(--space-2)); padding: var(--space-2) var(--gutter); scroll-padding-inline: var(--gutter); }
  .proof-tabs::-webkit-scrollbar { display: none; }
  .proof-tab { flex: none; }
  /* The stage is about 350 x 200 here: smaller name tags so they do not cover the art they name. */
  .proof-label { top: var(--space-2); padding: var(--space-1) var(--space-2); }
  .proof-label-before { left: var(--space-2); }
  .proof-label-after { right: var(--space-2); }
  .proof-play { right: var(--space-2); bottom: var(--space-2); }
}

/* The footer is the end credits since 2026-10-11: assets/credits.css, shared with the other pages. */

/* ---------- Reveal (JS sets the from-state; no-JS users see everything) ----------
   The from-state is opacity and a transform only, so a block waiting for its reveal keeps its controls in the Tab order
   and the accessibility tree (home.js, initReveals). .reveal-wait keeps it out of hit testing until the reveal starts,
   the one thing visibility: hidden did that opacity does not. */
.reveal-wait { pointer-events: none; }
html.rm [data-reveal], html.cap [data-reveal] { opacity: 1 !important; transform: none !important; }
html.rm .glass-plate, html.cap .glass-plate { transform: none !important; }
html.rm .steps, html.cap .steps { --rail: 1; }
html.rm .step > *, html.cap .step > * { translate: none !important; }
html.rm .glass-plate::after, html.cap .glass-plate::after, html.rm .glass-bend::after, html.cap .glass-bend::after { display: none; }
/* Reduced motion keeps the How screen's crossfade and drops its slide and scale, and hover stops moving things: the
   glow, the colour and the dimming stay (review B5.3, audit A15). */
html.rm :is(.how-media img, .how-media video) { transform: none; }
html.rm :is(.work-media, .work-cta svg, .proof-cta svg) { transform: none !important; }

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
  .nav, .btn-cta, .btn-comic, .glass-bend::after, .nav-link::after, .menu-link { transition-duration: .001ms !important; }
  .ai-unfold { width: auto !important; }
}

/* ---------- Reduced motion: the clips play by choice (home.js, initReducedVideos) ----------
   Every clip has the browser's own controls under html.rm. The player sits above its picture's darkening gradient so
   its controls are not dimmed, and the How screen's clip is hidden while another step shows (so it cannot take focus
   unseen). The two films, Geometry Lock and the Pillars film, keep their static layouts instead. */
html.rm .work-media video { opacity: 1; z-index: 1; }
html.rm .how-media video { z-index: 1; }
html.rm .how-media video:not(.is-active) { visibility: hidden; }

/* ---------- Capture mode (?cap=1): fixed-height hero, no motion, everything visible ---------- */
html.cap *, html.cap *::before, html.cap *::after { transition: none !important; animation: none !important; }
html.cap .hero { min-height: 860px; }
html.cap .ai-unfold { width: auto !important; }
html.cap .hero-poster:not(.is-missing) { opacity: 1; }
html.cap.gl-ready .hero-poster { opacity: 0; }
html.cap .nav { transform: none !important; }

/* ---------- Responsive ---------- */
/* The header always has its right side: the links and "Book a demo" down to 901px (they fit, with about 120px to
   spare at 901), then the logo and the menu button. Until 2026-10-11 the button went at 1024 and the menu button
   came only at 900, so an iPad in landscape got neither (audit S4). */
@media (max-width: 1024px) {
  .nav-links { gap: var(--space-4); }
}
@media (max-width: 900px) {
  .nav-inner { grid-template-columns: auto 1fr; }
  .nav-links { display: none; }
  .nav-demo { display: none; }
  .nav-hamburger { display: inline-flex; }
  /* Phone header: logo and hamburger (the compact "Open app" went with the app links on 2026-10-06, until the app is
     ready, and the speaker went with the sound the same day). Everything is at least 44px. */
}
/* Portrait phones and tablets: the board above the copy, in the room the copy leaves (see .hero-board). Until
   2026-10-12 the room was 64vw whatever the height, so the hero fitted only Chrome's 844 px phone: a real iPhone at
   load (about 664 px) had Book a demo below the fold, and a tablet had 212 px of empty gradient between the board and
   the eyebrow. Sideways phones keep the wide composition, text left and board right (it was board only, no words, on
   the first screen). */
@media (max-width: 900px) and (orientation: portrait) {
  /* A grid, not a flex column: Chrome resolves the room's cqh to 0 when its height comes from flex-grow, and to the
     track's height in a grid row (measured 2026-10-12). The floor goes on the row, not on the room: a min-height on
     the room gave cqh 0 again once the copy pushed the hero past the screen (375 x 553). */
  .hero { display: grid; grid-template-rows: minmax(calc(2 * var(--space-7)), 1fr) auto; grid-template-columns: 100%; align-items: stretch; padding-top: var(--nav-h); padding-bottom: var(--space-5); }
  .hero-room { position: relative; inset: auto; }
  .hero-board { --hero-board-w: min(57.6cqw, 403.2px, calc((100cqh - 2 * var(--space-4)) / 1.2)); left: 50%; top: calc(50% + .15 * var(--hero-board-w)); }
  .hero-board > .hero-poster { left: -56.32%; width: 108.29%; margin-top: -73.52%; }
}
/* Short portrait screens, such as an iPhone SE in Safari, 375 x 553 at load (2026-10-13, Eric: "let's make sure the
   iphone SE can see the book a demo"). The stacked hero above needs 615 px: the nav (64), the room's floor (128, an
   80 px board), the copy (399 at 360 to 414 wide: the eyebrow on two lines, the title's two 64 px lines, the lead on
   three) and the --space-5 under the button. Below that the copy runs past the first screen, and on the SE Book a demo
   sat at 543 to 591. Here the headline takes the next display step down and the copy's three gaps the next spacing
   step down, 89 px in all, and the room takes them, so the board grows (80 to 102 px) instead of shrinking. 360 x 640
   and taller keep the rule above. Phone browsers evaluate the query against the layout viewport, sized with their
   toolbars showing and not resized as the toolbars collapse, so the headline should not change size mid-scroll (not
   yet checked on a device). */
@media (max-width: 900px) and (orientation: portrait) and (max-height: 614px) {
  .hero-title { font-size: var(--display-serif-m); }
  .hero-eyebrow { margin-bottom: var(--space-4); }
  .hero-sub { margin-top: var(--space-4); }
  .hero-ctas { margin-top: var(--space-5); }
}
@media (max-width: 640px) {
  .hero-ctas .btn-lg { flex: 1 1 100%; }
}

/* ---------- Phones: tap targets, body text, the reshaped Steps ---------- */
@media (max-width: 900px) {
  .proof-tab, .intro-skip { min-height: var(--tap-min); }
  .how-grid { grid-template-columns: 1fr; gap: 0; }
  .how-media-wrap { display: none; }
  .step { padding-left: 0; }
  .step:not(.is-active) { opacity: 1; }
  .step-num { position: static; display: block; margin-bottom: var(--space-2); }
  .step-num, .step-title { text-box: normal; }
  .steps::before { display: none; }
  /* The dot marks a place on the rail; with the rail gone it only sat on the "0" of "01" (audit V15, P03). */
  .step.is-active::before { display: none; }
  .step-media { display: block; position: relative; aspect-ratio: 16 / 10; overflow: hidden; margin-top: var(--space-4); background: var(--surface-sunken); }
  .step-media img, .step-media video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; }
}
/* The phone layout's resets for a mouse on a narrow window: the lifts above do not apply under 900px. */
@media (max-width: 900px) and (hover: hover) and (pointer: fine) {
  .step:hover > :not(.step-num) { translate: none; }
}
/* People who turn off transparency get solid plates instead of glass: the two backdrop layers are hidden and the roots get opaque fills. */
@media (prefers-reduced-transparency: reduce) {
  .glass-refract, .glass-bend { display: none; }
  .glass, .nav.is-scrolled, .menu, .proof-label, .proof-play { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--surface-glass-solid); }
  .btn-cta { background: var(--accent-gradient); }
  .glass-plate { background: var(--surface-raised); }
}
