/* =========================================================================
   Whisper — built to www.interfaces.new-DESIGN.md
   Warm near-black (#130F0C) · pure white type · TeX Gyre Heros 400 · rounded
   4px spacing scale · monochrome (no red / orange accents)
   ========================================================================= */

@font-face {
  font-family: "TeX Gyre Heros";
  src: url("fonts/texgyreheros-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "TeX Gyre Heros";
  src: url("fonts/texgyreheros-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

:root {
  color-scheme: dark;
  /* Static fallbacks only. paint() writes live --bg/--fg on body so a scroll
     frame does not invalidate every color-mix inheritor hung off :root.
     --ground on :root is the document-canvas wash (html + Safari chrome). */
  --bg:            #000000;
  --fg:            #FFFFFF;
  --ground:        #000000;

  --accent-cool:   #00D9FF;   /* focus ring only */
  --accent-yellow: #FFD700;   /* invalid field ring */
  --accent-red:    #E11D2E;   /* form error copy */

  /* Spacing scale — multiples of 4px only */
  --s-1:   4px;
  --s-2:   8px;
  --s-3:  12px;
  --s-4:  16px;
  --s-5:  20px;
  --s-6:  24px;
  --s-8:  32px;
  --s-12: 48px;
  --s-14: 56px;
  /* Section title → first content block (rail, bands, FAQ list). */
  --gap-title-block: var(--s-14);
  /* Shared mobile tile height: How bands, Overheard quotes, Bartow cards. */
  --tile-h: 420px;
  --s-20: 80px;
  --s-27: 108px;

  /* Radius */
  --r-sm:   8px;   /* inputs */
  /* Ramp How we build cards: --homepage-card-radius: 40px with
     --homepage-card-corner-shape: superellipse(1.6) — between round (1)
     and squircle (2), not a circular arc. Photos/tiles/imagery follow;
     pills stay capsules. */
  --r-md:  40px;
  --r-md-shape: superellipse(1.6);
  --r-pill: 999px; /* CTA buttons */

  /* Type */
  --font: "TeX Gyre Heros", Helvetica, "Helvetica Neue", Arial, sans-serif;
  --font-quote: "EB Garamond", "Times New Roman", Times, serif;
  --t-micro:   12px;
  --t-caption: 13px;
  --t-body:    16px;
  --t-h2:      18px;

  --wrap: 1440px;
  --pad: var(--s-6);

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

/* Ramp pairs 40px radius with superellipse(1.6). Unsupported browsers
   ignore corner-shape and keep the circular 40px arc — same fallback as
   ramp.design. Pills, buttons, stepper, and Donate stay off this list. */
.photo,
.slot,
.show__viewport,
.quote,
.pop img,
.video,
.gath__img,
.gath__stage,
.agenda__img,
.held__shot,
.story__card,
.path,
.step,
.giftfan,
.band__inner,
.stats > .stat,
.stats > .surgeon {
  corner-shape: var(--r-md-shape);
}
.photo img,
.gath__img img,
.held__shot img {
  border-radius: inherit;
  corner-shape: inherit;
}

/* Phone: match overheard’s ~40% cut under section titles */
@media (max-width: 767px) {
  :root { --gap-title-block: calc(var(--s-14) * 0.6); }
}

/* ── Ground ──────────────────────────────────────────────────────────────
   One fixed layer holds the page colour. script.js interpolates it between
   the grounds declared on sections, so every change arrives as a fade rather
   than a hard edge at a section boundary. Text tokens follow the ground's
   luminance.

   iOS Safari: fixed inset:0 stops at the layout viewport while carousels /
   fixed chips still paint under the floating URL bar. Extend the wash past
   that edge, and keep html on the same --ground so the document canvas (the
   band Safari reveals behind chrome, plus overscroll) matches the section. */
.ground {
  position: fixed;
  inset: 0;
  bottom: calc(-1 * (env(safe-area-inset-bottom, 0px) + 96px));
  z-index: -2;
  background: var(--ground, #000000);
  pointer-events: none;
}
body { background: rgba(0, 0, 0, 0); }

.lightmark,
.groundmark { display: block; height: 0; }

/* spacing for the deep-blue end of the ramp: three stops need room to cross
   into each other, so the ramp is scroll distance, not decoration. It used to
   carry all 360px of that distance alone; the deck pitch now sits above it and
   supplies most of it, so the ramp only has to make up the difference — any
   more and it reads as a hole in the page. */
.groundramp {
  display: grid;
  height: 120px;
  align-content: space-between;
}

@media (min-width: 768px)  { :root { --pad: var(--s-8);  } }
@media (min-width: 1024px) { :root { --pad: var(--s-12); } }
@media (min-width: 1440px) { :root { --pad: var(--s-14); } }

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

/* overflow-anchor off: the ground ladder and the back-to-top jump both depend on
   scroll position meaning what it says, and anchoring silently rewrites it when
   media above the viewport resolves */
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  overflow-anchor: none;
  /* Safari paints this canvas behind bottom chrome / overscroll; paint() keeps
     --ground on :root in sync with the fixed .ground wash. */
  background-color: var(--ground, #000000);
  min-height: 100%;
  min-height: 100dvh;
  /* body alone is not enough on iOS — a rotated or max-content child can still
     widen the document and let the page pan sideways. hidden first for older
     WebKit; clip where supported so the page does not grow a second scrollbar. */
  overflow-x: hidden;
  overflow-x: clip;
}
/* Anchor targets clear the sticky nav plus its 125% progressive blur.
   Split between 48px (titles under frost) and 128/188 (too much air). */
.section[id] { scroll-margin-top: calc(var(--s-20) + var(--s-4)); } /* 96px */
#top { scroll-margin-top: 0; }
/* Footer signup keeps the old #newsletter hash. */
#newsletter { scroll-margin-top: calc(var(--s-20) + var(--s-4)); }
@media (min-width: 900px) {
  .section[id],
  #newsletter { scroll-margin-top: calc(var(--s-27) + var(--s-12)); } /* 156px */
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  /* Mixes live here so they recompute from body's --fg, not :root's. */
  --fg-60:         color-mix(in srgb, var(--fg) 62%, transparent);
  --fg-50:         color-mix(in srgb, var(--fg) 52%, transparent);
  --fg-35:         color-mix(in srgb, var(--fg) 35%, transparent);
  --rule:          color-mix(in srgb, var(--fg) 16%, transparent);
  --rule-strong:   color-mix(in srgb, var(--fg) 32%, transparent);
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 24px;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  overflow-x: clip;
}

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

a { color: var(--fg); text-decoration: none; transition: opacity .18s var(--ease); }
a:hover { opacity: .7; }
a:active { opacity: .5; }

:focus-visible { outline: 2px solid var(--fg); outline-offset: 4px; }

/* Selection inverts the ground rather than using the OS blue: --fg and --bg are
   both rewritten per-scroll, so the highlight is black on the light sections and
   white on the dark ones, and the selected text is always the ground it sits on.
   ::selection does not inherit, hence declaring it rather than relying on a
   parent — but it does resolve custom properties, so it tracks the scroll. */
::selection { background: var(--fg); color: var(--bg); }
::-moz-selection { background: var(--fg); color: var(--bg); }

.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;
}

.skip {
  position: absolute; left: var(--s-4); top: -100px; z-index: 100;
  background: var(--fg); color: var(--bg); padding: var(--s-3) var(--s-4);
  border-radius: var(--r-pill);
  font-size: var(--t-caption); line-height: 19.5px;
}
.skip:focus { top: var(--s-4); }

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
}

/* ── Type roles ─────────────────────────────────────────────────────────── */

.display {
  /* 52px / 14vw owned the phone so hard that only fragments of the line were
     on screen. Back to a firm hero without blowing past the column: ~40px at
     375, still a step up from the old 38px floor, ceiling unchanged for desktop. */
  /* 42/10.5vw: a hair above the 40/10 rollback, still well under the 52/14
     phone takeover that clipped the line. */
  font-size: clamp(42px, 10.5vw, 86px);
  /* as tight as this face goes before a descender meets the next line's ascender */
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-weight: 400;
  text-wrap: balance;
}

.h1 {
  /* Floor used to sit at 28px, then 36px — still caption-adjacent for a
     two-line section title on a phone. 42px at 375 is the smallest that
     still reads as a chapter head next to body. */
  font-size: clamp(42px, 10vw, 48px);
  line-height: 1.05;
  letter-spacing: -0.026em;
  font-weight: 400;
  text-wrap: balance;
}

.h2-lg {
  font-size: clamp(24px, 5.2vw, 32px);
  line-height: 1.2;
  letter-spacing: -0.012em;
  font-weight: 400;
  text-wrap: balance;
  max-width: 26ch;
}

/* Arrow standing in for "to" — stroke matched to the typeface's stem weight */
.arrow {
  display: inline-block;
  width: 0.95em;
  height: 1em;
  vertical-align: -0.24em;
}
.arrow svg { width: 100%; height: 100%; overflow: visible; }
.arrow path {
  fill: none;
  stroke: currentColor;
  stroke-width: 3.5;   /* 40 units = 1em, so ~0.088em — Helvetica's stem */
  stroke-linecap: butt;
  stroke-linejoin: miter;
}
/* ↗ on Shop links — Heros's U+2197 is a hairline. Same stem as .arrow, cap-height, baseline. */
.arrow--ne {
  width: 0.78em;
  height: 0.73em;
  margin-left: 0.16em;
  vertical-align: 0;
}
.arrow--ne path { stroke-width: 1.4; } /* 12-unit box at cap-height ≈ Heros stem */

.nobr { white-space: nowrap; }

.lede {
  font-size: clamp(16px, 1.7vw, 20px);
  line-height: 1.55;
  color: var(--fg-60);
  max-width: 46ch;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  font-size: var(--t-body);
  line-height: 22px;
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: opacity .18s var(--ease), border-color .18s var(--ease), transform .18s var(--ease);
}
.btn--lg { min-height: 44px; padding-inline: var(--s-8); }

/* Marquee label: the text runs continuously inside the pill on hover.

   The second copy of the label is drawn by CSS from data-label and taken out of
   flow, parked one label-width to the right. In flow it would count toward the
   button's intrinsic width and the pill would come out twice as wide, since
   overflow:hidden only clips what is painted — it does not shrink the box.
   Translating by exactly that offset lands copy two where copy one began, so
   the loop has no seam and the button keeps its normal width.

   Named btn__label, not .marquee: .marquee is already the schools row's
   component further down this file, and reusing the name here handed the button
   that component's box. script.js does the wrapping, so every .btn gets this. */
.btn--marquee { overflow: hidden; }
.btn--marquee .btn__label {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  line-height: inherit;
}
.btn--marquee .btn__label::after {
  content: attr(data-label);
  position: absolute;
  top: 0;
  left: 100%;
  padding-left: var(--s-6);
  /* Hidden at rest. On a wide pill — the phone menu's Donate stretches to the
     sheet — overflow:hidden still leaves room for this second copy to peek
     beside the first, so "Donate Donate" was painting. It only needs to exist
     while the marquee is running. */
  opacity: 0;
}
.btn--marquee:hover .btn__label,
.btn--marquee:focus-visible .btn__label { animation: btnMarquee 2s linear infinite; }
.btn--marquee:hover .btn__label::after,
.btn--marquee:focus-visible .btn__label::after { opacity: 1; }
@keyframes btnMarquee { to { transform: translateX(calc(-100% - var(--s-6))); } }

/* the second copy only earns its keep while it is moving */
@media (prefers-reduced-motion: reduce) {
  .btn--marquee:hover .btn__label,
  .btn--marquee:focus-visible .btn__label { animation: none; }
  .btn--marquee:hover .btn__label::after,
  .btn--marquee:focus-visible .btn__label::after { opacity: 0; }
}

.btn--solid {
  background: var(--fg);
  color: var(--bg);
  border: 1px solid var(--fg);
  padding: calc(var(--s-2) - 1px) var(--s-6) calc(var(--s-2) + 1px);
}
.btn--solid:hover { opacity: .8; }
.btn--solid:active { opacity: .6; transform: translateY(1px); }

.btn--ghost {
  background: rgba(0, 0, 0, 0);
  color: var(--fg);
  border: 1px solid var(--rule-strong);
  padding: calc(var(--s-2) - 1px) var(--s-6) calc(var(--s-2) + 1px);
}
.btn--ghost:hover { opacity: 1; border-color: var(--fg); }
.btn--ghost:active { opacity: .6; }

.cta-row { margin-top: var(--s-14); display: flex; justify-content: center; }
.cta-row--left { justify-content: flex-start; margin-top: var(--s-8); }

/* ── Navigation ─────────────────────────────────────────────────────────── */

.nav {
  /* Fixed overlay so logo / Donate / stepper can compact on scroll without
     shrinking a sticky box (that shoved main). A .nav-spacer holds the rest
     height so the hero does not jump. Vertical pad stays --pad at every
     scroll position — the bar must not climb toward the viewport edge. */
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  background: rgba(0, 0, 0, 0);
  --nav-compact: 0;
  /* Burger disc (44 → 36). */
  --nav-disc-rest: 44px;
  --nav-disc-delta: 8px;
  --nav-disc: calc(var(--nav-disc-rest) - var(--nav-disc-delta) * var(--nav-compact, 0));
  /* Stepper track: 2× pad (3→2) + links (40→32) = 46→36. Not nav pad. */
  --nav-track-compact: calc(2 * (3px - 1px) + (var(--s-2) - 1px - 2px) + (24px - 4px) + (var(--s-2) + 1px - 2px));
  /* Clicks pass through the empty overlay; children opt back in. */
  pointer-events: none;
  /* Own stacking context so .nav__blur (z-index 0) cannot paint over
     .nav__inner (60) — the earlier mobile fog was the band sitting on top of
     the bare wordmark and burger. */
  isolation: isolate;
}
.nav__inner { pointer-events: auto; }
/* Matches rest .nav__inner (pad + 36px mark). JS overwrites with the
   measured rest height so breakpoint padding stays in lockstep. */
.nav-spacer {
  height: calc(var(--pad) * 2 + 36px);
  pointer-events: none;
}

/* Progressive blur: three stacked layers (was six). Each backdrop-filter is a
   full-viewport sample — six of them on every scrolled frame was the main
   chrome jank. Opacity snaps on scroll (no fade) so the GPU is not rebuilding
   blur while interpolating; the cheap ::after wash still eases in. */
.nav__blur {
  position: absolute;
  inset: 0 0 auto 0;
  height: 125%;
  pointer-events: none;
  opacity: 0;
  /* opacity:0 still samples backdrop-filter; hide the layers until scrolled */
  visibility: hidden;
  z-index: 0;
  contain: layout style paint;
}
.nav[data-scrolled] .nav__blur { opacity: 1; visibility: visible; }

.nav__blur::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--bg) 40%, transparent) 0%,
    color-mix(in srgb, var(--bg) 18%, transparent) 50%,
    color-mix(in srgb, var(--bg) 0%, transparent) 100%);
  opacity: 0;
  transition: opacity .35s var(--ease);
}
.nav[data-scrolled] .nav__blur::after { opacity: 1; }
.nav__blur span {
  position: absolute;
  inset: 0;
}
/* Extra empty spans in the markup are harmless no-ops. */
.nav__blur span:nth-child(1) {
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  -webkit-mask-image: linear-gradient(to top, transparent 0%, #000 18% 40%, transparent 62%);
  mask-image: linear-gradient(to top, transparent 0%, #000 18% 40%, transparent 62%);
}
.nav__blur span:nth-child(2) {
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  -webkit-mask-image: linear-gradient(to top, transparent 22%, #000 42% 64%, transparent 86%);
  mask-image: linear-gradient(to top, transparent 22%, #000 42% 64%, transparent 86%);
}
.nav__blur span:nth-child(3) {
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  -webkit-mask-image: linear-gradient(to top, transparent 48%, #000 70% 100%);
  mask-image: linear-gradient(to top, transparent 48%, #000 70% 100%);
}
.nav__blur span:nth-child(n + 4) { display: none; }

.nav__inner {
  position: relative;
  /* Above .menu (55): the dismiss layer is fixed inset:0 inside .nav, and
     at z-index 1 the brand + burger sat under it. The dropdown hangs below
     via --menu-top. */
  z-index: 60;
  max-width: var(--wrap);
  margin-inline: auto;
  /* Rest padding at every scroll position. Compact shrinks chrome, not the
     gap from the viewport top. */
  padding: var(--pad);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--s-6) - 8px * var(--nav-compact, 0));
}

/* color re-declared, not inherited: body sets color: var(--fg), but a resolved
   inherited rgb would stick when the ground rewrites --fg on body — and the
   wordmark (fill: currentColor) would stay white on a pale scrolled ground. */
.nav__brand {
  font-size: var(--t-h2);
  line-height: 28px;
  display: inline-flex;
  align-items: center;
  color: var(--fg);
}
/* White Ramp sheet under the bar while paint() ground is still charcoal —
   force dark ink so the wordmark and burger stay readable. */
.nav[data-on-sheet] {
  --fg: #130F0C;
  color: #130F0C;
}
.nav[data-on-sheet] .burger {
  color: #130F0C;
}


/* Brand mark — inlined as a sprite so it inherits currentColor */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.logo {
  height: 36px;
  width: calc(36px * 351.4 / 92.3);
  fill: currentColor;
  display: block;
}
/* Rest stays 36px. Compact uses Donate's rendered 40→36 scale (90%), so
   the mark goes 36→32.4 — same % as the CTA, not the old 22px tuck and not
   a lock to stepper/burger's 36px (which cancelled the morph). */
.nav__brand .logo {
  --logo-h: calc(36px * (1 - 0.1 * var(--nav-compact, 0)));
  height: var(--logo-h);
  width: calc(var(--logo-h) * 351.4 / 92.3);
}
.logo--footer {
  height: 36px;
  width: calc(36px * 351.4 / 92.3);
}
/* The nav mark does a slow front roll: around its own horizontal axis, in
   depth — up and over, not left-to-right like a globe. The perspective lives
   on the link so the mark has somewhere to turn in. Slow and linear, no beat. */
.nav__brand { perspective: 900px; }
/* The reverse is never shown and the mark is never away for long: it rolls to
   edge-on (a thin horizontal line), and at that instant it cuts to the
   opposite edge and keeps rolling the same way back to square. Reads as one
   continuous somersault, but every visible moment is the wordmark the right
   way up. Hiding the backface instead left it gone for half of every cycle. */
.nav__brand .logo {
  animation: brandTurn 14s linear infinite;
  transform-origin: 50% 50%;
  transform-style: preserve-3d;
  /* the hover lift rides the `scale` property rather than `transform`, so it
     composes with the turn above instead of overwriting it */
  transition: scale .3s var(--ease);
}
.nav__brand:hover .logo { scale: 1.03; }
@keyframes brandTurn {
  0%      { transform: rotateX(0deg); }
  49.99%  { transform: rotateX(90deg); }
  50%     { transform: rotateX(-90deg); }
  100%    { transform: rotateX(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .nav__brand .logo { animation: none; }
}
/* the hit area was exactly the wordmark's 137×36 box — a click a few pixels off
   the glyphs landed on the nav and did nothing. The padding grows the target,
   the negative margin keeps the mark where it was. Footer is separate: its
   negative margin pulled the mark under the contact sheet (z-index 2) and
   clipped the top of the wordmark. */
.nav__brand { padding: var(--s-2); margin: calc(var(--s-2) * -1); }

.footer__brand {
  display: inline-block;
  color: inherit;
  transition: opacity .25s var(--ease);
  /* Hit padding without escaping the footer — no negative margin. */
  padding: var(--s-2);
  margin: 0;
}
.footer__brand:hover { opacity: .65; }
.nav__cta {
  display: none;
  flex-shrink: 0;
  min-height: calc(40px - 8px * var(--nav-compact, 0));
  font-size: calc(var(--t-body) - 2px * var(--nav-compact, 0));
  line-height: calc(22px - 4px * var(--nav-compact, 0));
  padding-inline: calc(var(--s-6) - 8px * var(--nav-compact, 0));
}
/* Desktop only — below 900 the sticky chip owns Donate. */
@media (min-width: 900px) { .nav__cta { display: inline-flex; } }

.nav__end {
  display: none;
  align-items: center;
  gap: var(--s-5);
  flex-shrink: 0;
}
@media (min-width: 900px) {
  .nav__end { display: flex; }
}
/* Donate page has no burger — keep Back on small screens too. */
.donatepage .nav__end { display: flex; }
.donatepage .nav__cta { display: inline-flex; }

/* Mobile Donate: fixed bottom-left. Same soft liquid glass as .burger /
   .folder / .totop — not the solid header CTA — so the foot chrome reads as
   one family. */
.sticky-donate {
  display: none;
  position: fixed;
  left: var(--pad);
  bottom: var(--s-6);
  z-index: 70;
  /* Beat .btn--solid: frost over the page, ink tracks the ground. */
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  color: var(--fg);
  border: 0;
  /* Slightly softer blur than 16px — still reads as glass, cheaper with the
     nav frost + burger also sampling the page. */
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
  overflow: hidden;
  transition:
    opacity .25s var(--ease),
    transform .25s var(--ease),
    background-color .25s var(--ease),
    box-shadow .25s var(--ease),
    color .25s var(--ease);
}
.sticky-donate:hover {
  opacity: 1;
  background: color-mix(in srgb, var(--fg) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent);
}
.sticky-donate:active {
  opacity: 1;
  transform: scale(0.96);
}
@media (max-width: 899px) {
  .sticky-donate { display: inline-flex; }
}
/* A glint across the Donate pill every few seconds — enough to catch the eye on
   a page the reader is scrolling past, not enough to become a loop they notice.
   Nearly the whole timeline is idle: the band only crosses in the last fifth,
   so it reads as intermittent rather than as a running animation. */
.nav__cta { position: relative; overflow: hidden; }
.nav__cta::after,
.sticky-donate::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 40%;
  /* Solid header CTA glints in --bg; the glass chip glints in --fg so the
     band reads on frost the same way it does on the opaque pill. */
  background: linear-gradient(100deg,
    transparent,
    color-mix(in srgb, var(--bg) 22%, transparent),
    transparent);
  /* Fully off-stage + invisible while idle. -160% of a 40%-wide band still
     peeked into the left rim for ~5s of every cycle, which read as a stuck
     shine. opacity:0 holds the rest pose; linear keeps the sweep even. */
  transform: translateX(-220%) skewX(-12deg);
  opacity: 0;
  pointer-events: none;
  animation: ctaGlint 7s linear 2.5s infinite;
}
.sticky-donate::after {
  background: linear-gradient(100deg,
    transparent,
    color-mix(in srgb, var(--fg) 28%, transparent),
    transparent);
}
@keyframes ctaGlint {
  0%, 78% {
    transform: translateX(-220%) skewX(-12deg);
    opacity: 0;
  }
  79% {
    opacity: 1;
  }
  100% {
    transform: translateX(360%) skewX(-12deg);
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .nav__cta::after,
  .sticky-donate::after { animation: none; opacity: 0; }
}

/* ── Phone menu ─────────────────────────────────────────────────────────
   Below 900px the section track will not fit. The burger opens a compact
   glass dropdown (right-aligned under the disc) — dark near-black frost,
   clean list, grouped with a hairline — not a full-screen sheet. */

.burger {
  /* flex-shrink:0 is load-bearing. The row is space-between with a ~137px
     wordmark; without a locked basis the disc was the first thing the flex
     algorithm ate, and on a phone it collapsed to a few empty pixels — which
     is why the menu looked like it had "disappeared" against the ground. */
  display: inline-grid;
  place-items: center;
  flex: 0 0 var(--nav-disc);
  width: var(--nav-disc);
  height: var(--nav-disc);
  min-width: var(--nav-disc);
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  /* Same soft glass as .stepper — a 1.5px / 32% ring read as a thick outline
     on dark grounds and made the disc feel like a different chrome family. */
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
  color: var(--fg);
  cursor: pointer;
  position: relative;
  z-index: 2;
  /* Scale is in the same transition as the wash so a press does not fight the
     colour change. deck.gallery's disc dips a few percent on click — enough to
     feel pressed, not enough to look like a bounce. */
  transition: background-color .25s var(--ease), transform .18s var(--ease),
              box-shadow .25s var(--ease), color .25s var(--ease);
}
.burger:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
.burger:active { transform: scale(0.92); }
@media (prefers-reduced-motion: reduce) {
  .burger { transition: background-color .25s var(--ease), color .25s var(--ease); }
  .burger:active { transform: none; }
}
@media (min-width: 900px) { .burger { display: none; } }

.burger__mark { display: grid; gap: 4px; width: 18px; }
.burger__mark i {
  display: block;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
  /* Center origin so the open state forms a real × — left origin pivoted both
     bars into a chevron. Width is static (no transition) so uneven closed bars
     do not reintroduce the open-path layout stutter. */
  transform-origin: center;
  transition: transform .3s var(--ease), opacity .2s var(--ease);
}
/* the three rules are deliberately uneven, longest first */
.burger__mark i:nth-child(1) { width: 100%; }
.burger__mark i:nth-child(2) { width: 72%; }
.burger__mark i:nth-child(3) { width: 44%; }

/* open: the outer two cross into × and the middle one goes */
.burger[aria-expanded="true"] .burger__mark i:nth-child(1) {
  width: 100%;
  transform: translateY(6px) rotate(45deg);
}
.burger[aria-expanded="true"] .burger__mark i:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] .burger__mark i:nth-child(3) {
  width: 100%;
  transform: translateY(-6px) rotate(-45deg);
}

/* Tap-catcher only — no opacity fade on this full-viewport layer (that was
   painting the whole screen every frame while the glass panel opened). */
.menu {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: transparent;
  pointer-events: none;
}
.menu.is-open { pointer-events: auto; }
.menu[hidden] { display: none; }

/* Compact panel under the button, right-aligned with the bar. --menu-top is
   kept current by script on scroll/resize (not measured in the open click).
   Colors locked to site charcoal so a mid-scroll ground cannot tint the glass.
   Open animates transform only; opacity snaps on so backdrop-filter is not
   rebuilt while fading. Blur is modest + fill more opaque — cheaper on iOS. */
.menu__sheet {
  position: absolute;
  top: var(--menu-top, 80px);
  right: var(--pad);
  width: min(280px, calc(100vw - var(--s-8)));
  display: grid;
  gap: 0;
  padding: var(--s-3);
  border-radius: 28px;
  color: #FFFFFF;
  --fg: #FFFFFF;
  --bg: #000000;
  --fg-60: color-mix(in srgb, #FFFFFF 62%, transparent);
  --fg-35: color-mix(in srgb, #FFFFFF 35%, transparent);
  --rule: color-mix(in srgb, #FFFFFF 14%, transparent);
  --rule-strong: color-mix(in srgb, #FFFFFF 28%, transparent);
  /* More opaque fill = less backdrop sampling while the panel opens. */
  background: color-mix(in srgb, #000000 94%, transparent);
  backdrop-filter: blur(12px) saturate(1.25);
  -webkit-backdrop-filter: blur(12px) saturate(1.25);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, #FFFFFF 10%, transparent),
    inset 0 1px 0 0 color-mix(in srgb, #FFFFFF 14%, transparent),
    0 18px 40px -20px rgba(0, 0, 0, .5);
  transform-origin: top right;
  transform: translate3d(0, -8px, 0) scale(.97);
  opacity: 0;
  /* Close starts immediately: opacity and transform together, short. Waiting
     260ms to snap opacity off was the hitch at the start of close. */
  transition: transform .16s var(--ease), opacity .12s linear;
  contain: layout style paint;
}
.menu.is-open .menu__sheet {
  transform: translate3d(0, 0, 0) scale(1);
  opacity: 1;
  /* Open: opacity on immediately so blur isn't faded in; only transform runs. */
  transition: transform .22s var(--ease), opacity 0s;
  will-change: transform;
}

.menu__group {
  display: grid;
  gap: 2px;
  padding: var(--s-2) 0;
}
.menu__group + .menu__group {
  border-top: 1px solid var(--rule);
}

/* FAQ / Contact / Shop / Donate as slightly rounded tiles. */
.menu__group--tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-2);
}
.menu__tile {
  display: grid;
  place-items: center;
  min-height: 52px;
  padding: var(--s-3) var(--s-2);
  border-radius: 12px;
  font-size: 15px;
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-align: center;
  color: #FFFFFF;
  background: color-mix(in srgb, #FFFFFF 9%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, #FFFFFF 8%, transparent);
  transition: background-color .2s var(--ease), opacity .2s var(--ease);
}
.menu__tile:hover,
.menu__tile:focus-visible {
  background: color-mix(in srgb, #FFFFFF 16%, transparent);
  outline: none;
}
.menu__tile--donate {
  background: #FFFFFF;
  color: #130F0C;
  box-shadow: none;
}
.menu__tile--donate:hover,
.menu__tile--donate:focus-visible {
  background: #FFFFFF;
  opacity: .88;
}

/* Clean left-aligned list — hover/focus gets a soft light pill, idle is bare. */
.menu__link {
  display: block;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-pill);
  font-size: 17px;
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: #FFFFFF;
  background: transparent;
  transition: background-color .2s var(--ease);
}
.menu__link:hover,
.menu__link:focus-visible {
  background: color-mix(in srgb, #FFFFFF 10%, transparent);
  outline: none;
}

.menu__social {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3) var(--s-2);
  border-top: 1px solid var(--rule);
}
.menu__social a {
  display: grid;
  place-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-pill);
  color: #FFFFFF;
  transition: background-color .2s var(--ease);
}
.menu__social a:hover,
.menu__social a:focus-visible {
  background: color-mix(in srgb, #FFFFFF 10%, transparent);
  outline: none;
}
.menu__social svg { width: 20px; height: 20px; }
.menu__social svg :not(.fill) { fill: none; stroke: currentColor; stroke-width: 1.6; }
.menu__social svg .fill { fill: currentColor; stroke: none; }

@media (prefers-reduced-motion: reduce) {
  .menu__sheet, .burger__mark i { transition: none; }
  .menu.is-open .menu__sheet { transition: none; will-change: auto; }
}

/* ── Section stepper ────────────────────────────────────────────────────── */

/* The five sections as one segmented control rather than five loose links.
   The ratio is the whole point: 3px of padding on the track against a marker
   the full height of what is left, so the pill reads as a position inside a
   control instead of a highlight floating in an empty bar. Option 01 on
   tests/nav.html; the version this replaced is archived there as 07. */
.stepper { display: none; }
@media (min-width: 900px) {
  .stepper {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: calc(3px - var(--nav-compact, 0) * 1px);
    border-radius: var(--r-pill);
    /* layout only — shell chrome (fill / blur / lift) lives on ::before so
       the section links stay readable at the hero tip */
    background: transparent;
    box-shadow: none;
  }
  .stepper::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    pointer-events: none;
    background: color-mix(in srgb, var(--fg) 10%, transparent);
    box-shadow:
      inset 0 1px 0 0 color-mix(in srgb, var(--fg) 12%, transparent),
      0 0 6px 0 rgba(0, 0, 0, .05),
      0 6px 30px 0 rgba(0, 0, 0, .10);
    /* Static blur when scrolled — never fade backdrop-filter (that rebuilds
       the sample every frame of the opacity tween and fought the nav lift). */
    backdrop-filter: blur(12px) saturate(1.35);
    -webkit-backdrop-filter: blur(12px) saturate(1.35);
    opacity: 0;
    transform: translateY(3px);
    /* opacity + settle only — same curve/duration as .nav__inner lift */
    transition:
      opacity .55s cubic-bezier(0.22, 1, 0.36, 1),
      transform .55s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .nav[data-scrolled] .stepper::before {
    opacity: 1;
    transform: translateY(0);
  }
}
.stepper__link {
  font-size: calc(var(--t-body) - 2px * var(--nav-compact, 0));
  line-height: calc(24px - 4px * var(--nav-compact, 0));
  color: var(--fg-50);
  /* geometrically the box is already centred; the 1px trim is optical — this
     face's ink sits low in its line box, so dead-centre padding reads low */
  padding: calc(var(--s-2) - 1px - 2px * var(--nav-compact, 0)) calc(var(--s-4) - 4px * var(--nav-compact, 0)) calc(var(--s-2) + 1px - 2px * var(--nav-compact, 0));
  border-radius: 999px;
  transition: color .25s var(--ease), background-color .25s var(--ease);
}
.stepper__link:hover { color: var(--fg); }
.stepper__link[aria-current="true"] { color: var(--fg); }

/* The marker is a single element that slides between the links, built by
   script and measured off whichever one is current. It sits on a lighter
   surface than the track it is in — the track is the recess, the marker is the
   key raised out of it: a lit top edge and one tight contact shadow.

   Width and height are animated alongside the position, so a move from a short
   label to a long one stretches into place instead of stepping. */
.stepper { position: relative; }
.stepper__pill {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--fg) 12%, transparent);
  box-shadow:
    inset 0 1px 0 0 color-mix(in srgb, var(--fg) 14%, transparent),
    0 1px 2px 0 rgba(0, 0, 0, .18);
  opacity: 0;
  pointer-events: none;
  transition:
    transform .42s var(--ease-pop, cubic-bezier(.22, 1.2, .36, 1)),
    width .42s var(--ease-pop, cubic-bezier(.22, 1.2, .36, 1)),
    height .42s var(--ease),
    opacity .25s var(--ease);
}
/* the labels have to sit over it */
.stepper__link { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .stepper__pill { transition: opacity .25s var(--ease); }
}

/* There is no bar behind the row any more — the segmented track is the only
   surface in the header, and the wordmark and Donate button sit straight on
   the page at either end of it. Three objects rather than one long pill, which
   is what the track's own shape earns once it is strong enough to hold on its
   own. The progressive blur band below still does the page-edge job.

   The track therefore has to carry what the bar used to: it takes the blur and
   the lift on scroll, when there is content passing underneath it. Links stay
   visible at the tip; only the ::before shell fades in once scrolled. */
@media (prefers-reduced-motion: reduce) {
  .stepper::before { transition: none; transform: none; }
}

/* Compact shrinks logo / Donate / stepper. Padding stays --pad so the bar
   does not sit closer to the top after scroll. */

/* the contents have to ride above the pill */
.nav__brand, .stepper, .nav__cta { position: relative; }

@media (prefers-reduced-motion: reduce) {
  .nav__inner::before { transition: none; transform: none; }
}

/* ── Sections ───────────────────────────────────────────────────────────── */

.section { padding-block: var(--s-20); border-top: 1px solid var(--rule); }
@media (min-width: 1024px) { .section { padding-block: var(--s-27); } }

.split { display: grid; gap: var(--s-8); align-items: start; }
@media (min-width: 900px) {
  .split { grid-template-columns: 1.05fr 0.95fr; gap: var(--s-14); }
}
.split__body p { color: var(--fg-60); max-width: 62ch; line-height: 1.6; }
.split__body .split__src {
  margin-top: var(--s-4);
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--fg-35);
  max-width: 52ch;
}
.split__body p + p { margin-top: var(--s-5); }

/* ── Sharpie on the stall door ───────────────────────────────────────────
   Three things a student might write where nobody is meant to see them,
   filling the dead space under the loneliness heading. The panel is absolute
   from the bottom of the heading, so it costs the page no height at all, and
   the scrawls inside it are absolute too — the column keeps the size it had.

   There is no handwriting face in this project, so the marker look is built:
   set small, tilted per phrase, kept under full white the way ink on a painted
   surface never reads as bright as type, and roughened by the SVG filter. The
   clip-path is the pen tip — the phrase is fully laid out from the start and a
   window opens across it, so nothing reflows as it writes. */
.split__head { position: relative; }
.stall {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 300px;
  pointer-events: none;
}
.scrawl {
  position: absolute;
  left: var(--x, 0);
  top: var(--y, 0);
  margin: 0;
  /* small: this is something muttered in a margin, never a headline */
  font-size: clamp(20px, 2.2vw, 30px);
  line-height: 1.15;
  letter-spacing: -.01em;
  color: color-mix(in srgb, var(--fg) 30%, transparent);
  /* nobody writing on a stall door reaches for the shift key, and lowercase
     keeps these quieter than the set type around them */
  text-transform: lowercase;
  transform: rotate(var(--rot, 0deg));
  transform-origin: 0 50%;
  white-space: nowrap;
  filter: url(#inkRough);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1s steps(24, end);
  transition-delay: var(--d, 0ms);
}
.stall.is-writing .scrawl { clip-path: inset(0 -2% 0 0); }

@media (max-width: 899px) {
  /* one column: there is no dead space beside the copy to write in */
  .stall { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  /* the writing is the effect, so with motion off it is simply already written */
  .scrawl { clip-path: none; transition: none; }
}

/* ── Hero ───────────────────────────────────────────────────────────────── */

/* 20px off the top: the headline sits closer to the nav than to the lede */
.hero { padding-block: calc(var(--s-20) - 20px) var(--s-20); }
@media (min-width: 1024px) { .hero { padding-block: calc(var(--s-27) - 20px) var(--s-27); } }

.hero__inner { display: grid; gap: var(--s-14); align-items: center; min-width: 0; }
@media (min-width: 1024px) { .hero__inner { grid-template-columns: 1.1fr 0.9fr; } }

.hero__copy { min-width: 0; max-width: 100%; }
/* Hero h1 a hair larger than global .display; gaps below nudged a tad tighter. */
.hero__copy .display {
  font-size: clamp(44px, 11vw, 92px);
  margin-bottom: var(--s-6); /* was s-8 (32) → 24 */
  max-width: 14ch;
}
@media (min-width: 1024px) {
  /* 11vw is already at the 92px ceiling when the two-column kicks in, so the
     nowrap "alone → known" walks into the collage. Scale off viewport instead
     so the line stays in the copy track; 92px still lands around 1700. */
  .hero__copy .display {
    font-size: clamp(52px, 5.4vw, 92px);
  }
}
.hero__copy .lede { margin-bottom: var(--s-8); } /* was s-12 (48) → 32 */
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-4); margin-bottom: var(--s-2); }
@media (max-width: 1023px) {
  /* Room between Get Started and the orange sticker — s-2 left them kissing. */
  .hero__actions { margin-bottom: var(--s-6); } /* was s-8 (32) → 24 */
}


/* max-width alone is not enough: the sticker's max-content track and its
   -1.5° tip both want to widen the column, so the slot itself clips */
.hero__stat {
  padding-top: var(--s-4); /* restored from s-2 (8) → 16; with actions mb = 24 desktop / 40 mobile */
  max-width: min(46ch, 100%);
  min-width: 0;
  overflow: hidden;
}

/* The stat as a bumper sticker: it brings its own ink and stock rather than
   reading --fg, because the whole idea is an object applied to the page — it
   should not change when the ground scrolls underneath it. Shares the CTAs' pill
   geometry so it adds an object without adding a new shape language. */
.statsticker {
  display: block;
  box-sizing: border-box;
  /* fixed width and a clip: the line has to have somewhere to run out of.
     max-width caps the pill even when a parent is sizing from content. */
  width: min(340px, 100%);
  max-width: 100%;
  overflow: hidden;
  padding: var(--s-3) var(--s-6);
  background: #FF4D00;
  color: #FFFFFF;
  border-radius: var(--r-pill);
  font-size: var(--t-caption);
  line-height: 19.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* applied by hand, so it never sits perfectly square — the tip is why the
     parent clips: without it, the rotated corners widen the document */
  transform: rotate(-1.5deg);
  transform-origin: left center;
  transition: transform .35s var(--ease);
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, .5);
}
@media (max-width: 600px) {
  /* the orange board was reading as a second hero on a phone — type, padding
     and width all step down so it supports the headline instead of matching it */
  .statsticker {
    width: min(260px, 100%);
    padding: var(--s-2) var(--s-4);
    font-size: 11px;
    line-height: 16px;
    letter-spacing: .08em;
    box-shadow: 0 4px 12px -8px rgba(0, 0, 0, .45);
  }
}
/* the number is the point, so it gets its tracking back */
.statsticker b { font-weight: 400; letter-spacing: .04em; }
.statsticker:hover { transform: rotate(0deg); }

/* two copies end to end, translated by exactly half the track, so the loop has
   no seam. It never stops — the sticker is a board, not a control. The track is
   allowed to be wider than the pill; the pill's overflow:hidden is the window. */
.statsticker__track {
  display: flex;
  width: max-content;
  max-width: none;
  animation: stickerRoll 13s linear infinite;
}
.statsticker__set { white-space: nowrap; padding-right: 3em; flex: none; }
@keyframes stickerRoll {
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .statsticker, .statsticker:hover { transition: none; }
  /* still clipped to the column — width:auto used to let the full sentence
     push the page wider than the phone */
  .statsticker { width: max-content; max-width: 100%; }
  .statsticker__track { animation: none; width: auto; }
  .statsticker__set + .statsticker__set { display: none; }
  .statsticker__set { padding-right: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* ── Hero collage ────────────────────────────────────────────────────────
   The print collateral collage, rebuilt from its layers. The artboard is
   493 × 371; every piece is placed as a percentage of that, and anything that
   has to keep its proportion to the drawing (radii, borders, the notecard's
   script, the sticker's type) is sized in cqw against .collage. So the whole
   thing scales as one object without a single media query.

   Three motions, all small. At rest the pile breathes — each piece drifts on
   its own gentle float (individual translate/rotate so it composes with the
   hover-and-pointer transform below). Hovering loosens the pile along each
   piece's --dx/--dy/--dr. Under that, the pointer's position in the box moves
   each piece by its --p depth, so nearer stickers travel further than the
   photographs behind them. */
.collage {
  position: relative;
  width: 100%;
  aspect-ratio: 493 / 371;
  /* the drawing is bigger than the column it sits in and bleeds out of both
     sides of it — only once the hero is two columns and there is room to */
  max-width: none;
  container-type: inline-size;
  /* the parallax pair, written by script.js in the -1…1 range */
  --mx: 0;
  --my: 0;
  contain: layout style;
}

.cl {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--w) * 1%);
  height: calc(var(--h) * 1%);
  display: grid;
  place-items: center;
  /* the box is only a coordinate: the artwork inside takes the pointer */
  pointer-events: none;
  transform: translate3d(
    calc((var(--hx, 0) + var(--mx) * var(--p, .5) * 1.7) * 1cqw),
    calc((var(--hy, 0) + var(--my) * var(--p, .5) * 1.2) * 1cqw), 0);
  transition: transform .42s var(--ease-pop, cubic-bezier(.22, 1.28, .38, 1));
  /* the shallow pieces settle a beat after the ones in front */
  transition-delay: calc((1 - var(--p, .5)) * 55ms);
  /* Idle float defaults — nth-child overrides stagger duration/phase/path so
     the pile doesn't bob as one rigid unit. Amplitude scales with --p. */
  --idle-dur: 10s;
  --idle-delay: 0s;
  animation: clFloatA var(--idle-dur) ease-in-out var(--idle-delay) infinite alternate;
}
/* Three paths, opposite drifts and tips, so neighbouring pieces don't sync. */
@keyframes clFloatA {
  from { translate: 0 0; rotate: -.3deg; }
  to   { translate: calc(var(--p, .5) * .5cqw) calc(var(--p, .5) * -1.1cqw); rotate: .55deg; }
}
@keyframes clFloatB {
  from { translate: 0 0; rotate: .25deg; }
  to   { translate: calc(var(--p, .5) * -.55cqw) calc(var(--p, .5) * -.85cqw); rotate: -.5deg; }
}
@keyframes clFloatC {
  from { translate: 0 0; rotate: -.2deg; }
  to   { translate: calc(var(--p, .5) * .4cqw) calc(var(--p, .5) * .7cqw); rotate: .4deg; }
}
/* Per-piece stagger: unique period + negative delay (phase) + path variant. */
.cl:nth-child(1)  { --idle-dur: 11.2s; --idle-delay: -1.4s; animation-name: clFloatA; }
.cl:nth-child(2)  { --idle-dur: 8.6s;  --idle-delay: -4.8s; animation-name: clFloatB; }
.cl:nth-child(3)  { --idle-dur: 9.8s;  --idle-delay: -2.2s; animation-name: clFloatC; }
.cl:nth-child(4)  { --idle-dur: 10.5s; --idle-delay: -6.1s; animation-name: clFloatA; }
.cl:nth-child(5)  { --idle-dur: 7.9s;  --idle-delay: -3.5s; animation-name: clFloatB; }
.cl:nth-child(6)  { --idle-dur: 9.2s;  --idle-delay: -5.7s; animation-name: clFloatC; }
.cl:nth-child(7)  { --idle-dur: 10.8s; --idle-delay: -.9s;  animation-name: clFloatB; }
.cl:nth-child(8)  { --idle-dur: 8.3s;  --idle-delay: -7.2s; animation-name: clFloatA; }
.cl:nth-child(9)  { --idle-dur: 9.5s;  --idle-delay: -1.8s; animation-name: clFloatC; }
.cl:nth-child(10) { --idle-dur: 11s;   --idle-delay: -4.1s; animation-name: clFloatA; }
.cl:nth-child(11) { --idle-dur: 8.9s;  --idle-delay: -6.6s; animation-name: clFloatB; }
.cl:nth-child(12) { --idle-dur: 10.2s; --idle-delay: -2.9s; animation-name: clFloatC; }
.cl:nth-child(13) { --idle-dur: 9s;    --idle-delay: -5.3s; animation-name: clFloatA; }
/* script.js pauses idle CSS while the collage is off-screen */
.collage.is-idle-paused .cl { animation-play-state: paused; }
.cl:has(:hover) { z-index: 5; }

.collage:hover .cl { --hx: var(--dx); --hy: var(--dy); }

/* --r is the angle the piece was applied at, --hr the tip it takes on hover */
.cl__rot,
.cl__free {
  pointer-events: auto;
  transform: rotate(calc(var(--r, 0deg) + var(--hr, 0deg))) scale(var(--k, 1));
  transition: transform .42s var(--ease-pop, cubic-bezier(.22, 1.28, .38, 1));
  transition-delay: inherit;
}
.collage:hover .cl__rot,
.collage:hover .cl__free { --hr: calc(var(--dr, 0) * 1deg); }

.cl__rot { width: var(--iw, 100%); height: var(--ih, 100%); }
.cl__free { position: absolute; }

/* picking up a single piece lifts it clear of the pile */
.cl__rot:hover,
.cl__free:hover { --k: 1.06; }

/* Photographs: white hairline, 16px artboard radius, one soft drop */
.cl__card {
  overflow: hidden;
  border-radius: 3.25cqw;
  border: max(1px, .1cqw) solid #FFFFFF;
  box-shadow: 0 1.2cqw 2.4cqw -1.2cqw rgba(0, 0, 0, .55);
  background: #201A16;
}
.cl__card > img,
.cl__card > .cl__crop { width: 100%; height: 100%; }
.cl img { display: block; object-fit: cover; }
.cl__card > img { object-position: center; }

/* the layers that were cropped inside their frame keep the exact offsets they
   were given rather than being re-fitted */
.cl__crop { position: relative; overflow: hidden; }
.cl__crop > img { position: absolute; max-width: none; }
/* a cropped piece that is also placed by hand keeps its own coordinates:
   .cl__crop's positioning context must not demote it back into the grid */
.cl__free.cl__crop { position: absolute; }

/* the “Listen.” sticker: the two red shapes are its outline, and the word is
   set over them, so it stays type rather than becoming another flat image */
.cl__listen {
  font-family: "Aguafina Script", var(--font-quote);
  font-size: 5.5cqw;
  line-height: 4.64cqw;
  color: #B1BDFF;
  white-space: nowrap;
  transform: translate(-50%, -50%) rotate(calc(var(--r, 0deg) + var(--hr, 0deg))) scale(var(--k, 1));
}

/* Notecard — live type, so the script stays sharp at any size */
.cl__note {
  background: #FFE600;
  border-radius: .72cqw;
  display: grid;
  place-items: center;
  box-shadow: 0 .8cqw 1.6cqw -1cqw rgba(0, 0, 0, .5);
}
.cl__note p {
  font-family: "Imperial Script", var(--font-quote);
  font-size: 2.1cqw;
  line-height: 1.62cqw;
  color: #0015D2;
  text-align: center;
  /* the hand runs to the card's edges on the printed piece, but not past them */
  width: 92%;
}

/* Round sticker */
.cl__badge { border-radius: 50%; }
.cl__badge img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; }
.cl__word {
  position: absolute;
  pointer-events: none;
  font-size: .99cqw;
  line-height: 1;
  letter-spacing: -.05em;
  color: #000000;
  text-align: center;
  white-space: nowrap;
  transform: translateY(-50%) rotate(calc(var(--r, 0deg) + var(--hr, 0deg)));
  transition: transform .42s var(--ease-pop, cubic-bezier(.22, 1.28, .38, 1));
}
.collage:hover .cl__word { --hr: calc(var(--dr, 0) * 1deg); }

/* Frosted whisper. pill — solid-enough wash, no live backdrop sample in the
   already-animated collage (blur here fought the idle floats). */
.cl__pill {
  background: rgba(255, 255, 255, .28);
  border-radius: var(--r-pill);
  box-shadow: 0 .57cqw .86cqw rgba(0, 0, 0, .25);
  display: grid;
  place-items: center;
}
.cl__pill img { width: 80%; height: auto; }

/* Phone: ~15% larger cluster. Width + equal negative margins keep the
   artboard centred; html/body overflow-x:clip holds the bleed (same pattern
   as the desktop 144% widen — not a transform, so cqw sizing stays honest). */
@media (max-width: 1023px) {
  .hero__media .collage { width: 115%; margin-inline: -7.5%; }
}
@media (min-width: 1024px) {
  .hero__media .collage { width: 144%; margin-inline: -22%; }
}

@media (prefers-reduced-motion: reduce) {
  .cl, .cl__rot, .cl__free, .cl__word { transition: none; }
  .cl { animation: none; }
  .collage { --mx: 0; --my: 0; }
}

/* ── Hero orbit (promoted from tests/hero-layouts #v19, ink halo) ──────── */
.hero--orbit {
  padding-block: var(--s-6) var(--s-20);
  overflow: visible;
}
.hero--orbit.hl,
.hl.hero--orbit {
  overflow: visible;
}
/* ── 19  Full collage orbit ────────────────────────────────────────────── */
.hl--19 {
  padding-block: var(--s-4) var(--s-20);
  overflow: visible;
}
/* Yaw/pitch live on custom props so the rings themselves stay untransformed.
   A transform on .hl19-ring would trap satellite z-index inside one stacking
   context and the title could never sit between front and back sats. */
@property --yaw {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --pitch {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
@property --behind {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
.hl19-stage {
  position: relative;
  width: min(100%, 1440px);
  margin-inline: auto;
  height: calc(min(28.8vw, 368px) * 2 + 260px);
  display: grid;
  place-items: center;
  perspective: 1800px;
  overflow: visible;
  transform-style: preserve-3d;
}
/* Grab the globe: pan-y stays until JS commits a drag, then none. */
.hl19-stage[data-orbit] {
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.hl19-stage[data-orbit].is-orbit-dragging {
  cursor: grabbing;
  touch-action: none;
}
.hl19-sat img {
  -webkit-user-drag: none;
  user-select: none;
}
.hl19-ring {
  position: absolute;
  inset: 0;
  --yaw: 0deg;
  --pitch: 0deg;
  transform-style: preserve-3d;
  /* JS owns yaw/pitch (finger + fling). A CSS transition lagged the
     gesture and cancelled Ramp’s inertia. */
  pointer-events: none;
}
.hl19-sat {
  position: absolute;
  left: 50%;
  top: 50%;
  margin: 0;
  --behind: 0;
  --z: 21;
  --bank: 48deg;
  --ly: 0;
  --ly-scale: 1;
  --rise: 40px;
  z-index: var(--z);
  animation: hl19orbit 48s linear infinite;
  pointer-events: auto;
  /* Opacity always tracks depth so the fade is continuous. Filter is
     gated on .is-back so front sats keep mix-blend (holo) unflattened. */
  opacity: calc(1 - var(--behind) * 0.34);
}
.hl19-sat.is-back {
  /* Ramp-soft: 6px blur, ~36% darker — readable, not a smudge. */
  filter:
    blur(calc(var(--behind) * 6px))
    brightness(calc(1 - var(--behind) * 0.36));
}
.hl19-ring--mid .hl19-sat { animation-duration: 44s; --bank: 46deg; }
.hl19-ring--in .hl19-sat { animation-duration: 40s; --bank: 54deg; }
.hl19-ring--out .hl19-sat { --bank: 36deg; }
/* Off-screen: keep the current pose, stop the compositor spinning 18 layers. */
.hero--orbit.is-orbit-paused .hl19-sat { animation-play-state: paused; }
.hl19-sat--out { --rad: min(36vw, 500px); }
.hl19-sat--mid { --rad: min(28.5vw, 392px); }
.hl19-sat--in  { --rad: min(21.5vw, 292px); }

/* Original collage photos — framed prints, not stickers. */
.hl19-photo { width: min(118px, 10.5vw); }
.hl19-photo--wide { width: min(156px, 14vw); }
.hl19-photo img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  display: block;
  border-radius: 10px;
  border: 1px solid #fff;
  outline: 0;
  box-shadow: none;
}
.hl19-photo--wide img { aspect-ratio: 16 / 10; }
.hl19-photo--wide img[src*="photo-f"] {
  aspect-ratio: 16 / 9;
  object-position: 50% 42%;
}
/* 3D carousel around the headline (Ramp): a steeper rotateX bank so sats
   occupy the hero in X and Y — above, below, and to the sides — while
   still traveling in front of the type and behind it. --ly is a screen-Y
   offset (fraction of --rad) so rings are a volume, not one equator.
   Billboard un-rotates so art stays camera-facing. --yaw/--pitch from
   pointer are folded in here (not on the ring). */
@keyframes hl19orbit {
  from {
    transform:
      translate(-50%, -50%)
      translateY(calc(var(--rise, 0px) + var(--ly, 0) * var(--ly-scale, 1) * var(--rad)))
      rotateX(calc(var(--bank) + var(--pitch, 0deg)))
      rotateY(calc(var(--a) + var(--yaw, 0deg)))
      translateZ(var(--rad))
      rotateY(calc(-1 * (var(--a) + var(--yaw, 0deg))))
      rotateX(calc(-1 * (var(--bank) + var(--pitch, 0deg))))
      rotate(var(--tilt, 0deg));
  }
  to {
    transform:
      translate(-50%, -50%)
      translateY(calc(var(--rise, 0px) + var(--ly, 0) * var(--ly-scale, 1) * var(--rad)))
      rotateX(calc(var(--bank) + var(--pitch, 0deg)))
      rotateY(calc(var(--a) + var(--yaw, 0deg) + 360deg))
      translateZ(var(--rad))
      rotateY(calc(-1 * (var(--a) + var(--yaw, 0deg) + 360deg)))
      rotateX(calc(-1 * (var(--bank) + var(--pitch, 0deg))))
      rotate(var(--tilt, 0deg));
  }
}

/* Real sticker files — native aspect, alpha-shaped shadow, no CSS mockups. */
.hl19-sticker {
  border: 0;
  outline: 0;
  background: none;
  overflow: visible;
}
.hl19-sticker img {
  width: 100%;
  height: auto;
  display: block;
  object-fit: contain;
  border: 0;
  outline: 0;
  border-radius: 0;
  box-shadow: none;
  background: none;
  filter: drop-shadow(0 16px 20px rgba(0, 0, 0, .38));
}
.hl19-sticker--card img {
  filter: none;
}
.hl19-sticker--hand img {
  filter: drop-shadow(0 22px 28px rgba(0, 0, 0, .45));
}
.hl19-sticker--ygt        { width: min(108px, 10vw); }
.hl19-sticker--listen     { width: min(148px, 13.5vw); }
.hl19-sticker--note       { width: min(168px, 15.5vw); }
.hl19-sticker--how        { width: min(156px, 14.5vw); }
.hl19-sticker--bread      { width: min(112px, 10.5vw); }
.hl19-sticker--yellow     { width: min(148px, 13.5vw); }
.hl19-sticker--phone      { width: min(128px, 12vw); }
.hl19-sticker--holo       {
  width: min(118px, 11vw);
  --mx: 0.5;
  --my: 0.32;
  --px: 50%;
  --py: 32%;
}
/* Kiss-cut holographic foil sits behind opaque black ink — substrate
   rainbows; type does not. Pointer --mx/--my shift the spectral bands. */
.hl19-holo {
  position: relative;
  display: block;
  isolation: isolate;
  filter: drop-shadow(0 16px 20px rgba(0, 0, 0, .38));
}
.hl19-sticker--holo img {
  position: relative;
  z-index: 1;
  filter: none;
  mix-blend-mode: multiply;
}
.hl19-holo__disc {
  position: absolute;
  /* SVG circle is inset ~4px in a 130px viewBox */
  inset: 3.1%;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
  background:
    radial-gradient(
      circle at var(--px) var(--py),
      rgba(255, 255, 255, 0.95) 0%,
      rgba(255, 255, 255, 0.18) 16%,
      transparent 40%
    ),
    repeating-linear-gradient(
      calc(118deg + (var(--mx) - 0.5) * 90deg),
      #ff7aa8 0 12%,
      #ffe08a 12% 24%,
      #7dffc6 24% 36%,
      #7ec8ff 36% 48%,
      #c9a5ff 48% 60%,
      #ff7aa8 60% 72%
    ),
    conic-gradient(
      from calc(var(--mx) * 360deg) at var(--px) var(--py),
      #f7f4ff,
      #e4f3fb,
      #eef8e8,
      #fff6dc,
      #fbe4f0,
      #f7f4ff
    );
  background-size: 100% 100%, 260% 260%, 170% 170%;
  background-position:
    0 0,
    calc(var(--mx) * 100%) calc(var(--my) * 100%),
    calc(var(--mx) * 70%) calc(var(--my) * 70%);
  background-blend-mode: overlay, overlay, normal;
  box-shadow: inset 0 0 0 0.6px rgba(0, 0, 0, .18);
}
@media (prefers-reduced-motion: reduce) {
  .hl19-holo__disc {
    background-position: 0 0, 50% 32%, 50% 32%;
  }
}
.hl19-sticker--substance  { width: min(156px, 14.5vw); }
.hl19-sticker--card       { width: min(86px, 8vw); }
.hl19-sticker--hand       { width: min(396px, 36vw); }

.hl19-core {
  position: relative;
  z-index: 21;
  transform: translateZ(0);
  display: grid;
  justify-items: center;
  justify-content: center;
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  pointer-events: none;
  padding: 2.6rem 1.2rem 2.4rem;
  text-align: center;
}
.hl19-title {
  position: relative;
  z-index: 1;
  margin: 0;
  width: 100%;
  text-align: center;
  font-weight: 400;
  color: #fff;
  pointer-events: none;
}
.hl19-lead {
  display: block;
  width: 100%;
  white-space: nowrap;
  text-align: center;
  font-size: clamp(24px, 2.8vw, 42px);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: rgba(255, 255, 255, .88);
}
.hl19-lead-break { display: none; }
.hl19-pair {
  display: block;
  width: 100%;
  margin-top: .06em;
  margin-inline: auto;
  text-align: center;
  white-space: nowrap;
  font-size: clamp(112px, 13.2vw, 192px);
  line-height: .92;
  letter-spacing: -0.04em;
  color: #fff;
}
.hl19-pair .arrow {
  width: 0.76em;
  height: 0.72em;
  margin-inline: 0.04em 0.06em;
  vertical-align: 0;
  color: inherit;
}

/* “alone”: solid type at rest; outline + broken-heart overlay on hover.
   “known”: solid type at rest and hover; glossy heart overlay only (no letter fill).
   Art from Figma 15:6. */
.hl19-alone,
.hl19-known {
  position: relative;
  display: inline-block;
  pointer-events: auto;
  isolation: isolate;
}
.hl19-alone { --hl19-heart: url("assets/hero/alone-heart.png"); }
.hl19-known { --hl19-heart: url("assets/hero/known-heart.png"); }
.hl19-alone::before,
.hl19-known::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1em;
  height: 1em;
  background: var(--hl19-heart) center / contain no-repeat;
  /* -36% (not -50%): heart art is top-heavy, so sit it on the glyphs, not above caps. */
  transform: translate(-50%, -36%) scale(.84);
  transform-origin: center;
  opacity: 0;
  pointer-events: none;
  z-index: 3;
  filter: drop-shadow(0 12px 18px rgba(0, 0, 0, .48));
  transition:
    opacity .28s ease,
    transform .42s cubic-bezier(.22, 1.28, .38, 1);
}
.hl19-alone:hover::before,
.hl19-alone.is-lit::before,
.hl19-known:hover::before,
.hl19-known.is-lit::before {
  opacity: 1;
  transform: translate(-50%, -36%) scale(1);
}
.hl19-alone__solid {
  position: relative;
  z-index: 1;
}
.hl19-alone__stroke {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
  color: transparent;
  -webkit-text-fill-color: transparent;
  -webkit-text-stroke: 1px #fff;
  paint-order: stroke fill;
  text-shadow: none;
  pointer-events: none;
  opacity: 0;
}
/* Hollow broken-heart needs an outline so “alone” stays readable on hover only. */
.hl19-alone:hover .hl19-alone__solid,
.hl19-alone.is-lit .hl19-alone__solid {
  color: transparent;
  -webkit-text-fill-color: transparent;
  text-shadow: none;
}
.hl19-alone:hover .hl19-alone__stroke,
.hl19-alone.is-lit .hl19-alone__stroke {
  opacity: 1;
}

.hl19-known {
  z-index: 1;
}
.hl19-known__spray {
  position: absolute;
  left: 50%;
  top: 42%;
  width: 0;
  height: 0;
  overflow: visible;
  pointer-events: none;
  z-index: 4;
}
.hl19-below {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-6);
  text-align: center;
  max-width: 36rem;
  /* Stage is sized for the 3D orbit (sats at 50%), so ~half its height sits
     empty under the title. Pull the lede up: 50px - 0.4*stageH leaves ~32px
     title→lede (~30px tighter than the prior ~62px remainder). */
  margin: calc(50px - 0.4 * (min(28.8vw, 368px) * 2 + 260px)) auto 0;
  padding-inline: var(--pad);
  padding-bottom: var(--s-14);
  position: relative;
  z-index: 40;
}
.hl19-below .lede { margin: 0; color: rgba(255, 255, 255, .62); }
.hl19-ctas {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

@media (prefers-reduced-motion: reduce) {
  .hl19-ring { transition: none; }
  .hl19-sat {
    animation: none;
    transform:
      translate(-50%, -50%)
      translateY(calc(var(--rise, 0px) + var(--ly, 0) * var(--ly-scale, 1) * var(--rad)))
      rotateX(calc(var(--bank) + var(--pitch, 0deg)))
      rotateY(calc(var(--a) + var(--yaw, 0deg)))
      translateZ(var(--rad))
      rotateY(calc(-1 * (var(--a) + var(--yaw, 0deg))))
      rotateX(calc(-1 * (var(--bank) + var(--pitch, 0deg))))
      rotate(var(--tilt, 0deg));
  }
  .hl19-alone::before,
  .hl19-known::before {
    transition: none;
  }
}
@media (max-width: 1023px) {
  /* Mobile keeps the 3D orbit (hl19orbit + drag). Radii/tiles shrink so the
     ring stays inside a clipped stage; stacked “alone → / known” stays. */
  .hero--orbit {
    overflow-x: clip;
    overflow-y: visible;
    /* Was s-4 (16px). +40px pushes the whole stage (title + orbit) down for
       breathing room under the nav without growing the centered stage void. */
    padding-block: calc(var(--s-4) + 40px) var(--s-12);
  }
  .hl19-stage {
    /* Was clamp(640px, 168vw, 820px). Title is place-items:center, so ~half
       the stage sat empty between the nav and “Guiding students”. Exact half
       of those clamps cuts that dead air ~50%; orbit still clips via overflow. */
    height: clamp(320px, 84vw, 410px);
    overflow: hidden;
    width: 100%;
    max-width: 100%;
    perspective: 1200px;
  }
  .hl19-below {
    margin-top: calc(24px - 0.28 * clamp(320px, 84vw, 410px));
  }
  .hl19-sat { --rise: 8px; --ly-scale: 0.72; }
  .hl19-sat--out { --rad: min(40vw, 152px); }
  .hl19-sat--mid { --rad: min(32vw, 122px); }
  .hl19-sat--in  { --rad: min(24vw, 92px); }
  .hl19-photo { width: 72px; }
  .hl19-photo--wide { width: 92px; }
  .hl19-sticker--ygt        { width: 58px; }
  .hl19-sticker--listen     { width: 74px; }
  .hl19-sticker--note       { width: 82px; }
  .hl19-sticker--how        { width: 76px; }
  .hl19-sticker--bread      { width: 60px; }
  .hl19-sticker--yellow     { width: 72px; }
  .hl19-sticker--phone      { width: 66px; }
  .hl19-sticker--holo       { width: 60px; }
  .hl19-sticker--substance  { width: 76px; }
  .hl19-sticker--card       { width: 44px; }
  .hl19-sticker--hand       { width: min(110px, 30vw); }
  .hl19-core {
    width: 100%;
    max-width: 100%;
    padding: 1.1rem var(--pad) 1.2rem;
    justify-items: center;
    justify-content: center;
    text-align: center;
    box-sizing: border-box;
  }
  .hl19-title { width: 100%; text-align: center; }
  .hl19-lead {
    width: 100%;
    max-width: min(100%, 9.6em);
    margin-inline: auto;
    text-align: center;
    white-space: normal;
    overflow-wrap: break-word;
    font-size: clamp(20px, 5.8vw, 28px);
    line-height: 1.2;
  }
  .hl19-lead-break { display: inline; }
  /* Stack like “Beyond / work”: first line “alone →”, second “known”.
     .nobr is nowrap sitewide — override so the pair can wrap. */
  .hl19-pair,
  .hl19-pair.nobr {
    display: block;
    width: 100%;
    max-width: 100%;
    margin-inline: auto;
    text-align: center;
    white-space: normal;
    font-size: clamp(56px, 18vw, 88px);
    letter-spacing: -0.045em;
    line-height: 0.9;
  }
  .hl19-known { display: block; }
}

/* Scatter styles retained inert (JS no longer enables .is-scatter on mobile). */
.hl19-stage.is-scatter {
  cursor: default;
  touch-action: pan-y;
  overflow: hidden;
  perspective: none;
}
.hl19-stage.is-scatter .hl19-sat {
  animation: none;
  left: 0;
  top: 0;
  margin: 0;
  cursor: grab;
  touch-action: none;
  transform:
    translate3d(var(--sx, 0px), var(--sy, 0px), 0)
    rotate(var(--tilt, 0deg))
    scale(var(--sc, 1));
  transform-origin: 50% 50%;
  will-change: transform;
  filter: none;
  opacity: 1;
  z-index: 28;
}
.hl19-stage.is-scatter .hl19-sat.is-back {
  filter: blur(8px) brightness(0.86);
  opacity: 0.72;
  z-index: 8;
}
.hl19-stage.is-scatter .hl19-core {
  z-index: 30;
  transform: none;
  pointer-events: none;
}
.hl19-stage.is-scatter .hl19-lead {
  position: relative;
  z-index: 31;
}
.hl19-stage.is-scatter .hl19-pair {
  position: relative;
  z-index: 31;
}
.hl19-stage.is-scatter .hl19-alone,
.hl19-stage.is-scatter .hl19-known {
  pointer-events: auto;
}
.hl19-stage.is-scatter .hl19-sat.is-scatter-drag {
  cursor: grabbing;
  z-index: 40;
}
@media (prefers-reduced-motion: reduce) {
  .hl19-stage.is-scatter .hl19-sat {
    will-change: auto;
  }
}

/* Image slots */
.photo {
  aspect-ratio: 4 / 5;
  border: 0;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  display: flex;
  align-items: flex-end;
  padding: var(--s-6);
}
.photo__label {
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-50);
}

.slot {
  border: 0;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  display: grid;
  place-content: center;
  margin-top: var(--s-20);
}
.slot--cards { aspect-ratio: 21 / 9; margin-top: var(--s-12); }
.slot__label {
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-50);
}

/* Figma iceberg-01 (13:3) — 1920×1080 letter. Depth is the gradient, not a
   drawn berg: white surface, #0064F3 at the water, #071450 in the deep. */
.slot--iceberg {
  --berg-blue: #0064F3;
  --berg-paper: #EBEEF3;
  container-type: size;
  aspect-ratio: 16 / 9;
  position: relative;
  display: block;
  overflow: hidden;
  color: #fff;
  background: linear-gradient(
    180deg,
    #ffffff 0%,
    #ebeef3 14.904%,
    #cee1ff 25%,
    #87b3ff 32.644%,
    #0064f3 41.178%,
    #002e8d 51.703%,
    #071450 65.357%
  );
}
.berg__q,
.berg__lead,
.berg__asks {
  position: absolute;
  z-index: 2;
  margin: 0;
  letter-spacing: -0.05em;
}
.berg__q {
  top: 4.167%;
  color: var(--berg-blue);
  font-size: 6.113cqw;
  line-height: 1.245;
  white-space: nowrap;
}
.berg__q--ask { left: 3.385%; }
.berg__q--reply {
  right: 2.969%;
  text-align: right;
}
.berg__lead {
  top: 58.148%;
  left: 4.896%;
  width: 19.844%;
  color: #fff;
  font-size: 2.42cqw;
  line-height: 1;
}
.berg__asks {
  top: 56.827%;
  left: 45.573%;
  width: 51.458%;
  list-style: none;
  padding: 0;
  color: #fff;
  font-size: 2.42cqw;
  line-height: 1.6;
}
@media (max-width: 640px) {
  .slot--iceberg { aspect-ratio: 3 / 4; }
  .berg__q {
    top: 5%;
    max-width: 46%;
    font-size: clamp(26px, 8.6cqw, 40px);
    line-height: 1.15;
    white-space: normal;
  }
  .berg__q--ask { left: 5%; }
  .berg__q--reply { right: 5%; }
  .berg__lead {
    top: 42%;
    left: 6%;
    width: 44%;
    font-size: clamp(16px, 4.6cqw, 22px);
    line-height: 1.15;
  }
  .berg__asks {
    top: 58%;
    left: 6%;
    width: 88%;
    font-size: clamp(15px, 4.2cqw, 20px);
  }
}

/* ── Schools ────────────────────────────────────────────────────────────── */

.schools { padding-block: var(--s-12); border-top: 1px solid var(--rule); }
.schools__head { margin-bottom: var(--s-8); }
.schools__lead {
  font-size: clamp(20px, 2.4vw, 28px);
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.schools__sub {
  font-size: clamp(20px, 2.4vw, 28px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--fg-50);
}

.marquee {
  overflow: hidden;
  padding-block: var(--s-5);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee__track { display: flex; width: max-content; will-change: transform; }
.marquee__set { display: flex; align-items: center; }
.marquee__set li {
  font-size: var(--t-h2);
  line-height: 28px;
  color: var(--fg-60);
  padding-inline: var(--s-6);
  white-space: nowrap;
  display: flex;
  align-items: center;
  transition: color .25s var(--ease);
}
.marquee__set li:hover { color: var(--fg); }
.marquee__set li::after {
  content: "";
  width: var(--s-12);
}
@media (prefers-reduced-motion: reduce) {
  .marquee__track { flex-wrap: wrap; width: 100%; transform: none !important; }
  .marquee { mask-image: none; }
}

/* ── Problem ────────────────────────────────────────────────────────────── */

.stats {
  margin-top: var(--s-20);
  display: grid;
  gap: var(--s-12);
  border-top: 1px solid var(--rule);
  padding-top: var(--s-12);
}
/* Three across, not two and a panel — the Surgeon General's line is the third
   column. Everything breaks on 900 together: below it the row stacks, because
   two of these columns beside each other at tablet width leaves the labels
   setting three and four words to a line. */
@media (min-width: 900px) { .stats { grid-template-columns: repeat(3, 1fr); gap: var(--s-12); } }

/* Phone: the three facts as a snap rail rather than a tall stack. Full-bleed
   like .rail so the next tile peeks past the content edge and reads as
   swipeable; desktop keeps the three-column grid above. */
@media (max-width: 899px) {
  .stats {
    display: flex;
    gap: var(--s-5);
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    padding-inline: var(--pad);
    padding-bottom: var(--s-2);
    /* Horizontal rail for the tiles. Do NOT set touch-action: pan-x or
       overscroll-behavior-y: none — those trapped vertical page scroll when
       the finger was over the carousel. overflow-y: hidden clips stray paint
       without creating a nested vertical scroller. */
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--pad);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    /* full-bleed width means the existing top rule spans the phone, which
       reads cleaner than a hairline that stopped at the first tile */
  }
  .stats::-webkit-scrollbar { display: none; }
  .stats > .stat,
  .stats > .surgeon {
    flex: 0 0 min(300px, 78vw);
    scroll-snap-align: start;
    min-width: 0;
    /* Quiet unit chrome for the carousel — enough to read as a tile, not a
       heavy card. Desktop keeps the bare three-column facts. */
    padding: var(--s-6);
    border-radius: var(--r-md);
    background: color-mix(in srgb, var(--fg) 5%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 8%, transparent);
  }
  /* give the third tile the same column rhythm as the numerals so a swipe
     does not jump the eye between two layouts */
  .stats > .surgeon { gap: var(--s-4); }
  .stats .stat__label,
  .stats .surgeon blockquote { max-width: none; }
  /* Tile numerals were ~40px tall with room to spare — step up hard on phones */
  .stats .stat__num {
    font-size: clamp(64px, 18vw, 84px);
    line-height: 0.9;
  }
  /* Carousel tile: 17px was caption-scale next to the tall numerals */
  .stats .surgeon blockquote {
    font-size: 21px;
    line-height: 1.3;
  }
}

.stat__num {
  /* 8vw filled a half-width column; in a third it collided with the label */
  font-size: clamp(44px, 6vw, 84px);
  line-height: 0.92;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
}
.stat__label {
  margin-top: var(--s-4);
  font-size: var(--t-h2);
  line-height: 28px;
  color: var(--fg-60);
  /* the measure that keeps all three columns wrapping to the same depth */
  max-width: 26ch;
}
/* attribution sits quiet under the claim — present, never competing */
.stat__src {
  margin-top: var(--s-3);
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--fg-35);
}
/* 100 dots, the share filled — the count does the arguing */
.dots {
  /* positioned so each dot's offsetLeft/offsetTop is measured against the field
     itself — the proximity paint compares them with pointer coordinates that
     are relative to the field's own box */
  position: relative;
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  /* a third of the original 260px field: a mark beside the numeral rather than
     a chart. The gap scales with it, or 6px of gutter on 6px dots reads as a
     grid of gaps. */
  gap: 2px;
  max-width: 87px;
  margin-bottom: var(--s-8);
}
.dots span {
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--fg) 14%, transparent);
  /* --dd staggers the fill sweep; it is set per dot in script.js. It rides its
     own property rather than transition-delay so the hover transform below
     isn't held back by it too. */
  transition: background-color .45s var(--ease) var(--dd, 0ms);
}

/* Proximity scale: the pointer's radius shrinks the fill it passes over,
   uncovering the empty socket underneath. Transform only, and will-change is
   carried just while the field is in use. */
.dots span.is-on {
  background: var(--fg);
  transform: scale(calc(1 - var(--t, 0)));
  transition: background-color .45s var(--ease) var(--dd, 0ms),
              transform .32s var(--ease);
}
.dots.is-live span { will-change: transform; }

/* The whole field arrives with the block as one object, then the share fills
   in sequence — colour only, so each dot lights in place rather than fading
   in from nothing on a timeline of its own. */
.reveal:not(.is-in) .dots span.is-on {
  background: color-mix(in srgb, var(--fg) 14%, transparent);
}

/* No surface of its own any more. Standing in the row it only has to keep the
   same rhythm as the two figures beside it — marks where their numeral sits,
   sentence where their label sits, source where their source sits — and a tinted
   card would have made one of three equal facts look like an aside. */
.surgeon {
  border: 0;
  display: grid;
  gap: var(--s-3);
  align-content: start;
  justify-items: start;
}

.surgeon__viz { display: flex; gap: 3px; align-items: flex-end; }
.surgeon__viz span {
  /* Same tally as tests/surgeon.html option 09 (.tally--sm): fifteen uniform
     marks, solid ink — the ash tip was a leftover from the full-size panel and
     read as a second colour against the dark ground. */
  width: 2px; height: 18px;
  border-radius: 1px;
  background: var(--fg);
  opacity: .9;
  transform-origin: bottom;
  transform: scaleY(0);
  transition: transform 420ms var(--ease);
}
.reveal.is-in .surgeon__viz span { transform: scaleY(1); }
.surgeon__viz span:nth-child(1)  { transition-delay: 40ms; }
.surgeon__viz span:nth-child(2)  { transition-delay: 80ms; }
.surgeon__viz span:nth-child(3)  { transition-delay: 120ms; }
.surgeon__viz span:nth-child(4)  { transition-delay: 160ms; }
.surgeon__viz span:nth-child(5)  { transition-delay: 200ms; }
.surgeon__viz span:nth-child(6)  { transition-delay: 240ms; }
.surgeon__viz span:nth-child(7)  { transition-delay: 280ms; }
.surgeon__viz span:nth-child(8)  { transition-delay: 320ms; }
.surgeon__viz span:nth-child(9)  { transition-delay: 360ms; }
.surgeon__viz span:nth-child(10) { transition-delay: 400ms; }
.surgeon__viz span:nth-child(11) { transition-delay: 440ms; }
.surgeon__viz span:nth-child(12) { transition-delay: 480ms; }
.surgeon__viz span:nth-child(13) { transition-delay: 520ms; }
.surgeon__viz span:nth-child(14) { transition-delay: 560ms; }
.surgeon__viz span:nth-child(15) { transition-delay: 600ms; }

.surgeon blockquote {
  /* A step above the old 17px so the third tile's claim matches the weight of
     the numerals beside it, without climbing toward display size (32px ran
     three columns deep next to two-line labels). */
  font-size: 19px;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--fg);
  max-width: 26ch;
}

/* no measure of its own — the two sources beside it run the full column, and
   capping this one is what broke a single citation across three lines */
.surgeon__src {
  font-size: var(--t-micro);
  line-height: 1.4;
  color: var(--fg-35);
}

/* ── Solution ───────────────────────────────────────────────────────────── */

/* the deck rail carries its own bottom padding, so the section needs less */
.solution { padding-bottom: var(--s-8); }

/* Solution no longer paints its own ramp — the blue sequence is carried by
   data-ground stops like every other section, so the whole viewport is one
   flat colour that crossfades as you scroll. */
.solution { position: relative; }

/* Kept as optional pins for blocks that still need a known ink pair while the
   ground is mid-crossfade (e.g. the solid CTA on .deckpitch). Prefer letting
   paint() derive --fg when the surrounding ground is already settled. */
.on-blue {
  --fg: #FFFFFF;
  --bg: #0056D8;
  color: var(--fg);
}
.on-paper {
  --fg: #130F0C;
  --bg: #EBEEF3;
  color: var(--fg);
}

.solution__body { padding-block: var(--s-20); display: grid; gap: var(--s-4); justify-items: center; }
/* centred, so the measure comes in — 68ch of centred text loses its left edge */
.solution__body p { color: var(--fg); line-height: 1.6; max-width: 58ch; text-align: center; }

/* The box's own title, sitting directly above the 3D stage. It is a 16ch block
   inside .wrap, which is a plain block container — so justify-self has nothing
   to act on and the centring has to come from auto margins. With only
   justify-self the heading rendered hard left of centre in Safari; Chrome
   happened to look right, which is what made this hard to see. */
.boxstage__head {
  margin-top: var(--s-20);
  max-width: 16ch;
  margin-inline: auto;
  justify-self: center;
  text-align: center;
}
@media (max-width: 700px) {
  /* Two empty blue bands (above the box, and between box and body) cut by ~1/3 */
  /* Match .slot--iceberg's top margin so the blue bands read equal. */
  .boxstage__head { margin-top: var(--s-20); }
  /* Gap under the Box Set headline — the stage's own top margin was a full
     s-20 of empty blue before the lid; cut by ~1/3 to match the head hold. */
  /* Half the empty blue under Box Set (was 2/3 of s-20). */
  .boxstage { margin-top: calc(var(--s-20) * 1 / 2); }
  .solution__body { padding-block: calc(var(--s-20) * 2 / 3); }
  .problem.section { padding-block: var(--s-14); }
}

/* trademark mark, matched to the one printed on the box lid */
.tm {
  font-size: 0.3em;
  vertical-align: 0.9em;
  letter-spacing: 0;
  /* the last 5px by hand: vertical-align is in the mark's own em, which is
     small enough that nudging it there moves in jumps rather than pixels */
  position: relative;
  top: -5px;
}
.solution__head { text-align: center; margin-bottom: var(--s-4); }
/* Centered over the orbit; margin-inline centers the .h2-lg max-width block. */
.deckrail__head {
  text-align: center;
  margin-inline: auto;
  margin-bottom: var(--s-4);
  /* ~30%+ air under solution body before Explore topics (was flush via body pad only). */
  margin-top: calc(var(--s-14) + var(--s-6));
}
@media (max-width: 700px) {
  .deckrail__head { margin-top: calc(var(--s-20) + var(--s-4)); }
}
/* No frozen white pin — that left "Get the Whisper…" white-on-pale and the
   solid CTA looking like a ghost when how's white arrived early. Headline,
   body, and .btn--solid all follow paint()'s --fg/--bg together. */
.deckpitch {
  margin-top: var(--s-20);
}

/* ── How we work ────────────────────────────────────────────────────────── */

.how .h1 { margin-bottom: var(--gap-title-block); max-width: 18ch; }

/* Full-bleed bands: each step is a row with the numeral in the
   margin, so the section reads as three chapters rather than three cards. The
   numeral is set large and at full strength — it is the chapter number, and at
   this size it carries the sequence on its own rather than waiting for a
   hover to earn its contrast. Hairlines span the full row, above the first
   and below the last, so the three chapters read as a ruled list. */
.bands { list-style: none; margin: 0; padding: 0; display: grid; }
.band { border-top: 1px solid var(--rule); }
.band:last-child { border-bottom: 1px solid var(--rule); }
.band__inner {
  display: grid;
  gap: var(--s-2) var(--s-8);
  padding-block: var(--s-14);
  align-items: start;
}
@media (min-width: 768px) { .band__inner { grid-template-columns: 180px 1fr 1fr; } }
.band__no {
  font-size: clamp(48px, 6vw, 96px);
  line-height: .82;
  letter-spacing: -0.05em;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  /* optical, not metric: the numeral's half-leading drops its cap a couple of
     pixels below the title's, and the eye reads the cap line, not the box */
  position: relative;
  top: -2px;
}
.band__inner h3 {
  font-size: clamp(22px, 2.8vw, 34px);
  line-height: 1.16;
  letter-spacing: -0.02em;
  font-weight: 400;
}
.band__inner p { color: var(--fg-60); line-height: 1.6; max-width: 46ch; }

/* A photograph carried along under the pointer while a row is hovered — the
   ‘follows the cursor’ option from tests/hover.html. The row is wide and quiet,
   so the picture arrives where the reader is already looking and leaves with
   them. It is decorative: empty alt, never focusable, and pointer-events:none
   so it can never intercept anything meant for the row. --mx/--my/--tilt are
   written by script.js; the transition on transform is what makes the frame lag
   behind the pointer rather than being pinned to it. */
.band { position: relative; }
.band__inner { position: relative; }
.pop {
  position: absolute;
  top: 0;
  left: 0;
  width: 260px;
  z-index: 3;
  opacity: 0;
  transform: translate3d(calc(var(--mx, 0px) - 50%), calc(var(--my, 0px) - 50%), 0)
             rotate(var(--tilt, 0deg));
  transition: opacity .22s var(--ease), transform .5s var(--ease);
  pointer-events: none;
}
.band:hover .pop { opacity: 1; }
/* One frame with no transform transition, set by script the moment the pointer
   enters: the frame is placed where the cursor already is and grows from there,
   instead of travelling in from wherever it was last left. */
.pop.is-placing { transition: opacity .22s var(--ease); }

/* the growth lives on the image, so it still reads as opening out of the
   cursor while the frame itself is busy trailing the pointer */
.pop img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-md);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .55);
  transform: scale(.86);
  transform-origin: center;
  transition: transform .44s var(--ease-pop, cubic-bezier(.22, 1.28, .38, 1));
}
.band:hover .pop img { transform: scale(1); }
/* Phone: How bands as a snap rail of image-bg tiles — photo fills the tile,
   big 01/02/03 pinned top-left over the photo; title + body on the bottom
   scrim. Desktop (≥768) keeps the three-column hover-pop row. */
@media (max-width: 767px) {
  .bands {
    display: flex;
    gap: var(--s-4);
    width: 100vw;
    max-width: 100vw;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--pad);
    padding-inline: var(--pad);
    /* Top pad would stack on --gap-title-block and overshoot overheard */
    padding-block: 0 var(--s-4);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .bands::-webkit-scrollbar { display: none; }
  .band {
    flex: 0 0 min(300px, 82vw);
    width: min(300px, 82vw);
    scroll-snap-align: start;
    border: 0;
  }
  .band:last-child { border-bottom: 0; }
  .band__inner {
    position: relative;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-areas:
      "spacer"
      "title"
      "copy";
    grid-template-rows: 1fr auto auto;
    align-content: end;
    align-items: start;
    gap: var(--s-2);
    height: var(--tile-h);
    min-height: var(--tile-h);
    padding: var(--s-6);
    border-radius: var(--r-md);
    overflow: hidden;
    color: #FFFFFF;
    --fg: #FFFFFF;
    --bg: #130F0C;
    isolation: isolate;
    /* .wrap pad would inset the tile — cancel it so the photo goes edge to edge */
    width: 100%;
    max-width: none;
    margin: 0;
    box-sizing: border-box;
  }
  .band__inner::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(
      180deg,
      rgba(19, 15, 12, .28) 0%,
      rgba(19, 15, 12, .2) 28%,
      rgba(19, 15, 12, .45) 55%,
      rgba(19, 15, 12, .82) 100%
    );
  }
  /* Chapter numeral owns the top-left of the photo — not the title row. */
  .band__no {
    position: absolute;
    top: var(--s-5);
    left: var(--s-5);
    z-index: 2;
    margin: 0;
    font-size: clamp(72px, 22vw, 96px);
    line-height: .82;
    letter-spacing: -0.05em;
    color: #FFFFFF;
    text-shadow: 0 2px 18px rgba(19, 15, 12, .45);
    pointer-events: none;
  }
  .band__inner h3 {
    grid-area: title;
    position: relative;
    z-index: 2;
    align-self: end;
    color: #FFFFFF;
  }
  .band__inner p {
    grid-area: copy;
    position: relative;
    z-index: 2;
    max-width: none;
    color: color-mix(in srgb, #FFFFFF 82%, transparent);
  }
  .pop {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    max-width: none;
    height: 100%;
    opacity: 1;
    transform: none;
    transition: none;
    pointer-events: none;
  }
  .pop img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    aspect-ratio: auto;
    transform: none;
    border-radius: 0;
    box-shadow: none;
  }
}
@media (prefers-reduced-motion: reduce) and (min-width: 768px) {
  .pop { display: none; }
}

.steps { display: grid; gap: var(--s-6); }
@media (min-width: 900px) { .steps { grid-template-columns: repeat(3, 1fr); } }

.step {
  border: 0;
  border-radius: var(--r-md);
  padding: var(--s-8);
  display: grid;
  align-content: start;
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  transition: background-color .3s var(--ease);
}
.step:hover { background: color-mix(in srgb, var(--fg) 11%, var(--bg)); }

.step__no {
  display: block;
  font-size: clamp(40px, 5vw, 64px);
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--s-8);
}

.step h3 {
  font-size: clamp(22px, 2.4vw, 28px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  font-weight: 400;
  margin-bottom: var(--s-4);
}
.step p { color: var(--fg-60); line-height: 1.6; max-width: 40ch; }

/* ── A gift for you ─────────────────────────────────────────────────────── */

.gift { padding-block: var(--s-6) var(--s-14); }
.how { padding-bottom: var(--s-8); }
.gift + .section { border-top: 0; }
/* Tan partner ground: the default section hairline sat in the beige when
   gatherings shared #DFD1CB. Expect is a light-blue stop again, so it keeps
   the default .section hairline at that colour edge. Leave the .section rule. */
.partner { border-top: 0; }
.gift__copy { display: grid; gap: var(--s-2); }
.gift__kicker {
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-50);
}
.gift__line {
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--fg-60);
  max-width: 58ch;
}

/* ── Video (click-to-load) ──────────────────────────────────────────────── */

.video {
  margin-top: var(--s-14);
  margin-inline: auto;
  max-width: 420px;
  aspect-ratio: var(--r, 9 / 16);
  border: 0;
  border-radius: var(--r-md);
  position: relative;
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  overflow: hidden;
}
.video--wide { max-width: 1000px; }

.video__btn {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  background: rgba(0, 0, 0, 0);
  border: 0; padding: 0;
  cursor: pointer;
  display: grid;
  place-content: center;
}
.video__btn::before {
  content: "";
  position: absolute; inset: 0;
  /* Poster + a soft centre scrim so the white Play control stays legible. */
  background-image:
    radial-gradient(ellipse 70% 70% at 50% 50%, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0) 70%),
    var(--thumb);
  background-size: cover;
  background-position: center;
  transition: transform .4s var(--ease);
}
/* Hover zooms the poster layer only — the Play control stays put (its own
   scale is never touched), and .video's overflow: hidden crops the growth. */
.video__btn:hover::before { transform: scale(1.06); }
.video__btn:active::before { transform: scale(1.04); }

/* Play control: big white triangle + the word "Play" in the display face,
   sized off the box width (cqi) so it holds its proportion at every size. */
.video { container-type: inline-size; }
.video__play {
  position: relative;
  z-index: 1;
  transform: none;
  display: flex;
  /* Baseline-align so the (empty) triangle box sits on the text baseline and
     its top meets the cap-height of "Play" — centring the line box would leave
     the letters riding above the triangle because of descender space. */
  align-items: baseline;
  gap: .3em;
  font-family: var(--font);
  font-weight: 500;
  font-size: clamp(2.5rem, 12vw, 8rem);
  font-size: clamp(2.5rem, 13cqi, 8.5rem);
  line-height: 1;
  letter-spacing: -.02em;
  color: #fff;
  /* Optical centring: the triangle carries more visual mass than the letters,
     so the geometric centre reads left-heavy — nudge the group a hair right.
     Vertically, split the difference between the cap-height block and the
     descender of "y" so the mass sits just above true centre. */
  left: .06em;
  top: .06em;
}
.video__play::before {
  content: "";
  width: .68em; height: .74em;
  background: currentColor;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.video__play::after { content: "Play"; }

.video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* HD inner viewport: YouTube keys quality off the iframe's pixel size, not
   the CSS layout box. Keep a real 1280×720 (or 720×1280) player and scale it
   into the overflow-hidden 16:9 box. Transform runs after click only.
   scale() needs a unitless factor: length/length, not length/number. */
.video__embed {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(var(--yt-w, 1280) * 1px);
  height: calc(var(--yt-h, 720) * 1px);
  transform: scale(calc(100cqw / (var(--yt-w, 1280) * 1px)));
  transform-origin: top left;
}
.video .video__embed iframe {
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ── Quotes ─────────────────────────────────────────────────────────────── */

/* Photo show above “Overheard at school”. Lives inside .wrap so it keeps
   --pad, and clips at --r-md (Ramp’s 40px homepage-card radius). One large
   frame at a time; arrows overlay the photo the way Ramp’s How we build
   strip does. */
.show {
  position: relative;
  /* Photo frame → “Overheard at school”: doubled from --s-8 (32 → 64). */
  margin-bottom: calc(var(--s-8) * 2);
}
.show__viewport {
  overflow: hidden;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--fg) 8%, var(--bg));
}
.show__scroller {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.show__scroller::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) {
  .show__scroller { scroll-behavior: auto; }
}
.show__slide {
  flex: 0 0 100%;
  width: 100%;
  margin: 0;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.show__slide img {
  display: block;
  width: 100%;
  height: clamp(220px, 52vw, 560px);
  object-fit: cover;
}
/* Phone: portrait frame instead of the 220px landscape strip. Desktop
   keeps the clamp above. */
@media (max-width: 767px) {
  .show__slide img {
    height: auto;
    aspect-ratio: 4 / 5;
  }
}
.show__nav {
  position: absolute;
  left: var(--s-4);
  bottom: var(--s-4);
  display: flex;
  gap: var(--s-3);
  z-index: 1;
}
.show__nav .railnav__btn {
  background: var(--bg);
  color: var(--fg);
}
@media (max-width: 700px) {
  .show__nav { left: var(--s-3); bottom: var(--s-3); }
}

.railhead {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: var(--s-8);
  margin-bottom: var(--gap-title-block);
}
.railhead .h1 { margin-bottom: 0; }

.railnav { display: flex; gap: var(--s-3); flex: none; }
.railnav__btn {
  width: 52px;
  height: 52px;
  display: grid;
  place-content: center;
  background: rgba(0, 0, 0, 0);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: border-color .2s var(--ease), opacity .2s var(--ease), transform .2s var(--ease);
}
.railnav__btn:hover { border-color: var(--fg); }
.railnav__btn:active { transform: scale(.94); }
.railnav__btn[disabled] { opacity: .3; cursor: default; border-color: var(--rule); }
.railnav__btn[disabled]:hover { border-color: var(--rule); }
.railnav__btn svg { width: 20px; height: 20px; }
.railnav__btn path {
  fill: none;
  stroke: currentColor;
  stroke-width: 3;
  stroke-linecap: butt;
  stroke-linejoin: miter;
}
@media (max-width: 700px) { .railnav { display: none; } }

/* Full-bleed drag rail: cards align to the content edge, nothing is clipped
   by the container padding on either side. */
.rail {
  --rail-gutter: max(var(--pad), calc((100vw - var(--wrap)) / 2 + var(--pad)));
  width: 100vw;
  margin-left: calc(50% - 50vw);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--rail-gutter);
  scrollbar-width: none;
  cursor: grab;
}
.rail::-webkit-scrollbar { display: none; }
.rail.is-dragging { cursor: grabbing; scroll-snap-type: none; }
.rail.is-dragging .quote,
.rail.is-dragging .rail__track > * { pointer-events: none; }

.rail__track {
  display: flex;
  gap: var(--s-6);
  width: max-content;
  padding-inline: var(--rail-gutter);
  /* Room for .quote--ask:hover translateY(-4px) — overflow-x stays auto. */
  padding-top: var(--s-3);
  padding-bottom: var(--s-2);
}

/* Portrait rather than square: a card the shape of a page reads as something
   set down on the rail, and the attribution has somewhere to sit far from the
   quote instead of crowding under it. */
.quote {
  /* Stepped down from 380×500 — the rail was reading as a wall of plaques
     rather than a set of cards you glance across. */
  flex: 0 0 min(300px, 72vw);
  scroll-snap-align: start;
  border: 0;
  border-radius: var(--r-md);
  padding: var(--s-8) var(--s-6);
  display: grid;
  gap: var(--s-5);
  grid-template-rows: 1fr auto;
  min-height: 468px;
  /* Black cards + white type on every ground — the rail reads as plaques. */
  background: #130F0C;
  color: #FFFFFF;
  --fg: #FFFFFF;
  --bg: #130F0C;
  transition: background-color .3s var(--ease), transform .3s var(--ease);
}
/* Same footprint as How tiles + Bartow cards on phones */
@media (max-width: 767px) {
  .quote,
  .quote--ask {
    /* Taller than --tile-h so long quotes aren't clipped; How/Bartow stay 420. */
    height: 468px;
    min-height: 468px;
    box-sizing: border-box;
    overflow: hidden;
  }
}
.quote:hover { background: #1c1814; }
/* Quote on a plaque, not a headline. Size follows the ~252–300px card —
   the previous vw clamp read as display type on desktop. */
.overheard .quote blockquote {
  font-family: var(--font-quote);
  font-size: 1.125rem;
  line-height: 1.4;
  letter-spacing: -0.008em;
}
@media (max-width: 767px) {
  .overheard .quote blockquote {
    font-size: 1.25rem;
    line-height: 1.4;
  }
}
.quote blockquote {
  font-family: var(--font-quote);
  font-size: clamp(18px, 2.1vw, 24px);
  line-height: 1.3;
  letter-spacing: -0.008em;
}
.quote figcaption {
  font-size: var(--t-caption);
  line-height: 19.5px;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: color-mix(in srgb, #FFFFFF 72%, transparent);
}
/* Portrait slot — a solid disc until real photography lands here. Each one
   carries its own --av so the rail reads as six people rather than six
   identical dots: the page's own orange, blue and yellow, plus three deeps
   that are only ever this size. */
.quote__av {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: var(--r-pill);
  background: var(--av, var(--fg));
}

/* The invitation that closes the rail. White sheet + dark ink — exception to
   the black plaques — with a real site CTA instead of tracked all-caps. */
.quote--ask {
  background: #FFFFFF;
  color: #130F0C;
  --fg: #130F0C;
  --bg: #FFFFFF;
  text-decoration: none;
  transition: background-color .3s var(--ease), transform .3s var(--ease),
              box-shadow .3s var(--ease);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, #130F0C 10%, transparent);
}
.quote--ask:hover {
  background: #FFFFFF;
  transform: translateY(-4px);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, #130F0C 14%, transparent),
              0 12px 28px -18px rgba(0, 0, 0, .35);
}
.quote__ask {
  font-family: var(--font-quote);
  font-size: clamp(18px, 2.1vw, 24px);
  line-height: 1.3;
  letter-spacing: -0.008em;
  text-wrap: pretty;
}
/* Matches .btn--solid voice: sentence case pill, not uppercase EMAIL US → */
.quote__go {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  justify-self: start;
  padding: 12px 20px;
  border-radius: var(--r-pill);
  background: #130F0C;
  color: #FFFFFF;
  font-size: var(--t-body);
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-transform: none;
  transition: transform .2s var(--ease), opacity .2s var(--ease);
}
.quote__go svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  transition: transform .3s var(--ease);
}
.quote--ask:hover .quote__go svg { transform: translateX(3px); }

/* ── Partner ────────────────────────────────────────────────────────────── */

.pull {
  margin-top: var(--s-20);
  display: grid;
  gap: var(--s-6);
}
.pull--tight { margin-top: var(--s-12); }

/* Video + quote side by side */
.feature {
  margin-top: var(--s-20);
  display: grid;
  gap: var(--s-12);
  align-items: center;
}
.feature > * { min-width: 0; }
@media (min-width: 1000px) {
  .feature { grid-template-columns: 1.15fr 0.85fr; gap: var(--s-14); }
}
.feature .video { margin: 0; max-width: none; width: 100%; }
.pull--beside {
  margin-top: 0;
  border-top: 0;
  padding-top: 0;
}
.pull--beside blockquote { max-width: 20ch; }

.feature--story { align-items: center; }
@media (min-width: 900px) {
  .feature--story { grid-template-columns: 0.85fr 1.15fr; }
}
.photo--story {
  position: relative;
  aspect-ratio: 4 / 3;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
.photo--story img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Desktop: pair is the feature grid. Phone: contents so each card snaps alone
   — photo card first in markup, so the image leads the quote in the rail. */
.story__pair { margin-top: 0; }
.story__pair > .story__card { min-width: 0; }
@media (max-width: 899px) {
  .story__pair { display: contents; }
}

.pull blockquote {
  font-size: clamp(22px, 3.4vw, 40px);
  line-height: 1.22;
  letter-spacing: -0.02em;
  max-width: 24ch;
  text-wrap: balance;
}
.pull--alt blockquote {
  font-size: clamp(18px, 2.2vw, 24px);
  line-height: 1.42;
  max-width: 48ch;
}
.pull figcaption {
  font-size: var(--t-caption);
  line-height: 19.5px;
}

.paths { margin-top: var(--s-20); display: grid; gap: var(--s-6); }
@media (min-width: 768px) { .paths { grid-template-columns: 1fr 1fr; } }

.path {
  border: 0;
  border-radius: var(--r-md);
  padding: var(--s-12) var(--s-8) var(--s-8);
  display: grid;
  gap: var(--s-4);
  align-content: start;
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  transition: background-color .3s var(--ease);
}
.path:hover { background: color-mix(in srgb, var(--fg) 11%, var(--bg)); }
.path h3 {
  font-size: clamp(22px, 2.4vw, 28px);
  line-height: 1.2;
  font-weight: 400;
  letter-spacing: -0.01em;
}
.path p { color: var(--fg-60); max-width: 38ch; }
.path .btn { justify-self: start; margin-top: var(--s-4); }
/* Modest mark, not a hero illustration — viewBox 24 keeps stroke weight */
.path__icon {
  width: 28px;
  height: 28px;
  color: var(--fg);
  margin-bottom: var(--s-2);
  overflow: visible;
}
/* Phone: the path tiles were ~square shells — pull padding/gaps in so icon,
   heading, body, and button sit denser without touching the 28px marks. */
@media (max-width: 767px) {
  .path {
    padding: var(--s-6) var(--s-5) var(--s-5);
    gap: var(--s-2);
  }
  .path__icon { margin-bottom: 0; }
  .path .btn { margin-top: var(--s-2); }
  /* Lead-in to the video was a full s-20 (80px) under the partner lede */
  .partner .feature { margin-top: var(--s-8); }
}

/* ── What you can expect ────────────────────────────────────────────────── */

/* ── What you can expect ─────────────────────────────────────────────────
   The five steps as an index, with one landscape frame beside it: the list is
   the control, the frame is the result. Step changes slide the outgoing card
   out and the incoming card in (direction follows the list), so the stage
   reads as one object turning over rather than as a hard cut. */
/* the intro split has to share the stepper's column ruler, or the body copy's
   left edge lands a few px off the photo frame below it */
@media (min-width: 900px) {
  .expect .split { grid-template-columns: 1fr 1fr; gap: var(--s-12); }
}

.gath {
  margin-top: var(--s-14);
  display: grid;
  gap: var(--s-12);
}
@media (min-width: 900px) { .gath { grid-template-columns: 1fr 1fr; align-items: start; } }

.gath__index { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.gath__item button {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  background: none;
  border: 0;
  padding: var(--s-2) 0;
  color: var(--fg-35);
  font: inherit;
  font-size: clamp(26px, 3.6vw, 44px);
  line-height: 1.1;
  letter-spacing: -0.026em;
  text-align: left;
  cursor: pointer;
  transition: color .3s var(--ease);
}
.gath__item button:hover { color: var(--fg-60); }
.gath__item.is-on button { color: var(--fg); }
/* The step number as a counter rather than a caption: a filled disc the same
   height as the label beside it (Greeting / Story). font-size % is of the
   button; width/height em are of that shrunk size — 1/0.48 restores a full
   title-em diameter (a custom-prop 1em was re-resolving on the child and
   collapsing the circle again). Digits sit at ~half the title so 01 reads
   inside the disc rather than as a micro caption. */
.gath__no {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  font-size: 48%;
  width: 2.083333em;
  height: 2.083333em;
  border-radius: var(--r-pill);
  background: var(--fg);
  color: var(--bg);
  line-height: 1;
  letter-spacing: 0;
  /* Optical centring. Digits carry no descender, so the ink sits about .08em
     below the middle of the line box — measured, not guessed. Half of this
     padding is what lifts it back onto the disc's true centre. */
  padding-bottom: .16em;
  /* Disc sat a hair above the label's visual midline; 2px drops it onto the word. */
  transform: translateY(2px);
  font-variant-numeric: tabular-nums;
  transition: background-color .3s var(--ease), opacity .3s var(--ease);
  /* the inactive steps sit back with their titles */
  opacity: .38;
}
.gath__item button:hover .gath__no { opacity: .65; }
.gath__item.is-on .gath__no { opacity: 1; }

/* the stage holds every frame stacked, so switching costs no layout */
/* The cards are stacked in one grid cell rather than positioned absolutely, so
   the stage takes the height of the tallest of them. Absolute cards were pinned
   to the stage's own height and the caption ran back up under the photograph
   whenever the frame was taller than that. */
.gath__stage { position: relative; display: grid; overflow: hidden; }
.gath__card {
  grid-area: 1 / 1;
  margin: 0;
  display: grid;
  grid-template-rows: auto auto;
  align-content: start;
  gap: var(--s-6);
  opacity: 0;
  transform: translateY(14px);
  pointer-events: none;
  z-index: 0;
  transition:
    opacity .42s var(--ease),
    transform .5s cubic-bezier(.22, 1.15, .36, 1);
}
.gath__card.is-on {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  z-index: 2;
}
/* Parked just off-stage so the enter transition has somewhere to come from.
   transition:none holds the pose for one frame; JS drops the class after. */
.gath__card.is-from-down {
  transition: none;
  opacity: 0;
  transform: translateY(22px);
}
.gath__card.is-from-up {
  transition: none;
  opacity: 0;
  transform: translateY(-22px);
}
.gath__card.is-out-up {
  opacity: 0;
  transform: translateY(-22px);
  z-index: 1;
}
.gath__card.is-out-down {
  opacity: 0;
  transform: translateY(22px);
  z-index: 1;
}
.gath__img {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-md);
  overflow: hidden;
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
}
.gath__img img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  object-position: 20% 40%;
}
.gath__img img[src*="photo-a"] { object-position: 28% 42%; }
.gath__img img[src*="photo-b"] { object-position: 8% 42%; }
.gath__img img[src*="photo-c"] { object-position: 22% 45%; }
.gath__img img[src*="photo-d"] { object-position: 26% 58%; }
.gath__img img[src*="photo-e"] { object-position: 12% 42%; }
/* ── Phone: the step's detail as a card at the foot of the screen ────────
   On a narrow screen the stage would sit a scroll below the list it belongs
   to, so tapping a step changes something the reader cannot see. Instead the
   detail rides at the bottom of the viewport while the section is on screen —
   the list stays under the thumb, and the card updates in place.

   It needs no new markup and no new state: the stage already holds every card
   with .is-on on the current one, so this is the same object re-placed. */
@media (max-width: 899px) {
  .expect .gath__stage {
    position: fixed;
    left: var(--s-3);
    right: var(--s-3);
    bottom: var(--s-3);
    z-index: 40;
    min-height: 0;
    border-radius: var(--r-md);
    /* Latest ask: black card + white type on phones — reads as a discrete
       note over the grey gatherings ground rather than another pale panel */
    background: #130F0C;
    color: #FFFFFF;
    --fg: #FFFFFF;
    --bg: #130F0C;
    box-shadow: 0 20px 50px -18px rgba(0, 0, 0, .55);
    /* ~1.5× the earlier phone card: the 120px strip was too thin to read the
       note against, and the thumbnail read as a stamp rather than a frame */
    padding: var(--s-5);
    /* Fully below the viewport at rest — slides up when the section is being
       read, and slides back down when the reader leaves, rather than a 12px
       fade that barely read as motion. */
    opacity: 0;
    transform: translateY(calc(100% + var(--s-3)));
    pointer-events: none;
    transition:
      opacity .35s var(--ease),
      transform .55s cubic-bezier(.22, 1.2, .36, 1);
  }
  .expect.is-near .gath__stage {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
  /* The sticky Donate chip shares this corner — tuck it away while the stage
     owns the foot, so the two never stack. */
  body:has(.expect.is-near) .sticky-donate {
    opacity: 0;
    pointer-events: none;
    transform: translateY(10px);
    transition: opacity .25s var(--ease), transform .25s var(--ease);
  }

  /* the card lies down: portrait thumb beside the note — narrower than the
     old 144 square so the caption gets the width, taller so it fills the
     card's own height rather than sitting as a stamp. */
  .expect .gath__card {
    grid-template-rows: none;
    grid-template-columns: 140px 1fr;
    gap: var(--s-5);
    align-items: stretch;
  }
  .expect .gath__img {
    aspect-ratio: auto;
    height: 100%;
    min-height: 148px;
    border-radius: var(--r-sm);
  }
  .expect .gath__card figcaption {
    align-content: center;
  }
  .expect .gath__label {
    /* Quiet against the dark card — sits under the large index labels, not with them */
    color: rgba(255, 255, 255, .55);
  }
  .expect .gath__note {
    font-size: 15px;
    line-height: 1.45;
    color: #FFFFFF;
  }

  /* No spacer between the step list and Volunteer — 156px here was the
     "way too much space" void. The fixed stage overlays the viewport foot;
     scrollspy no longer needs document height to reach Action. */
  .expect.is-near .gath { padding-bottom: 0; }
  .expect .cta-row { margin-top: var(--s-14); margin-bottom: var(--s-6); }

  /* Phone: type stays large; spacing lands between the old s-5/s-4 ladder
     (too loose) and the 0/2px pass (rows nearly touching). */
  .expect .gath__index { gap: var(--s-2); }
  .expect .gath__item button {
    /* ~40px at 375 — was reading as a caption next to the foot card */
    font-size: clamp(38px, 10.5vw, 48px);
    padding: var(--s-2) 0;
    gap: var(--s-4);
  }
}

.gath__card figcaption {
  display: grid;
  gap: var(--s-2);
  align-content: start;
  max-width: 46ch;
}
/* Small step title on the card — index + name, quieter than the list labels */
.gath__label {
  font-size: var(--t-caption);
  line-height: 1.3;
  letter-spacing: 0.01em;
  color: var(--fg-50);
  font-variant-numeric: tabular-nums;
}
@media (min-width: 900px) {
  .gath__label { display: none; }
}
.gath__note {
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--fg-60);
}
@media (prefers-reduced-motion: reduce) {
  .gath__card,
  .gath__card.is-from-down,
  .gath__card.is-from-up,
  .gath__card.is-out-up,
  .gath__card.is-out-down { transition: none; transform: none; }
  .expect .gath__stage {
    transition: opacity .3s var(--ease);
    transform: none;
  }
  .expect.is-near .gath__stage { transform: none; }
}

/* archived treatment, kept for tests/expect.html option 12 */
.agenda { margin-top: var(--s-14); }

.agenda__row {
  display: grid;
  gap: var(--s-4) var(--s-8);
  padding-block: var(--s-8);
  border-top: 1px solid var(--rule);
  align-items: start;
}
.agenda__row:last-child { border-bottom: 1px solid var(--rule); }
@media (min-width: 768px) {
  .agenda__row {
    /* every row the same height, whatever the copy runs to */
    grid-template-columns: 200px minmax(150px, 220px) 1fr;
    min-height: 214px;
  }
}

.agenda__img {
  margin: 0;
  aspect-ratio: 4 / 3;
  border: 0;
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  display: grid;
  place-content: center;
}
@media (max-width: 767px) { .agenda__img { max-width: none; width: 100%; } }
.agenda__row h3 {
  font-size: clamp(20px, 2.2vw, 24px);
  line-height: 1.25;
  font-weight: 400;
  letter-spacing: -0.01em;
}
.agenda__row p { color: var(--fg-60); line-height: 1.6; max-width: 60ch; }

/* ── Story / results ────────────────────────────────────────────────────── */

/* Desktop held photo: left column pins (photo + quote) while the narrative
   scrolls on the right. Phones keep the swipe rail, so this block is hidden
   under 900px. */
.story__held { display: none; }
@media (min-width: 900px) {
  .story .story__rail,
  .story .story__donate { display: none; }

  .story__held {
    display: grid;
    grid-template-columns: 0.9fr 1.1fr;
    gap: var(--s-14);
    align-items: start;
  }
  /* Clear the sticky nav; the right column supplies the travel height. */
  .held__stick { position: sticky; top: 96px; }
  .held__shot {
    position: relative;
    margin: 0;
    aspect-ratio: 4 / 5;
    border-radius: var(--r-md);
    overflow: hidden;
    background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  }
  .held__shot img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    opacity: 0;
  }
  .held__shot img:first-child { opacity: 1; }
  /* David portraits sit in a 4:5 frame — keep his face and the table group. */
  .held__shot img[src*="david-listen"] { object-position: 50% 18%; }
  .held__shot img[src*="david-tables"] { object-position: 48% 22%; }
  .held__shot img[src*="david-circle"] { object-position: 50% 28%; }
  /* Landscape cafeteria assembly — keep the crowd, not the empty tables. */
  .held__shot img[src*="david-assembly"] { object-position: 50% 62%; }
  .held__quote { margin: var(--s-6) 0 0; }
  .held__quote blockquote {
    font-family: var(--font);
    font-size: clamp(19px, 2vw, 24px);
    font-weight: 400;
    font-style: normal;
    line-height: 1.35;
    letter-spacing: -0.01em;
    color: var(--fg);
  }
  .held__quote figcaption {
    margin-top: var(--s-3);
    font-size: var(--t-caption);
    line-height: 19.5px;
    color: var(--fg-50);
  }
  .held__flow > .h1 { margin: 0 0 var(--s-8); max-width: 16ch; }
  .held__flow p { color: var(--fg-60); line-height: 1.6; max-width: 58ch; }
  .held__flow p + p { margin-top: var(--s-8); }
  .held__turn {
    color: var(--fg) !important;
    font-size: clamp(21px, 2.4vw, 30px);
    line-height: 1.2 !important;
    letter-spacing: -0.02em;
    max-width: 24ch !important;
  }
  .held__results {
    margin-top: var(--s-14);
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--s-8);
    row-gap: var(--s-3);
  }
  .held__resultsTitle {
    grid-column: 1 / -1;
    white-space: nowrap;
    max-width: none !important;
    font-size: var(--t-micro) !important;
    line-height: 1.4;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--fg-50) !important;
    margin: 0;
  }
  .held__results span {
    display: block;
    font-size: clamp(38px, 5vw, 64px);
    line-height: .92;
    letter-spacing: -0.04em;
    color: var(--fg);
    font-variant-numeric: tabular-nums;
  }
  .held__results p { margin-top: var(--s-2); max-width: 26ch; color: var(--fg-60); }
  .held__flow .cta-row--left { margin-top: var(--s-14); }
}

.story__body p { color: var(--fg-60); }
.story__body p + p { margin-top: var(--s-5); }
/* Section title above the held block — type from .h1, left with wrap content. */
.story__head {
  display: flex;
  align-items: center;
  gap: 0.28em;
  margin: 0 0 var(--s-8);
  text-align: left;
  color: var(--fg);
}
.story__eye {
  width: 0.88em;
  height: 0.88em;
  flex: 0 0 auto;
  display: block;
  color: inherit;
  /* Lashes sit in the top of the viewBox — drop the glyph onto the word. */
  transform: translateY(0.1em);
}
@media (min-width: 900px) {
  .story__head { margin: 0 0 var(--s-12); }
}
.story__turn {
  color: var(--fg) !important;
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.4;
  padding-block: var(--s-3);
  margin: 0;
}

/* Desktop: vertical beats; open card keeps the old two-column split. */
.story__rail {
  display: grid;
  gap: var(--s-12);
}
.story__card { margin: 0; min-width: 0; }
.story__card--open {
  position: relative;
  display: grid;
  gap: var(--s-8);
  overflow: hidden;
  background: #000000;
  color: #FFFFFF;
  --fg: #FFFFFF;
  --bg: #000000;
  border-radius: var(--r-md);
  padding: var(--s-8);
}
.story__card--open .story__body p {
  color: color-mix(in srgb, #FFFFFF 82%, transparent);
}
@media (min-width: 900px) {
  .story__card--open {
    grid-template-columns: 1fr 1fr;
    gap: var(--s-12) var(--s-14);
    align-items: start;
    padding: var(--s-12);
  }
  .story__card--cta .cta-row { margin-top: 0; }
  .story__card--results .results { margin-top: 0; }
}

/* Phone: swipeable story beats — same copy, one card per narrative step. */
@media (max-width: 899px) {
  .story__rail {
    display: flex;
    gap: var(--s-5);
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    padding-inline: var(--pad);
    padding-bottom: var(--s-2);
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--pad);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .story__rail::-webkit-scrollbar { display: none; }
  /* One footprint for every beat: equal flex basis + stretch so open, photo,
     quote, results, and CTA share width and height. Overflow clips long copy
     rather than letting one tile set a tall empty shell for the rest. */
  .story__rail { align-items: flex-start; }
  .story__card {
    flex: 0 0 min(320px, 84vw);
    width: min(320px, 84vw);
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--s-4);
    padding: var(--s-5);
    border-radius: var(--r-md);
    /* White tiles on the peach Bartow ground — not a grey mix of --fg. */
    background: #FFFFFF;
    color: #130F0C;
    --fg: #130F0C;
    --bg: #FFFFFF;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, #130F0C 8%, transparent);
    height: var(--tile-h);
    min-height: var(--tile-h);
    overflow: hidden;
    box-sizing: border-box;
  }
  .story__card--open {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    /* Override the white tile paint from sibling cards */
    background: #000000;
    color: #FFFFFF;
    --fg: #FFFFFF;
    --bg: #000000;
    box-shadow: none;
    padding: var(--s-6);
  }
  .story__card--open .story__tail {
    margin-top: auto;
    display: grid;
    gap: var(--s-4);
  }
  .story__card--open .h1 { font-size: clamp(28px, 8vw, 40px); }
  .story__card--turn { justify-content: center; }
  .story__card--turn .story__turn {
    font-size: clamp(22px, 6vw, 28px);
    line-height: 1.3;
    padding-block: 0;
  }
  .story__card--photo { padding: 0; justify-content: stretch; }
  .story__card--photo .photo--story {
    border-radius: var(--r-md);
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    flex: 1;
  }
  .story__card--photo img[src*="david-listen"] { object-position: 50% 18%; }
  .story__card--photo img[src*="david-tables"] { object-position: 48% 22%; }
  .story__card--photo img[src*="david-circle"] { object-position: 50% 28%; }
  .story__card--photo img[src*="david-assembly"] { object-position: 50% 62%; }
  .story__card--quote { justify-content: center; }
  .story__card--quote .pull--beside blockquote { max-width: none; }
  .story__card--results {
    justify-content: flex-start;
    height: auto;
    min-height: var(--tile-h);
    padding: var(--s-6) var(--s-6);
    gap: var(--s-5);
  }
  .story__card--results .results { margin-top: 0; }
  .story__card--results .results__title {
    margin-bottom: var(--s-5);
    line-height: 1.3;
  }
  .story__card--results .results__grid { gap: var(--s-6); }
  .story__card--results .result { padding-top: var(--s-5); }
  .story__card--results .result__label {
    max-width: none;
    line-height: 1.35;
    margin-top: var(--s-3);
  }
  .story__card--cta {
    align-items: center;
    justify-content: center;
  }
  .story__card--cta .cta-row { margin: 0; }
}

.results { margin-top: var(--s-20); }
.results__title {
  font-size: clamp(20px, 2.4vw, 28px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--fg);
  margin-bottom: var(--s-8);
}
.results__grid { display: grid; gap: var(--s-12); }
@media (min-width: 768px) { .results__grid { grid-template-columns: 1fr 1fr; gap: var(--s-14); } }

.result { border-top: 1px solid var(--rule); padding-top: var(--s-6); }
.result__num {
  font-size: clamp(48px, 8vw, 96px);
  line-height: 0.95;
  letter-spacing: -0.04em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}
.result__label {
  margin-top: var(--s-4);
  font-size: var(--t-h2);
  line-height: 28px;
  color: var(--fg-60);
  max-width: 30ch;
}

/* ── FAQ ────────────────────────────────────────────────────────────────── */

.faq .h1 { margin-bottom: var(--gap-title-block); }
.faq__list { border-top: 1px solid var(--rule); }

.qa { border-bottom: 1px solid var(--rule); }
.qa summary {
  list-style: none;
  cursor: pointer;
  padding-block: var(--s-6);
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8);
  font-size: clamp(17px, 1.9vw, 21px);
  line-height: 1.35;
  transition: opacity .2s var(--ease);
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary:hover { opacity: .7; }
.qa summary i { position: relative; width: 16px; height: 16px; flex: none; }
.qa summary i::before,
.qa summary i::after {
  content: ""; position: absolute; background: var(--fg);
  transition: transform .3s var(--ease);
}
.qa summary i::before { left: 0; right: 0; top: 7.5px; height: 1px; }
.qa summary i::after  { top: 0; bottom: 0; left: 7.5px; width: 1px; }
.qa[open] summary i::after { transform: scaleY(0); }
.qa__body { padding-bottom: var(--s-8); }
/* full strength, not the 60% every other secondary paragraph takes: on the
   FAQ's orange a dimmed white drops close to the ground it sits on */
.qa__body p { color: var(--fg); line-height: 1.6; max-width: 68ch; }
.qa[open] .qa__body { animation: fadeUp .35s var(--ease); }
@keyframes fadeUp { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .qa[open] .qa__body { animation: none; } }

/* ── Forms ──────────────────────────────────────────────────────────────── */

.contact__inner { display: grid; gap: var(--s-14); align-items: start; }
@media (min-width: 900px) { .contact__inner { grid-template-columns: 0.85fr 1.15fr; } }

/* the volunteer form explains the role before asking for details */
.contact__lede {
  margin-top: var(--s-6);
  color: var(--fg-60);
  line-height: 1.6;
  max-width: 46ch;
}

.form { display: grid; gap: var(--s-6); }
@media (min-width: 600px) { .form { grid-template-columns: 1fr 1fr; } }
.field { display: grid; gap: var(--s-2); }
.field--full { grid-column: 1 / -1; }

.field label,
.signup label {
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-60);
}

.field input,
.field textarea,
.signup input {
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 24px;
  font-weight: 400;
  color: var(--fg);
  appearance: none;
  -webkit-appearance: none;
  /* Slight ink wash, not opaque white — ground color shows through as it shifts. */
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-4);
  min-height: 44px;
  width: 100%;
  transition: border-color .2s var(--ease);
}
.field textarea { resize: vertical; }
.field input:focus,
.field textarea:focus,
.signup input:focus { outline: none; border-color: var(--fg); }
.field [aria-invalid="true"],
.signup [aria-invalid="true"] { border-color: var(--accent-yellow); }

.form__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-6); }
.form__status { font-size: var(--t-caption); line-height: 19.5px; color: var(--fg-60); }
.form__status[data-error] { color: var(--accent-red); }

.social { display: flex; gap: var(--s-4); }
.social a {
  display: grid;
  place-content: center;
  width: 40px;
  height: 40px;
  /* Same token as Shop: global `a { color: var(--fg) }` would stick on the
     reveal foot, where --fg is pinned white while `color` keyframes. */
  color: inherit;
}
.social svg {
  width: 22px;
  height: 22px;
  color: inherit;
}
.social svg :not(.fill) { fill: none; stroke: currentColor; stroke-width: 1.6; }
.social svg .fill { fill: currentColor; stroke: none; }

/* ── Footer ─────────────────────────────────────────────────────────────── */

/* Default in-flow foot (donate + simple pages). Homepage reveal is
   .footer--reveal below — scoped so fixed positioning does not leak. */
.footer {
  position: relative;
  z-index: 1;
  border-top: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  margin-top: 0;
  /* Bottom matches .wrap's horizontal gutter (--pad) so the logo/socials
     sit in an even frame. Top stays the larger section inset. */
  padding: var(--s-14) 0 var(--pad);
  background: transparent;
  color: #FFFFFF;
  --fg: #FFFFFF;
  --bg: #130F0C;
  /* paint() rewrites body --fg from the white sheet; inherited --fg-60
     would stay dark-on-charcoal. Pin the muted on-dark mix locally. */
  --fg-60: color-mix(in srgb, #FFFFFF 62%, transparent);
  overflow: visible;
}
/* Keep body on the live --bg token (paint()). A pinned charcoal here made
   iOS Safari’s bottom chrome band stay black while .ground / content colour
   stopped above it. Footer charcoal still arrives via the ground ladder. */
body:has(.footer--reveal) {
  background: var(--bg);
}
@media (min-width: 1024px) {
  .footer { padding: var(--s-20) 0 var(--pad); }
}

/* isolation:isolate still traps nav/Donate backdrop-filter. Main stays above
   the footer; no page-sized wash lives behind it. The foot's own box is
   pulled 140px under the sheet so only the corner wedges show the bands. */
body:has(.footer--reveal) main {
  position: relative;
  z-index: 2;
  isolation: isolate;
  margin-bottom: 0;
}

.footer--reveal {
  position: relative;
  z-index: 1;
  /* Clip the scrolling strip to this box — never the document. */
  overflow: hidden;
  /* Pull the clipped box up under the sheet radius (wedges only). */
  margin-top: -140px;
  /* Visible top inset doubled (--s-8 → 64px). 140px is the sheet tuck. */
  padding-top: calc(var(--s-8) * 2 + 140px);
  /* Ink is keyframed in lockstep with the wash — difference failed on tan/
     lavender (stayed white). Donate .footer keeps the on-dark tokens above. */
  color: #FFFFFF;
  --fg: #FFFFFF;
  --bg: #130F0C;
  --fg-60: color-mix(in srgb, currentColor 70%, transparent);
  animation: footerInk 34s linear infinite;
}
.footer--reveal.is-offscreen,
.footer--reveal.is-offscreen::before {
  animation-play-state: paused;
}
.footer--reveal.is-offscreen::before { will-change: auto; }
/* Six homepage grounds, each ~80% of the footer (20% shorter than a full
   foot), tiled twice. Adjacent hues blend — no hard stripe cuts. Height is
   % of the footer so the strip cannot cover the page. */
.footer--reveal::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 960%;
  z-index: 0;
  pointer-events: none;
  background-image: linear-gradient(
    to top,
    #000000 0%,
    #000000 4.2%,
    #FF4D00 8.333%,
    #FF4D00 12.5%,
    #D1D8FF 16.666%,
    #D1D8FF 20.8%,
    #DFD1CB 25%,
    #DFD1CB 29.2%,
    #0B5D3B 33.333%,
    #0B5D3B 37.5%,
    #0056D8 41.666%,
    #0056D8 45.8%,
    #000000 50%,
    #000000 54.2%,
    #FF4D00 58.333%,
    #FF4D00 62.5%,
    #D1D8FF 66.666%,
    #D1D8FF 70.8%,
    #DFD1CB 75%,
    #DFD1CB 79.2%,
    #0B5D3B 83.333%,
    #0B5D3B 87.5%,
    #0056D8 91.666%,
    #0056D8 95.8%,
    #000000 100%
  );
  animation: footerWashUp 34s linear infinite;
  will-change: transform;
}
@keyframes footerWashUp {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}
/* Same 34s linear clock as the strip. Window slides down the gradient
   (strip moves up): black / blue / olive (white) → tan / lavender (ink)
   → orange / black (white). */
@keyframes footerInk {
  0%, 36% { color: #FFFFFF; }
  42%, 68% { color: #130F0C; }
  74%, 100% { color: #FFFFFF; }
}
@media (prefers-reduced-motion: reduce) {
  .footer--reveal {
    animation: none;
    color: #130F0C;
  }
  .footer--reveal::before {
    animation: none;
    transform: translateY(-25%);
    will-change: auto;
  }
  .footer--reveal .footer__aka { color: color-mix(in srgb, #130F0C 62%, transparent); }
}
@media (min-width: 1024px) {
  .footer--reveal { padding-top: calc(var(--s-8) * 2 + 140px); }
}

.footer__inner {
  display: grid;
  gap: var(--s-14);
  width: 100%;
  overflow: visible;
  position: relative;
  z-index: 2;
}
@media (min-width: 1024px) {
  .footer__inner { gap: var(--s-20); }
}

/* Digital-locker copy stacked above the Mailchimp pill — single column. */
.footer__signup {
  display: grid;
  gap: var(--s-8);
  align-items: stretch;
  justify-items: start;
  min-width: 0;
}
.footer__signup > * { min-width: 0; max-width: 100%; }
.footer__signup .signup { width: min(100%, 36rem); }
.footer__copy .h2-lg { max-width: 28ch; }
.footer__aka {
  margin-top: var(--s-4);
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-60);
}

/* Inherit the keyframed ink. Signup stays a white pill + charcoal button.
   Re-declare on the <a> / svg so currentColor tracks the animation — inherited
   color on .social was overwritten by `a { color: var(--fg) }`. */
.footer--reveal .footer__copy,
.footer--reveal .footer__brand,
.footer--reveal .footer__links,
.footer--reveal .footer__links a,
.footer--reveal .social,
.footer--reveal .social a,
.footer--reveal .social svg {
  color: inherit;
}
.footer--reveal .footer__aka {
  color: color-mix(in srgb, currentColor 72%, transparent);
}
.footer--reveal .signup {
  isolation: isolate;
  position: relative;
  z-index: 3;
}
.footer--reveal .signup__field {
  background: #FFFFFF;
}
.footer--reveal .signup__field input {
  color: #130F0C;
}
.footer--reveal .signup__field input::placeholder {
  color: color-mix(in srgb, #130F0C 45%, transparent);
}
.footer--reveal .signup__go {
  background: #130F0C;
  color: #FFFFFF;
}
.footer--reveal .form__status {
  color: inherit;
}
.footer--reveal .form__status[data-error] {
  color: var(--accent-red);
}
.footer--reveal .footer__links a {
  opacity: 1;
}
.footer--reveal .footer__links a:hover { opacity: .7; }

/* Wordmark + socials under the signup. */
.footer__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-8) var(--s-12);
  width: 100%;
}
.footer__bar-end {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  flex: 0 0 auto;
  justify-content: flex-end;
  gap: var(--s-4);
  width: auto;
  /* Own row after the wordmark wraps: auto margin still parks the cluster
     on the right instead of stretching Shop … icons across the foot. */
  margin-left: auto;
}
.footer__links a {
  font-size: 15px;
  line-height: 1;
  letter-spacing: -0.01em;
  color: inherit;
  opacity: .72;
  transition: opacity .25s var(--ease);
}
.footer__links a:hover { opacity: 1; }
.donatepage .footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8) var(--s-12);
}

/* Opaque white for Ramp (covers sticky charcoal foot). Soft top fade lets
   paint()'s lavender→white crossfade show through — no hard lilac cut.
   Bottom radius + charcoal shadow (follows the curve) kills the light AA
   hairline under the corners without a full-width rectangular edge. */
.contact--sheet {
  position: relative;
  z-index: 2;
  background: linear-gradient(
    to bottom,
    transparent 0,
    #FFFFFF clamp(48px, 12vw, 96px)
  );
  color: #130F0C;
  --fg: #130F0C;
  --bg: #FFFFFF;
  border-top: 0;
  border-radius: 0 0 48px 48px;
  padding-block: var(--s-20);
  margin-bottom: 0;
}
@media (min-width: 1024px) {
  .contact--sheet {
    border-radius: 0 0 64px 64px;
    padding-block: var(--s-27);
  }
}
body:has(.footer--reveal) .contact--sheet {
  box-shadow: none;
}
.contact--sheet .contact__lede,
.contact--sheet .field label { color: color-mix(in srgb, #130F0C 62%, transparent); }
.contact--sheet > .wrap { position: relative; z-index: 1; }

/* Sheet pins --fg dark, but paint() keeps rewriting body --fg to white on the
   charcoal ground — so var(--rule-strong) on inputs was ~white-on-white. Pin
   the field chrome to the sheet ink so Name/Email/Message stay visible. */
.contact--sheet .field input,
.contact--sheet .field textarea {
  color: #130F0C;
  border-color: color-mix(in srgb, #130F0C 28%, transparent);
  background: color-mix(in srgb, #130F0C 8%, transparent);
}
.contact--sheet .field input:focus,
.contact--sheet .field textarea:focus {
  border-color: #130F0C;
}
.contact--sheet .field input::placeholder,
.contact--sheet .field textarea::placeholder {
  color: color-mix(in srgb, #130F0C 40%, transparent);
}
/* the icons carry their own 8px of optical padding, so the row is pulled back by
   that much to keep the right edge flush with the page's gutter */
.footer__bar .social { margin-right: -8px; }

.signup { display: grid; gap: var(--s-3); max-width: 520px; }
.signup__row { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.signup input { flex: 1 1 240px; width: auto; }
/* the honeypot has to be in the DOM and reachable by the post, just never seen */
.signup__bot { position: absolute; left: -5000px; }

/* ── Signup pill (footer newsletter) ────────────────────────────────────── */

.signup--lg { max-width: none; width: 100%; min-width: 0; }

/* Single pill with the button sunk into its right end, rather than a field and a
   button sitting side by side. No stroke — the light fill is the field. */
.signup__field {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  max-width: 100%;
  /* Grid/flex kids default to min-width:auto (content size). The placeholder +
     Sign up button were wider than the phone column, so the pill ran past the
     gutter and html's overflow-x:clip ate the right end. */
  min-width: 0;
  /* Right inset is two steps wider than top/bottom so the Sign up pill's
     right curve matches the top/bottom gap optically (equal padding reads
     cramped against the outer rim). Left stays a step wider than top —
     the email text also carries its own padding-inline. */
  padding: var(--s-2) var(--s-4) var(--s-2) var(--s-3);
  border: none;
  outline: none;
  box-shadow: none;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0);
}

/* the input gives up its own chrome — the wrapper is the field now */
.signup__field input {
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  min-height: 40px;
  border: 0;
  border-radius: 0;
  background: none;
  /* the placeholder is this field's label, so it is set at reading size and given
     a proper gutter rather than being tucked against the pill's edge */
  font-size: var(--t-h2);
  padding-inline: var(--s-5);
}
.signup__field input:focus { outline: none; border: 0; }
.signup__field input::placeholder { color: var(--fg-35); }
/* the wrapper shows the error state, so the input must not draw a second one */
.signup__field input[aria-invalid="true"] { border: 0; }

.signup__go {
  flex: 0 0 auto;
  max-width: 100%;
  font: inherit;
  font-size: var(--t-body);
  line-height: 22px;
  min-height: 40px;
  padding: calc(var(--s-2) - 1px) var(--s-6) calc(var(--s-2) + 1px);
  border: 0;
  border-radius: var(--r-pill);
  background: var(--fg);
  color: var(--bg);
  cursor: pointer;
  transition: opacity .18s var(--ease);
}
.signup__go:hover { opacity: .8; }
.signup__go:active { opacity: .6; }
.signup__go:disabled { opacity: .55; cursor: wait; }
@media (max-width: 899px) {
  /* Keep the whole pill inside the wrap gutter — never clip the right rim. */
  .footer .signup,
  .footer .signup__field { min-width: 0; max-width: 100%; }
  .footer .signup__field {
    padding: var(--s-2) var(--s-4) var(--s-2) var(--s-2);
    gap: var(--s-2);
  }
  .footer .signup__field input {
    padding-inline: var(--s-3);
    font-size: var(--t-body);
  }
  .footer .signup__go { padding-inline: var(--s-5); }
}


/* ── Quote ticker (partner) ─────────────────────────────────────────────── */

/* Two copies of the line sit end to end. The track is max-content; translating
   it by -50% lands the second copy exactly where the first started, so the
   loop has no seam. Type matches the live split-head h2 (.h1): Heros, not the
   Garamond the lined card used. */
.qticker {
  margin: var(--s-20) calc(var(--pad) * -1) 0;
  overflow: hidden;
  padding-block: var(--s-6);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.qticker__track {
  display: flex;
  width: max-content;
  will-change: transform;
  animation: qtickerRoll 63.6s linear infinite;
}
.qticker__item {
  flex: none;
  margin: 0;
  padding-right: 1.2em;
  white-space: nowrap;
  font-family: var(--font);
  font-size: clamp(42px, 10vw, 48px);
  line-height: 1.05;
  letter-spacing: -0.026em;
  font-weight: 400;
}
.qticker__who { color: var(--fg-50); }

@keyframes qtickerRoll {
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .qticker {
    margin-inline: 0;
    overflow: visible;
    mask-image: none;
  }
  .qticker__track { animation: none; display: block; width: auto; }
  .qticker__item {
    white-space: normal;
    padding-right: 0;
    line-height: 1.2;
    text-wrap: pretty;
  }
  .qticker__item + .qticker__item { display: none; }
  .qticker__who {
    display: block;
    margin-top: var(--s-4);
    color: var(--fg-50);
  }
}

/* ── Donate page ────────────────────────────────────────────────────────── */

/* Standalone page, so it gets a fixed dark ground rather than joining the
   homepage's scrolling ladder — a donor who arrived here has already decided,
   and colour changing under the form would only be noise. */
.donatepage { background: #000000; }

.donate__inner { display: grid; gap: var(--s-14); align-items: start; }
@media (max-width: 899px) {
  /* Widget first, closer to the header — the stacked copy was pushing Donorbox
     below the fold. Keep the lede; just don't make people scroll to give. */
  .donatepage .donate.section {
    padding-top: var(--s-6);
    padding-bottom: var(--s-14);
    border-top: 0;
  }
  .donate__inner { gap: var(--s-8); }
  .donate__form { order: -1; }
}
@media (min-width: 900px) {
  /* the form leads on the right; the argument sits beside it, not above it, so
     both are on screen together without scrolling. Column is the 425px widget
     plus a 1px white ring on the wrapper. */
  .donate__inner { grid-template-columns: 1fr minmax(250px, 427px); gap: var(--s-20); }
}

.donate__lede {
  margin-top: var(--s-6);
  font-size: clamp(16px, 1.7vw, 20px);
  line-height: 1.55;
  color: var(--fg-60);
  max-width: 46ch;
}
.donate__lede--follow { margin-top: var(--s-6); }
.donate__helps {
  margin-top: var(--s-5);
  max-width: 46ch;
  font-size: clamp(16px, 1.7vw, 20px);
  line-height: 1.45;
  color: var(--fg-60);
  list-style-type: disc;
  list-style-position: outside;
  padding-inline-start: 1.25em;
}
.donate__helps li + li { margin-top: var(--s-2); }
.donate__helps li::marker { color: currentColor; }

.donate__facts {
  display: grid;
  gap: var(--s-8);
  margin-top: var(--s-14);
  padding-top: var(--s-8);
  border-top: 1px solid var(--rule);
}
.donate__facts li { display: grid; gap: var(--s-1); }
.donate__num {
  font-size: clamp(32px, 4vw, 44px);
  line-height: 1;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.donate__lab { font-size: var(--t-caption); line-height: 19.5px; color: var(--fg-60); max-width: 34ch; }

.donate__note {
  margin-top: var(--s-12);
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--fg-35);
  max-width: 44ch;
}
.donate__note a { text-decoration: underline; }

/* Donorbox's card is ~8px rounded inside a square iframe. Clipping this
   wrapper at --r-md (16px) left dark page-ground in the four corners, between
   the widget and the frame. Clip at --r-sm to match the card, and keep a 1px
   white sheet around the iframe (we cannot style inside it) so the edge reads
   even. Height is set by widget.js. */
.donate__form {
  width: 100%;
  max-width: 427px; /* 425px widget + 1px white ring */
  justify-self: start;
  padding: 1px;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: #FFFFFF;
  box-shadow: 0 20px 50px -24px rgba(0, 0, 0, .6);
}
.donate__form iframe {
  display: block;
  width: 100%;
  max-width: 425px;
  border: 0;
  border-radius: calc(var(--r-sm) - 1px);
}

.donate-other { border-top: 1px solid var(--rule); }
.donate-other .h2-lg { margin-bottom: var(--s-12); }

/* ── The room, below the footer ─────────────────────────────────────────── */

/* [hidden] has to be able to win: .room sets a background and padding, and the
   UA sheet's bare display:none loses to any later class rule */
.room[hidden] { display: none; }

/* The site's own dark ground, not a blacker black — this reads as the bottom of
   the page, not as a panel dropped onto it. */
.room {
  /* --bg is rewritten per-scroll by the ground, so the literal is what pins this
     to the page's base dark rather than to whatever colour scrolled past last */
  background: #130F0C;
  border-top: 1px solid rgba(255, 255, 255, .1);
  padding-block: var(--s-12);
}
/* no title, no hint, no legend — the field is the whole thing, and a visitor who
   clicks it finds out what it does faster than a caption could tell them */
.room__inner { display: grid; }

/* A real grid, so every dot lands on a shared ruler and the field reads as a
   room of desks rather than as scattered points. auto-fill rather than a fixed
   column count: the room fills whatever width it is given and reflows on its
   own at every size, so there is no breakpoint to keep in sync. */
.room__board {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
  grid-auto-rows: 38px;
  place-items: center;
  gap: 0;
}
@media (min-width: 768px) {
  .room__board { grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); grid-auto-rows: 48px; }
}

.room__dot {
  width: min(16px, 100%);
  aspect-ratio: 1;
  border: 0;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  /* everyone starts as an outline: present in the room, not yet known */
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .5);
  cursor: pointer;
  transition: background-color .32s var(--ease), box-shadow .32s var(--ease), transform .32s var(--ease);
}
.room__dot:hover { transform: scale(1.2); }
/* filled dots keep their colour — the room has been changed, not briefly lit */
.room__dot[data-fill] {
  background: var(--fill);
  box-shadow: inset 0 0 0 1.5px var(--fill);
}

@media (prefers-reduced-motion: reduce) {
  .room__dot { transition: none; }
  .room__dot:hover { transform: none; }
}

/* ── Reveal ─────────────────────────────────────────────────────────────── */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .7s var(--ease) var(--d, 0ms), transform .7s var(--ease) var(--d, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }
/* Phone Bartow rail: overflow clips off-screen cards, so IntersectionObserver
   treated each swipe as a new arrival. Content is already in the card. */
.story__rail .reveal {
  opacity: 1;
  transform: none;
  animation: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ── Sticky folder button → test pages ──────────────────────────────────── */
/* Centred along the bottom edge now, since back-to-top has taken the corner.
   Always display:none until script.js adds .is-unlocked after fff (or a
   restored unlock). Never put display:grid on the base rule — that outranked
   the UA [hidden] style and showed the icon on Vercel before deferred JS. */
.folder {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: var(--s-6);
  z-index: 70;
  /* Same 44 disc and soft glass as .burger / .stepper — opaque pills with a
     hard rule read as a third chrome language against the frosted nav. */
  width: 44px;
  height: 44px;
  display: none;
  place-content: center;
  border: 0;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
  transition: background-color .2s var(--ease), transform .2s var(--ease),
              box-shadow .2s var(--ease);
}
.folder.is-unlocked {
  display: grid;
}
/* the translate has to be restated in every transform, or hovering would drop the
   centring and the button would jump to the right */
.folder:hover {
  background: color-mix(in srgb, var(--fg) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent);
  transform: translateX(-50%) translateY(-2px);
}
.folder:active { transform: translateX(-50%) translateY(0); }
.folder svg { width: 18px; height: 18px; fill: var(--fg); }
.folder[hidden] { display: none; }

/* ── Back to top ────────────────────────────────────────────────────────── */

/* Only appears once there is something to come back from, so it is never a
   button that does nothing. Same 44 glass disc as .burger and .folder. */
.totop {
  position: fixed;
  right: var(--s-6);
  bottom: var(--s-6);
  z-index: 70;
  width: 44px;
  height: 44px;
  display: grid;
  place-content: center;
  border: 0;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  backdrop-filter: blur(12px) saturate(1.3);
  -webkit-backdrop-filter: blur(12px) saturate(1.3);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  /* visibility:hidden while idle so backdrop-filter is not sampled off-stage */
  visibility: hidden;
  transition: opacity .25s var(--ease), transform .25s var(--ease),
              background-color .2s var(--ease), box-shadow .2s var(--ease),
              visibility 0s linear .25s;
}
.totop.is-in {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  visibility: visible;
  transition: opacity .25s var(--ease), transform .25s var(--ease),
              background-color .2s var(--ease), box-shadow .2s var(--ease),
              visibility 0s;
}
.totop:hover {
  opacity: 1;
  background: color-mix(in srgb, var(--fg) 10%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent);
  transform: translateY(-2px);
}
.totop svg { width: 18px; height: 18px; fill: none; stroke: var(--fg); stroke-width: 2; }

@media (prefers-reduced-motion: reduce) {
  .totop { transition: none; }
}

/* ── Card deck rail ─────────────────────────────────────────────────────── */

.deckrail {
  /* A scroll container clips at its padding edge, so the shadows need room
     inside it. The negative margins hand that room back to the layout —
     offset by the gap we actually want above and below the cards. */
  padding-block: 80px;
  /* the gap the cards actually want under their heading is closer to 24px than
     the 56 this started with — the padding above is only shadow room */
  margin-top: calc(var(--s-6) - 80px);
  margin-bottom: calc(var(--s-8) - 80px);
}
.deckrail .rail__track {
  align-items: flex-start;
  perspective: 1200px;
  padding-block: 0;
  gap: var(--s-5);
}

.deckcard {
  position: relative;
  flex: 0 0 236px;
  scroll-snap-align: start;
  margin: 0;
  transform-style: preserve-3d;
  transform:
    perspective(900px)
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translateZ(var(--tz, 0px));
  /* Transform only — animating filter:drop-shadow re-blurs every frame. */
  transition: transform .5s var(--ease);
}
.deckcard.is-tilting { transition: transform .08s linear; }

.deckcard__img {
  width: 100%;
  aspect-ratio: 5 / 7;
  object-fit: cover;
  border-radius: 10px;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
  transform: translateZ(30px);
  box-shadow: 0 8px 20px -6px rgba(0, 0, 0, .28);
  transition: box-shadow .35s var(--ease);
}
.deckcard:hover .deckcard__img {
  box-shadow: 0 16px 32px -8px rgba(0, 0, 0, .38);
}


/* ── Conversations box (CSS 3D) ─────────────────────────────────────────────
   Panel proportions come straight from the print files:
   lid 723×600, front/back 725×434, sides 594×428  →  W 724 · D 597 · H 431 */

.boxstage {
  /* unitless companions: CSS sqrt() only accepts plain numbers, so the
     rotation-diagonal math below has to be done unit-free and re-dimensioned */
  --box-w-n: 313;
  --box-d-n: 259;
  --box-w: calc(var(--box-w-n) * 1px);
  --box-d: calc(var(--box-d-n) * 1px);
  --box-h: 187px;
  --hw: calc(var(--box-w) / 2);
  --hd: calc(var(--box-d) / 2);
  --hh: calc(var(--box-h) / 2);
  position: relative;
  display: grid;
  justify-items: center;
  margin-top: var(--s-20);
  /* Room above/below the scene for the chat ring so bubbles don't collide
     with the Box Set title or the solution lede. */
  padding-block: var(--s-8) var(--s-12);
  /* Opaque fill: main is transparent for the footer reveal, so without this
     the chat pads can show the fixed charcoal foot through them. */
  background: var(--bg);
}

/* ── iMessage ring around the Conversations box ─────────────────────────
   Questions land as received (grey) bubbles; answers as sent (blue). They
   sit in the blue field around the 3D box and pop in as the stage scrolls
   through the viewport. Pointer lean/magnetism is driven from JS on
   .boxstage. Empty stage space stays pointer-events: none so the 3D box
   keeps drag; each Q&A pair is pointer-auto so it can be grabbed as one. */
.boxchats {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  overflow: visible;
  perspective: 720px;
}
.imsg-pair {
  position: absolute;
  left: var(--x, 0%);
  top: var(--y, 0%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  width: max-content;
  max-width: min(220px, 48vw);
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.imsg-pair.is-dragging { cursor: grabbing; z-index: 6; }
.imsg-pair .imsg--me { margin-inline-start: 12px; }
.imsg {
  /* --imx/--imy magnetic float; --itx/--ity tilt; --ims proximity scale */
  --imx: 0px;
  --imy: 0px;
  --itx: 0deg;
  --ity: 0deg;
  --ims: 1;
  position: relative;
  width: max-content;
  max-width: min(200px, 42vw);
  margin: 0;
  padding: 8px 12px;
  font-size: 13px;
  line-height: 1.35;
  letter-spacing: -0.01em;
  border-radius: 18px;
  box-shadow: 0 8px 24px -12px rgba(0, 0, 0, .45);
  transform: translateY(14px) scale(0.86) rotate(var(--r, 0deg));
  opacity: 0;
  transition:
    opacity .45s var(--ease),
    transform .55s cubic-bezier(.22, 1.35, .36, 1);
  transform-style: preserve-3d;
}
.imsg.is-in {
  opacity: 1;
  transform:
    translate3d(var(--imx), var(--imy), 0)
    rotateX(var(--itx))
    rotateY(var(--ity))
    rotate(var(--r, 0deg))
    scale(var(--ims));
}
/* While the pointer is over the stage, JS eases the custom props each frame —
   drop the transform transition so it doesn't fight the rAF follow.
   will-change only while tracking — permanent promotion on every bubble was
   keeping eight composited layers alive down the page. */
.boxchats.is-tracking .imsg.is-in {
  transition: opacity .45s var(--ease);
  will-change: transform;
}
/* Received — the deck's question */
.imsg--them {
  background: #E9E9EB;
  color: #111111;
  border-bottom-left-radius: 5px;
}
/* Sent — a student's possible answer */
.imsg--me {
  background: #0B93F6;
  color: #FFFFFF;
  border-bottom-right-radius: 5px;
}
/* Phone: two Q&A pairs max, parked in top/bottom padding bands so they never
   sit on the 3D box face. (Desktop keeps the full 4-pair ring.)
   !important beats the inline --x/--y/--r from markup — without it, mobile
   overrides lose to style="" and bubbles keep desktop coords on the box. */
@media (max-width: 699px) {
  .boxstage {
    /* Tall pads ≈ one bubble stack above + below the scene */
    padding-block: 108px 112px;
  }
  .imsg {
    max-width: min(200px, 70vw);
    font-size: 12px;
    padding: 7px 11px;
  }
  .imsg-pair { max-width: min(220px, 76vw); }
  .imsg-pair:nth-child(n + 3) { display: none; }

  /* Top band — one Q&A stack above the box; bottom band shifted right */
  .imsg-pair:nth-child(1) { --x: 5% !important;  --y: 2% !important; }
  .imsg-pair:nth-child(2) { --x: 40% !important; --y: 79% !important; }
  .imsg-pair:nth-child(1) .imsg--them { --r: -2deg !important; }
  .imsg-pair:nth-child(1) .imsg--me { --r: 2deg !important; }
  .imsg-pair:nth-child(2) .imsg--them { --r: 2deg !important; }
  .imsg-pair:nth-child(2) .imsg--me { --r: -2deg !important; }
}
@media (prefers-reduced-motion: reduce) {
  .imsg {
    transition: opacity .3s var(--ease);
    transform: rotate(var(--r, 0deg));
  }
  .imsg.is-in { transform: rotate(var(--r, 0deg)); }
  .boxchats.is-tracking .imsg.is-in { transition: opacity .3s var(--ease); }
}

.boxstage__scene {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 640px;
  height: 340px;
  display: grid;
  place-items: center;
  perspective: 1600px;
  perspective-origin: 50% 46%;
  touch-action: none;
  cursor: grab;
  user-select: none;
}
.boxstage__scene.is-dragging { cursor: grabbing; }

.box3d {
  position: relative;
  width: var(--box-w);
  height: var(--box-h);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, -18deg)) rotateY(var(--ry, -32deg));
  will-change: transform;
}

.face {
  position: absolute;
  margin: 0;
  overflow: hidden;
  background: #FDFDFC;
  backface-visibility: hidden;
  box-shadow: inset 0 0 0 1px rgba(21, 21, 21, .06);
}
.face img { width: 100%; height: 100%; object-fit: cover; display: block; }
.face::after { content: ""; position: absolute; inset: 0; pointer-events: none; }

.face--top,
.face--bottom { width: var(--box-w); height: var(--box-d); left: 0; top: calc(var(--hh) - var(--hd)); }
.face--front,
.face--back   { width: var(--box-w); height: var(--box-h); left: 0; top: 0; }
.face--left,
.face--right  { width: var(--box-d); height: var(--box-h); left: calc(var(--hw) - var(--hd)); top: 0; }

.face--top    { transform: rotateX(90deg)  translateZ(var(--hh)); }
.face--bottom { transform: rotateX(-90deg) translateZ(var(--hh)); }
.face--front  { transform: translateZ(var(--hd)); }
.face--back   { transform: rotateY(180deg) translateZ(var(--hd)); }
.face--right  { transform: rotateY(90deg)  translateZ(var(--hw)); }
.face--left   { transform: rotateY(-90deg) translateZ(var(--hw)); }

/* directional light: the lid catches it, the underside is darkest */
.face--top::after    { background: linear-gradient(160deg, rgba(255,255,255,.55), rgba(21,21,21,.04)); }
.face--front::after  { background: rgba(21, 21, 21, .07); }
.face--back::after   { background: rgba(21, 21, 21, .12); }
.face--right::after  { background: rgba(21, 21, 21, .12); }
.face--left::after   { background: rgba(21, 21, 21, .05); }
.face--bottom::after { background: rgba(21, 21, 21, .22); }

@media (max-width: 700px) {
  .boxstage { --box-w-n: 242; --box-d-n: 199; --box-h: 144px; }
  /* Slightly shorter scene so the padded chat bands stay in view on ~390 phones */
  .boxstage__scene { height: 270px; }
}

/* ── A gift for you — card fan ──────────────────────────────────────────── */

/* No containing panel — the fan and the copy sit on the ground. A tinted box
   around them made the offer look like a card of its own on a page that already
   has enough of those. */
.giftfan {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-12);
  width: 100%;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  color: inherit;
  text-decoration: none;
  /* Beat the global a:hover fade so the fan and CTA don't dim as one lump. */
  opacity: 1;
}
.giftfan:hover,
.giftfan:focus-visible { opacity: 1; }

/* the fan sets the row's height, so it and the art scale together */
.giftfan__fan {
  position: relative;
  width: 164px;
  height: 172px;
  flex: none;
}
.giftfan__fan img {
  position: absolute;
  left: 0;
  top: 0;
  width: 110px;
  border-radius: 8px;
  transform-origin: 50% 100%;
  /* Waiting faces sit in the back of the pile until JS promotes them. */
  transform: rotate(-5deg);
  opacity: 0;
  z-index: 0;
  pointer-events: none;
  transition: transform .45s var(--ease), opacity .28s var(--ease);
  filter: drop-shadow(0 5px 14px rgba(0, 0, 0, .28));
}
.giftfan__fan img.is-fan {
  opacity: 1;
  z-index: calc(1 + var(--i, 0));
  transform: rotate(calc((var(--i) - 1) * 5deg)) translateX(calc(var(--i) * 12px));
}
.giftfan:hover .giftfan__fan img.is-fan:not(.is-deal),
.giftfan:focus-visible .giftfan__fan img.is-fan:not(.is-deal),
.giftfan.is-shuffling .giftfan__fan img.is-fan:not(.is-deal) {
  transform: rotate(calc((var(--i) - 1) * 13deg)) translateX(calc(var(--i) * 26px)) translateY(-6px);
}
.giftfan__fan img.is-deal {
  opacity: 0;
  z-index: 9;
  transform: rotate(18deg) translateX(64px) translateY(-14px);
}

.giftfan__copy { display: grid; gap: var(--s-2); }
.giftfan__kicker { font-size: var(--t-caption); line-height: 19.5px; color: var(--fg-50); }
.giftfan__line {
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.4;
  max-width: 34ch;
}
.giftfan__cta { margin-top: var(--s-4); justify-self: start; }
.giftfan:hover .giftfan__cta,
.giftfan:focus-visible .giftfan__cta { opacity: .8; }

@media (max-width: 700px) {
  .giftfan {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--s-8);
    /* Quiet tile on the white How stretch — padded so the fan + copy read as one unit. */
    padding: var(--s-8) var(--s-6);
    border-radius: var(--r-md);
    background: color-mix(in srgb, var(--fg) 6%, var(--bg));
  }
  .giftfan__copy { justify-items: center; }
  .giftfan__line { max-width: 28ch; }
  .giftfan__cta { justify-self: center; }
  /* Cards are left:0 in the fan box, and the back face leans left — so a
     centered box still reads left/high. Nudge the painted mass into the tile. */
  .giftfan__fan img.is-fan {
    transform: rotate(calc((var(--i) - 1) * 5deg)) translateX(calc(var(--i) * 12px + 22px)) translateY(8px);
  }
  .giftfan:hover .giftfan__fan img.is-fan:not(.is-deal),
  .giftfan:focus-visible .giftfan__fan img.is-fan:not(.is-deal),
  .giftfan.is-shuffling .giftfan__fan img.is-fan:not(.is-deal) {
    transform: rotate(calc((var(--i) - 1) * 13deg)) translateX(calc(var(--i) * 26px + 10px)) translateY(2px);
  }
}
@media (prefers-reduced-motion: reduce) {
  .giftfan__fan img { transition: none; }
}


/* Donate under the Bartow rail (not a swipe card). */
.story__donate { margin-top: var(--s-10, 40px); justify-content: flex-start; }
@media (max-width: 899px) {
  .story__donate { margin-top: var(--s-8); justify-content: center; }
}
/* ── 3D orbiting conversation-card ring ─────────────────────────────────── */

.orbit {
  --orbit-r: 340px;
  --card-w: 150px;
  --card-h: calc(var(--card-w) * 7 / 5);
  position: relative;
  margin-block: var(--s-6);
}

.orbit--tight {
  --orbit-r: 220px;
  --card-w: 128px;
}

.orbit--mid {
  --orbit-r: 280px;
  --card-w: 136px;
}

.orbit__stage {
  position: relative;
  height: min(520px, 70vh);
  perspective: 1400px;
  perspective-origin: 50% 46%;
  overflow: hidden;
  /* pan-y lets the page scroll vertically through the stage; horizontal
     flicks are claimed in JS only after intent is clear. */
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
  contain: layout style;
}
.orbit__stage.is-dragging {
  /* Once we're spinning the ring, lock touch to the gesture. */
  touch-action: none;
  cursor: grabbing;
}
.orbit__stage--aim { cursor: crosshair; }
.orbit__stage--pick { cursor: default; }
.orbit__stage--pick.is-dragging { cursor: grabbing; }

.orbit__ring {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 8deg)) rotateY(var(--ry, 0deg));
  will-change: transform;
  contain: layout style;
}

/* Each slot is planted on the ring: yaw by index, then push out along Z.
   Cards face outward — so the far side of the orbit shows their backs. */
.orbit__slot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--card-w);
  height: var(--card-h);
  margin: calc(var(--card-h) / -2) calc(var(--card-w) / -2);
  transform-style: preserve-3d;
  transform:
    rotateY(calc(var(--i) * 360deg / var(--n)))
    translateZ(var(--orbit-r));
  /* Keep the cylinder plane clickable. Cards pitch a few degrees inside the
     slot, so the visual top often misses .orbit__card and lands here. */
  /* Kill inspector-/UA-looking blue rims; focus lives on the card when zoomed. */
  outline: none;
  border: 0;
  box-shadow: none;
  -webkit-tap-highlight-color: transparent;
}

.orbit__card {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  /* A little lean so the ring reads as a shallow cylinder, not a flat dial. */
  transform:
    rotateX(calc(2deg + var(--tilt-x, 0deg)))
    rotateY(var(--tilt-y, 0deg))
    translateZ(var(--lift, 0px))
    scale(var(--zoom, 1));
  /* No filter transitions — drop-shadow on spinning 3D cards was a major
     paint cost; hover/focus deepen .orbit__face box-shadow instead. */
  transition:
    transform .4s cubic-bezier(.22, 1.25, .36, 1),
    opacity .35s var(--ease, cubic-bezier(.16, 1, .3, 1));
  cursor: inherit;
  outline: none;
  -webkit-tap-highlight-color: transparent;
}
.orbit__card:focus { outline: none; }
.orbit__card:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 4px;
}

.orbit__face {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  overflow: hidden;
  /* Flatten each face so backface-visibility can cull the reverse side. */
  transform-style: flat;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  outline: none;
  box-shadow:
    0 18px 40px -16px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(255, 255, 255, .06);
  background: #130F0C;
  -webkit-tap-highlight-color: transparent;
  transition: box-shadow .35s var(--ease, cubic-bezier(.16, 1, .3, 1));
}
.orbit__face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
  /* Avoid image paint leaking through the culled face. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
/* Front faces out (camera / ring exterior). Tiny Z offset kills z-fight. */
.orbit__face--front {
  transform: translateZ(0.5px);
}
/* Back faces in (ring center). Far side of the orbit shows these. */
.orbit__face--back {
  transform: rotateY(180deg) translateZ(0.5px);
  /* Inward faces sit in the ring’s volume and steal clicks from the fan.
     Rear cards (backs toward camera) still hit via the slot; JS then
     ignores the back hemisphere. */
  pointer-events: none;
}

/* ── Mode: per-card hover lift ──────────────────────────────────────────── */
.orbit[data-orbit-mode="lift"] .orbit__card {
  pointer-events: auto;
  cursor: pointer;
}
.orbit[data-orbit-mode="lift"] .orbit__card.is-hot {
  --lift: 56px;
  z-index: 2;
}
.orbit[data-orbit-mode="lift"] .orbit__card.is-hot .orbit__face {
  box-shadow:
    0 28px 48px -14px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(255, 255, 255, .08);
}

/* ── Mode: magnetic aim ─────────────────────────────────────────────────── */
.orbit[data-orbit-mode="magnet"] .orbit__card {
  pointer-events: auto;
  cursor: pointer;
}
.orbit[data-orbit-mode="magnet"] .orbit__slot.is-aimed .orbit__card {
  --lift: 28px;
}
.orbit[data-orbit-mode="magnet"] .orbit__slot.is-aimed .orbit__face {
  box-shadow:
    0 22px 40px -14px rgba(0, 0, 0, .5),
    0 0 0 1px rgba(255, 255, 255, .08);
}

/* ── Mode: throw + idle drift + quiet hover tilt ────────────────────────── */
.orbit[data-orbit-mode="throw"] .orbit__stage {
  cursor: grab;
}
.orbit[data-orbit-mode="throw"] .orbit__stage.is-dragging { cursor: grabbing; }
.orbit[data-orbit-mode="throw"] .orbit__card {
  pointer-events: auto;
  cursor: grab;
}
.orbit[data-orbit-mode="throw"] .orbit__card.is-hot {
  --lift: 28px;
  z-index: 2;
  cursor: grab;
}
.orbit[data-orbit-mode="throw"] .orbit__card.is-hot .orbit__face {
  box-shadow:
    0 22px 36px -14px rgba(0, 0, 0, .48),
    0 0 0 1px rgba(255, 255, 255, .08);
}

/* ── Mode: steer / tilt — aiming cursors already on the stage ───────────── */
.orbit[data-orbit-mode="tilt"] .orbit__stage {
  cursor: move;
}

.orbit__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-top: var(--s-6);
}
.orbit__hint {
  margin: 0;
  font-size: var(--t-caption);
  line-height: 19.5px;
  color: var(--fg-50);
}

@media (max-width: 699px) {
  .orbit { --orbit-r: 210px; --card-w: 110px; }
  .orbit--tight { --orbit-r: 170px; --card-w: 96px; }
  .orbit--mid { --orbit-r: 190px; --card-w: 104px; }
  .orbit__stage { height: min(420px, 62vh); }
  .orbit[data-orbit-mode="lift"] .orbit__card.is-hot { --lift: 36px; }
}

@media (prefers-reduced-motion: reduce) {
  .orbit__stage { cursor: default; }
  .orbit__card {
    transition: none;
    --lift: 0px !important;
    --tilt-x: 0deg !important;
    --tilt-y: 0deg !important;
  }
}


/* Live homepage conversation-topics orbit
   Default orientation: fronts outside (toward camera), blue backs inside. */
.orbit--live {
  --orbit-r: 300px;
  --card-w: 140px;
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: var(--s-4);
  margin-bottom: var(--s-8);
  overflow: hidden;
}
.orbit--live .orbit__stage {
  height: min(480px, 64vh);
}
.orbit--live .orbit__controls { display: none; }

/* Click-to-zoom: one card forward, neighbors quiet; spin pauses in JS. */
.orbit__close {
  position: absolute;
  top: var(--s-4);
  right: var(--s-4);
  z-index: 6;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--fg) 12%, transparent);
  color: var(--fg);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: scale(0.92);
  transition: opacity .28s var(--ease, cubic-bezier(.16, 1, .3, 1)),
              transform .28s var(--ease, cubic-bezier(.16, 1, .3, 1)),
              background-color .2s var(--ease, cubic-bezier(.16, 1, .3, 1));
}
.orbit__close svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.orbit__close:hover { background: color-mix(in srgb, var(--fg) 18%, transparent); }
.orbit.is-focused .orbit__close {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
.orbit.is-focused .orbit__stage { cursor: pointer; }
.orbit.is-focused .orbit__slot.is-focus {
  z-index: 4;
}
.orbit.is-focused .orbit__slot.is-focus .orbit__card {
  --lift: 36px;
  --zoom: 1.32;
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  cursor: default;
  z-index: 4;
}
.orbit.is-focused .orbit__slot.is-focus .orbit__face {
  box-shadow:
    0 28px 52px -12px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(255, 255, 255, .1);
}
/* Keep neighbors solid — emphasize the pick with scale/z only, not washout. */
.orbit.is-focused .orbit__slot.is-dim {
  opacity: 1;
  pointer-events: none;
}
.orbit.is-focused .orbit__slot.is-dim .orbit__card {
  --lift: 0px;
  --zoom: 0.94;
}

@media (max-width: 699px) {
  /* Fit 2–3 cards in ~390px: radius/card under half-viewport after perspective.
     Stage soft-clips far edges; 6-card thinned deck stays in JS. */
  .orbit--live {
    --orbit-r: 132px;
    --card-w: 126px;
    width: 100%;
    max-width: 100%;
    margin-left: 0;
    margin-right: 0;
    overflow: hidden;
  }
  .orbit--live .orbit__stage {
    height: min(400px, 58vh);
    perspective: 900px;
    perspective-origin: 50% 48%;
    overflow: hidden;
  }
  .orbit.is-focused .orbit__slot.is-focus .orbit__card {
    --lift: 20px;
    --zoom: 1.16;
  }
}

@media (prefers-reduced-motion: reduce) {
  .orbit.is-focused .orbit__slot.is-focus .orbit__card,
  .orbit.is-focused .orbit__slot.is-dim .orbit__card,
  .orbit__close {
    transition: none;
  }
}
