/* ============================================================
   AURORA PORTFOLIO — COMPONENTS
   Keynote surfaces: glass header, staggered menu, rotating hero,
   count-up metrics, big rounded cards, encrypted-gate views.
   ============================================================ */

/* ------------------------------------------------------------
   Containers
   ------------------------------------------------------------ */
/* Gutters hold at --sp-4 until a cutout needs more, which is landscape on a
   notched phone — max() rather than addition, so the upright case is
   untouched and only the side that is actually obstructed grows. */
.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 max(var(--sp-4), var(--safe-r)) 0 max(var(--sp-4), var(--safe-l));
}
.container--prose {
  max-width: 47rem;
}

main {
  overflow-x: clip;
}

/* iOS Safari will not clip a filtered or transformed descendant to its
   parent's rounded corners from overflow:hidden alone — the blurred aurora
   fields and the scaled ACHROMA iframe each painted square over the radius,
   reading as a broken corner. An opaque mask forces WebKit to composite the
   subtree into a layer that the border radius then clips. Applied only to
   the three surfaces that actually own such a child; every mask is another
   layer, so this is not a blanket. */
.results__card,
.highlight__card,
.highlight__browser {
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}

/* ------------------------------------------------------------
   Header — fixed glass bar
   ------------------------------------------------------------ */
/* transparent at the top so the logo and menu float layered over the hero;
   once the hero scrolls past, main.js adds .is-scrolled and the bar docks
   as glass and stays there — everything below the hero (including the
   sticky case-study filter bar) anchors to it. */
/* Above the menu overlay, not below it. The toggle is the only control that
   opens or closes the menu, so it has to stay reachable while the panel is
   out — which also means the panel needs no close button of its own, and a
   wide screen no longer shows two ways to do the same thing. */
/* The bar carries the status-bar strip as padding rather than starting below
   it. Its glass layer is inset:0, so growing the box upward is what puts the
   material behind the clock and the dynamic island; the inner row keeps its
   own padding and so stays clear of them. Without this the page scrolls
   edge-to-edge under the cutout while the bar docks beneath it, and the hero
   reads through the gap. */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 80;
  padding-top: var(--safe-t);
}
.topbar::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border-bottom: 1px solid var(--glass-border);
  opacity: 0;
  transition: opacity var(--t-micro) var(--ease);
}
.topbar.is-scrolled::before {
  opacity: 1;
}
/* Pages that put dark artwork directly behind the bar wear the dark theme's
   palette on the bar until it docks. In light mode --ink is near-black and
   the story film is dark in both themes, so the wordmark and the controls
   were disappearing into it.

   The whole surface set is re-declared, not just --ink. Overriding the ink
   alone left every other token at its light value, and --bg-soft in light
   mode is #f5f5f7 — the very same white — so the theme toggle's hover fill
   painted the icon's own color behind it and the icon vanished at 1.00:1.
   A foreground cannot be flipped without the grounds it sits on. */
body[data-topbar="over-film"] .topbar:not(.is-scrolled) {
  --bg: #0a0a0c;
  --bg-soft: #151518;
  --ink: #f5f5f7;
  --ink-secondary: #a5a5ae;
  --glass: rgba(21, 21, 24, 0.6);
  --glass-strong: rgba(21, 21, 24, 0.8);
  --glass-border: rgba(245, 245, 247, 0.22);
  --edge-far: rgba(245, 245, 247, 0.16);
  --edge-mid: rgba(245, 245, 247, 0.05);
  --edge-near: rgba(245, 245, 247, 0.36);
  color: #f5f5f7;
}
.topbar__inner {
  position: relative;
  max-width: var(--container);
  margin: 0 auto;
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}
/* the SVG wordmark is applied as a mask and filled with the theme's ink,
   so it recolors itself for contrast against whatever ground it floats
   over in either theme. Large while layered over the hero; shrinks into
   the glass bar once the visitor scrolls. */
.topbar__mark {
  display: block;
  width: min(22rem, 56vw);
  aspect-ratio: 1029.1 / 101.7;
  background: var(--ink);
  -webkit-mask: url("../img/Michael-Frazier_Logo.svg") no-repeat left center / contain;
  mask: url("../img/Michael-Frazier_Logo.svg") no-repeat left center / contain;
  /* shrink via transform, not width — a width transition can wedge at the
     pre-layout 0 state on first paint, and transform stays off the layout
     thread anyway */
  transform-origin: left center;
  transition: transform 0.35s var(--ease);
}
.topbar.is-scrolled .topbar__mark {
  transform: scale(0.6);
}
.topbar__cluster {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
/* the disciplines, stated once beside the controls — desktop only, where
   there is room for it without crowding the mark or the menu */
.topbar__tagline {
  margin-right: var(--sp-2);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink);
  white-space: nowrap;
}
@media (max-width: 1100px) {
  .topbar__tagline {
    display: none;
  }
}

/* ------------------------------------------------------------
   Tagline flip (employer pages).

   Two labels in one grid cell: the disciplines while the hero is
   still on screen, whose page this is once you start reading. The
   cell is as wide and as tall as the longer label, so the swap is
   transform-only and the controls beside it never move.

   It rides .topbar.is-scrolled, which main.js already toggles at
   8px — the same signal that brings in the glass, so the header
   changes character once rather than twice.
   ------------------------------------------------------------ */
.topflip {
  display: grid;
  overflow: hidden;
  /* Both labels end at the same point, hard against the controls beside
     them. The cell is as wide as the longer of the two, so left-aligning
     would have left the shorter one starting somewhere else entirely and
     the swap would read as a sideways jump rather than a vertical turn. */
  justify-items: end;
  text-align: right;
  /* the cell is one line tall; the outgoing label leaves through the top
     and the incoming one arrives from the bottom */
  line-height: 1.4;
}
.topflip__item {
  grid-area: 1 / 1;
  white-space: nowrap;
  transition: transform 0.5s var(--ease), opacity 0.5s var(--ease);
}
.topflip__item--b {
  opacity: 0;
  transform: translateY(105%);
}
.topbar.is-scrolled .topflip__item--a {
  opacity: 0;
  transform: translateY(-105%);
}
.topbar.is-scrolled .topflip__item--b {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  /* the information still swaps; it simply does not travel to do it */
  .topflip__item {
    transition: opacity 0.2s linear;
    transform: none !important;
  }
}
/* Restated after the display:grid above, which otherwise outranks the
   tagline's own breakpoint rule by coming later in the file and put the
   label back on phones — where it pushed the menu button off the screen. */
@media (max-width: 1100px) {
  .topflip {
    display: none;
  }
}

/* the header overlays content — heroes run underneath it, so the page
   does NOT reserve space for the bar; interior surfaces pad themselves */

/* theme toggle — icon pill; the shown icon is the mode you'd switch TO */
.theme-toggle {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  border: 1px solid var(--glass-border);
  transition: background-color var(--t-micro) var(--ease);
}
.theme-toggle:hover {
  background: var(--bg-soft);
}
.theme-toggle svg {
  width: 1.15rem;
  height: 1.15rem;
}
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
html[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
html[data-theme="dark"] .theme-toggle .icon-moon { display: none; }

/* menu button — big bold text + burger-to-X, floating chromeless over the
   hero like the logo */
.menu-toggle {
  display: flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.35em 0.2em;
  font-weight: 800;
  font-size: var(--step-1);
  letter-spacing: -0.02em;
  transition: opacity var(--t-micro) var(--ease);
}
.menu-toggle:hover {
  opacity: 0.65;
}
.menu-toggle__lines {
  position: relative;
  width: 1.3rem;
  height: 0.8rem;
}
/* Both bars live on the same center line and are pushed apart to make the
   closed state. Previously they were pinned to top and bottom and traveled
   a fixed 0.25rem to cross — which was short of the distance between them,
   so the X met off-center and its arms read as unequal lengths. Rotating
   two identical bars about one shared origin can only produce equal arms. */
.menu-toggle__lines i {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--t-micro) var(--ease);
}
.menu-toggle__lines i:nth-child(1) { transform: translateY(-0.3rem); }
.menu-toggle__lines i:nth-child(2) { transform: translateY(0.3rem); }
html.menu-open .menu-toggle__lines i:nth-child(1) { transform: rotate(45deg); }
html.menu-open .menu-toggle__lines i:nth-child(2) { transform: rotate(-45deg); }

/* ------------------------------------------------------------
   Staggered menu — right-side overlay with aurora prelayers
   ------------------------------------------------------------ */
.smenu {
  position: fixed;
  inset: 0;
  z-index: 70;
  pointer-events: none;
}
.smenu__scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ink) 30%, transparent);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.smenu__layer {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: min(26rem, 100%);
  transform: translateX(102%);
}
/* two aurora prelayers race ahead of the panel */
.smenu__layer--a {
  background: linear-gradient(160deg, var(--aurora-violet), var(--aurora-blue));
  transition: transform 0.45s var(--ease);
}
.smenu__layer--b {
  background: linear-gradient(160deg, var(--aurora-cyan), var(--aurora-pink));
  transition: transform 0.45s var(--ease) 70ms;
}
/* fogged panel: heavy frosted blur instead of a solid sheet, so the page
   (and the aurora prelayers sliding beneath) glow through the material */
.smenu__panel {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: min(26rem, 100%);
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  -webkit-backdrop-filter: blur(36px) saturate(1.5);
  backdrop-filter: blur(36px) saturate(1.5);
  border-left: 1px solid var(--glass-border);
  transform: translateX(102%);
  transition: transform 0.45s var(--ease) 140ms;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* The panel is full-height, so its first item clears the cutout and its
     socials row clears the home indicator. The top offset already reserves
     room for the bar floating over it; the inset is added to that. */
  padding: calc(4.25rem + var(--sp-6) + var(--safe-t)) max(var(--sp-6), var(--safe-r))
    calc(var(--sp-6) + var(--safe-b));
}
html.menu-open .smenu { pointer-events: auto; }
html.menu-open .smenu__scrim { opacity: 1; }
html.menu-open .smenu__layer,
html.menu-open .smenu__panel { transform: translateX(0); }

/* The lists and their headings, held together so the panel's
   space-between still has exactly two children to push apart once the
   portal group is injected below the site links. */
.smenu__nav {
  display: grid;
  gap: var(--sp-5);
}
/* A signed-in menu is two groups tall and on a short screen — a phone in
   landscape, a small laptop — that can outrun the panel, so the tail has
   to stay reachable. Gated on height rather than left on permanently:
   a display link's descender puts a pixel or two of scrollable overflow
   into a box that is otherwise exactly the right size, and a scroll
   container with 2px to scroll is a scrollbar down the middle of the menu
   for no reason anybody can see. */
@media (max-height: 36rem) {
  .smenu__nav {
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}
.smenu__list {
  list-style: none;
  display: grid;
  gap: var(--sp-3);
}
.smenu__item,
.smenu__section {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
/* The stagger is driven by an index rather than nth-child, because the
   portal's rows are injected into a second list where nth-child would
   restart at one and re-run the first three delays. --i is the item's
   place in the whole menu, wherever it is written; admin-nav.js sets it on
   what it appends, and the same 60ms cadence continues through. */
html.menu-open .smenu__item,
html.menu-open .smenu__section {
  opacity: 1;
  transform: none;
  transition-delay: calc(0.22s + var(--i, 0) * 0.06s);
}
.smenu__list > .smenu__item:nth-child(1) { --i: 0; }
.smenu__list > .smenu__item:nth-child(2) { --i: 1; }
.smenu__list > .smenu__item:nth-child(3) { --i: 2; }
.smenu__list > .smenu__item:nth-child(4) { --i: 3; }

/* A heading and the links it names travel together, so the heading keeps
   its own small gap to them while the groups stay --sp-5 apart. */
.smenu__group {
  display: grid;
  gap: var(--sp-2);
}
/* Names the group under it. It is a caption, not a link, so it takes the
   label size the rest of the site gives one and sits in secondary ink —
   the eye should reach the destinations, not the word above them. */
.smenu__section {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}

.smenu__link {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  text-decoration: none;
  font-weight: 800;
  font-size: var(--step-3);
  letter-spacing: -0.03em;
  line-height: 1.05;
  transition: color var(--t-micro) var(--ease);
}
.smenu__link:hover {
  color: var(--ink-secondary);
}
.smenu__index {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--ink-secondary);
}
/* The menu's tail is an invitation, not a footnote: the same line the page
   footer closes on, the primary action as a real button, and the two ways
   to reach a person — all on one glass card sitting over the aurora that
   slid in behind the panel. */
.smenu__cta {
  position: relative;
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-4);
  border-radius: var(--r-card);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(1.5);
  backdrop-filter: blur(20px) saturate(1.5);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  isolation: isolate;
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.45s var(--ease) 0.42s, transform 0.45s var(--ease) 0.42s;
  /* iOS will not clip the wash to these corners without a mask */
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}
.smenu__cta::before {
  content: "";
  position: absolute;
  inset: -40%;
  z-index: -1;
  background:
    radial-gradient(circle at 20% 15%, color-mix(in srgb, var(--aurora-violet) 30%, transparent), transparent 58%),
    radial-gradient(circle at 85% 90%, color-mix(in srgb, var(--aurora-pink) 26%, transparent), transparent 58%);
  filter: blur(30px);
}
html.menu-open .smenu__cta {
  opacity: 1;
  transform: none;
}
.smenu__cta-line {
  font-size: var(--step-1);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.15;
  text-wrap: balance;
}
.smenu__cta-btn {
  width: 100%;
  justify-content: center;
}
.smenu__cta-icons {
  display: flex;
  gap: var(--sp-2);
}

/* ------------------------------------------------------------
   The way out, injected by admin-nav.js in place of the
   invitation card once a session is live.

   Signed in, this menu is not a visitor's menu: it lists the
   portal's sections above and it needs a door out at the bottom.
   The card that normally closes it — the line, the Contact
   button, the two socials — is addressed to someone who is not
   the person holding the password, so it is removed rather than
   pushed down, and the door takes its place at the panel's foot
   inside the same padding.

   Quiet on purpose. It is the full width of the panel so it
   cannot be missed, and glass rather than ink so it never
   outranks the destinations above it — leaving is not the thing
   you opened the menu to do.
   ------------------------------------------------------------ */
.smenu__out {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  width: 100%;
  padding: 1em 1.2em;
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  color: var(--ink);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  /* Last in, on the same 60ms cadence the links above it run on rather
     than the fixed beat the card it replaced used. That beat was written
     for a menu of three links; with the portal's two added it landed the
     way out before the last destination had arrived. admin-nav.js sets
     --i to one past the final link. */
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease),
    background-color var(--t-micro) var(--ease);
  transition-delay: calc(0.22s + var(--i, 5) * 0.06s), calc(0.22s + var(--i, 5) * 0.06s), 0s;
}
.smenu__out svg {
  width: 1.15em;
  height: 1.15em;
  flex: none;
}
html.menu-open .smenu__out {
  opacity: 1;
  transform: none;
}
.smenu__out:hover {
  background: var(--glass-strong);
}

/* ------------------------------------------------------------
   The way in, injected by admin-nav.js when there is no session.
   Signing in is an identity action, so it rides the socials row
   and fills the space their two circles leave rather than
   sitting beside them as a third one.

   Signed in it is gone, along with the whole card: see
   [data-mode] below and .smenu__out above.
   ------------------------------------------------------------ */
.smenu__panel[data-mode="portal"] .smenu__cta {
  display: none;
}
.smenu__auth {
  /* takes the rest of the socials row rather than sitting beside the
     circles as a third one — it is a different kind of thing */
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  height: 3rem;
  padding: 0 1.15em;
  border-radius: var(--r-pill);
  color: var(--ink);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  transition: transform var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease);
}
.smenu__auth svg {
  width: 1.1em;
  height: 1.1em;
}
.smenu__auth:hover {
  transform: translateY(-3px);
  background: var(--glass-strong);
}

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

/* ------------------------------------------------------------
   Gradient edge — a hairline border that shades around the card,
   for every surface the traveling beam does not already own.
   (Technique after Florian Kiem's gradient-border plugin: a ring
   painted by an inset pseudo-element whose center is masked out,
   which is the only way to put a gradient on a border and have it
   follow the radius.)

   It is deliberately quiet. The beam is an event; this is just the
   edge of an object catching the light, and it inverts between
   themes rather than being one stroke recolored.
   ------------------------------------------------------------ */
.work-card::after,
.study-nav__card::after,
.gallery__tile::after,
.shots__frame::after,
.plate__frame::after,
.highlight__card::after,
.results__card::after,
.spec::after,
.testimonial::after,
.smenu__cta::after,
.gate__card::after,
.form__done-card::after,
.videoplate::after,
.carousel__viewport::after,
.split__figure::after,
.orbit__card::after,
.feedback__portrait::after,
.feedback__persona::after,
.feedback__voice::after,
.storyboard__shot::after,
.hypo__face::after,
.ledger__item::after,
.bento__tile::after,
.plan__panel::after,
.glassnode::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(315deg, var(--edge-far), var(--edge-mid) 50%, var(--edge-near));
  /* paint the ring only: fill the box, then knock out everything inside the
     padding so what survives is exactly the 1px band */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
/* some of these are positioned by their own rules; the rest need a
   containing block before an inset pseudo-element means anything */
.work-card,
.study-nav__card,
.gallery__tile,
.shots__frame,
.plate__frame,
.testimonial,
.gate__card,
.carousel__viewport,
.plan__panel,
.glassnode {
  position: relative;
}

/* ------------------------------------------------------------
   Border beam — a two-tone shine traveling the border ring.
   Applied to every button and to hero/highlight image frames.
   ------------------------------------------------------------ */
@property --beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.beam::after,
.btn::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1.5px;
  background:
    conic-gradient(from var(--beam-angle), transparent 0deg 300deg, var(--aurora-cyan) 330deg, transparent 360deg),
    conic-gradient(from calc(var(--beam-angle) + 180deg), transparent 0deg 300deg, var(--aurora-pink) 330deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: beam-rotate 5s linear infinite;
  pointer-events: none;
}
.beam,
.btn {
  position: relative;
}
@keyframes beam-rotate {
  to { --beam-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .beam::after,
  .btn::after {
    display: none;
  }
}

/* ------------------------------------------------------------
   Buttons & chips
   ------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.8em 1.6em;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--bg);
  font-weight: 600;
  font-size: var(--step--1);
  letter-spacing: 0.01em;
  text-decoration: none;
  border: 1px solid var(--ink);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.btn:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-card);
}
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--glass-border);
}
.btn--ghost:hover {
  background: var(--bg-soft);
}
/* solid white secondary, for use over the hero's tinted aurora in either
   theme — literal palette values because it must stay white when --bg
   flips to near-black in dark mode. No border beam: the beam is reserved
   for the primary ink buttons. */
.btn--light {
  background: #ffffff;
  color: #0a0a0c;
  border-color: #ffffff;
}
.btn--light::after {
  content: none;
}
/* The exception: the menu's primary action is white *and* keeps the ring,
   because it is the only button on that surface and has to read as the
   thing you came for. */
.btn--light.btn--beam::after {
  content: "";
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.45em 1em;
  border-radius: var(--r-pill);
  /* fixed size (the smallest step) so tags read identical everywhere —
     cards, filters, and case-page meta never drift apart */
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
}
.chip svg {
  width: 0.85em;
  height: 0.85em;
}
/* When a study was made, in the system's one tag shape so meta never drifts
   apart from the category and status beside it. Informational only — the
   filter bar deliberately has no year, since nobody browses a portfolio by
   picking a calendar year out of a list. */
.chip--year {
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------
   Rotating hero
   ------------------------------------------------------------ */
.hero {
  position: relative;
  min-height: min(88vh, 56rem);
  display: grid;
  overflow: hidden;
  isolation: isolate;
}
.hero__slide {
  grid-area: 1 / 1;
  position: relative;
  display: grid;
  /* start, not center: centering makes each slide sit by its own content
     height, so slides with an extra metric line begin at a different
     vertical point than those without */
  align-content: start;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.8s var(--ease), visibility 0s linear 0.8s;
}
.hero__slide.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.8s var(--ease);
  z-index: 1;
}
/* aurora field: two blurred drifting blobs, hues set per slide */
.hero__aurora {
  position: absolute;
  inset: -20%;
  z-index: -1;
  pointer-events: none;
}
.hero__aurora::before,
.hero__aurora::after {
  content: "";
  position: absolute;
  width: 55%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.5;
}
.hero__aurora::before {
  background: radial-gradient(circle, var(--a1, var(--aurora-violet)), transparent 65%);
  top: 5%;
  left: 8%;
  animation: aurora-drift-a 24s ease-in-out infinite alternate;
}
.hero__aurora::after {
  background: radial-gradient(circle, var(--a2, var(--aurora-cyan)), transparent 65%);
  bottom: 0;
  right: 5%;
  animation: aurora-drift-b 28s ease-in-out infinite alternate;
}
@keyframes aurora-drift-a {
  from { transform: translate(0, 0) scale(1); }
  to { transform: translate(8%, 10%) scale(1.15); }
}
@keyframes aurora-drift-b {
  from { transform: translate(0, 0) scale(1.1); }
  to { transform: translate(-9%, -7%) scale(0.95); }
}

.hero__content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  /* every slide starts its copy at the same fixed vertical point (the
     header floats above), and the hero's height comes from the stacked
     grid cell, so changing text heights never resize the section */
  padding: clamp(7rem, 18vh, 11rem) var(--sp-4) var(--sp-7);
  display: grid;
  gap: var(--sp-6);
  align-content: start;
  /* the content layer spans the full width above the artwork, so it would
     otherwise swallow every click aimed at the image behind it; only the
     real controls inside it take pointer events */
  pointer-events: none;
}
.hero__content a,
.hero__content button {
  pointer-events: auto;
}
/* work slides: text owns the left column; the artifact lives in the
   background scene, not in the grid */
.hero__slide--work .hero__content {
  grid-template-columns: minmax(0, 44rem);
}
/* project titles run one step below the introduction's display size */
.hero__slide--work .hero__title {
  font-size: var(--step-4);
}
/* Hero copy is full-strength ink, never the secondary gray: the ground
   behind it is an aurora wash and project artwork rather than a flat
   surface, so the gray that measures fine on white loses contrast here.
   Hierarchy comes from size and weight instead of color. */
.hero__kicker {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  margin-bottom: var(--sp-3);
}
.hero__title {
  font-size: var(--step-5);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.02;
  max-width: 14ch;
}
.hero__sub {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  color: var(--ink);
  max-width: 46ch;
}
.hero__cta {
  margin-top: var(--sp-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
.hero__metric {
  margin-top: var(--sp-4);
  font-size: var(--step-4);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
}

/* the artifact anchored off the right edge — a clean rounded capture with
   a traveling border shine, clickable through to its case study */
.hero__scene {
  /* the resting opacity lives in a custom property so the entrance
     animation's fill can end at the responsive value (mobile fades the
     scene to a backdrop) instead of overriding it */
  --scene-o: 1;
  position: absolute;
  top: 50%;
  /* The overhang eases off as the screen widens: at 1440 it matches the
     original -18%, and by 2560 the artwork has walked ~180px back toward
     the headline rather than stranding itself against the far edge. The
     clamp keeps a crop at every width — the artwork always runs off. */
  right: clamp(-20rem, 15.9vw - 30.5rem, -4rem);
  width: min(60vw, 70rem);
  transform: translateY(-50%);
  opacity: var(--scene-o);
  z-index: 0;
  display: block;
  border-radius: var(--r-card);
  box-shadow: var(--shadow-pop);
  cursor: pointer;
}
/* interactive state: the artwork is a real link to its case study. The
   cue is a ::before pill (::after already carries the border beam) and
   the shadow deepens — transform is off-limits here because the entrance
   animation and float keyframes own it. */
.hero__scene::before {
  content: "View case study →";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  padding: 0.8em 1.5em;
  border-radius: var(--r-pill);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  color: var(--ink);
  font-weight: 700;
  font-size: var(--step--1);
  white-space: nowrap;
  opacity: 0;
  transition: opacity var(--t-micro) var(--ease);
  pointer-events: none;
}
.hero__scene:hover::before,
.hero__scene:focus-visible::before {
  opacity: 1;
}
.hero__scene:hover {
  box-shadow: 0 40px 100px rgba(10, 10, 12, 0.28);
}
.hero__scene img {
  width: 100%;
  height: auto;
  border-radius: var(--r-card);
  animation: scene-float 9s ease-in-out infinite alternate;
  /* held until decoded (main.js adds .is-loaded) — on a phone this artwork
     is the whole backdrop, and an undecoded one reads as a bare panel */
  opacity: 0;
  transition: opacity 0.9s var(--ease);
}
.hero__scene.is-loaded img {
  opacity: 1;
}
@keyframes scene-float {
  from { transform: translateY(-0.6%); }
  to { transform: translateY(1.2%); }
}

/* hero entrance choreography, strictly sequenced: (1) the title reveals
   under a gradient sweep, (2) description fades in, (3) call to action,
   (4) the thumbnail scene arrives last */
@keyframes hero-rise {
  from { opacity: 0; transform: translateY(26px); }
  to { opacity: 1; transform: none; }
}
@keyframes hero-scene-in {
  from { opacity: 0; transform: translateY(-50%) translateX(64px) scale(1.03); }
  to { opacity: var(--scene-o, 1); transform: translateY(-50%); }
}
/* Gradient text reveal, used by every hero header and by revealed content.
   A bare .dia is plain solid ink — the gradient (which renders most of the
   string near-transparent at its start position) is only applied while a
   trigger class is present, so text is never invisible without JS. */
@keyframes dia-reveal {
  from { background-position: 100% 0; }
  to { background-position: 0% 0; }
}
/* The display line-height (1.02) is tighter than the font's own glyph box,
   so ascenders and descenders sit outside the element's background painting
   area. Harmless for normal text, but these headings are painted *by* their
   background via background-clip:text — anything outside that box simply
   isn't painted, which sheared the tails off letters like g and y. The
   padding grows the paint area; the negative margin gives back the space so
   vertical rhythm is unchanged. Applied unconditionally so arming the
   animation never shifts layout. */
.dia {
  padding-block: 0.1em 0.18em;
  margin-block: -0.1em -0.18em;
}
/* Armed only once webfonts have landed (main.js sets .fonts-ready): a swap
   mid-sweep leaves WebKit repainting a text-clipped gradient while the glyph
   geometry moves under it, which is what stranded the first hero heading as
   a ghost on iOS while later slides — animating on an idle page — were fine.
   Wrapped in :where() so the trigger costs no specificity and .is-lit below
   can reliably override it. */
:where(.fonts-ready)
  :where(.hero__slide.is-active, .study-hero, .eh.is-arriving, .story, .reveal.is-visible, .voice.is-active)
  .dia {
  background-image: linear-gradient(
    100deg,
    var(--ink) 0%,
    var(--ink) 38%,
    var(--aurora-violet) 46%,
    var(--aurora-cyan) 52%,
    color-mix(in srgb, var(--ink) 6%, transparent) 60%,
    color-mix(in srgb, var(--ink) 6%, transparent) 100%
  );
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: dia-reveal 2s var(--ease) 0.2s both;
}
/* The sweep's timeline is reliable even where its paint is not, so main.js
   pins the heading to solid ink when the animation ends. A dropped repaint
   can cost the effect; it can never cost the words. Left animatable so a
   re-entering hero slide sweeps again. */
.dia.is-lit {
  background-image: none;
  color: var(--ink);
}
/* sequence, at half speed: header sweep → description → CTA → thumbnail */
.hero__slide.is-active .hero__kicker { animation: hero-rise 1s var(--ease) both; }
.hero__slide.is-active .hero__metric { animation: hero-rise 1.2s var(--ease) 1.7s both; }
.hero__slide.is-active .hero__sub { animation: hero-rise 1.2s var(--ease) 1.9s both; }
.hero__slide.is-active .hero__cta { animation: hero-rise 1.2s var(--ease) 2.3s both; }
.hero__slide.is-active .hero__scene { animation: hero-scene-in 1.8s var(--ease) 2.6s both; }
@media (prefers-reduced-motion: reduce) {
  :where(.fonts-ready)
    :where(.hero__slide.is-active, .study-hero, .eh.is-arriving, .story, .reveal.is-visible)
    .dia {
    background: none;
    color: var(--ink);
    animation: none;
  }
}

/* ------------------------------------------------------------
   Drift wall — perspective-tilted columns of work tiles drifting
   vertically at alternating speeds, dissolving toward the headline
   on the left and bleeding off the right edge of the screen.
   ------------------------------------------------------------ */
.driftwall {
  position: absolute;
  top: 0;
  bottom: 0;
  right: -10%;
  width: min(68vw, 76rem);
  z-index: 0;
  overflow: hidden;
  perspective: 1400px;
  pointer-events: none;
  /* two masks intersected: fade out toward the text, and soften top/bottom */
  -webkit-mask-image: linear-gradient(90deg, transparent 4%, #000 46%),
    linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(90deg, transparent 4%, #000 46%),
    linear-gradient(180deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
  mask-composite: intersect;
}
.driftwall__plane {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  /* flex-start, not the default stretch: a stretched column reports the
     tallest column's height, and the loop translates by a percentage of
     the element's own height. One column running long would have thrown
     every other column's shift out of register with its own seam. */
  align-items: flex-start;
  gap: 1.25rem;
  transform: translate(-50%, -50%) rotateX(8deg) rotateY(-24deg) rotateZ(6deg) translateZ(-90px);
  transform-style: preserve-3d;
}
/* No flex gap here on purpose: with N items flex renders N-1 gaps, so a
   -50% shift lands half a gap out of register and the loop visibly jumps.
   Spacing lives on each tile's margin instead, making the two stacked
   copies exactly equal and the loop seamless. */
.driftwall__col {
  display: flex;
  flex-direction: column;
  animation: drift-up 52s linear infinite;
}
.driftwall__col:hover {
  animation-play-state: paused;
}
.driftwall__col:nth-child(2) {
  animation-name: drift-down;
  animation-duration: 42s;
}
.driftwall__col:nth-child(3) {
  animation-duration: 62s;
}
.driftwall__col:nth-child(4) {
  animation-name: drift-down;
  animation-duration: 48s;
}
.driftwall__tile {
  position: relative;
  display: block;
  width: clamp(11rem, 15vw, 17rem);
  margin-bottom: 1.25rem;
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  opacity: 0.85;
  filter: saturate(0.75);
  transition: transform var(--t-micro) var(--ease), opacity var(--t-micro) var(--ease),
    filter var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
/* Nothing shows before its own image has decoded; each then rises on a
   delay set per row by driftwall.js, so the wall assembles down the column
   instead of tiles popping in mid-drift. The entrance is an animation with
   `backwards` fill only — it ends on the resting values above, leaving the
   hover transition to run at its own snappier timing. */
.driftwall__tile:not(.is-in) {
  opacity: 0;
}
.driftwall__tile.is-in {
  animation: tile-in 0.75s var(--ease) var(--in-delay, 0ms) backwards;
}
@keyframes tile-in {
  from {
    opacity: 0;
    transform: translateY(18px) scale(0.97);
  }
}
/* the wall is inert by default; on pointer devices it becomes a real
   browsable surface — hover pauses its column, lifts the tile back to
   full color, and the tile links through to that case study */
.driftwall__tile:hover,
.driftwall__tile:focus-visible {
  transform: scale(1.09);
  opacity: 1;
  filter: saturate(1);
  box-shadow: var(--shadow-pop);
  z-index: 2;
}
.driftwall__tile img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top;
}
/* Three copies of the run, shifted by exactly one of them.

   The plane is centered on the wall rather than anchored to its top, so a
   two-copy column shifted by -50% ended the loop spanning [-2R, 0] and
   left the whole bottom half of the wall empty until it wrapped. With
   three copies the shift is -1/3, the column ends at [-2.5R, +0.5R], and
   because driftwall.js builds every run at least as tall as the wall,
   that +0.5R still clears the bottom edge on the last frame. */
@keyframes drift-up {
  from { transform: translateY(0); }
  to { transform: translateY(-33.3333%); }
}
@keyframes drift-down {
  from { transform: translateY(-33.3333%); }
  to { transform: translateY(0); }
}
@media (hover: hover) and (min-width: 861px) {
  .driftwall {
    pointer-events: auto;
  }
}
@media (max-width: 860px) {
  .driftwall {
    right: -30%;
    width: 130vw;
    opacity: 0.3;
  }
  /* two columns instead of four (see driftwall.js) — the tiles grow so the
     field still fills the stage rather than reading as a thin ribbon */
  .driftwall__tile {
    width: clamp(13rem, 42vw, 20rem);
  }
  /* At 0.3 opacity behind a headline, none of this is legible as depth,
     and every one of them is a composited layer a phone has to keep
     repainting under a moving 3D transform. The tiles keep their shape and
     lose the two properties that cost the most to draw. */
  .driftwall__tile {
    box-shadow: none;
    filter: none;
  }
}

/* controls */
.hero__controls {
  /* one source of truth for the rotation: the dot fill animates for this
     long, and hero-rotator.js advances when it ends */
  --hero-interval: 9000ms;
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  /* lifted clear of the home indicator, which otherwise sits right on the
     play button at the bottom of a full-height hero */
  bottom: calc(var(--sp-4) + var(--safe-b));
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
}
/* The dots ride in their own glass pill, the same surface the arrows and
   the rest of the site's chrome use. The voices__ selectors are the about
   page's testimonial rotator wearing the identical chrome — only its
   timing rules live apart, next to its own keyframe. */
.hero__dots,
.voices__dots {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  /* same height as the arrows and the play button flanking it, so the
     whole control group reads as one row */
  height: 2.4rem;
  padding: 0 0.95rem;
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
}
.hero__dot,
.voices__dot {
  position: relative;
  overflow: hidden;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 22%, transparent);
  transition: width 0.5s var(--ease), background-color var(--t-micro) var(--ease);
}
/* the open slide's dot stretches into a track; the fill inside it is the
   timer, so you can see how long the slide has left */
.hero__dot[aria-current="true"],
.voices__dot[aria-current="true"] {
  width: 2.75rem;
}
.hero__dot-fill,
.voices__dot-fill {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
}
.hero__dot[aria-current="true"] .hero__dot-fill {
  animation: hero-progress var(--hero-interval) linear forwards;
}
.hero__controls[data-running="false"] .hero__dot-fill {
  animation-play-state: paused;
}
@keyframes hero-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
.hero__playpause,
.voices__playpause {
  width: 2.4rem;
  height: 2.4rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  transition: transform var(--t-micro) var(--ease);
}
.hero__playpause:hover,
.voices__playpause:hover {
  transform: scale(1.08);
}
.hero__playpause svg,
.voices__playpause svg {
  width: 0.9rem;
  height: 0.9rem;
}
/* the icon shows the user's own choice, not the incidental pause that
   hovering a slide causes — otherwise it would flicker under the cursor */
.hero__playpause .hero__icon-play,
.hero__playpause[data-state="paused"] .hero__icon-pause,
.voices__playpause .voices__icon-play,
.voices__playpause[data-state="paused"] .voices__icon-pause {
  display: none;
}
.hero__playpause[data-state="paused"] .hero__icon-play,
.voices__playpause[data-state="paused"] .voices__icon-play {
  display: block;
}
/* No auto-advance under reduced motion, so there is no time to show and
   nothing to pause: the open dot simply reads as filled. */
@media (prefers-reduced-motion: reduce) {
  .hero__playpause,
  .voices__playpause {
    display: none;
  }
  .hero__dot[aria-current="true"] .hero__dot-fill,
  .voices__dot[aria-current="true"] .voices__dot-fill {
    animation: none;
    transform: scaleX(1);
  }
}
.hero__arrow,
.voices__arrow {
  width: 2.4rem;
  height: 2.4rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  transition: transform var(--t-micro) var(--ease);
}
.hero__arrow:hover,
.voices__arrow:hover {
  transform: scale(1.08);
}
.hero__arrow svg,
.voices__arrow svg {
  width: 1rem;
  height: 1rem;
}

/* ------------------------------------------------------------
   Metrics — Apple-keynote count-ups
   ------------------------------------------------------------ */
.stats {
  padding: var(--sp-8) 0;
}
.stats__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--sp-5);
}
.stat {
  text-align: center;
  padding: var(--sp-5) var(--sp-3);
}
.stat__value {
  display: block;
  font-size: var(--step-5);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat__label {
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink);
}
.stat__detail {
  display: block;
  margin-top: 0.25em;
  font-size: var(--step--1);
  color: var(--ink-secondary);
}

/* metrics on a color block: soft card with aurora glow behind */
.stats--block {
  position: relative;
}
.stats--block .stats__card {
  position: relative;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  padding: var(--sp-6) var(--sp-4);
  overflow: hidden;
  isolation: isolate;
}
.stats--block .stats__card::before {
  content: "";
  position: absolute;
  inset: -40%;
  z-index: -1;
  background:
    radial-gradient(circle at 20% 20%, color-mix(in srgb, var(--aurora-violet) 22%, transparent), transparent 55%),
    radial-gradient(circle at 85% 75%, color-mix(in srgb, var(--aurora-cyan) 20%, transparent), transparent 55%);
  filter: blur(40px);
}

/* ------------------------------------------------------------
   Section headings
   ------------------------------------------------------------ */
.section {
  padding: var(--sp-8) 0;
}
.section__head {
  margin-bottom: var(--sp-6);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
}
.section__title {
  font-size: var(--step-3);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.section__lede {
  font-size: var(--step-1);
  color: var(--ink-secondary);
  max-width: var(--measure);
}

/* ------------------------------------------------------------
   Work grid — search, chips, big cards
   ------------------------------------------------------------ */
/* sticks just below the docked glass header (its measured height is
   published as --topbar-h by main.js) while the visitor is in the
   case-study section */
.filterbar {
  position: sticky;
  top: calc(var(--topbar-h, 5.5rem) + var(--sp-2));
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-card);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
}
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
}
.chips .chip {
  transition: background-color var(--t-micro) var(--ease), color var(--t-micro) var(--ease);
}
.chips .chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  gap: var(--sp-5);
  /* filtering down to a single card must not collapse the page under the
     sticky bar — a floor here keeps the scroll position meaningful */
  min-height: 70vh;
}
/* image-dominant cards: the category tag rides top-left, the lock rides
   top-right, and the title arrives on hover as a bottom-left scrim
   overlay (always visible on touch widths, where hover doesn't exist) */
.work-card {
  display: block;
  text-decoration: none;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.work-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}

/* ------------------------------------------------------------
   Pointer tilt (desktop pointers only) — a clickable card leans
   toward the cursor in 3D, then springs back on leave. tilt.js
   publishes --rx/--ry/--ts; the transform below composes them
   with whatever the element already carried, so the existing
   hover lift and the hero art's centering translate both survive.
   ------------------------------------------------------------ */
@media (hover: hover) and (min-width: 861px) {
  .work-card,
  .study-nav__card,
  .gallery__tile,
  .shots__item,
  .highlight__browser,
  .storyboard__shot,
  .bento__tile,
  .split__figure {
    transform: perspective(800px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg))
      translateY(var(--lift, 0)) scale(var(--ts, 1));
  }
  .work-card:hover,
  .study-nav__card:hover,
  .gallery__tile:hover,
  .gallery__tile:focus-visible,
  .shots__item:hover,
  .shots__item:focus-visible {
    --lift: -4px;
  }
  /* while the pointer is on it the card tracks tightly; letting go hands it
     to the longer eased curve, which is what reads as the spring settling */
  .work-card.is-tilting,
  .study-nav__card.is-tilting,
  .gallery__tile.is-tilting,
  .shots__item.is-tilting,
  .highlight__browser.is-tilting,
  .storyboard__shot.is-tilting,
  .bento__tile.is-tilting,
  .split__figure.is-tilting,
  .hero__slide--work .hero__scene.is-tilting {
    transition: transform 0.1s linear, box-shadow var(--t-micro) var(--ease);
    will-change: transform;
  }
  .highlight__browser {
    transition: transform 0.5s var(--ease), box-shadow var(--t-micro) var(--ease);
  }
  /* the hero art is vertically centered by a translate and floats on its own
     keyframes, so its tilt has to be folded in rather than replace it */
  .hero__slide--work .hero__scene {
    transform: perspective(1100px) translateY(-50%) rotateX(var(--rx, 0deg))
      rotateY(var(--ry, 0deg)) scale(var(--ts, 1));
    transition: transform 0.5s var(--ease);
  }
  /* `both` fill would pin the entrance transform forever and swallow the
     tilt; `backwards` still covers the delay and then lets go */
  .hero__slide.is-active .hero__scene {
    animation: hero-scene-in 1.8s var(--ease) 2.6s backwards;
  }
}
/* the card's markup is spans (it lives inside an <a>), so the media wrapper
   must be blockified explicitly — as an inline box, aspect-ratio does not
   apply and the whole card collapses to zero height */
.work-card__media {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
}
.work-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  transition: transform 0.5s var(--ease);
}
.work-card:hover .work-card__media img {
  transform: scale(1.03);
}
/* Category and year read as one group on the left — what it is, and when.
   The right-hand corner is reserved for what happens on click: a padlock
   where the study is gated, "Live" where the card opens the running
   product. One corner, one question, answered the same way grid-wide. */
.work-card__badges {
  position: absolute;
  z-index: 2;
  top: var(--sp-3);
  left: var(--sp-3);
  right: var(--sp-3);
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  gap: var(--sp-2);
}
.work-card__lock,
.work-card__live {
  margin-left: auto;
}
.work-card__overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  padding: var(--sp-4);
  background: linear-gradient(
    180deg,
    transparent 45%,
    color-mix(in srgb, #0a0a0c 78%, transparent) 100%
  );
  opacity: 0;
  transition: opacity var(--t-micro) var(--ease);
}
.work-card:hover .work-card__overlay,
.work-card:focus-visible .work-card__overlay {
  opacity: 1;
}
.work-card__title {
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: #f5f5f7;
  text-align: left;
  max-width: 24ch;
}
@media (hover: none), (max-width: 860px) {
  .work-card__overlay {
    opacity: 1;
  }
}
/* ------------------------------------------------------------
   Live-prototype card — the three deployed projects that also get
   the full-stage treatment above the grid. It is the same object as
   every other case card (lift, tilt, 1.03 image scale, gradient
   edge, badges, hover title), but its cover is composed rather than
   photographed: a still of the product in a browser frame, floating
   on the same bloom the big highlight cards stand on. The frame runs
   off the bottom edge so the page reads as continuing past the crop.

   Everything inside the frame is sized in container units, so the
   fake chrome keeps its proportions whether the card is one of two
   on a desktop row or the full width of a phone.
   ------------------------------------------------------------ */
.work-card--live .work-card__media {
  container-type: inline-size;
  background: var(--bg-soft);
}
.work-card__bloom,
.work-card__grain,
.work-card__frame {
  position: absolute;
  z-index: 0;
}
.work-card__bloom {
  inset: -30%;
  background:
    radial-gradient(circle at 14% 18%, color-mix(in srgb, var(--bloom-a) 32%, transparent), transparent 55%),
    radial-gradient(circle at 88% 86%, color-mix(in srgb, var(--bloom-b) 26%, transparent), transparent 55%);
  filter: blur(40px);
}
/* only the stage is grained, not the screenshot standing on it: the
   product still has to survive being read at thumbnail size */
.work-card__grain {
  inset: 0;
}
/* the frame starts just below the badge row rather than at a percentage
   of the media, so the chips never sit on top of the fake chrome when the
   grid drops to one-up and the whole cover loses a third of its height.
   Its height comes from its contents, not from a bottom offset — see
   .work-card__shot, which is what decides how tall the window is. */
.work-card__frame {
  left: 8%;
  right: 8%;
  top: calc(var(--sp-3) + 2.8rem);
  display: flex;
  flex-direction: column;
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: var(--bg);
  box-shadow: var(--shadow-pop);
}
.work-card__chrome {
  display: flex;
  align-items: center;
  gap: 1.2cqw;
  padding: clamp(0.28rem, 1cqw, 0.65rem) clamp(0.45rem, 1.5cqw, 1rem);
  background: var(--bg-soft);
  border-bottom: 1px solid var(--glass-border);
}
.work-card__chrome i {
  width: clamp(0.3rem, 1.6cqw, 0.65rem);
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 18%, transparent);
}
.work-card__url {
  margin-inline: auto;
  padding: 0.25em 1.2em;
  border-radius: var(--r-pill);
  background: var(--bg);
  font-size: clamp(0.625rem, 2cqw, 0.8125rem);
  font-weight: 600;
  /* the body's 1.65 would make this pill the tallest thing in a 30px bar */
  line-height: 1.4;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  white-space: nowrap;
}
/* The window is sized to the capture, not the other way round. The three
   stills are 16:10, so a viewport of the same ratio shows the site at its
   full width and lets the page run off the bottom of the card — which is
   what a browser does. Pinning the frame to a bottom offset instead made
   the viewport narrower than the capture, and `cover` paid for it by
   slicing the site's left and right edges off.

   The shared .work-card__media img rule still covers and scales it on hover. */
.work-card__shot {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
}
/* each project's own light, sampled from the product rather than
   borrowed from the house aurora — the same pairings the full-stage
   cards above already stand on, so the two read as one project */
.work-card--lighthouse {
  --bloom-a: var(--aurora-blue);
  --bloom-b: #eab308;
}
.work-card--achroma {
  --bloom-a: var(--aurora-violet);
  --bloom-b: var(--aurora-orange);
}
.work-card--michael-ui {
  --bloom-a: #2b91f0;
  --bloom-b: #7c0095;
}
/* the club's own --hop and --foul, read off the storefront's token sheet:
   field green and foul-pole gold. No other card in the grid leads with
   green, so the pair identifies this one at thumbnail size. */
.work-card--hops {
  --bloom-a: #35e070;
  --bloom-b: #ffd400;
}

.work-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: var(--sp-7) var(--sp-4);
  color: var(--ink-secondary);
}
.work-empty b {
  display: block;
  font-size: var(--step-1);
  color: var(--ink);
  margin-bottom: var(--sp-1);
}
[hidden] {
  display: none !important;
}

/* ------------------------------------------------------------
   About band
   ------------------------------------------------------------ */
/* ------------------------------------------------------------
   About — the one band that reads the same in either theme.
   The dark palette is redeclared locally rather than the surface
   just being painted dark, so everything inside (ink, secondary
   ink, glass, shadows) inherits a consistent set instead of light
   type on a dark ground. Pinning it is the point: it is the only
   place on the site that does not follow the toggle, which is
   what makes it read as a held moment.
   ------------------------------------------------------------ */
.about {
  --bg: #0a0a0c;
  --bg-soft: #151518;
  --ink: #f5f5f7;
  --ink-secondary: #a5a5ae;
  --glass: rgba(21, 21, 24, 0.6);
  --glass-strong: rgba(21, 21, 24, 0.8);
  --glass-border: rgba(245, 245, 247, 0.1);
  --shadow-card: 0 12px 40px rgba(0, 0, 0, 0.5);
  --shadow-card-hover: 0 24px 60px rgba(0, 0, 0, 0.6);
  --shadow-pop: 0 32px 80px rgba(0, 0, 0, 0.7);

  position: relative;
  isolation: isolate;
  /* The terrain deliberately overhangs this band so the parallax has room to
     travel; without a clip that overhang simply spills into the sections
     above and below. `clip` rather than `hidden` so no scroll container is
     created — the same choice `main` makes. */
  overflow: clip;
  background: var(--bg-soft);
  color: var(--ink);
}
/* The terrain arrives once the whole sequence has played out and had a
   moment to settle — the last thing to happen, not competing with any of
   it. Cropped so its horizon is the file's exact vertical center, which is
   why `center` alone keeps that line on the copy block's midline at every
   width, without a single measurement. */
.about__terrain {
  display: none;
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: url("../img/about-terrain.webp");
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
  opacity: 0;
  /* A symmetric ease rather than the site's usual ease-out: over three
     seconds an exponential curve spends most of its change in the first
     moment and then creeps, which reads as arriving quickly and settling.
     Easing evenly in and out is what actually feels gradual. */
  transition: opacity 3s cubic-bezier(0.4, 0, 0.6, 1);
}
@media (min-width: 1100px) and (hover: hover) {
  /* more room around the statement, so the scene behind it is something you
     actually see rather than a strip glimpsed between two blocks of text */
  .about {
    padding-block: 11rem;
  }
  .about__terrain {
    display: block;
    /* overhangs the band top and bottom, which is the room the parallax
       travels through — without it the drift would expose a hard edge */
    top: -14%;
    bottom: -14%;
    /* The grade is baked into the file rather than applied here: it costs
       nothing at runtime on an element that repaints with the parallax, and
       it was chosen against the real composite — at 30% over this ground a
       filter that looks right on its own comes out washed. */
    transform: translate3d(0, var(--parallax, 0px), 0);
  }
  .about.is-settled .about__terrain {
    opacity: 0.3;
  }
}
@media (prefers-reduced-motion: reduce) {
  .about__terrain {
    transition: none;
  }
}
/* ------------------------------------------------------------
   Selected statement — the design-tool conceit: a collaborator's
   cursor drifts in, clicks the statement, and a selection frame
   with corner handles snaps around it. Decorative and inert; the
   text underneath is ordinary selectable copy.
   (After the CSS-only cursor/selection idea by Darlley, MIT.)
   ------------------------------------------------------------ */
.selectable {
  position: relative;
  width: fit-content;
  margin-inline: auto;
}
.selectable__frame {
  position: absolute;
  inset: calc(-1 * var(--sp-3));
  z-index: 1;
  border: 2px solid var(--aurora-blue);
  border-radius: 6px;
  opacity: 0;
  pointer-events: none;
  animation: select-frame 9s var(--ease) infinite;
}
.selectable__frame i {
  position: absolute;
  width: 9px;
  height: 9px;
  background: var(--bg);
  border: 2px solid var(--aurora-blue);
  border-radius: 2px;
}
.selectable__frame i:nth-child(1) { top: -6px; left: -6px; }
.selectable__frame i:nth-child(2) { top: -6px; right: -6px; }
.selectable__frame i:nth-child(3) { bottom: -6px; left: -6px; }
.selectable__frame i:nth-child(4) { bottom: -6px; right: -6px; }
/* The traveling box is the size of the block itself, so percentage
   translates read as "one block width/height" — the path then lands beside
   the block at any width instead of at a fixed pixel offset that would sit
   inside the text on desktop. The pointer rides its top-left corner. */
.selectable__cursor {
  position: absolute;
  inset: 0;
  z-index: 2;
  transform-origin: 0 0;
  opacity: 0;
  pointer-events: none;
  animation: select-cursor 9s var(--ease) infinite;
}
.selectable__pointer {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
}
.selectable__cursor svg {
  width: 1.4rem;
  height: 1.4rem;
  color: var(--aurora-blue);
  filter: drop-shadow(0 2px 6px rgba(10, 10, 12, 0.25));
}
.selectable__tag {
  padding: 0.28em 0.7em;
  border-radius: var(--r-pill);
  background: var(--aurora-blue);
  color: #ffffff;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
/* the frame exists from the click until the cursor leaves */
@keyframes select-frame {
  0%, 30% { opacity: 0; }
  34%, 90% { opacity: 1; }
  100% { opacity: 0; }
}
/* in from off-screen right → to the block's left edge → click → down to the
   bottom right → a small lift → back off-screen right.
   X/Y percentages are one block width/height. */
@keyframes select-cursor {
  0% { opacity: 0; transform: translate(calc(100% + 55vw), 30%) scale(1); }
  6% { opacity: 1; transform: translate(calc(100% + 40vw), 30%) scale(1); }
  /* lands on the word itself — main.js measures it and publishes the offset */
  28% { opacity: 1; transform: translate(var(--click-x, 20%), var(--click-y, 40%)) scale(1); }
  /* the click */
  31% { transform: translate(var(--click-x, 20%), var(--click-y, 40%)) scale(0.84); }
  35% { transform: translate(var(--click-x, 20%), var(--click-y, 40%)) scale(1); }
  /* hold on the selection, then travel down to the bottom right */
  48% { transform: translate(var(--click-x, 20%), var(--click-y, 40%)) scale(1); }
  68% { transform: translate(var(--park-x, calc(100% + 0.5rem)), 92%) scale(1); }
  /* the small lift */
  82% { opacity: 1; transform: translate(var(--park-x, calc(100% + 0.5rem)), 60%) scale(1); }
  92% { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(100% + 55vw), 52%) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .selectable__cursor {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .selectable__frame {
    animation: none;
    opacity: 1;
  }
}
/* On a phone the statement runs nearly the full width, so the desktop
   geometry pushed the selection frame into the screen edge and parked the
   cursor a full block-width to the right — off-screen for a third of the
   loop. Tighten the frame and bring the parking spot inside the block. */
@media (max-width: 860px) {
  .selectable__frame {
    inset: calc(-1 * var(--sp-2));
  }
  .selectable__cursor {
    --park-x: 72%;
  }
}
/* When the clicked word sits near the right edge the label would run off it;
   main.js measures the overflow and hangs the label off the other side of
   the pointer, which still reads as one cursor with its name attached. */
.selectable__pointer.is-flipped {
  flex-direction: row-reverse;
  transform: translateX(-100%);
}
/* Flipped, the arrow stands to the right of its label, so it has to point
   that way too — a cursor aiming back through its own name reads broken. */
.selectable__pointer.is-flipped svg {
  transform: scaleX(-1);
}

/* ------------------------------------------------------------
   About sequence (desktop pointers only, plays once).

   A cursor arrives, selects the statement, drags it left to clear
   room, wanders, and leaves — the selection releasing only once it
   is gone. After a beat the same cursor drags in a circular video,
   selects it in the same blue, moves around it and exits, and that
   selection releases too. Nothing loops: this is a thing that
   happens once, the way a real edit does.

   Every waypoint is a measured pixel value published by main.js,
   because a percentage of the text block would drift with the
   viewport and the orb lives outside that block entirely.
   ------------------------------------------------------------ */
.about__stage {
  position: relative;
}
.about__orb {
  display: none;
}

@media (min-width: 1100px) and (hover: hover) {
  .about__orb {
    display: block;
    position: absolute;
    /* its right edge lines up with the content column, the way the text's
       left edge lines up with the card below */
    right: var(--sp-4);
    top: 50%;
    /* capped at the source's native 380px so the circle is never upscaled */
    width: clamp(17rem, 26vw, 23.75rem);
    height: clamp(17rem, 26vw, 23.75rem);
    margin: 0;
    opacity: 0;
    transform: translateY(-50%) translateX(var(--orb-in, 60rem));
  }
  .about__orb-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
    display: block;
    box-shadow: var(--shadow-pop);
  }
  /* the same selection language the text gets: a bounding frame with
     corner handles, which is what a design tool shows around a circle */
  .about__orb-frame {
    position: absolute;
    inset: calc(-1 * var(--sp-3));
    border: 2px solid var(--aurora-blue);
    border-radius: 6px;
    opacity: 0;
    pointer-events: none;
  }
  .about__orb-frame i {
    position: absolute;
    width: 9px;
    height: 9px;
    background: var(--bg);
    border: 2px solid var(--aurora-blue);
    border-radius: 2px;
  }
  .about__orb-frame i:nth-child(1) { top: -6px; left: -6px; }
  .about__orb-frame i:nth-child(2) { top: -6px; right: -6px; }
  .about__orb-frame i:nth-child(3) { bottom: -6px; left: -6px; }
  .about__orb-frame i:nth-child(4) { bottom: -6px; right: -6px; }

  /* the looping desktop cursor stands down; the sequence takes over */
  .about__stage .selectable__cursor,
  .about__stage .selectable__frame {
    animation: none;
    opacity: 0;
  }
  .about__stage.is-playing .selectable {
    animation: seq-drag var(--seq, 12.4s) var(--ease) forwards;
  }
  .about__stage.is-playing .selectable__frame {
    animation: seq-frame var(--seq, 12.4s) linear forwards;
  }
  .about__stage.is-playing .selectable__cursor {
    animation: seq-cursor var(--seq, 12.4s) var(--ease) forwards;
  }
  .about__stage.is-playing .about__orb {
    animation: seq-orb var(--seq, 12.4s) var(--ease) forwards;
  }
  .about__stage.is-playing .about__orb-frame {
    animation: seq-frame-orb var(--seq, 12.4s) linear forwards;
  }
}

@keyframes seq-drag {
  0%, 17% { transform: translateX(0); }
  26%, 100% { transform: translateX(var(--drag, -8rem)); }
}
/* the text's selection holds until the cursor is off-screen, then releases */
@keyframes seq-frame {
  0%, 15% { opacity: 0; }
  16%, 50% { opacity: 1; }
  55%, 100% { opacity: 0; }
}
@keyframes seq-frame-orb {
  0%, 61% { opacity: 0; }
  68%, 92% { opacity: 1; }
  97%, 100% { opacity: 0; }
}
/* The circle is carried in, not revealed. It shares the cursor's easing and
   its exact timing across 57–68%, so the two travel locked together from
   off-screen until the drop; after that the cursor moves on alone. */
@keyframes seq-orb {
  0%, 56% { opacity: 0; transform: translateY(-50%) translateX(var(--orb-in, 60rem)); }
  57% { opacity: 1; transform: translateY(-50%) translateX(var(--orb-in, 60rem)); }
  68%, 100% { opacity: 1; transform: translateY(-50%) translateX(0); }
}
@keyframes seq-cursor {
  0% { opacity: 0; transform: translate(var(--enter-x), 30%); }
  3% { opacity: 1; }
  /* to the word, and click */
  11% { transform: translate(var(--click-x), var(--click-y)); }
  14% { transform: translate(var(--click-x), var(--click-y)) scale(0.84); }
  16% { transform: translate(var(--click-x), var(--click-y)) scale(1); }
  /* holds the block while it is dragged left — the cursor rides with it */
  26% { transform: translate(var(--click-x), var(--click-y)); }
  /* down, then diagonally back up, then away */
  33% { transform: translate(var(--click-x), calc(var(--click-y) + 7rem)); }
  41% { transform: translate(calc(var(--click-x) + 11rem), calc(var(--click-y) - 2rem)); }
  48% { opacity: 1; transform: translate(var(--exit-x), calc(var(--click-y) - 5rem)); }
  50%, 57% { opacity: 0; transform: translate(var(--exit-x), calc(var(--click-y) - 5rem)); }
  /* Comes back holding the circle. These two waypoints are the same span
     and the same easing as seq-orb's, so the cursor stays fixed on the
     grab point while the pair travels — they are bound until the drop. */
  56% { opacity: 0; transform: translate(calc(var(--grab-x) + var(--orb-in)), var(--grab-y)); }
  57% { opacity: 1; transform: translate(calc(var(--grab-x) + var(--orb-in)), var(--grab-y)); }
  68% { opacity: 1; transform: translate(var(--grab-x), var(--grab-y)); }
  /* released — from here it moves alone */
  75% { transform: translate(calc(var(--grab-x) - 2.5rem), calc(var(--grab-y) + 3rem)); }
  82% { transform: translate(calc(var(--grab-x) + 4rem), calc(var(--grab-y) - 1.5rem)); }
  87% { transform: translate(var(--grab-x), var(--grab-y)); }
  93% { opacity: 1; transform: translate(var(--exit-x), calc(var(--grab-y) - 2.5rem)); }
  96%, 100% { opacity: 0; transform: translate(var(--exit-x), calc(var(--grab-y) - 2.5rem)); }
}
/* Reduced motion keeps the payload and drops the performance: the circle
   simply sits there, and no cursor crosses the screen. */
@media (prefers-reduced-motion: reduce) and (min-width: 1100px) {
  .about__orb {
    opacity: 1;
    transform: translateY(-50%);
    animation: none;
  }
  .about__stage.is-playing .selectable,
  .about__stage.is-playing .selectable__cursor,
  .about__stage.is-playing .selectable__frame,
  .about__stage.is-playing .about__orb,
  .about__stage.is-playing .about__orb-frame {
    animation: none;
  }
}

/* one large statement, no heading — left-aligned text with the block
   itself centered on the screen */
/* 600 was tuned when this band followed the theme and could sit dark-on-
   light. Pinned dark, the same weight reads noticeably heavier — light type
   on a dark ground always does — and the step up to the bold words closed
   almost shut. Lightening the plain text reopens that gap; the bold is
   untouched, so the contrast is restored rather than re-invented. */
.about__statement {
  font-size: var(--step-2);
  font-weight: 450;
  letter-spacing: -0.02em;
  line-height: 1.4;
  color: var(--ink-secondary);
  max-width: 36ch;
  margin-inline: auto;
  text-align: left;
  text-wrap: balance;
}
/* `b` as well as `strong`: the portal's toolbar goes through execCommand,
   which emits <b> in Chrome, so a statement bolded there arrived here with
   the browser's default weight and the surrounding secondary ink instead
   of the white the homepage gives it. */
.about__statement strong,
.about__statement b {
  color: var(--ink);
  font-weight: 800;
}

/* ------------------------------------------------------------
   Product highlight — one live project given a full-stage moment,
   deliberately unlike the case-study grid
   ------------------------------------------------------------ */
.highlight__card {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 6fr);
  gap: var(--sp-6);
  align-items: center;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  padding: var(--sp-7) var(--sp-6);
  overflow: hidden;
  isolation: isolate;
}
.highlight__card + .highlight__card {
  margin-top: var(--sp-6);
}
.highlight__card::before {
  content: "";
  position: absolute;
  inset: -40%;
  z-index: -1;
  background:
    radial-gradient(circle at 12% 20%, color-mix(in srgb, var(--aurora-violet) 24%, transparent), transparent 55%),
    radial-gradient(circle at 90% 85%, color-mix(in srgb, var(--aurora-orange) 18%, transparent), transparent 55%);
  filter: blur(45px);
}
/* category + year, the same glass chip the case-study grid already uses —
   a highlighted project reads as one more piece of "selected work" rather
   than a separate badge language of its own */
.highlight__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: var(--sp-4);
}
.highlight__title {
  font-size: var(--step-3);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.highlight__sub {
  margin-top: var(--sp-3);
  color: var(--ink-secondary);
  max-width: 44ch;
}
.highlight__cta {
  margin-top: var(--sp-5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
/* the thumbnail is the actual live site, embedded in a browser window */
.highlight__browser {
  display: block;
  border-radius: var(--r-thumb);
  overflow: hidden;
  box-shadow: var(--shadow-pop);
  background: var(--bg);
  text-decoration: none;
}
.highlight__chrome {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.65rem 1rem;
  background: var(--bg-soft);
  border-bottom: 1px solid var(--glass-border);
}
.highlight__chrome i {
  width: 0.65rem;
  height: 0.65rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 18%, transparent);
}
.highlight__url {
  margin-left: auto;
  margin-right: auto;
  padding: 0.25em 1.2em;
  border-radius: var(--r-pill);
  background: var(--bg);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.highlight__viewport {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
}
.highlight__viewport iframe {
  /* render the real site at desktop width, scaled down to fit the frame */
  width: 1280px;
  height: 800px;
  border: 0;
  transform: scale(var(--embed-scale, 0.42));
  transform-origin: top left;
  pointer-events: none;
}
/* shared by every still image in the frame — the live-embed cards' phone
   fallback and any card that is a screenshot at every width, like Michael
   UI below, which has no iframe to fall back from */
.highlight__viewport img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
  /* a quiet fill, not a broken-image glyph, if a screenshot is ever
     missing or mid-load */
  background: var(--bg-soft);
}
/* the phone's stand-in for the live embed specifically: hidden on desktop
   by default, since highlight-embed.js only ever grants that card's
   iframe a src once matchMedia confirms desktop — a phone never fetches
   the embedded site at all, not merely a hidden copy of it. */
.highlight__poster {
  display: none;
}
/* a second live project, mirrored: preview on the left, copy on the right.
   Same two children as the base card, just swapped in the markup — the
   grid only needs its column weights swapped to match, no order property
   required on desktop. */
.highlight__card--reverse {
  grid-template-columns: minmax(0, 6fr) minmax(0, 4fr);
}
/* the film-grain texture, as a real element instead of the `.grain`
   utility's ::after: this card's own ::after is already spoken for by the
   gradient-edge border every case-study surface shares, and a single node
   can only host one ::after. Splitting the two apart means the border
   renders at its own full opacity instead of inheriting grain's 0.35 and
   blend mode, which is what it was silently doing before. The grid's
   live-prototype cards stand on the same bloom and borrow the same node. */
.highlight__grain,
.work-card__grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.35;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
/* Lighthouse's own glow: the site's beam is navy and gold, so the ambient
   light behind its card gets the same pairing instead of the house
   violet/orange — still a glow field, never a flat fill, so the
   light-not-paint rule holds. */
/* on the highlight card the grain sits over everything, the browser
   included; on the grid card it stays under the frame (z-index 0, set with
   the rest of that card's layers) so the screenshot reads clean at
   thumbnail size */
.highlight__grain {
  z-index: 1;
}
.highlight__card--lighthouse {
  --lh-gold: #eab308;
}
.highlight__card--lighthouse::before {
  background:
    radial-gradient(circle at 10% 22%, color-mix(in srgb, var(--aurora-blue) 26%, transparent), transparent 55%),
    radial-gradient(circle at 90% 82%, color-mix(in srgb, var(--lh-gold) 22%, transparent), transparent 55%);
}
/* Michael UI's own glow: its primary and secondary tokens, sampled
   straight from the product's own color-primitives page rather than
   approximated from the house aurora hues, which both read noticeably
   lighter than this system's actual blue and its actual purple runs
   darker toward magenta than aurora-violet does. */
.highlight__card--michael-ui {
  --mu-blue: #2b91f0;
  --mu-purple: #7c0095;
}
.highlight__card--michael-ui::before {
  background:
    radial-gradient(circle at 10% 22%, color-mix(in srgb, var(--mu-blue) 26%, transparent), transparent 55%),
    radial-gradient(circle at 90% 82%, color-mix(in srgb, var(--mu-purple) 22%, transparent), transparent 55%);
}
/* The Hops store's own pairing, from the same token sheet the grid card
   samples: field green lit from the copy side, foul-pole gold behind the
   browser. Both run a few points cooler than the raw tokens do, because
   this card carries a photographic capture rather than flat product UI
   and the glow has to sit under it rather than compete with it. */
.highlight__card--hops {
  --hops-green: #35e070;
  --hops-gold: #ffd400;
}
.highlight__card--hops::before {
  background:
    radial-gradient(circle at 10% 22%, color-mix(in srgb, var(--hops-green) 22%, transparent), transparent 55%),
    radial-gradient(circle at 90% 82%, color-mix(in srgb, var(--hops-gold) 18%, transparent), transparent 55%);
}
@media (max-width: 860px) {
  .highlight__card {
    grid-template-columns: 1fr;
    padding: var(--sp-6) var(--sp-4);
  }
  /* stacked, copy still reads before the embed regardless of which side
     it sat on at the wider width — a recruiter should know what they're
     looking at before meeting a scaled-down iframe */
  .highlight__card--reverse .highlight__copy {
    order: 1;
  }
  .highlight__card--reverse .highlight__browser {
    order: 2;
  }
  /* the still image stands in for the live embed below the desktop
     breakpoint — see highlight-embed.js, which withholds the iframe's
     src at this width instead of just hiding a copy that already loaded */
  .highlight__poster {
    display: block;
  }
  .highlight__viewport iframe {
    display: none;
  }
}

/* ------------------------------------------------------------
   Footer
   ------------------------------------------------------------ */
/* ------------------------------------------------------------
   Footer — the closing statement, not a legal strip. An aurora
   field under an oversized invitation, with the mark and the
   three ways to reach out.
   ------------------------------------------------------------ */
.footer {
  position: relative;
  margin-top: var(--sp-8);
  /* the last thing on the page, so it owns clearing the home indicator —
     its wash still bleeds to the true bottom edge, only the content lifts */
  padding: var(--sp-9) 0 calc(var(--sp-7) + var(--safe-b));
  /* The wash below is authored to bleed ~200px above the footer, which is
     what closes the gap between the footer and whatever section precedes
     it. `overflow: hidden` was clipping every pixel of that bleed away, so
     the gap read as a flat black bar with a hard edge top and bottom. The
     clip is only needed sideways, to keep the -10% insets off the page
     width. Engines without `overflow: clip` keep the hidden fallback. */
  overflow: hidden;
  overflow: clip visible;
  isolation: isolate;
}
.footer::before {
  content: "";
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 150%;
  z-index: -1;
  background:
    radial-gradient(ellipse at 20% 30%, color-mix(in srgb, var(--aurora-violet) 30%, transparent), transparent 58%),
    radial-gradient(ellipse at 80% 70%, color-mix(in srgb, var(--aurora-cyan) 26%, transparent), transparent 58%),
    radial-gradient(ellipse at 55% 100%, color-mix(in srgb, var(--aurora-pink) 22%, transparent), transparent 60%);
  filter: blur(70px);
  animation: aurora-drift-a 30s ease-in-out infinite alternate;
}
.footer__inner {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: var(--sp-5);
}
.footer__line {
  font-size: var(--step-5);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.02;
  max-width: 16ch;
  text-wrap: balance;
}
.footer__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
}
.footer__icon {
  width: 3rem;
  height: 3rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  color: var(--ink);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  transition: transform var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease);
}
.footer__icon:hover {
  transform: translateY(-3px);
  background: var(--bg-soft);
}
.footer__icon svg {
  width: 1.25rem;
  height: 1.25rem;
}
@media (max-width: 640px) {
  .footer {
    padding: var(--sp-8) 0 calc(var(--sp-6) + var(--safe-b));
  }
  .footer__line {
    font-size: var(--step-3);
  }
}

/* ------------------------------------------------------------
   Case-study pages
   ------------------------------------------------------------ */
/* Grainient hero: an animated, grain-textured gradient field whose hues
   are pulled at runtime from the case study's own hero image (study-tint.js
   sets --tint1/--tint2/--tint3 from the dominant sampled colors; the aurora
   ramp is the pre-JS and no-image fallback). Kept at low mix strength over
   the page ground so ink text always holds contrast. */
.study-hero {
  position: relative;
  padding: calc(4.25rem + var(--sp-7)) 0 var(--sp-6);
  overflow: hidden;
  isolation: isolate;
}
.study-hero::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: -2;
  background:
    radial-gradient(ellipse at 18% 12%, color-mix(in srgb, var(--tint1, var(--aurora-violet)) 30%, transparent), transparent 55%),
    radial-gradient(ellipse at 85% 25%, color-mix(in srgb, var(--tint2, var(--aurora-cyan)) 26%, transparent), transparent 55%),
    radial-gradient(ellipse at 50% 95%, color-mix(in srgb, var(--tint3, var(--aurora-pink)) 20%, transparent), transparent 60%);
  filter: blur(60px);
  animation: grainient-drift 26s ease-in-out infinite alternate;
  transition: background 0.8s var(--ease);
}
@keyframes grainient-drift {
  from { transform: translate(0, 0) rotate(0deg) scale(1); }
  to { transform: translate(3%, 4%) rotate(4deg) scale(1.08); }
}
/* A 60px blur across a layer 160% of the hero, drifting forever, is cheap on
   a desktop GPU and expensive on a phone — it was still compositing while
   the reveals below were waiting for a frame. Mobile keeps the color and
   drops the motion. */
@media (max-width: 860px) {
  .study-hero::before {
    animation: none;
    inset: -15%;
    filter: blur(40px);
  }
}
.study-hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.study-hero__title {
  font-size: var(--step-4);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.04;
  max-width: 18ch;
}
.study-hero__sub {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  color: var(--ink-secondary);
  max-width: 56ch;
}
.study-hero__figure {
  position: relative;
  margin-top: var(--sp-6);
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-pop);
}
.study-hero__figure img {
  width: 100%;
  height: auto;
}
/* the hero image floats over its own aurora wash */
.study-hero__glow {
  position: absolute;
  inset: auto -10% -30% -10%;
  height: 70%;
  z-index: -1;
  background:
    radial-gradient(ellipse at 30% 100%, color-mix(in srgb, var(--aurora-violet) 26%, transparent), transparent 60%),
    radial-gradient(ellipse at 75% 100%, color-mix(in srgb, var(--aurora-cyan) 24%, transparent), transparent 60%);
  filter: blur(50px);
}

/* study hero entrance: same sequence and speed as the homepage heroes */
.study-hero__meta { animation: hero-rise 1s var(--ease) both; }
.study-hero__sub { animation: hero-rise 1.2s var(--ease) 1.9s both; }
.study-hero__figure { animation: hero-rise 1.6s var(--ease) 2.4s both; }

.chapter {
  /* padding-block only: these elements also carry .container, and the
     padding shorthand would wipe its inline padding — which is invisible
     on desktop but rides the screen edge on mobile */
  padding-block: var(--sp-6);
}
.chapter__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-4);
}
.chapter__body {
  max-width: var(--measure);
  color: var(--ink-secondary);
}
.chapter__body p + p {
  margin-top: var(--sp-3);
}
/* A chapter of steps: each head set a size up and bold in ink, its
   sentence at body size under it, and the set spaced like paragraphs.
   The numbers are written into the heads, so the list draws none. */
.chapter__steps {
  display: grid;
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.chapter__step-head {
  margin: 0 0 var(--sp-1);
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.025em;
  color: var(--ink);
  text-wrap: balance;
}
.chapter__step p {
  margin: 0;
}

/* figure card — dispersed imagery (keeps .plate + <template> lightbox API) */
.figure-block {
  padding-block: var(--sp-4);
}
/* A graphic shown whole and centered in the reading column, for artwork that
   arrives already composed — its own ground, its own margins. The plate would
   put a card behind it and frame it twice, so this one carries no surface,
   no shadow and no zoom: only the image and its number. --fig-w is the
   graphic's own comfortable width, set per figure, and it never exceeds the
   column. */
.figure-inline {
  margin-block: var(--sp-7);
  text-align: center;
}
.figure-inline img {
  display: block;
  margin-inline: auto;
  width: 100%;
  max-width: min(100%, var(--fig-w, 34rem));
  height: auto;
}
.figure-inline__caption {
  display: block;
  margin-top: var(--sp-3);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-secondary);
  text-wrap: balance;
}
.figure-inline__caption b {
  color: var(--ink);
  margin-right: 0.5em;
}

/* ------------------------------------------------------------
   Gallery — for the record whose story is breadth. A stack of
   full-width plates makes you scroll past the point; a grid lets
   the range land in one view. Each tile is still a .plate, so the
   shared lightbox opens on click and keyboard alike.
   ------------------------------------------------------------ */
.gallery {
  padding-block: var(--sp-7);
}
.gallery__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.gallery__lede {
  margin-top: var(--sp-3);
  max-width: var(--measure);
  color: var(--ink-secondary);
}
.gallery__grid {
  margin-top: var(--sp-5);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr));
  gap: var(--sp-4);
}
.gallery__tile {
  position: relative;
  display: block;
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
  /* iOS will not clip the scaling image to these corners on its own */
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}
.gallery__frame {
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
}
.gallery__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: transform 0.8s var(--ease);
}
/* the caption rides a scrim that lifts on approach, so a wall of frames
   stays a wall of frames until you ask one of them what it is */
.gallery__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0.35rem;
  padding: var(--sp-4);
  text-align: left;
  color: #f5f5f7;
  background: linear-gradient(180deg, transparent 35%, rgba(10, 10, 12, 0.82));
  opacity: 0;
  transition: opacity var(--t-micro) var(--ease);
}
.gallery__tile:hover,
.gallery__tile:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-pop);
}
.gallery__tile:hover .gallery__veil,
.gallery__tile:focus-visible .gallery__veil {
  opacity: 1;
}
.gallery__tile:hover .gallery__frame img,
.gallery__tile:focus-visible .gallery__frame img {
  transform: scale(1.05);
}
.gallery__no {
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #a5a5ae;
}
.gallery__cap {
  font-weight: 600;
  line-height: 1.35;
  text-wrap: balance;
}
/* Touch has no hover to reveal with, so the caption simply stays put. */
@media (hover: none) {
  .gallery__veil {
    opacity: 1;
    background: linear-gradient(180deg, transparent 45%, rgba(10, 10, 12, 0.85));
  }
}
.gallery + .gallery {
  padding-top: 0;
}

/* ------------------------------------------------------------
   Carousel — the photography, at the width it was shot for.
   A grid tile crops a desk scene down to a thumbnail; these run
   full-width and advance as one set. carousel.js owns the state.
   ------------------------------------------------------------ */
.carousel {
  position: relative;
  margin-top: var(--sp-5);
}
/* The window and the thing that moves through it have to be two elements.
   Put overflow:hidden on the track itself and it clips its own children to
   its own box — which then slides off-screen, taking every slide but the
   first with it. The viewport stays put and does the clipping; the track
   only travels. */
.carousel__viewport {
  overflow: hidden;
  border-radius: var(--r-card);
  box-shadow: var(--shadow-pop);
  /* iOS will not clip the slides to these corners without a mask */
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}
.carousel__track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  transition: transform 0.6s var(--ease);
  touch-action: pan-y;
}
.carousel__slide {
  flex: 0 0 100%;
  min-width: 0;
}
.carousel__plate {
  position: relative;
  display: block;
  width: 100%;
  border-radius: 0;
  cursor: zoom-in;
}
.carousel__plate img {
  display: block;
  width: 100%;
  height: auto;
  /* the set is mixed 3:2 and 16:10; one frame keeps the track from jumping */
  aspect-ratio: 3 / 2;
  object-fit: cover;
  -webkit-user-drag: none;
  user-select: none;
}
.carousel__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
  padding: var(--sp-6) var(--sp-5) var(--sp-4);
  text-align: left;
  color: #f5f5f7;
  font-weight: 600;
  background: linear-gradient(180deg, transparent, rgba(10, 10, 12, 0.8));
}
.carousel__no {
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #a5a5ae;
}
.carousel__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.carousel__arrow {
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  color: var(--ink);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  transition: transform var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease);
}
.carousel__arrow:hover {
  transform: scale(1.06);
  background: var(--bg-soft);
}
.carousel__arrow svg {
  width: 1.15rem;
  height: 1.15rem;
}
.carousel__dots {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.carousel__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 25%, transparent);
  transition: background-color var(--t-micro) var(--ease), transform var(--t-micro) var(--ease);
}
.carousel__dot:hover {
  transform: scale(1.3);
}
.carousel__dot[aria-current="true"] {
  background: var(--ink);
  transform: scale(1.3);
}
@media (max-width: 640px) {
  .carousel__cap {
    padding: var(--sp-5) var(--sp-4) var(--sp-3);
    font-size: var(--step--1);
  }
}
@media (prefers-reduced-motion: reduce) {
  .carousel__track {
    transition: none;
  }
}

/* ------------------------------------------------------------
   Video plate — the real product film, held behind its own poster.
   Nothing is requested from YouTube until the visitor asks for it,
   so an embed can't tax the page or set a cookie on arrival.
   ------------------------------------------------------------ */
.videoplate {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  padding: 0;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-pop);
  cursor: pointer;
  /* the poster is a transformed child; iOS needs the mask to clip it */
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}
.videoplate img,
.videoplate iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  transition: transform 0.7s var(--ease);
}
.videoplate:hover img {
  transform: scale(1.03);
}
.videoplate::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, transparent 30%, rgba(10, 10, 12, 0.55));
}
.videoplate__play {
  position: absolute;
  z-index: 2;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: 5.5rem;
  height: 5.5rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  color: #f5f5f7;
  background: rgba(21, 21, 24, 0.55);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid rgba(245, 245, 247, 0.22);
  transition: transform var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease);
}
.videoplate:hover .videoplate__play,
.videoplate:focus-visible .videoplate__play {
  transform: scale(1.08);
  background: rgba(21, 21, 24, 0.72);
}
.videoplate__play svg {
  width: 1.9rem;
  height: 1.9rem;
  /* the triangle's own mass sits left of center; nudge it back optically */
  margin-left: 0.22rem;
}
.videoplate__label {
  position: absolute;
  z-index: 2;
  left: var(--sp-5);
  right: var(--sp-5);
  bottom: var(--sp-4);
  color: #f5f5f7;
  font-weight: 600;
  font-size: var(--step-0);
  text-align: left;
  text-shadow: 0 1px 12px rgba(10, 10, 12, 0.5);
}
.videoplate.is-playing::before,
.videoplate.is-playing .videoplate__play,
.videoplate.is-playing .videoplate__label {
  display: none;
}
@media (max-width: 640px) {
  .videoplate__play {
    width: 4rem;
    height: 4rem;
  }
  .videoplate__play svg {
    width: 1.4rem;
    height: 1.4rem;
  }
}
.plate {
  display: block;
  width: 100%;
  text-align: left;
  cursor: zoom-in;
  border-radius: var(--r-card);
}
.plate__frame {
  display: block;
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  background: var(--bg-soft);
  aspect-ratio: 4 / 3;
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.plate:hover .plate__frame,
.plate:focus-visible .plate__frame {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.plate__frame img,
.plate__frame svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* ------------------------------------------------------------
   Shot grid — phone screenshots as one exhibit, not a stack.
   A 9:16 frame run full-width stands over a thousand pixels tall
   and buries whatever follows it; four abreast stay comparable
   and cost one screen between them.
   ------------------------------------------------------------ */
.shots {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
  align-items: start;
}
.shots__item {
  display: block;
  width: 100%;
  text-align: left;
  cursor: zoom-in;
}
.shots__frame {
  display: block;
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
  /* iOS will not clip the scaling image to these corners on its own */
  -webkit-mask-image: -webkit-radial-gradient(#000, #000);
  mask-image: radial-gradient(#000, #000);
}
.shots__item:hover .shots__frame,
.shots__item:focus-visible .shots__frame {
  transform: translateY(-4px);
  box-shadow: var(--shadow-pop);
}
.shots__frame img {
  display: block;
  width: 100%;
  height: auto;
}
@media (max-width: 860px) {
  .shots {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-3);
  }
}

.plate__caption {
  display: block;
  padding: var(--sp-2) var(--sp-1) 0;
  font-size: var(--step--1);
}
.plate__caption b {
  font-weight: 700;
  letter-spacing: -0.01em;
}
.plate__caption span {
  display: block;
  color: var(--ink-secondary);
  margin-top: 0.15em;
}

/* ------------------------------------------------------------
   Prototype embed — the live thing, sitting under the results it
   backs up. Desktop only: the frame holds a phone-sized app, and on
   an actual phone that nests a scrollable viewport inside a
   scrollable page with no way for a thumb to tell them apart. Small
   screens get the link instead, which is the honest affordance.
   ------------------------------------------------------------ */
.protoembed {
  display: none;
  padding-block: var(--sp-6);
}
@media (min-width: 861px) {
  .protoembed {
    display: block;
  }
}
/* heading and its note on the left, the escape hatch on the right, the two
   centered against each other so the row reads as one bar */
.protoembed__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
}
.protoembed__intro {
  min-width: 0;
}
.protoembed__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.protoembed__note {
  margin-top: var(--sp-3);
  max-width: var(--measure);
  color: var(--ink-secondary);
}
.protoembed__open {
  flex: none;
}
.protoembed__open svg {
  width: 1.05em;
  height: 1.05em;
  transition: transform var(--t-micro) var(--ease);
}
.protoembed__open:hover svg {
  transform: translateX(2px);
}
.protoembed__frame {
  position: relative;
  margin-top: var(--sp-5);
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-pop);
  /* tall enough that the prototype's own phone frame is never cropped */
  height: min(86vh, 60rem);
}
/*
  The prototype is a different origin, so its scrollbar can't be styled from
  here. Instead the frame is overscanned: the iframe is wider than its box by
  a gutter on each side, and the wrapper's overflow clips both. The right
  gutter takes the scrollbar with it, the equal left offset keeps the app's
  centered phone on the visual center, and the content stays scrollable.
*/
.protoembed__frame {
  --proto-gutter: 20px;
}
.protoembed__frame iframe {
  display: block;
  width: calc(100% + var(--proto-gutter) * 2);
  margin-left: calc(var(--proto-gutter) * -1);
  height: 100%;
  border: 0;
}
.protoembed__foot {
  margin-top: var(--sp-4);
}
.protoembed__link {
  font-weight: 600;
}

/* results — metrics on the color block */
.results {
  padding-block: var(--sp-6);
}
.results__card {
  position: relative;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  padding: var(--sp-6) var(--sp-5);
  overflow: hidden;
  isolation: isolate;
}
.results__card::before {
  content: "";
  position: absolute;
  inset: -40%;
  z-index: -1;
  background:
    radial-gradient(circle at 15% 25%, color-mix(in srgb, var(--aurora-violet) 22%, transparent), transparent 55%),
    radial-gradient(circle at 85% 80%, color-mix(in srgb, var(--aurora-pink) 18%, transparent), transparent 55%);
  filter: blur(45px);
}
/* the film-grain texture, as a real element instead of the `.grain`
   utility's ::after: this card's own ::after is already spoken for by the
   gradient-edge border every case-study surface shares, and a single node
   can only host one ::after. Splitting the two apart means the border
   renders at its own full opacity instead of inheriting grain's 0.35 and
   blend mode, which is what it was silently doing before (same fix as
   .highlight__grain on the homepage). */
.results__grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.35;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
.results__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-3);
}
.results__narrative {
  color: var(--ink-secondary);
  max-width: var(--measure);
}
.results__grid {
  margin-top: var(--sp-5);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--sp-5);
}

/* outcome — testimonial on the left, live metrics on the right.
   The before/after bar chart that used to render these numbers as a
   flat SVG image is gone; the numbers are real text that counts up. */
.outcome__grid {
  display: grid;
  gap: var(--sp-5);
  align-items: start;
  margin-top: var(--sp-5);
}
.outcome__grid--split {
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--sp-6);
}
.outcome__metrics {
  display: grid;
  gap: var(--sp-4);
}
/* full-width row (no testimonial): lay the metrics out at their natural
   size — an auto-fit grid stretches a lone metric across the whole column */
.outcome__grid:not(.outcome__grid--split) .outcome__metrics {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-5) var(--sp-8);
}
.outcome__grid:not(.outcome__grid--split) .stat {
  flex: 0 1 auto;
  min-width: 12rem;
  max-width: 22rem;
}
.outcome__metrics .stat {
  text-align: left;
  padding: 0;
}
.outcome__metrics .stat__value {
  font-size: var(--step-4);
}
/* Word-led highlights and numbers are the same component: identical size,
   weight and casing, and the same animated arrival. Long words simply wrap
   on the display's own tight leading. */
.stat__value {
  text-wrap: balance;
}

/* testimonial card */
.testimonial {
  margin: 0;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  padding: var(--sp-6);
  box-shadow: var(--shadow-card);
}
.testimonial__stars {
  display: flex;
  gap: 0.15em;
  color: var(--aurora-orange);
  margin-bottom: var(--sp-4);
}
.testimonial__stars svg {
  width: 1.15rem;
  height: 1.15rem;
}
.testimonial__quote {
  margin: 0;
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.35;
  color: var(--ink);
}
.testimonial__by {
  margin-top: var(--sp-5);
}
.testimonial__by b {
  display: block;
  font-weight: 700;
  font-size: var(--step-0);
}
.testimonial__by span {
  font-size: var(--step--1);
  color: var(--ink-secondary);
}
@media (max-width: 860px) {
  .outcome__grid--split {
    grid-template-columns: 1fr;
  }
}
/* On a phone the quote sat three paddings deep — page gutter, results card,
   then its own card — leaving the words about half the screen to live in.
   The nested card dissolves into a full-bleed band across the results card:
   same content and emphasis, one layer of inset instead of three. */
@media (max-width: 640px) {
  .testimonial {
    margin-inline: calc(-1 * var(--sp-5));
    padding: var(--sp-5) var(--sp-5);
    border-radius: 0;
    box-shadow: none;
    border-block: 1px solid var(--glass-border);
  }
  .testimonial__quote {
    font-size: var(--step-1);
    line-height: 1.4;
  }
  .testimonial__by {
    margin-top: var(--sp-4);
  }
}

/* prev / next */
.study-nav {
  padding-block: var(--sp-7);
}
.study-nav__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}
.study-nav__card {
  display: block;
  text-decoration: none;
  background: var(--bg-soft);
  border-radius: var(--r-card);
  padding: var(--sp-5);
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.study-nav__card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.study-nav__dir {
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--ink-secondary);
}
.study-nav__title {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: -0.02em;
}
.study-nav__card--next {
  text-align: right;
}

/* ------------------------------------------------------------
   Encrypted gate — locked view + unlock card
   ------------------------------------------------------------ */
.gate {
  position: relative;
  padding: var(--sp-8) 0;
  isolation: isolate;
}
/* record-lock.js scrolls here the instant a password succeeds, landing the
   visitor on the first unlocked chapter instead of wherever collapsing the
   gate happened to leave the scroll position. The margin keeps that first
   chapter clear of the fixed header instead of tucking under it. */
#record-content {
  scroll-margin-top: calc(var(--topbar-h, 5.5rem) + var(--sp-3));
}
/* The glow was clipped square by overflow:hidden on a box the blobs were
   deliberately hanging out of, so a soft field ended on three hard edges.
   It now bleeds past the section and simply falls off, and it takes its
   color from the record's own artwork — the same --tint values the study
   header samples — so the lock reads as part of that case study. */
.gate__aurora {
  position: absolute;
  inset: -30% -20%;
  z-index: -1;
  pointer-events: none;
}
.gate__aurora::before,
.gate__aurora::after {
  content: "";
  position: absolute;
  width: 46%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.5;
}
.gate__aurora::before {
  background: radial-gradient(circle, var(--tint1, var(--aurora-violet)), transparent 65%);
  top: 16%;
  left: 16%;
  animation: aurora-drift-a 26s ease-in-out infinite alternate;
}
.gate__aurora::after {
  background: radial-gradient(circle, var(--tint3, var(--aurora-pink)), transparent 65%);
  bottom: 14%;
  right: 16%;
  animation: aurora-drift-b 30s ease-in-out infinite alternate;
}
.gate__card {
  max-width: 28rem;
  margin: 0 auto;
  text-align: center;
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-card);
  padding: var(--sp-6) var(--sp-5);
  box-shadow: var(--shadow-pop);
}
.gate__lock {
  width: 3rem;
  height: 3rem;
  margin: 0 auto var(--sp-4);
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: var(--bg-soft);
  border: 1px solid var(--glass-border);
}
.gate__lock svg {
  width: 1.3rem;
  height: 1.3rem;
}
/* shackle pivots open on success */
.gate__lock-shackle {
  transform-box: fill-box;
  transform-origin: 100% 100%;
  transition: transform 0.45s var(--ease);
}
.gate--granted .gate__lock-shackle {
  transform: rotate(-38deg) translate(-1.5px, -1px);
}
.gate__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.gate__note {
  margin: var(--sp-2) auto var(--sp-4);
  font-size: var(--step--1);
  color: var(--ink-secondary);
  max-width: 32ch;
}
.gate__form {
  display: flex;
  gap: var(--sp-2);
  justify-content: center;
  flex-wrap: wrap;
}
.gate__input {
  flex: 1 1 11rem;
  padding: 0.7em 1.2em;
  border-radius: var(--r-pill);
  border: 1px solid var(--glass-border);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: var(--step--1);
  text-align: center;
}
.gate__input:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 2px;
}
.gate__input.has-error:focus-visible {
  outline-color: var(--aurora-pink);
}
@media (max-width: 860px) {
  /* iOS Safari zooms the whole page in on focus for any text input under
     16px, and 11rem plus the button's own width happened to sit right on
     the edge of what this row could fit — sometimes wrapping, sometimes
     not, depending on font metrics. Stack on purpose instead of by luck:
     the input takes the row's own width, like every other text field on
     the site does once stacked, and the button keeps its normal pill
     size rather than stretching into a shape nothing else on the page
     uses. */
  .gate__form {
    flex-direction: column;
    align-items: center;
  }
  .gate__input {
    flex: 0 0 auto;
    width: 100%;
    font-size: 16px;
  }
}
.gate__error {
  min-height: 1.4em;
  margin-top: var(--sp-3);
  font-size: var(--step--1);
  font-weight: 600;
  color: var(--aurora-pink);
}
.gate__error.is-shown {
  animation: gate-shake 0.3s var(--ease);
}
@keyframes gate-shake {
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}
.gate--granted .gate__form,
.gate--granted .gate__error {
  display: none;
}
.gate__granted {
  display: none;
  font-weight: 600;
  color: var(--ink-secondary);
}
.gate--granted .gate__granted {
  display: block;
}

/* ------------------------------------------------------------
   Figure lock — a single figure held behind its own password,
   independent of the page-level gate. The visible image is a real
   blurred raster; the scrim and form sit on top of it.
   ------------------------------------------------------------ */
.plate--locked {
  cursor: default;
}
.plate--locked .plate__frame {
  position: relative;
}
.figlock {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--sp-4);
  background: rgba(10, 10, 12, 0.46);
}
.figlock__lock {
  width: 2.6rem;
  height: 2.6rem;
  margin-bottom: var(--sp-3);
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.32);
  color: #fff;
}
.figlock__lock svg {
  width: 1.1rem;
  height: 1.1rem;
}
.figlock__note {
  color: #fff;
  font-size: var(--step--1);
  font-weight: 600;
  max-width: 24ch;
  margin-bottom: var(--sp-3);
}
.figlock__form {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
  justify-content: center;
}
.figlock__input {
  flex: 1 1 8rem;
  padding: 0.6em 1.1em;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255, 255, 255, 0.35);
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font: inherit;
  font-size: var(--step--2);
  text-align: center;
}
.figlock__input::placeholder {
  color: rgba(255, 255, 255, 0.7);
}
.figlock__input:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 2px;
}
.figlock__input.has-error:focus-visible {
  outline-color: var(--aurora-pink);
}
.figlock__btn {
  padding: 0.6em 1.2em;
  font-size: var(--step--2);
}
.figlock__error {
  min-height: 1.2em;
  margin-top: var(--sp-2);
  font-size: var(--step--2);
  font-weight: 600;
  color: #ffb4c6;
}
.figlock__error.is-shown {
  animation: gate-shake 0.3s var(--ease);
}
.plate--unlocked .figlock {
  display: none;
}

/* decrypted content reveals chapter-by-chapter */
.declassify > * {
  opacity: 0;
  transform: translateY(14px);
  animation: declassify-in 0.6s var(--ease) forwards;
}
@keyframes declassify-in {
  to { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------
   Contact — a brief, not a support ticket
   ------------------------------------------------------------ */
.contact {
  position: relative;
  padding: calc(4.25rem + var(--sp-7)) 0 var(--sp-8);
  isolation: isolate;
  overflow: hidden;
}
/* The wash used to stop around 60% of the section, which the tall form hid:
   there was always content over the dead zone. The short success panel
   exposed it as a flat, unlit band between the card and the footer. It now
   runs the full section and carries a third low ember, so the bottom edge
   hands off to the footer instead of falling off a cliff. */
.contact::before {
  content: "";
  position: absolute;
  inset: -30% -10% 0 -10%;
  z-index: -1;
  background:
    radial-gradient(ellipse at 22% 16%, color-mix(in srgb, var(--aurora-violet) 26%, transparent), transparent 58%),
    radial-gradient(ellipse at 82% 34%, color-mix(in srgb, var(--aurora-cyan) 22%, transparent), transparent 58%),
    radial-gradient(ellipse at 58% 96%, color-mix(in srgb, var(--aurora-violet) 15%, transparent), transparent 62%);
  filter: blur(65px);
  animation: aurora-drift-b 28s ease-in-out infinite alternate;
}
.contact__title {
  font-size: var(--step-5);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.02;
  max-width: 16ch;
}
.contact__lede {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  color: var(--ink-secondary);
  max-width: 52ch;
}
.form {
  margin-top: var(--sp-7);
  max-width: 46rem;
  display: grid;
  gap: var(--sp-5);
}
.form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}
.field {
  display: grid;
  gap: var(--sp-2);
}
.field > label,
.field > legend {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
/* A fieldset's legend is rendered outside its box's normal flow, so the
   grid gap that separates every other label from its control never applied
   to it — which is why this one sat tight against the chips. Restated as a
   margin so it matches the rest of the form exactly. */
.field > legend {
  padding: 0;
  margin-bottom: var(--sp-2);
}
.field:where(fieldset) {
  border: 0;
  padding: 0;
  margin: 0;
}
.field__hint {
  font-weight: 400;
  color: var(--ink-secondary);
}
.field input,
.field textarea {
  width: 100%;
  padding: 0.85em 1.1em;
  border-radius: var(--r-thumb);
  border: 1px solid var(--glass-border);
  background: var(--bg-soft);
  color: var(--ink);
  font: inherit;
  font-size: var(--step-0);
}
.field textarea {
  min-height: 9rem;
  resize: vertical;
}
/* Placeholders sat at the same weight as secondary copy, close enough to a
   typed value that a filled field and an empty one read alike at a glance.
   Mixed toward the field's own surface instead, so the prompt stays legible
   but never impersonates an answer. Every field carries a real label, so the
   placeholder is an example rather than the only thing naming the input. */
.field input::placeholder,
.field textarea::placeholder {
  color: color-mix(in srgb, var(--ink-secondary) 70%, var(--bg-soft));
  opacity: 1; /* Firefox dims placeholders by default; the mix is the control */
}
/* project type: real radios, styled as selectable cards */
.choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--sp-3);
}
.choice {
  position: relative;
}
.choice input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.choice span {
  display: block;
  padding: 1em 1.1em;
  border-radius: var(--r-thumb);
  border: 1px solid var(--glass-border);
  background: var(--bg-soft);
  font-weight: 600;
  font-size: var(--step--1);
  text-align: center;
  transition: background-color var(--t-micro) var(--ease), color var(--t-micro) var(--ease),
    border-color var(--t-micro) var(--ease);
}
.choice input:checked + span {
  background: var(--ink);
  color: var(--bg);
  border-color: var(--ink);
}
.choice input:focus-visible + span {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 3px;
}
.form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
}
.form__note {
  font-size: var(--step--1);
  color: var(--ink-secondary);
  max-width: 34ch;
}
.form__status {
  min-height: 1.4em;
  font-size: var(--step--1);
  font-weight: 600;
}
.form__status[data-state="error"] {
  color: var(--aurora-pink);
}
.form__status[data-state="ok"] {
  color: var(--ink);
}
/* The bot trap. Taken out of the layout entirely rather than hidden with
   display:none, which some bots check for — and kept off the tab order so
   nobody using a keyboard can land in it by accident. */
.form__trap {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Replaces the form on success, in place — the visitor keeps their spot on
   the page and the outcome is unmistakable.

   It reserves roughly the height the form occupied. Without that the
   section collapses to the height of a short message, and the aurora wash
   behind it (which is sized as a share of the section) stops well above
   the footer, leaving a bare band of page background. Holding the height
   keeps the backdrop covering its own section. */
.form__done {
  margin-top: var(--sp-7);
  min-height: 40rem;
  display: grid;
  align-content: center;
  animation: form-done-in 0.6s var(--ease) both;
}
@media (max-width: 640px) {
  /* on a phone the reserved height would be dead scroll, not composure */
  .form__done {
    min-height: 0;
  }
}
.form__done:focus {
  outline: none;
}
/* Same material as the gate's unlock card — glass, hairline, deep shadow —
   because both are the same moment: one panel standing in for the thing
   that was there. Left-aligned, unlike the gate, since it sits under a
   left-aligned page rather than centered in its own field. */
.form__done-card {
  max-width: 34rem;
  display: grid;
  justify-items: start;
  gap: var(--sp-4);
  text-align: left;
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-card);
  padding: var(--sp-6) var(--sp-5);
  box-shadow: var(--shadow-pop);
}
.form__done-mark {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--r-pill);
  color: var(--bg);
  background: var(--ink);
}
.form__done-mark svg {
  width: 1.4rem;
  height: 1.4rem;
}
.form__done-title {
  font-size: var(--step-3);
  font-weight: 800;
  letter-spacing: -0.03em;
}
.form__done-note {
  color: var(--ink-secondary);
  max-width: 42ch;
}
.form__done-cta {
  margin-top: var(--sp-2);
}
@keyframes form-done-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
}
.field input:user-invalid,
.field textarea:user-invalid {
  border-color: var(--aurora-pink);
}
@media (max-width: 640px) {
  .form__row {
    grid-template-columns: 1fr;
  }
}

/* ------------------------------------------------------------
   Lightbox — dark glass stage (same IDs/classes as lightbox.js)
   ------------------------------------------------------------ */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: color-mix(in srgb, #0a0a0c 88%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  display: none;
  align-items: center;
  justify-content: center;
  /* full-screen, so the frame and its chrome have to clear the cutout and
     the home indicator on their own */
  padding: calc(var(--sp-5) + var(--safe-t)) max(var(--sp-5), var(--safe-r))
    calc(var(--sp-5) + var(--safe-b)) max(var(--sp-5), var(--safe-l));
}
.lightbox[data-open="true"] {
  display: flex;
}
.lightbox__stage {
  max-width: min(72rem, 100%);
  max-height: 100%;
  display: grid;
  gap: var(--sp-3);
}
.lightbox__frame {
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-pop);
}
.lightbox__frame img,
.lightbox__frame svg {
  display: block;
  max-width: 100%;
  max-height: 72vh;
  width: auto;
  margin: 0 auto;
}
.lightbox__caption {
  text-align: center;
  color: #a5a5ae;
  font-size: var(--step--1);
}
.lightbox__title {
  display: block;
  color: #f5f5f7;
  font-weight: 700;
}
.lightbox__close,
.lightbox__nav {
  position: absolute;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--r-pill);
  display: grid;
  place-items: center;
  color: #f5f5f7;
  background: rgba(21, 21, 24, 0.6);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid rgba(245, 245, 247, 0.1);
  font-size: var(--step-1);
  transition: transform var(--t-micro) var(--ease);
}
.lightbox__close:hover,
.lightbox__nav:hover {
  transform: scale(1.08);
}
/* absolutely positioned, so it measures from the padding edge and the stage's
   own safe-area padding does not move it — it has to clear the cutout itself */
.lightbox__close {
  top: calc(var(--sp-4) + var(--safe-t));
  right: calc(var(--sp-4) + var(--safe-r));
}
/* vertically centered, so only the side cutouts of landscape can reach them */
.lightbox__prev {
  left: max(var(--sp-4), var(--safe-l));
  top: 50%;
  transform: translateY(-50%);
}
.lightbox__next {
  right: max(var(--sp-4), var(--safe-r));
  top: 50%;
  transform: translateY(-50%);
}
.lightbox__prev:hover {
  transform: translateY(-50%) scale(1.08);
}
.lightbox__next:hover {
  transform: translateY(-50%) scale(1.08);
}

/* ------------------------------------------------------------
   Responsive
   ------------------------------------------------------------ */
@media (max-width: 860px) {
  .work-grid {
    grid-template-columns: 1fr;
  }
  /* filters become a single swiped row: pills crop at the container edge,
     rest flush left at the start and flush right at the end */
  .filterbar {
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    overflow: hidden;
  }
  .chips {
    flex: 1 0 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* the row runs edge to edge inside the padded bar, so a pill can crop
       against the container rather than stopping short of it */
    margin: 0 calc(-1 * var(--sp-3));
    padding: 0 var(--sp-3);
    /* snap targets align to the padded edge, not the raw scrollport — without
       this the first pill snaps flush to the bleed and the row starts life
       scrolled by exactly its own padding */
    scroll-padding-inline: var(--sp-3);
  }
  .chips::-webkit-scrollbar {
    display: none;
  }
  .chips .chip {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }
  .chips .chip:last-child {
    scroll-snap-align: end;
  }
  /* Sticky bar, docked: the filters stop floating below the header and
     become the rest of it — one continuous glass surface, full-bleed, with
     a hairline where the two meet. Scrolling back above the work section
     releases it and both return to their own shapes. */
  .filterbar {
    top: var(--topbar-h, 5.5rem);
    /* radius only — the width change is a layout property and animating it
       would jank the sticky bar at the exact moment it docks */
    transition: border-radius 0.2s var(--ease);
  }
  .filterbar.is-docked {
    margin-inline: calc(-1 * var(--sp-4));
    padding-inline: var(--sp-4);
    border-radius: 0;
    border-inline: 0;
    border-bottom: 0;
    border-top: 1px solid var(--glass-border);
  }
  .study-nav__grid {
    grid-template-columns: 1fr;
  }
  .study-nav__card--next {
    text-align: left;
  }
}
@media (max-width: 640px) {
  .smenu__panel {
    padding-left: max(var(--sp-4), var(--safe-l));
    padding-right: max(var(--sp-4), var(--safe-r));
  }
}

/* ------------------------------------------------------------
   Mobile hero — a fixed stage every slide fills identically:
   clear header room, the artwork peeking from the top behind the
   copy and dissolving into it, then text, CTA, and the controls
   sitting close beneath. Nothing here may depend on how long a
   slide's text is, or the slides would change height as they turn.
   ------------------------------------------------------------ */
@media (max-width: 860px) {
  .hero {
    min-height: 40rem;
  }
  .hero__content {
    /* the peeking artwork occupies everything above this line */
    padding-top: 15rem;
    padding-bottom: var(--sp-7);
    gap: var(--sp-4);
  }
  /* one type scale for every slide, so nothing resizes as they rotate */
  .hero__title,
  .hero__slide--work .hero__title {
    font-size: var(--step-3);
    max-width: 18ch;
  }
  .hero__metric {
    font-size: var(--step-2);
    margin-top: var(--sp-2);
  }
  .hero__sub {
    font-size: var(--step-0);
    margin-top: var(--sp-2);
  }
  .hero__cta {
    margin-top: var(--sp-4);
  }
  /* the artwork becomes an atmospheric wash across the whole stage —
     the same low-opacity treatment the drift wall gets on the opening
     slide — so the headline keeps its contrast instead of fighting a
     full-strength screenshot */
  .hero__scene {
    --scene-o: 0.28;
    inset: 0 -12%;
    width: auto;
    height: 100%;
    transform: none;
    border-radius: 0;
    box-shadow: none;
    cursor: default;
    /* still dissolves toward the bottom so it never ends on a hard edge */
    -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 74%, transparent 100%);
    mask-image: linear-gradient(180deg, #000 0%, #000 74%, transparent 100%);
  }
  .hero__scene img {
    height: 100%;
    object-fit: cover;
    object-position: top center;
  }
  .hero__scene::before {
    content: none;
  }
  .hero__scene img {
    border-radius: 0;
    animation: none;
  }
  /* the desktop entrance keyframes carry translateY(-50%), which would undo
     the top-anchored mobile placement — swap in a vertical-neutral one */
  @keyframes hero-scene-in-mobile {
    from { opacity: 0; transform: translateY(-14px); }
    to { opacity: var(--scene-o, 1); transform: none; }
  }
  .hero__slide.is-active .hero__scene {
    animation-name: hero-scene-in-mobile;
  }
  .hero__controls {
    bottom: calc(var(--sp-4) + var(--safe-b));
  }
}

/* ------------------------------------------------------------
   Gradient edge, continued — these two must sit at the end of the
   sheet. Both cards declare their own flat 1px border further down,
   which on equal specificity would win on source order and leave the
   gradient painting uselessly just inside it. The border keeps its
   space in the box; the ring is what you actually see.
   ------------------------------------------------------------ */
.gate__card,
.form__done-card,
.smenu__cta {
  border-color: transparent;
}
.gate__card::after,
.form__done-card::after,
.smenu__cta::after {
  inset: -1px;
}

/* ============================================================
   EMPLOYER LANDING PAGES  (/for, served at /<employer-slug>)

   One template, filled from a record in the admin portal. It is
   the homepage's own material — same aurora, same drift wall,
   same dark about band, same card ring — with one difference
   that matters: the light is the employer's color, not the
   site's. --tint1/2/3 are derived from their hex in employer.js,
   so the page arrives lit by their brand without a single flat
   fill of it anywhere (the Light-Not-Paint rule holds).
   ============================================================ */

.eh {
  position: relative;
  min-height: min(92vh, 58rem);
  display: grid;
  align-content: center;
  overflow: hidden;
  isolation: isolate;
}

/* ------------------------------------------------------------
   The arrival.

   Three glow fields bloom outward from their own resting points,
   staggered, each handing over to the slow ambient drift the
   homepage aurora already uses — so the entrance doesn't stop,
   it hands off. Transform, opacity and filter only.
   ------------------------------------------------------------ */
.eh__bloom {
  position: absolute;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
}
.eh__bloom i {
  position: absolute;
  width: 58%;
  aspect-ratio: 1;
  border-radius: 50%;
  opacity: 0;
  filter: blur(90px);
  /* every blob starts collapsed at its own resting point, so the bloom
     reads as light being turned up rather than shapes flying in */
  transform: scale(0.28);
}
/* The two colors the employer actually chose take the top two corners —
   primary left, accent right — so the pair reads as one sky. The third
   blob is the blend of the two and sits low, keeping the bottom of the
   hero lit without introducing a color neither of them owns. */
.eh__bloom i:nth-child(1) {
  top: 2%;
  left: 6%;
  background: radial-gradient(circle, var(--tint1, var(--aurora-violet)), transparent 66%);
}
.eh__bloom i:nth-child(2) {
  top: 2%;
  right: 6%;
  background: radial-gradient(circle, var(--tint3, var(--aurora-cyan)), transparent 66%);
}
.eh__bloom i:nth-child(3) {
  bottom: -6%;
  right: 20%;
  width: 42%;
  background: radial-gradient(circle, var(--tint2, var(--aurora-pink)), transparent 68%);
}

.eh.is-arriving .eh__bloom i {
  animation: bloom-in 1.9s var(--ease) both, aurora-drift-a 26s ease-in-out 1.9s infinite alternate;
}
.eh.is-arriving .eh__bloom i:nth-child(2) {
  animation: bloom-in 1.9s var(--ease) 0.22s both, aurora-drift-b 30s ease-in-out 2.1s infinite alternate;
}
.eh.is-arriving .eh__bloom i:nth-child(3) {
  animation: bloom-in 2.1s var(--ease) 0.42s both, aurora-drift-b 34s ease-in-out 2.5s infinite alternate;
}
@keyframes bloom-in {
  from {
    opacity: 0;
    transform: scale(0.28);
    /* over-blurred at the start and resolving as it opens, so the field
       looks like it is coming into focus rather than simply fading up */
    filter: blur(140px);
  }
  60% {
    opacity: 0.62;
  }
  to {
    opacity: 0.5;
    transform: scale(1);
    filter: blur(90px);
  }
}

/* the wall is scenery, and arrives under everything else */
/* The homepage drops the wall to 0.3 on phones, where it shares the screen
   with the headline instead of sitting beside it. The arrival animation was
   landing on a hard-coded 1 and overriding that, which is why the hero copy
   had nothing to sit against on mobile. It now ends on whatever the
   breakpoint asked for. */
.eh__wall {
  --wall-o: 1;
  opacity: 0;
}
.eh.is-arriving .eh__wall {
  animation: wall-in 2.2s var(--ease) 0.5s both;
}
@keyframes wall-in {
  from { opacity: 0; transform: translateX(3%); }
  to { opacity: var(--wall-o); transform: none; }
}

/* Above the phone layout the wall crosses the introduction rather than
   sitting beside it, and it keeps doing so much further up than it looks.
   The wall is 68vw pulled 10% off the right edge, and its near edge
   finishes fading in at 46% of its own width — a proportion, so as the
   viewport grows the opaque edge chases the paragraph, which is pinned at
   60ch. At 861px roughly 200px of every line sits on full-strength tiles;
   the coverage thins as the screen widens but does not actually clear the
   text until about 1900px. A screenshot tile is mostly white, so at full
   strength that is 1.2:1 against the copy.

   The wall steps back across that whole range instead of only where it is
   most obvious, because a boundary where legibility flips is worse than
   either side of it. Each step is the strongest the wall can be while a
   white tile still leaves the introduction above 4.5:1 in both themes;
   the floor across every width from 861px up is 4.7:1. The 0.4 nearest
   the phone layout sits one step above the 0.3 it uses, where the wall is
   behind the copy rather than crossing it. */
@media (min-width: 861px) and (max-width: 1440px) {
  .eh__wall {
    --wall-o: 0.4;
  }
}
@media (min-width: 1441px) and (max-width: 1900px) {
  .eh__wall {
    --wall-o: 0.62;
  }
}

.eh__content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: clamp(7rem, 16vh, 10rem) var(--sp-4) var(--sp-7);
}

/* The employer's mark, on a white plate in both themes — a logo is drawn
   for one background, and flipping the surface under it is how you get an
   invisible one. */
.eh__mark {
  display: inline-grid;
  place-items: center;
  width: 4.5rem;
  height: 4.5rem;
  margin-bottom: var(--sp-4);
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: #ffffff;
  border: 1px solid rgba(10, 10, 12, 0.08);
  box-shadow: var(--shadow-card);
}
/* The mark fills its plate. The plate is light so a dark logo drawn for a
   white ground survives this dark hero, but padding on top of that turned
   the square crop into a logo floating in a white frame — which is what
   you notice first on the page it is meant to introduce. */
.eh__mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.eh__title {
  font-size: var(--step-5);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.035em;
  max-width: 16ch;
  text-wrap: balance;
}
.eh__role {
  margin-top: var(--sp-4);
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
}
/* This paragraph sits over a glow field and a drifting wall, not over a
   flat ground, so the secondary ink every other supporting line uses came
   out muddy against it in dark mode. Brighter and a step larger: it is the
   only thing on this screen doing any persuading. */
.eh__line {
  margin-top: var(--sp-4);
  font-size: var(--step-1);
  line-height: 1.55;
  color: color-mix(in srgb, var(--ink) 86%, transparent);
  max-width: 60ch;
  text-wrap: pretty;
}
/* The one thing asking to be acted on in this viewport, so it carries the
   full ink weight rather than the secondary tint every other cue on the
   site uses — over a glow field at display scale, a quiet label simply
   disappears. */
.eh__cue {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  margin-top: var(--sp-6);
  color: var(--ink);
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: -0.015em;
  text-decoration: none;
  transition: gap var(--t-micro) var(--ease);
}
.eh__cue svg {
  width: 1.05em;
  height: 1.05em;
  stroke-width: 2.4;
}
.eh__cue:hover {
  gap: 0.85em;
}
@media (prefers-reduced-motion: no-preference) {
  .eh.is-arriving .eh__cue svg {
    animation: cue-nudge 2.6s var(--ease) 4s infinite;
  }
}
@keyframes cue-nudge {
  0%, 70%, 100% { transform: none; }
  80% { transform: translateY(3px); }
}

/* the copy follows the light in, on the homepage's own rise */
.eh.is-arriving .eh__mark { animation: hero-rise 1.1s var(--ease) 0.6s both; }
.eh.is-arriving .eh__role { animation: hero-rise 1.2s var(--ease) 1.9s both; }
.eh.is-arriving .eh__line { animation: hero-rise 1.2s var(--ease) 2.15s both; }
.eh.is-arriving .eh__cue { animation: hero-rise 1.2s var(--ease) 2.5s both; }
/* the greeting's sweep starts after the light has opened, not with it */
.eh.is-arriving .dia { animation-delay: 0.95s; }

@media (prefers-reduced-motion: reduce) {
  /* the arrival collapses to its own last frame: lit, settled, still */
  .eh.is-arriving .eh__bloom i,
  .eh.is-arriving .eh__wall,
  .eh.is-arriving .eh__mark,
  .eh.is-arriving .eh__role,
  .eh.is-arriving .eh__line,
  .eh.is-arriving .eh__cue,
  .eh.is-arriving .eh__cue svg {
    animation: none;
  }
  .eh.is-arriving .eh__bloom i {
    opacity: 0.5;
    transform: none;
  }
  .eh.is-arriving .eh__wall {
    opacity: var(--wall-o);
  }
}

/* The about band on these pages carries the portal's text and no cursor
   sequence — there is no fixed word for the pointer to click when the copy
   is written per employer. Everything else about the band is the
   homepage's: the pinned dark palette, the terrain, the parallax. */
/* nothing to add: the band, the statement measure, the circle and the whole
   cursor sequence are the homepage's, and employer.js supplies the words */

/* ------------------------------------------------------------
   Case-study rows — one per study, full width, in portal order
   ------------------------------------------------------------ */
/* Said once, above the list, rather than repeated on every locked card:
   why some of these ask for a password and how to get one. */
.section__sub {
  /* The section head is a flex row, so this was sizing to its own text and
     stopping short of the column. Claiming a full basis puts it on its own
     line at the column's full width, under the heading rather than beside
     it. One sentence of housekeeping, not a passage anyone settles into. */
  flex: 1 0 100%;
  margin-top: var(--sp-3);
  max-width: none;
  color: var(--ink-secondary);
  font-size: var(--step-0);
  line-height: 1.6;
}
.section__sub a {
  color: var(--ink);
  text-underline-offset: 3px;
}

/* The list sat on bare ground while everything above it was lit, so the
   page went flat exactly where the work starts. One large field of the
   employer's own color, thrown from the top left so it falls across the
   heading and the first row and fades out down the stack. Same light as
   the hero, a long way down the page. */
.curated {
  position: relative;
  isolation: isolate;
  overflow: clip;
}
.curated__bloom {
  position: absolute;
  z-index: -1;
  top: -28rem;
  left: -22rem;
  width: 72rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent, var(--aurora-violet)), transparent 66%);
  filter: blur(130px);
  opacity: 0.3;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .curated__bloom {
    animation: aurora-drift-a 34s ease-in-out infinite alternate;
  }
}

.caselist {
  display: grid;
  gap: var(--sp-4);
}
.caselist__empty {
  padding: var(--sp-7) 0;
  color: var(--ink-secondary);
  max-width: var(--measure);
}
.caserow {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-5);
  padding: var(--sp-4);
  border-radius: var(--r-card);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  color: inherit;
  text-decoration: none;
  /* No overflow clip here: the artwork is clipped by .caserow__media's own
     radius, and the only thing this would catch is the password tooltip
     trying to leave the card. */
  /* the homepage card's hover, exactly: lift, deepen, scale the art */
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.caserow::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(315deg, var(--edge-far), var(--edge-mid) 50%, var(--edge-near));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.caserow:hover,
.caserow:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.caserow__media {
  display: block;
  border-radius: var(--r-thumb);
  overflow: hidden;
  aspect-ratio: 16 / 10;
  background: var(--bg);
}
.caserow__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--ease);
}
.caserow:hover .caserow__media img,
.caserow:focus-visible .caserow__media img {
  transform: scale(1.03);
}
.caserow__body {
  display: block;
  min-width: 0;
  padding-right: var(--sp-3);
}
.caserow__title {
  display: block;
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
}
.caserow__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-top: var(--sp-3);
}
.caserow__desc {
  display: block;
  margin-top: var(--sp-3);
  color: var(--ink-secondary);
  max-width: 60ch;
}
/* What this row will ask for before it opens. Deliberately the smallest
   thing in the card and the only bold one, so it registers on the way past
   without competing with the title. */
.caserow__gated {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  margin-top: var(--sp-3);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--ink);
}
.caserow__gated {
  position: relative;
}
.caserow__gated::before {
  content: "";
  width: 0.4em;
  height: 0.4em;
  border-radius: 50%;
  background: var(--accent, var(--aurora-violet));
}

/* What the lock will ask for, said in full on hover or keyboard focus.
   Glass over the card rather than a native title attribute: it arrives at
   once instead of after a browser delay, it is styled like the rest of the
   surface, and its text sits in the accessibility tree either way. */
.caserow__tip {
  position: absolute;
  bottom: calc(100% + 0.65rem);
  left: -0.5rem;
  z-index: 6;
  width: max-content;
  /* never wider than the screen it has to sit on: at 22rem this ran 17px
     past the right edge of a 375px phone */
  max-width: min(22rem, calc(100vw - 3rem));
  padding: 0.7em 0.95em;
  border-radius: var(--r-thumb);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-pop);
  color: var(--ink);
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: 0;
  text-wrap: pretty;
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity var(--t-micro) var(--ease), transform var(--t-micro) var(--ease),
    visibility 0s linear var(--t-micro);
  pointer-events: none;
}
.caserow__gated:hover .caserow__tip,
.caserow:focus-visible .caserow__tip {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  .caserow__tip {
    transform: none;
    transition: none;
  }
}

/* ------------------------------------------------------------
   Closing — the shortlist ends, the rest of the work is offered
   ------------------------------------------------------------ */
.closing {
  margin-top: var(--sp-7);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--glass-border);
  text-align: center;
}
.closing__line {
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.closing__note {
  margin: var(--sp-2) auto var(--sp-5);
  max-width: 48ch;
  color: var(--ink-secondary);
}

/* ------------------------------------------------------------
   No page behind this URL — a real state, not a bare 404
   ------------------------------------------------------------ */
.voidstate {
  position: relative;
  min-height: 82vh;
  display: grid;
  place-items: center;
  isolation: isolate;
  overflow: hidden;
}
.voidstate__aurora {
  position: absolute;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
}
.voidstate__aurora::before,
.voidstate__aurora::after {
  content: "";
  position: absolute;
  width: 48%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.4;
}
.voidstate__aurora::before {
  background: radial-gradient(circle, var(--tint1, var(--aurora-violet)), transparent 65%);
  top: 12%;
  left: 12%;
  animation: aurora-drift-a 26s ease-in-out infinite alternate;
}
.voidstate__aurora::after {
  background: radial-gradient(circle, var(--tint3, var(--aurora-cyan)), transparent 65%);
  bottom: 10%;
  right: 12%;
  animation: aurora-drift-b 30s ease-in-out infinite alternate;
}
.voidstate__inner {
  text-align: center;
}
.voidstate__title {
  font-size: var(--step-4);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.04;
  text-wrap: balance;
}
.voidstate__note {
  margin: var(--sp-4) auto var(--sp-5);
  max-width: 46ch;
  color: var(--ink-secondary);
  font-size: var(--step-1);
}
.voidstate__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-3);
}

/* ------------------------------------------------------------
   Narrow screens — the hero matches the homepage's own phone
   configuration, and the case row stacks art over copy
   ------------------------------------------------------------ */
@media (max-width: 860px) {
  .eh {
    min-height: min(90vh, 44rem);
  }
  .eh__title {
    max-width: 12ch;
  }
  /* matches the homepage: on a phone the wall is behind the copy rather
     than beside it, so it drops back to let the headline win */
  .eh__wall {
    --wall-o: 0.3;
  }
  .eh__line {
    font-size: var(--step-0);
  }
  .section__sub {
    font-size: var(--step--1);
  }
  .curated__bloom {
    top: -16rem;
    left: -14rem;
    width: 42rem;
    filter: blur(90px);
  }
  .caserow {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
  }
  .caserow__body {
    padding-right: 0;
  }
  .caserow__title {
    font-size: var(--step-1);
  }
}

/* ------------------------------------------------------------
   Holding screen for an employer page.

   Both <main> elements start hidden while the record is fetched,
   which on a phone was several seconds of black. This ships in
   the markup so it is on screen at first paint, and employer.js
   removes it on every path out of the fetch.
   ------------------------------------------------------------ */
.bootscreen {
  position: fixed;
  inset: 0;
  z-index: 5;
  display: grid;
  align-content: center;
  justify-items: center;
  background: var(--bg);
  isolation: isolate;
  overflow: hidden;
}
.bootscreen__glow {
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 50%;
  width: min(36rem, 90vw);
  aspect-ratio: 1;
  margin: -18rem 0 0 -18rem;
  border-radius: 50%;
  background: radial-gradient(circle, var(--aurora-violet), transparent 66%);
  filter: blur(90px);
  opacity: 0.32;
  pointer-events: none;
}
/* The skeleton is laid out like the hero it stands in for, at the same
   width and the same vertical position, so the page reads as resolving
   rather than as one screen being swapped for another. */
.bootscreen__stage {
  display: grid;
  justify-items: start;
  gap: var(--sp-3);
  width: 100%;
  padding-inline: var(--sp-4);
}
.skel {
  display: block;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--ink) 11%, transparent);
}
/* One animated element, not eight: the whole group breathes together on a
   single opacity track. Eight independently shimmering bars would be
   eight composited layers, which is the opposite of the point on the
   phones this screen exists for. */
@media (prefers-reduced-motion: no-preference) {
  .bootscreen__stage {
    animation: boot-pulse 1.7s ease-in-out infinite;
  }
}
@keyframes boot-pulse {
  50% { opacity: 0.5; }
}
.skel--mark {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: var(--r-thumb);
  margin-bottom: var(--sp-2);
}
/* the greeting is display scale, so its bars are too */
.skel--title {
  /* tracks the greeting's own line box (display scale, line-height 1.02)
     so the bars read as the headline they stand in for */
  height: clamp(2.6rem, 1.7rem + 4.4vw, 5.8rem);
  width: min(30rem, 82%);
  border-radius: var(--r-thumb);
}
.skel--title--short {
  width: min(19rem, 54%);
}
.skel--role {
  height: clamp(1.6rem, 1.2rem + 1.2vw, 2.2rem);
  width: min(15rem, 46%);
  margin-top: var(--sp-2);
}
.skel--line {
  height: 0.85rem;
  width: min(34rem, 92%);
  margin-top: 0.15rem;
}
.skel--line--short {
  width: min(22rem, 62%);
}
.skel--cue {
  height: 1.35rem;
  width: 11rem;
  margin-top: var(--sp-5);
}

/* ------------------------------------------------------------
   Phone compositing budget.

   backdrop-filter is the most expensive property on this site and
   the menu panel applies it full-height over whatever the page is
   already animating — the drift wall, the aurora, the glows. On a
   phone that is what made a tap on a menu link feel like nothing
   happened for a second or two. The panel keeps its shape and
   takes a near-opaque ground instead, which over a dark or light
   page reads almost identically and costs nothing to draw.
   ------------------------------------------------------------ */
@media (max-width: 860px) {
  /* min(26rem, 100%) is 416px, so anything wider than that — a 430px Pro
     Max — left a sliver of the page down the left edge. The drawer takes
     the whole screen on a phone. */
  .smenu__panel,
  .smenu__layer {
    width: 100%;
  }
  .smenu__panel {
    border-left: 0;
  }
  .smenu__panel {
    background: color-mix(in srgb, var(--bg) 94%, transparent);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .smenu__cta {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg-soft) 88%, transparent);
  }
  .chip,
  .smenu__auth,
  .smenu__out,
  .footer__icon {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  /* the drawer's ground is near-opaque on a phone, so the glass tokens
     these two ride on have nothing left to blur — give them a real fill
     instead of a translucent one over an already-solid panel */
  .smenu__out {
    background: color-mix(in srgb, var(--bg-soft) 88%, transparent);
  }
  /* the hero's three glow fields are the page's other big cost; two of
     them carry the color and the third only deepened it */
  .eh__bloom i:nth-child(3) {
    display: none;
  }

  /* The bar is fixed, full width, and sits over whatever is scrolling, so
     its blur is recomposited every single frame — the most expensive thing
     on the page during a scroll. A near-opaque ground reads the same at
     this size and costs nothing. */
  .topbar::before {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  /* the same trade for every small glass pill: the rotator chrome on the
     homepage hero and on the testimonials */
  .hero__dots,
  .hero__arrow,
  .hero__playpause,
  .voices__dots,
  .voices__arrow,
  .voices__playpause {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg-soft) 92%, transparent);
  }
}

/* ============================================================
   ABOUT — the story block (/about.html)

   A tall scroll track whose stage pins to the viewport: the
   astronaut footage scrubs with the scroll while the career
   timeline walks backwards from today to 2007.

   Composition: the words own the left, the figure owns the
   right — the canvas draw biases the subject toward 65% of the
   frame so nothing ever stands in front of him. The header
   sweeps in once (the heroes' own dia animation) and stays;
   beneath it one role at a time rises in and lifts away, its
   avatar seated on a dotted line that runs the stage's height.
   The rail on the right edge is the scroller: one tick per
   role, years on hover, arrows at both ends.

   Like the homepage's about band, the section pins the dark
   palette in both themes — the footage is a night scene, and a
   white ground behind it would read as a hole in the page.

   Two modes. The default is the static page: poster, header,
   all nine roles in normal flow — complete without JavaScript,
   and exactly what reduced-motion and Save-Data visitors get.
   about-scrub.js adds .is-enhanced for the pinned scrub.
   ============================================================ */
.story {
  --bg: #0a0a0c;
  --bg-soft: #151518;
  --ink: #f5f5f7;
  --ink-secondary: #a5a5ae;
  --glass: rgba(21, 21, 24, 0.6);
  --glass-strong: rgba(21, 21, 24, 0.8);
  --glass-border: rgba(245, 245, 247, 0.1);
  --shadow-card: 0 12px 40px rgba(0, 0, 0, 0.5);
  --shadow-pop: 0 32px 80px rgba(0, 0, 0, 0.7);
  --edge-far: rgba(245, 245, 247, 0.16);
  --edge-mid: rgba(245, 245, 247, 0.05);
  --edge-near: rgba(245, 245, 247, 0.36);
  /* how tall the film stands when it is a hero rather than a backdrop —
     see the static-mode block below */
  --film-h: min(70vh, 40rem);

  position: relative;
  background: var(--bg);
  color: var(--ink);
  isolation: isolate;
}
.story.is-enhanced {
  /* one intro beat plus nine roles' worth of scroll */
  height: calc(110vh + 9 * 52vh);
}

.story__stage {
  position: relative;
  overflow: hidden;
  min-height: 100vh;
}
.story.is-enhanced .story__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  min-height: 0;
}

/* footage: poster first, canvas fades over it once frames arrive */
.story__poster,
.story__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 62% 42%;
}
.story__canvas {
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.story__canvas.is-live {
  opacity: 1;
}

/* The left side is bought back from the footage the way the hero buys
   its copy zone from the drift wall: a deep fade toward the words, and
   a floor vignette to hold the bottom edge. */
.story__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The copy column runs to 57% of the stage, but this used to thin to
     0.25 by 52% — so the ends of the longest lines sat on bare film and
     measured 2.1:1 on the role and 2.5:1 on the description. It now holds
     through the words and falls away sharply after them, which keeps the
     figure at 70% in the clear. */
  background:
    linear-gradient(90deg, rgba(10, 10, 12, 0.94) 0%, rgba(10, 10, 12, 0.9) 36%, rgba(10, 10, 12, 0.76) 58%, rgba(10, 10, 12, 0.08) 72%, rgba(10, 10, 12, 0) 84%),
    linear-gradient(180deg, rgba(10, 10, 12, 0.5) 0%, rgba(10, 10, 12, 0) 20%, rgba(10, 10, 12, 0) 72%, rgba(10, 10, 12, 0.68) 100%);
}

/* The site's light, pooled between the scrim and the words: two aurora
   fields drifting on the homepage's own loops. They sit above the scrim
   so the color reads as atmosphere in the room, not tint on the film. */
.story__glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.story__glow i {
  position: absolute;
  width: 44rem;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(110px);
  opacity: 0.34;
}
.story__glow i:nth-child(1) {
  top: -18%;
  left: -14%;
  background: radial-gradient(circle, var(--aurora-violet), transparent 64%);
  animation: aurora-drift-a 28s ease-in-out infinite alternate;
}
.story__glow i:nth-child(2) {
  bottom: -24%;
  left: 6%;
  background: radial-gradient(circle, var(--aurora-cyan), transparent 64%);
  animation: aurora-drift-b 34s ease-in-out infinite alternate;
  opacity: 0.22;
}

/* --------------------------- static mode --------------------------- */
/* Every fallback lands here — no-JS, reduced motion, Save-Data, a frame
   manifest that never arrived — so it has to read as a layout somebody
   chose rather than one that failed.

   Without the pin the stage is as tall as nine roles in normal flow, and
   the film layer, being inset:0, took the poster with it: a two-thousand
   pixel astronaut sliding under the text, which is what a broken page
   looks like. The film is bounded to a hero band instead. The header
   stands in it, the roles begin on the ground below, and the foot of the
   film dissolves into that ground instead of stopping on a line. */
.story:not(.is-enhanced) .story__poster,
.story:not(.is-enhanced) .story__canvas,
.story:not(.is-enhanced) .story__scrim,
.story:not(.is-enhanced) .story__glow {
  height: var(--film-h);
  bottom: auto;
}
.story:not(.is-enhanced) .story__scrim::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  background: linear-gradient(180deg, rgba(10, 10, 12, 0) 0%, rgba(10, 10, 12, 0.96) 88%, #0a0a0c 100%);
}
/* The pin's top padding is already the topbar's clearance, so what is
   left of the band is the header's to stand in — which lands the deck
   on the film's bottom edge once .story__deck adds its own margin. */
.story:not(.is-enhanced) .story__head {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: calc(var(--film-h) - var(--topbar-h, 5rem) - var(--sp-6) * 2);
}
/* nine roles in a column want air between them; the pinned mode stacks
   them in one slot and never sees this */
.story:not(.is-enhanced) .jobs {
  display: grid;
  gap: var(--sp-5);
}

/* ------------------------------ layout ----------------------------- */
.story__pin {
  position: relative;
  z-index: 2;
  max-width: var(--container);
  margin: 0 auto;
  padding: calc(var(--topbar-h, 5rem) + var(--sp-6)) var(--sp-4) var(--sp-7);
}
/* The timeline's spine: one dotted line running the stage's height at the
   left edge, threaded through the active job's avatar. Drawn by the pin so
   it needs no markup, and only in the pinned mode — a static document has
   no single line for nine stacked roles to share. */
.story.is-enhanced .story__pin::before {
  content: "";
  position: absolute;
  left: calc(var(--sp-4) + 2.5rem - 1px);
  top: calc(var(--topbar-h, 5rem) + var(--sp-4));
  bottom: var(--sp-6);
  width: 2px;
  background-image: repeating-linear-gradient(180deg, var(--glass-border) 0 5px, transparent 5px 11px);
}
/* header and job copy share one left axis, clear of the line's gutter */
.story.is-enhanced .story__head {
  margin-left: calc(5rem + var(--sp-4));
}
.story.is-enhanced .story__pin {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-3);
  padding-top: calc(var(--topbar-h, 5rem) + var(--sp-4));
  padding-bottom: var(--sp-6);
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
}

/* the header: sweeps in once, then holds its ground */
.story__title {
  font-size: var(--step-4);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.04;
  max-width: 14ch;
  text-wrap: balance;
}
.story__lede {
  margin-top: var(--sp-3);
  font-size: var(--step-1);
  /* Not --ink-secondary: this sits on moving film, and a flat gray has
     nothing to hold on to when a bright frame passes under it. Tinted
     down from the foreground instead, the way .job__desc already is. */
  color: color-mix(in srgb, var(--ink) 90%, transparent);
  max-width: 36ch;
  /* `pretty` still left "began." alone on the last line at desktop width
     (436/480/103); `balance` evens all three (348/328/344), which is what
     a three-line lede under a display heading wants anyway */
  text-wrap: balance;
}
@media (prefers-reduced-motion: no-preference) {
  .story.is-enhanced .story__lede {
    animation: hero-rise 1.2s var(--ease) 1.4s both;
  }
  .story.is-enhanced .rail {
    animation: hero-rise 1.2s var(--ease) 1.8s both;
  }
  .story.is-enhanced .jobs {
    animation: hero-rise 1.2s var(--ease) 2.1s both;
  }
}

/* ------------------------------ deck ------------------------------- */
.story__deck {
  margin-top: var(--sp-6);
}
.story.is-enhanced .story__deck {
  margin-top: 0;
  max-width: 49rem;
}

/* ------------------------------ rail ------------------------------- */
/* the grok-style scroller, pinned to the stage's right edge */
/* Centered by stretching the full height and centering its own content, not
   by translateY(-50%): the rail's entrance animation ends on
   `transform: none`, which would wipe a centering transform and leave the
   rail sitting half its own height too low once the animation settled. */
.rail {
  position: absolute;
  z-index: 3;
  right: 0.9rem;
  top: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  pointer-events: none;
}
.rail > * {
  pointer-events: auto;
}
.rail__ticks {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.rail__tick {
  position: relative;
  width: 1.8rem;
  height: 0.9rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
/* the visible line is a pseudo-element so the hit target stays thumbable */
.rail__tick::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 0;
  width: 0.9rem;
  height: 2px;
  border-radius: 1px;
  background: color-mix(in srgb, var(--ink) 38%, transparent);
  transition: width 0.3s var(--ease), background-color 0.3s var(--ease);
}
.rail__tick:hover::before,
.rail__tick:focus-visible::before {
  background: var(--ink);
}
.rail__tick[aria-current="true"]::before {
  width: 1.7rem;
  background: var(--ink);
}
.rail__tick:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 2px;
  border-radius: 4px;
}
/* the years, on hover or focus */
.rail__tick::after {
  content: attr(data-tip);
  position: absolute;
  top: 50%;
  right: calc(100% + 0.6rem);
  transform: translateY(-50%);
  padding: 0.3em 0.75em;
  border-radius: var(--r-pill);
  background: var(--glass-strong);
  border: 1px solid var(--glass-border);
  color: var(--ink);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s var(--ease);
  pointer-events: none;
}
.rail__tick:hover::after,
.rail__tick:focus-visible::after {
  opacity: 1;
  visibility: visible;
}
.rail__arrow {
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--glass-border);
  background: var(--glass-strong);
  color: var(--ink);
  cursor: pointer;
  transition: background-color var(--t-micro) var(--ease), transform var(--t-micro) var(--ease);
}
.rail__arrow:hover {
  transform: translateY(-1px);
  background: var(--bg-soft);
}
.rail__arrow:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 2px;
}
.rail__arrow svg {
  width: 1rem;
  height: 1rem;
}

/* ------------------------------ jobs ------------------------------- */
/* No boxes. The words stand directly in the light, the way the heroes'
   do — the scrim and the blooms are the container. */
.jobs {
  position: relative;
  min-width: 0;
}
.job {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
  padding: var(--sp-3) 0;
}
/* The active job's mark, seated on the dotted line. Initials until real
   logos exist — an image element dropped into the slot fills the circle. */
.job__avatar {
  width: 5rem;
  height: 5rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--aurora-violet) 45%, var(--glass-border));
  background:
    radial-gradient(circle at 32% 26%, color-mix(in srgb, var(--aurora-violet) 38%, transparent), transparent 72%),
    var(--bg-soft);
  box-shadow:
    0 0 0 5px color-mix(in srgb, var(--aurora-violet) 18%, transparent),
    0 0 34px color-mix(in srgb, var(--aurora-violet) 40%, transparent);
  font-size: var(--step-0);
  font-weight: 800;
  letter-spacing: 0.01em;
}
.job__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.job__body {
  min-width: 0;
}
/* Pinned mode keeps the base grid (avatar gutter + copy) and only adds
   the stacking machinery: every job occupies the same slot, vertically
   centered by align-content, and the crossfade decides which one shows. */
.story.is-enhanced .job {
  position: absolute;
  inset: 0;
  padding: 0;
  align-content: center;
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
  pointer-events: none;
}
.story.is-enhanced .job.is-active {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* a role the scroll has moved past lifts up and away; one still to come
   waits below — so the direction of travel is legible in the motion */
.story.is-enhanced .job.is-past {
  transform: translateY(-22px);
}
.job__role {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.12;
  text-wrap: balance;
}
.job__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4em 1em;
  margin-top: var(--sp-3);
}
.job__company {
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.job__tenure {
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink-secondary);
  font-variant-numeric: tabular-nums;
}
.job__desc {
  margin-top: var(--sp-3);
  font-size: var(--step-0);
  line-height: 1.6;
  max-width: 52ch;
  color: color-mix(in srgb, var(--ink) 88%, transparent);
}

/* ------------------------------ phones ----------------------------- */
@media (max-width: 860px) {
  .story__scrim {
    /* Full-width words here, so the scrim has to carry every one of them.
       The old curve thinned to 0.2 at 42% — precisely where the header
       (40–51%) and the lede (55–65%) sit — and measured 1.48:1 and
       1.64:1 against bright frames. The film now breathes in the top
       third, which is empty, and the scrim deepens before the first
       word and never lets go. */
    background:
      linear-gradient(180deg,
        rgba(10, 10, 12, 0.86) 0%,
        rgba(10, 10, 12, 0.44) 16%,
        rgba(10, 10, 12, 0.44) 33%,
        rgba(10, 10, 12, 0.82) 45%,
        rgba(10, 10, 12, 0.88) 62%,
        rgba(10, 10, 12, 0.94) 78%,
        rgba(10, 10, 12, 0.97) 100%);
  }
  .story__glow i {
    width: 26rem;
    filter: blur(80px);
  }
  .story__glow i:nth-child(2) {
    display: none;
  }
  .story.is-enhanced .story__pin {
    justify-content: flex-end;
  }
  .story.is-enhanced .story__pin::before {
    left: calc(var(--sp-4) + 1.625rem - 1px);
    bottom: calc(var(--sp-5) + var(--safe-b));
  }
  .story.is-enhanced .story__head {
    margin-left: calc(3.25rem + var(--sp-3));
  }
  .story__title {
    font-size: var(--step-3);
  }
  .story__lede {
    font-size: var(--step-0);
  }
  .job {
    grid-template-columns: 3.25rem minmax(0, 1fr);
    gap: var(--sp-3);
  }
  .job__avatar {
    width: 3.25rem;
    height: 3.25rem;
    font-size: var(--step--1);
    box-shadow:
      0 0 0 4px color-mix(in srgb, var(--aurora-violet) 18%, transparent),
      0 0 24px color-mix(in srgb, var(--aurora-violet) 38%, transparent);
  }
  .job__role {
    font-size: var(--step-1);
  }
  .job__company {
    font-size: var(--step-0);
  }
  .job__desc {
    font-size: var(--step--1);
  }
  /* room for the rail's tooltip flyout, and thumbable ticks */
  .rail {
    right: 0.35rem;
  }
  /* the copy stops before the rail's tick zone instead of running the
     last few pixels underneath it */
  .story.is-enhanced .job {
    padding-right: 2.1rem;
  }
  .rail__tick {
    width: 1.6rem;
  }
  /* the jobs slot needs a floor: absolute children take their height
     from it, and flex-end stacking wants the deck bottom-anchored */
  .story.is-enhanced .jobs {
    min-height: 15.5rem;
  }
}
@media (min-width: 861px) {
  .story.is-enhanced .jobs {
    min-height: 20rem;
  }
}

/* ============================================================
   DESIGN STACK — one row, always traveling.

   The row holds its cards twice and slides by exactly half its
   own width, so the second copy lands frame-for-frame on the
   first and the loop has no seam — the same arithmetic the drift
   wall uses vertically.

   Deliberately short: one row, modest heading. This is a
   footnote to the timeline above it, not a second act.
   ============================================================ */
.stack {
  /* one variable feeds both the gap between cards and the track's trailing
     gap; they must stay equal or the -50% seam opens up */
  --stack-gap: var(--sp-3);
  /* Twenty-six cards to a track: the duration scales with the count so the
     travel speed stays the calm ~33px/s it was at eight. One variable so
     the reduced-motion opt-in below cannot drift out of step with it. */
  --stack-duration: 182s;
  padding: var(--sp-7) 0 var(--sp-8);
  overflow: hidden;
}
.stack__head {
  margin-bottom: var(--sp-5);
  text-align: center;
}
/* headline scale, not display scale — the page already has its title */
.stack__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.02em;
}
.stack__lede {
  margin-top: var(--sp-2);
  font-size: var(--step-0);
  color: var(--ink-secondary);
  max-width: 52ch;
  margin-inline: auto;
}

/* The fade lives on the still container, not on the row — a mask on a
   translating element travels with it, and the ends would swim.

   The padding is paint room for the cards' drop shadow, which clipping to
   the card box would otherwise cut off flat; the matching negative margin
   hands the space back so the section's height is unchanged. */
.stack__rows {
  overflow: hidden;
  padding-block: 2.5rem 5.5rem;
  margin-block: -2.5rem -5.5rem;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.stack__row {
  display: flex;
  width: max-content;
}
/* Two layouts of the same twenty-six cards: one long line for wide
   screens, a counter-traveling pair for phones. Both are in the markup
   because the cards are grouped differently in each and CSS cannot
   regroup children; display:none on the one not in use keeps it out of
   the accessibility tree too, so the stack is announced exactly once. */
.stack__rows--split {
  display: none;
}
/* The trailing gap belongs to the track, not between the tracks. A gap on
   the row would make the row 2·track + 1·gap while the repeat unit is
   track + gap, and -50% would land one gap short of the seam every pass. */
.stack__track {
  display: flex;
  gap: var(--stack-gap);
  margin: 0;
  padding: 0 var(--stack-gap) 0 0;
  list-style: none;
  flex: none;
}
.stack__row {
  animation: stack-slide var(--stack-duration) linear infinite;
}
/* let someone read a card they are pointing at */
.stack__rows:hover .stack__row {
  animation-play-state: paused;
}
/* The one animation on the site that keeps running under
   prefers-reduced-motion, by request: the stack is the section's whole
   idea, and stopped it is a wall of logos rather than a thing with a
   pulse. base.css collapses every animation on the page to 0.01ms with
   !important, so opting back in has to be exactly as loud, and only for
   the two properties that reset actually sets. Everything else on the
   page still stands still for these visitors.

   Worth knowing what this costs: hover-pause is the only way to stop the
   row, and a touch screen has no hover, so on a phone there is now no
   mechanism to pause it at all. */
@media (prefers-reduced-motion: reduce) {
  .stack__row {
    animation-duration: var(--stack-duration) !important;
    animation-iteration-count: infinite !important;
  }
}
/* -50% is exactly one copy of the track pair, hence the seamless wrap */
@keyframes stack-slide {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* the card: the site's glass and its gradient ring, at a size that lets
   a logo actually be a logo */
.stackcard {
  position: relative;
  flex: none;
  width: 13.5rem;
  height: 9.5rem;
  display: grid;
  /* One full-width column, and the mark is centered in the card.

     It used to be centered in a track sized to whichever child was wider,
     which is almost always the label — so the icon's horizontal position
     was a function of the width of the word underneath it, and only
     landed on the card's center because that track was itself centered.
     Two centrings that compose to the right answer as long as both
     measurements are taken against the same text metrics. Anything that
     makes them disagree — a webfont swapping in underneath a running
     animation is the obvious one — slides the mark off center, and it
     does so per card, which is why it read as random. Nothing about the
     mark's position depends on the label now. */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  justify-items: center;
  gap: var(--sp-3);
  border-radius: var(--r-card);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.stackcard::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(315deg, var(--edge-far), var(--edge-mid) 50%, var(--edge-near));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.stackcard:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
/* A definite, full-width box with the SVG filling it, so the art is placed
   by the SVG's own preserveAspectRatio rather than by CSS box alignment.

   This was a shrink-to-fit grid item wrapped around an <svg> with
   width:auto — which means the icon's horizontal position depended on a
   viewBox aspect ratio resolving into a CSS width, then on the mark
   shrink-wrapping that width, then on the mark being centered in the card.
   Chrome resolves that chain to markW === svgW === the art's own width, so
   centering the mark centers the art. WebKit does not size an auto-width
   <svg> from its viewBox the same way, so the chain lands somewhere else
   and every mark sits left of center on iOS by roughly the same amount —
   which is what kept coming back after each CSS-level fix, because each
   one was correcting a position the engine was computing differently
   further upstream.

   Nothing here is intrinsically sized now. The box is the card's width,
   the SVG is the box, and xMidYMid meet — the most interoperable geometry
   in SVG — puts the art in the middle of it. Rendered art size is
   unchanged: every mark in the set is height-limited at both breakpoints,
   which is what the old max-width was quietly relying on anyway. */
.stackcard__mark {
  display: block;
  width: 100%;
  height: 2.9rem;
}
.stackcard__mark svg {
  display: block;
  width: 100%;
  height: 100%;
  /* Vercel's mark is monochrome and takes the page's ink; the rest carry
     their own brand color and ignore this. */
  color: var(--ink);
}
.stackcard__name {
  /* full width for the same reason the mark is: a definite box centers its
     own text, where a shrink-to-fit one has to be measured first */
  width: 100%;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.01em;
  /* the long names — Adobe Creative Suite, Final Cut Pro — break to a
     second centered line instead of forcing a wider card */
  text-align: center;
  line-height: 1.3;
  padding-inline: 0.7rem;
}

@media (max-width: 860px) {
  .stack {
    --stack-gap: var(--sp-2);
    /* Thirteen cards to a line instead of twenty-six is exactly half the
       distance, so half the duration holds the same ~19px/s the single
       line traveled — the pair reads as one mechanism at one speed, not
       as two carousels that happen to be adjacent. */
    --stack-duration: 91s;
    padding: var(--sp-6) 0 var(--sp-7);
  }
  /* the phone trades the one long line for two shorter ones */
  .stack__rows--wide {
    display: none;
  }
  .stack__rows--split {
    display: block;
  }
  /* enough for the lower line to clear the upper one's drop shadow */
  .stack__rows--split .stack__row + .stack__row {
    margin-top: var(--sp-4);
  }
  /* sized so a phone sees most of three cards rather than a pair — one row
     needs the density to still read as a row */
  .stackcard {
    width: 7.75rem;
    height: 6.5rem;
    gap: var(--sp-2);
  }
  /* only the band's height changes; the box stays the card's width, and
     the art stays height-limited inside it exactly as it is at desktop */
  .stackcard__mark {
    height: 2rem;
  }
  .stackcard__name {
    font-size: 0.8125rem;
  }
  /* The card centered its whole content block, so a label that wrapped
     made that block taller and lifted its icon above the rest of the row
     — "Adobe Creative Suite" rode 8.4px high. The content is anchored
     from the top instead, at the offset that centers the single-line
     case: (6.5rem card − 2rem mark − 0.5rem gap − 1.3 lines) / 2. Every
     icon then sits at the same height whatever its label does, the
     twenty-four ordinary cards stay optically centered, and the one that
     wraps simply reaches further down into the space it already had. */
  .stackcard {
    align-content: start;
    padding-top: 1.47rem;
  }
}
/* The lower line runs the same keyframes backwards; nothing else about it
   differs, and the reduce-motion opt-in above carries it too because the
   duration it re-asserts is the same variable. */
@media (max-width: 860px) {
  .stack__row--rev {
    animation-direction: reverse;
  }
}

/* ============================================================
   PRODUCT DESIGN LIFECYCLE — three glass hexagons on one soft
   line of light, over a faint full-bleed dotted rail.

   Cadence, strictly left to right (top to bottom on a phone):
   the rail fades in, then each shape draws its ring and
   immediately fills with glass — ring at --s × slot, glass a
   breath behind it, words a breath behind that. Short aurora
   links breathe in between shapes, and the band gradient fades
   to transparent at its outer tips, so the line dissolves into
   the rail instead of terminating in a dot.

   Stacking inside the stage: rail (0), the phone's under-band
   (1), glass (2), the drawn bands (3), words (4) — on a phone
   the pulse rides UNDER the glass, blurring through each
   station.

   Static-first: the resting stylesheet state is the finished
   band. lifecycle.js adds .lc-armed only when motion is
   allowed; .is-live plays it. No-JS and reduced-motion
   visitors never see a wound spring.
   ============================================================ */
.lifecycle {
  /* the shapes' head start, the per-shape slot, the pulse period,
     the resting weight of the drawn line, and the rail's whisper.
     The rail doesn't get a beat of its own: it fades in across the
     whole shape sequence, arriving with them rather than waiting
     alone on stage. */
  --lc-lead: 0.35s;
  --lc-slot: 0.75s;
  --lc-rail-fade: 2.6s;
  --lc-pulse-t: 8s;
  --lc-rest: 0.62;
  --lc-rail-o: 0.32;
  position: relative;
  isolation: isolate;
  /* blooms may bleed up into the story and down into the stack,
     never sideways into a scrollbar */
  overflow-x: clip;
}
[data-theme="dark"] .lifecycle {
  --lc-rest: 0.74;
  --lc-rail-o: 0.28;
}

/* ------------------------------ blooms ----------------------------- */
/* The site's light, pooled behind the band: one high on the right
   under the timeline's exit, one at the seam where this section hands
   over to the stack. Same material as the story's glow — radial aurora,
   heavy blur, slow drift. */
.lifecycle__bloom {
  position: absolute;
  z-index: -1;
  width: 42rem;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(110px);
  opacity: 0.26;
  pointer-events: none;
}
.lifecycle__bloom--a {
  top: -7rem;
  right: -4rem;
  background: radial-gradient(circle, var(--aurora-violet), transparent 64%);
  animation: aurora-drift-a 30s ease-in-out infinite alternate;
}
.lifecycle__bloom--b {
  bottom: -14rem;
  left: 8%;
  background: radial-gradient(circle, var(--aurora-cyan), transparent 64%);
  animation: aurora-drift-b 36s ease-in-out infinite alternate;
  opacity: 0.2;
}
[data-theme="dark"] .lifecycle .lifecycle__bloom--a { opacity: 0.34; }
[data-theme="dark"] .lifecycle .lifecycle__bloom--b { opacity: 0.26; }

.lifecycle__head {
  margin-bottom: var(--sp-6);
  text-align: center;
}
.lifecycle__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.02em;
}
.lifecycle__lede {
  margin-top: var(--sp-2);
  font-size: var(--step-0);
  color: var(--ink-secondary);
  max-width: 52ch;
  margin-inline: auto;
}

/* The stage: rail and bands paint the line, glass carries the depth,
   HTML stands in the cells. All four share the same viewBox-fraction
   geometry, so everything lands in the hexagons at every width
   without measurement. */
.lc-stage {
  position: relative;
  max-width: 74rem;
  margin-inline: auto;
  aspect-ratio: 1240 / 300;
}
.lc-band {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  z-index: 3;
}
.lc-band--under {
  display: none;
  z-index: 1;
}
.lc-band--tall {
  display: none;
}

/* ------------------------------ the rail --------------------------- */
/* The reference line, edge to edge of the viewport and faint — the
   page's own ink at a whisper, in the story spine's dash rhythm. The
   stage clips nothing (the section clips sideways), so the rail runs
   the whole screen and dissolves under the glass. */
.lc-rail {
  position: absolute;
  z-index: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100vw;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  background-image: repeating-linear-gradient(90deg, var(--ink) 0 5px, transparent 5px 11px);
  opacity: var(--lc-rail-o);
}

/* ------------------------------ the glass -------------------------- */
/* The hexagon as glass that reads as glass: a barely-there fill with a
   diagonal sheen, real backdrop blur, and a soft drop shadow that
   follows the clipped shape. The aurora ring above is its edge.
   bboxes: x 44–396 / 452–804 / 860–1212 of 1240, y 34–266 of 300. */
.lc-glass {
  position: absolute;
  z-index: 2;
  top: 11.33%;
  height: 77.34%;
  width: 28.39%;
  clip-path: polygon(0% 50%, 17.05% 0%, 82.95% 0%, 100% 50%, 82.95% 100%, 17.05% 100%);
  background:
    linear-gradient(160deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.03) 40%, rgba(255, 255, 255, 0) 70%),
    rgba(255, 255, 255, 0.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.35);
  backdrop-filter: blur(14px) saturate(1.35);
  filter: drop-shadow(0 10px 22px rgba(10, 10, 12, 0.14));
}
[data-theme="dark"] .lifecycle .lc-glass {
  background:
    linear-gradient(160deg, rgba(245, 245, 247, 0.09), rgba(245, 245, 247, 0.02) 40%, rgba(245, 245, 247, 0) 70%),
    rgba(21, 21, 24, 0.32);
  filter: drop-shadow(0 12px 26px rgba(0, 0, 0, 0.4));
}
.lc-glass--1 { left: 3.55%; }
.lc-glass--2 { left: 36.45%; }
.lc-glass--3 { left: 69.35%; }

/* ------------------------------ the line --------------------------- */
/* Rings draw; links only breathe in — the soft tissue between shapes.
   The gradient's alpha-faded tips finish the dissolve at both ends.

   Weights are stated in user units and scale with the diagram. They
   used to be pinned with vector-effect: non-scaling-stroke, which
   quietly broke the draw: Chromium dashes a non-scaling stroke along
   the path measured in device space while still scaling
   stroke-dasharray by pathLength in user space, so wherever those two
   disagree — a 2× screen, a phone at 3× — a dasharray of 1 no longer
   covers the whole path. Measured on a ring spanning x 44–396: wound
   at offset 1 it still painted 220–396, and finished at offset 0 it
   painted only 44–219. The tail of every hexagon stood there before
   its turn, swept away as the ring drew, and the shape was left half
   built. Rings are not allowed a constant stroke for that reason;
   letting the line scale costs a fraction of a pixel at the narrow
   end and puts it on the same footing as the dots, which have always
   scaled. --lc-w is calibrated to the size each band is actually
   read at, not to its extreme: the wide band spends nearly all its
   life at the 1184px cap it reaches from a 1232px viewport up, where
   2.09 units across 1240 land on 2.00px; the tall band is calibrated
   to a phone of 390–430px rather than to its 480px cap, which only a
   tablet ever reaches, so 2.2 across 420 lands on 1.8–2.0px in hand
   and 2.5px on the tablet. Ends of the range: 1.4px on the narrowest
   desktop, 1.7px on the narrowest phone. */
.lc-band { --lc-w: 2.09px; }
.lc-band--tall,
.lc-band--under { --lc-w: 2.2px; }
.lc-ring,
.lc-link {
  fill: none;
  stroke: url(#lcg-w);
  stroke-width: var(--lc-w);
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: var(--lc-rest);
}
.lc-band--tall .lc-ring,
.lc-band--tall .lc-link { stroke: url(#lcg-t); }
.lc-dot {
  fill: url(#lcg-w);
  transform-box: fill-box;
  transform-origin: center;
}
.lc-band--tall .lc-dot { fill: url(#lcg-t); }

/* The pulse: the line lighting up to full strength as the beat passes,
   a wide soft copy underneath standing in for bloom — no filters, so a
   phone composites nothing extra. Invisible until .is-live loops it. */
.lc-pulse {
  fill: none;
  stroke: url(#lcg-w);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 0.045 0.955;
  stroke-dashoffset: 1;
  opacity: 0;
}
.lc-band--under .lc-pulse { stroke: url(#lcg-u); }
/* same trap as the rings, and it was caught here too: a non-scaling
   stroke would repeat this dash pattern once per device-space span of
   the path, so a 2× screen ran two beats round the loop at once and
   neither traveled the whole way. Held to the band's own weight. */
.lc-pulse--glow {
  stroke-width: calc(var(--lc-w) * 4.5);
  --lc-po: 0.3;
}
.lc-pulse--core {
  stroke-width: calc(var(--lc-w) * 1.25);
  --lc-po: 1;
}

/* ------------------------------ the cells -------------------------- */
.lc-step {
  position: absolute;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--sp-2);
  /* hexagon cores: x 104–336 / 512–744 / 920–1152 of 1240, y 34–266
     of 300 */
  top: 11.33%;
  height: 77.34%;
  width: 18.71%;
}
.lc-step:nth-of-type(1) { left: 8.39%; }
.lc-step:nth-of-type(2) { left: 41.29%; }
.lc-step:nth-of-type(3) { left: 74.19%; }
.lc-step__icon {
  display: block;
  width: 2rem;
  height: 2rem;
}
.lc-step__icon svg {
  display: block;
  width: 100%;
  height: 100%;
}
/* the step name owns the cell — display presence, not label presence */
.lc-step__title {
  font-size: clamp(1.35rem, 1.02rem + 1.1vw, 1.8rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.05;
}
.lc-step__copy {
  font-size: clamp(0.8rem, 0.7rem + 0.3vw, 0.9rem);
  font-weight: 600;
  line-height: 1.5;
  /* on glass, secondary copy tints down from the foreground rather
     than falling to the flat gray */
  color: color-mix(in srgb, var(--ink) 84%, transparent);
  max-width: 24ch;
  text-wrap: balance;
}

/* ---------------------------- choreography ------------------------- */
/* Armed: wind everything to its start. Only lifecycle.js sets this,
   and only when prefers-reduced-motion allows, so the static page
   never depends on it. */
.lifecycle.lc-armed .lc-rail {
  opacity: 0;
}
/* 1.04, not 1: a dash of exactly one path length leaves a sub-pixel
   sliver at the far end once the rasteriser rounds, and a round
   linecap turns a sliver into a whole dot — three of them sitting on
   the rail before their shapes existed, measured at 3×. The 4% of
   slack costs nothing (lc-draw runs to 0, so the ring still closes)
   and buys the ends a margin no rounding can reach. */
.lifecycle.lc-armed .lc-ring {
  stroke-dasharray: 1.04;
  stroke-dashoffset: 1.04;
  opacity: 1;
}
.lifecycle.lc-armed .lc-link,
.lifecycle.lc-armed .lc-dot {
  opacity: 0;
}
.lifecycle.lc-armed .lc-glass,
.lifecycle.lc-armed .lc-step {
  opacity: 0;
  transform: translateY(12px);
}

/* Live: play. The rail breathes in across the whole sequence while
   shape by shape — ring draws, glass filling with its first stroke,
   words right behind — each beat lands at --s × slot after the lead.
   Links and dots come up softly with their shape; everything dims to
   its resting weight once the last shape lands. */
.lifecycle.lc-armed.is-live .lc-rail {
  transition: opacity var(--lc-rail-fade) var(--ease);
  opacity: var(--lc-rail-o);
}
.lifecycle.lc-armed.is-live .lc-ring {
  animation:
    lc-draw 0.55s var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot)) both,
    lc-settle 0.9s var(--ease) calc(var(--lc-lead) + 3 * var(--lc-slot) + 0.3s) both;
}
.lifecycle.lc-armed.is-live .lc-link {
  transition: opacity 0.7s var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.5s);
  opacity: var(--lc-rest);
}
.lifecycle.lc-armed.is-live .lc-link--in {
  transition-delay: var(--lc-lead);
}
.lifecycle.lc-armed.is-live .lc-dot--in {
  animation: lc-dot-pop 0.45s var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot)) both;
}
.lifecycle.lc-armed.is-live .lc-dot--out {
  animation: lc-dot-pop 0.45s var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.45s) both;
}
/* the fill rises with the ring's first stroke — a ring alone on stage
   reads as a stray mark, not a shape */
.lifecycle.lc-armed.is-live .lc-glass {
  transition:
    opacity var(--t-reveal) var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.05s),
    transform var(--t-reveal) var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.05s);
  opacity: 1;
  transform: none;
}
.lifecycle.lc-armed.is-live .lc-step {
  transition:
    opacity var(--t-reveal) var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.2s),
    transform var(--t-reveal) var(--ease) calc(var(--lc-lead) + var(--s) * var(--lc-slot) + 0.2s);
  opacity: 1;
  transform: none;
}
.lifecycle.lc-armed.is-live .lc-pulse {
  animation: lc-pulse var(--lc-pulse-t) linear calc(var(--lc-lead) + 3 * var(--lc-slot) + 0.9s) infinite;
}
/* park the loop while the stage is off screen — no reason to spend
   frames on a band nobody can see */
.lifecycle.lc-parked .lc-pulse {
  animation-play-state: paused;
}

@keyframes lc-draw {
  from { stroke-dashoffset: 1.04; }
  to { stroke-dashoffset: 0; }
}
@keyframes lc-settle {
  from { opacity: 1; }
  to { opacity: var(--lc-rest); }
}
@keyframes lc-dot-pop {
  0% { opacity: 0; transform: scale(0); }
  60% { opacity: 1; transform: scale(1.35); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes lc-pulse {
  0% { stroke-dashoffset: 1; opacity: 0; }
  4% { opacity: var(--lc-po); }
  92% { opacity: var(--lc-po); }
  100% { stroke-dashoffset: 0; opacity: 0; }
}

/* narrow desktop: the cells shrink faster than the display-size type
   does — the tighter gap, smaller icon, and capped title keep three
   lines of copy inside the hexagon */
@media (min-width: 861px) and (max-width: 1100px) {
  .lc-step {
    gap: var(--sp-1);
  }
  .lc-step__icon {
    width: 1.75rem;
    height: 1.75rem;
  }
  .lc-step__title {
    font-size: 1.45rem;
  }
}

/* ------------------------------ phones ----------------------------- */
@media (max-width: 860px) {
  .lifecycle {
    --lc-lead: 0.25s;
    --lc-slot: 0.8s;
    --lc-rail-fade: 2.8s;
    --lc-pulse-t: 9s;
    padding: var(--sp-6) 0 var(--sp-7);
  }
  .lifecycle__bloom {
    width: 26rem;
    filter: blur(70px);
  }
  .lifecycle__bloom--a { top: -4rem; right: -6rem; }
  .lifecycle__bloom--b { bottom: -8rem; left: 4%; }
  .lc-stage {
    aspect-ratio: 420 / 872;
    max-width: 30rem;
  }
  .lc-band--wide { display: none; }
  .lc-band--under,
  .lc-band--tall { display: block; }
  /* the rail turns vertical, straight down the middle of the stage */
  .lc-rail {
    left: 50%;
    right: auto;
    transform: none;
    top: 1.15%;
    bottom: 1.15%;
    height: auto;
    width: 2px;
    margin-top: 0;
    margin-left: -1px;
    background-image: repeating-linear-gradient(180deg, var(--ink) 0 5px, transparent 5px 11px);
  }
  /* glass bboxes: x 38–382 of 420, cells y 36–260 / 324–548 / 612–836
     of 872 */
  .lc-glass {
    width: 81.9%;
    height: 25.69%;
    clip-path: polygon(0% 50%, 16.86% 0%, 83.14% 0%, 100% 50%, 83.14% 100%, 16.86% 100%);
  }
  .lc-glass--1 { left: 9.05%; top: 4.13%; }
  .lc-glass--2 { left: 9.05%; top: 37.16%; }
  .lc-glass--3 { left: 9.05%; top: 70.18%; }
  .lc-step {
    width: 54.29%;
    height: 25.69%;
  }
  .lc-step:nth-of-type(1) { left: 22.86%; top: 4.13%; }
  .lc-step:nth-of-type(2) { left: 22.86%; top: 37.16%; }
  .lc-step:nth-of-type(3) { left: 22.86%; top: 70.18%; }
  .lc-step__title {
    font-size: 1.4rem;
  }
  .lc-step__copy {
    font-size: 0.84rem;
  }
}


/* ============================================================
   DESIGN-SYSTEM LOOP — one rail, two currents.

   The lifecycle band's sibling, built from the same materials
   (dotted rail, glass panes, drawn outlines, a traveling pulse)
   with two deliberate departures: the stations are rounded
   rectangles rather than hexagons, and color is reserved
   entirely for the two directions of travel. The outlines are a
   neutral hairline and the rail is the page's own ink, so the
   only colored things on the stage are the cool current going
   out and the warm one coming back.

   Stacking inside the stage: rail (0), the forward current (1)
   which rides UNDER the glass and blurs through each station,
   glass (2), the drawn outlines and the return arc (3), words
   (4).

   The resting stylesheet state is the finished diagram — every
   outline closed, glass up, words visible, both flow lines
   drawn. Entry choreography is layered on top only once main.js
   adds .is-visible, so no-JS and reduced-motion visitors get a
   complete diagram rather than a wound spring, and gated bodies
   injected after unlock get it too (main.js rescans .reveal on
   content:injected).
   ============================================================ */
.dsl {
  --dsl-lead: 0.3s;
  --dsl-slot: 0.55s;
  --dsl-pulse-t: 7s;
  /* the outlines and rail are the page's ink at a whisper — white on the
     dark ground, near-black on the light one, so "white lines" holds where
     it should without hard-coding a color that only works in one theme */
  --dsl-line: 0.3;
  --dsl-rail-o: 0.28;
  --dsl-flow-o: 0.5;
  position: relative;
  margin: 0;
  isolation: isolate;
}
[data-theme="dark"] .dsl {
  --dsl-line: 0.34;
  --dsl-rail-o: 0.26;
  --dsl-flow-o: 0.62;
}
.dsl__bloom {
  position: absolute;
  z-index: -1;
  width: 34rem;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(100px);
  opacity: 0.2;
  pointer-events: none;
}
/* the blooms sit under the two ends the currents run between, so the stage
   is lit by the same two colors the flows are drawn in */
.dsl__bloom--a {
  top: -8rem;
  left: -6rem;
  background: radial-gradient(circle, var(--aurora-blue), transparent 64%);
}
.dsl__bloom--b {
  bottom: -9rem;
  right: -5rem;
  background: radial-gradient(circle, var(--aurora-pink), transparent 64%);
}
[data-theme="dark"] .dsl .dsl__bloom { opacity: 0.28; }

.dsl__stage {
  position: relative;
  /* must track the wide band's viewBox exactly: any mismatch letterboxes the
     SVG inside the box and every percentage-positioned pane drifts off its
     drawn outline */
  aspect-ratio: 1240 / 306;
}
.dsl-band,
.dsl-under {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.dsl-band { z-index: 3; }
.dsl-under { z-index: 1; }
.dsl-band--tall,
.dsl-under--tall { display: none; }

/* ------------------------------ the rail --------------------------- */
/* The reference line, edge to edge of the viewport in the story spine's
   dash rhythm — main clips sideways, so running past the container costs
   no scrollbar. It sits at the stations' own center line (not the stage's,
   which is lower because the return arc lives below the cards), so the
   dotted line and the drawn blue one are the same line. Both ends fade out,
   and the forward gradient fades to nothing at its tips to meet them, so
   the drawn line dissolves into the dotted one instead of stopping against
   it. */
.dsl-rail {
  position: absolute;
  z-index: 0;
  left: 50%;
  transform: translateX(-50%);
  /* the stations' own center line: cardY 66 + cardH/2 75 = 141 of the 306
     viewBox. NOT 50% — the stage is taller than the cards because the
     return arc lives beneath them. */
  width: 100vw;
  top: 46.078%;
  height: 2px;
  margin-top: -1px;
  background-image: repeating-linear-gradient(90deg, var(--ink) 0 5px, transparent 5px 11px);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 14%, #000 86%, transparent);
  opacity: var(--dsl-rail-o);
}

/* ------------------------------ the glass -------------------------- */
/* Properly fogged, not merely tinted: the blur is heavy enough that the
   rail's dots visibly soften as they pass behind a pane, which is what
   sells these as glass rather than as translucent rectangles. */
.dsl-glass {
  position: absolute;
  z-index: 2;
  left: var(--wx);
  top: 21.569%;
  width: 20.968%;
  height: 49.020%;
  border-radius: 1.55rem;
  background:
    linear-gradient(160deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.12) 42%, rgba(255, 255, 255, 0.04) 72%),
    rgba(255, 255, 255, 0.52);
  -webkit-backdrop-filter: blur(26px) saturate(1.7);
  backdrop-filter: blur(26px) saturate(1.7);
  box-shadow: 0 10px 24px rgba(10, 10, 12, 0.12);
}
[data-theme="dark"] .dsl .dsl-glass {
  background:
    linear-gradient(160deg, rgba(245, 245, 247, 0.16), rgba(245, 245, 247, 0.05) 42%, rgba(245, 245, 247, 0.01) 72%),
    rgba(21, 21, 24, 0.58);
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
}

/* ------------------------------ the lines -------------------------- */
/* Widths are user units and scale with the diagram. They are NOT pinned
   with vector-effect: non-scaling-stroke — Chromium dashes a non-scaling
   stroke in device space while scaling stroke-dasharray by pathLength in
   user space, which leaves these outlines half-drawn on any screen denser
   than 1x. Same trap the lifecycle band was caught by. */
.dsl-ring,
.dsl-return,
.dsl-flow {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dsl-ring {
  stroke: var(--ink);
  stroke-width: 1.7px;
  opacity: var(--dsl-line);
  /* drawn at rest: the dash covers the whole outline with 4% of slack, so
     no rounding at any density can leave a cap dot stranded */
  stroke-dasharray: 1.04;
  stroke-dashoffset: 0;
}
/* the two flows, and the only color on the stage */
.dsl-flow {
  stroke: url(#dsl-fwd-uw);
  stroke-width: 2.2px;
  opacity: var(--dsl-flow-o);
}
.dsl-under--tall .dsl-flow { stroke: url(#dsl-fwd-ut); }
.dsl-return {
  stroke: url(#dsl-ret-w);
  stroke-width: 2.2px;
  opacity: var(--dsl-flow-o);
  stroke-dasharray: 1.04;
  stroke-dashoffset: 0;
}
.dsl-band--tall .dsl-return { stroke: url(#dsl-ret-t); }
.dsl-return-head {
  fill: var(--aurora-pink);
  opacity: var(--dsl-flow-o);
}
/* Small glass tags, the same material as the site's chips. Real text rather
   than SVG <text>: one size at every breakpoint instead of scaling with the
   viewBox, and selectable. Desktop centers the forward tag in the band above
   the stations and sits the return tag directly ON its own line, breaking it
   the way a label on a route map breaks the route. */
.dsl-tag {
  position: absolute;
  z-index: 5;
  margin: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.34em 0.85em;
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  white-space: nowrap;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink-secondary);
}
/* forward tag centered in the band above the stations */
.dsl-tag--fwd { top: 9.608%; }
/* return tag centered on the arc's own horizontal run */
.dsl-tag--ret { top: 92.157%; }
/* each arrow takes its line's color, so the tag belongs to the line it
   names instead of merely floating near it */
.dsl-tag__arrow {
  display: block;
  width: 0.95rem;
  height: 0.95rem;
  flex: none;
}
.dsl-tag--fwd .dsl-tag__arrow { color: var(--aurora-blue); }
.dsl-tag--ret .dsl-tag__arrow { color: var(--aurora-pink); }

/* the traveling beats: a short bright dash running each flow on a loop,
   which is what makes the two directions read as directions */
.dsl-current {
  fill: none;
  stroke-linecap: round;
  stroke-width: 3.4px;
  stroke-dasharray: 0.05 0.95;
  stroke-dashoffset: 1;
  opacity: 0;
}
.dsl-current--fwd { stroke: url(#dsl-fwd-uw); }
.dsl-under--tall .dsl-current--fwd { stroke: url(#dsl-fwd-ut); }
.dsl-current--ret { stroke: url(#dsl-ret-w); }
.dsl-band--tall .dsl-current--ret { stroke: url(#dsl-ret-t); }

/* ------------------------------ the cells -------------------------- */
.dsl-step {
  position: absolute;
  z-index: 4;
  left: var(--wx);
  top: 21.569%;
  width: 20.968%;
  height: 49.020%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 0.35rem;
  padding: 0 1.1rem;
}
/* the same size the lifecycle band's icons are, and in ink rather than
   aurora — color on this stage means direction of travel, so the icons
   stay out of it */
.dsl-step__icon {
  display: block;
  width: 2rem;
  height: 2rem;
  color: var(--ink);
  opacity: 0.72;
}
.dsl-step__icon svg {
  display: block;
  width: 100%;
  height: 100%;
}
.dsl-step__title {
  font-size: clamp(0.95rem, 0.72rem + 0.72vw, 1.25rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.dsl-step__line {
  font-size: clamp(0.68rem, 0.58rem + 0.32vw, 0.82rem);
  font-weight: 600;
  line-height: 1.4;
  /* on glass, tinted down from the foreground rather than dropped to a flat
     gray that would sit at the same weight on both themes */
  color: color-mix(in srgb, var(--ink) 78%, transparent);
  text-wrap: balance;
}
.dsl__caption {
  display: flex;
  gap: 0.6rem;
  margin-top: var(--sp-4);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.dsl__caption b {
  color: var(--ink);
  white-space: nowrap;
}

/* ---------------------------- choreography ------------------------- */
/* Station by station, left to right: the outline draws, the glass rises
   into it, the words follow. The flows draw once the last station lands,
   and only then do the two beats start their loop — the diagram explains
   itself in the order a reader would build it. */
.dsl.is-visible .dsl-ring {
  animation: dsl-draw 0.62s var(--ease) calc(var(--dsl-lead) + var(--i) * var(--dsl-slot)) both;
}
.dsl.is-visible .dsl-glass {
  animation: dsl-rise 0.6s var(--ease) calc(var(--dsl-lead) + var(--i) * var(--dsl-slot) + 0.08s) both;
}
.dsl.is-visible .dsl-step {
  animation: dsl-rise 0.6s var(--ease) calc(var(--dsl-lead) + var(--i) * var(--dsl-slot) + 0.2s) both;
}
.dsl.is-visible .dsl-flow,
.dsl.is-visible .dsl-return-head,
.dsl.is-visible .dsl-tag {
  animation: dsl-fade 0.7s var(--ease) calc(var(--dsl-lead) + 4 * var(--dsl-slot)) both;
}
.dsl.is-visible .dsl-return {
  animation:
    dsl-draw 0.9s var(--ease) calc(var(--dsl-lead) + 4 * var(--dsl-slot)) both,
    dsl-fade 0.7s var(--ease) calc(var(--dsl-lead) + 4 * var(--dsl-slot)) both;
}
/* One keyframe serves both beats: each runs its dash along its own path's
   direction, and the return path is authored last-station-to-first, so the
   two travel against each other on screen without a reversed animation. */
.dsl.is-visible .dsl-current {
  animation: dsl-beat var(--dsl-pulse-t) linear calc(var(--dsl-lead) + 4 * var(--dsl-slot) + 0.7s) infinite;
}

@keyframes dsl-draw {
  from { stroke-dashoffset: 1.04; }
  to { stroke-dashoffset: 0; }
}
@keyframes dsl-rise {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
@keyframes dsl-fade {
  from { opacity: 0; }
}
@keyframes dsl-beat {
  0% { stroke-dashoffset: 1; opacity: 0; }
  6% { opacity: 1; }
  90% { opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* the resting stylesheet is already the finished diagram; the traveling
     beats are the one thing with no static equivalent, so they simply go */
  .dsl.is-visible .dsl-current {
    animation: none;
  }
}

/* ------------------------------ phones ----------------------------- */
@media (max-width: 860px) {
  /* the figure itself takes the stage's width so the two margin labels sit
     against the diagram's edges rather than the container's */
  .dsl {
    --dsl-slot: 0.5s;
    --dsl-pulse-t: 8s;
    max-width: 30rem;
    margin-inline: auto;
  }
  .dsl__stage {
    aspect-ratio: 420 / 960;
  }
  .dsl__bloom {
    width: 22rem;
    filter: blur(70px);
  }
  .dsl-band--wide,
  .dsl-under--wide { display: none; }
  .dsl-band--tall,
  .dsl-under--tall { display: block; }
  /* the rail stands up, on the stations' center line, fading at both ends */
  .dsl-rail {
    left: 50%;
    right: auto;
    top: 0;
    bottom: 0;
    width: 2px;
    height: auto;
    margin-top: 0;
    margin-left: -1px;
    transform: none;
    background-image: repeating-linear-gradient(180deg, var(--ink) 0 5px, transparent 5px 11px);
    -webkit-mask-image: linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
    mask-image: linear-gradient(180deg, transparent, #000 6%, #000 94%, transparent);
  }
  .dsl-glass,
  .dsl-step {
    left: 18.095%;
    top: var(--tt);
    width: 63.810%;
    height: 19.792%;
  }
  .dsl-glass { border-radius: 1.25rem; }
  .dsl-step {
    padding: 0 0.9rem;
  }
  /* the tags stand on end in opposite margins, both reading top to bottom,
     so the column stays balanced and neither label is upside down */
  .dsl-tag {
    writing-mode: vertical-rl;
    top: 50%;
    left: auto;
    transform: translateY(-50%);
    padding: 0.85em 0.34em;
    font-size: 0.7rem;
  }
  .dsl-tag--fwd { right: 0; }
  .dsl-tag--ret { left: 0; }
  /* an SVG is an atomic inline: a vertical writing mode leaves it upright
     rather than turning it with the text, so each arrow is turned by hand —
     one rotation serves both, pointing the forward one down its run and the
     return one back up its own */
  .dsl-tag__arrow { transform: rotate(90deg); }
  .dsl-step__icon { width: 1.8rem; height: 1.8rem; }
  .dsl-step__title { font-size: 1.05rem; }
  .dsl-step__line { font-size: 0.75rem; }
}


/* ============================================================
   THEME SPEC CARD — the design system's own numbers, in markup
   rather than as a screenshot of its overview page.

   Chips and swatches only: the source capture boxed each group,
   which here would put a card inside a card. The groups are
   separated by their labels and the grid instead.
   ============================================================ */
.spec {
  position: relative;
  margin: 0;
  padding: var(--sp-6) var(--sp-5);
  background: var(--bg-soft);
  border-radius: var(--r-card);
  overflow: hidden;
  isolation: isolate;
}
.spec__grain {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.3;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
.spec > *:not(.spec__grain) { position: relative; z-index: 2; }
.spec__head {
  max-width: 60ch;
}
.spec__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  text-wrap: balance;
}
.spec__lede {
  margin-top: var(--sp-3);
  color: var(--ink-secondary);
  font-size: var(--step--1);
  line-height: 1.6;
}
.spec__lede code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  padding: 0.1em 0.4em;
  border-radius: 0.35rem;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  color: var(--ink);
}
.spec__lede em { font-style: italic; }

.spec__group-title {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.spec__group-title--wide { margin-top: var(--sp-6); }

/* the seed row: a swatch is the one place a literal color belongs, since
   the color is the content */
.spec__seeds {
  list-style: none;
  margin: var(--sp-3) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--sp-3);
}
.spec__seed {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.6rem;
  align-items: center;
}
.spec__swatch {
  grid-row: 1 / 3;
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 0.6rem;
  background: var(--sw);
  box-shadow: inset 0 0 0 1px rgba(10, 10, 12, 0.14);
}
[data-theme="dark"] .spec .spec__swatch {
  box-shadow: inset 0 0 0 1px rgba(245, 245, 247, 0.18);
}
.spec__seed-name {
  align-self: end;
  font-size: 0.8125rem;
  font-weight: 700;
}
.spec__seed-hex {
  align-self: start;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
  color: var(--ink-secondary);
  font-variant-numeric: tabular-nums;
}

.spec__chips {
  list-style: none;
  margin: var(--sp-3) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.spec__chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.45em 0.9em;
  border-radius: var(--r-pill);
  background: var(--glass);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid var(--glass-border);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.spec__chip b {
  color: var(--ink);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
/* the three figures the overview leads with, read as chips rather than as
   the big-number tiles the capture used */
.spec__chips--lead {
  margin-top: var(--sp-5);
}
.spec__chip--lead {
  font-size: 0.875rem;
  padding: 0.5em 1.1em;
}
.spec__chip--lead b {
  font-size: 1.15rem;
  letter-spacing: -0.02em;
}

.spec__groups {
  margin-top: var(--sp-6);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-5);
}
.spec__caption {
  display: flex;
  gap: 0.6rem;
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--glass-border);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.spec__caption b {
  color: var(--ink);
  white-space: nowrap;
}

@media (max-width: 860px) {
  .spec {
    padding: var(--sp-5) var(--sp-4);
  }
  .spec__groups {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
  }
  .spec__seeds {
    grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
    gap: var(--sp-2);
  }
  .spec__swatch { width: 2rem; height: 2rem; }
  .spec__chip { font-size: 0.75rem; }
  .spec__chip--lead b { font-size: 1rem; }
}


/* ============================================================
   ONCE AN ARTIST — one photograph, full container width.

   The header lives inside the frame, bottom left, on its own
   scrim so the words never depend on what the photo happens to
   be doing behind them. --ink is re-scoped to white inside the
   caption: the photo's scrim is dark in both themes, and the
   .dia sweep paints with --ink, so the override carries the
   sweep and its settled color in one move.
   ============================================================ */
.artist {
  /* The testimonials below own an isolated stacking context, so their
     bloom — even at z-index -1 — paints with that context, which lands
     on top of everything earlier in the document. The bloom is meant to
     pass *under* this photograph, so the section takes a layer of its
     own above it. Measured: without this the bloom tinted 38.8% of the
     card. */
  position: relative;
  z-index: 1;
  padding: var(--sp-7) 0 var(--sp-6);
}
.artist__frame {
  position: relative;
  margin: 0;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
}
/* the site's gradient ring, over the photo's corners */
.artist__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(315deg, var(--edge-far), var(--edge-mid) 50%, var(--edge-near));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.artist__frame img {
  display: block;
  width: 100%;
  /* the width/height attributes are presentational hints; height must
     return to auto or the aspect-ratio below never gets a say */
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  /* both faces sit in the upper middle-right of the photograph */
  object-position: 60% 28%;
}
.artist__caption {
  --ink: #fff;
  position: absolute;
  inset: auto 0 0 0;
  padding: clamp(4.5rem, 9vw, 8rem) var(--sp-5) var(--sp-5);
  background: linear-gradient(180deg, transparent, rgba(10, 10, 12, 0.55) 52%, rgba(10, 10, 12, 0.86));
}
.artist__title {
  color: #fff;
  font-size: var(--step-3);
  font-weight: 800;
  letter-spacing: -0.02em;
  text-wrap: balance;
  max-width: 22ch;
}
@media (max-width: 860px) {
  .artist {
    padding: var(--sp-6) 0 var(--sp-5);
  }
  .artist__frame img {
    aspect-ratio: 4 / 3;
    object-position: 62% 28%;
  }
  .artist__caption {
    padding: clamp(3.5rem, 18vw, 5rem) var(--sp-4) var(--sp-4);
  }
  .artist__title {
    font-size: var(--step-2);
  }
}

/* ============================================================
   IN THEIR WORDS — the editorial spotlight.

   No card, no glass: the quotes stand on the background the way
   the hero's copy does. A giant quotation mark hangs in the left
   margin wearing the aurora on a slow drift; each arriving quote
   paints itself in with the .dia ink sweep the hero headers use.
   The deck is a grid with every quote in the same cell, so the
   tallest one reserves the height and the controls below never
   jump as quotes trade places. The timing chrome is the hero's
   own (see the hero controls block); only the clock lives here,
   on its own keyframe name so each rotator hears only its own
   animation end.
   ============================================================ */
.voices {
  position: relative;
  isolation: isolate;
  padding: var(--sp-7) 0 var(--sp-8);
}
/* The same soft field the curated list wears, breathing behind the block's
   right side. It deliberately overruns the section's own top edge so the
   light carries up under the photograph above rather than stopping at a
   horizontal line — so the section must NOT clip, and the sideways
   overrun is caught by overflow-x on <main> instead. */
.voices__bloom {
  position: absolute;
  z-index: -1;
  top: -34rem;
  right: -20rem;
  width: 64rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--aurora-cyan), transparent 66%);
  filter: blur(130px);
  opacity: 0.22;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .voices__bloom {
    animation: aurora-drift-b 38s ease-in-out infinite alternate;
  }
}
.voices__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-bottom: var(--sp-6);
}
/* the whole block centers as one column; everything inside it stays on
   its shared left edge */
.voices__stage {
  position: relative;
  max-width: 72rem;
  margin-inline: auto;
}
/* The mark is drawn, not typed: a proper double quotation mark as an SVG
   mask over the animated aurora, so the drift survives the shape swap.
   It hangs at low opacity behind the top-left corner of the quote — a
   watermark, not a column. (Glyph geometry: Bootstrap Icons "quote",
   MIT, rotated 180 into an opening mark.) */
.voices__glyph {
  position: absolute;
  z-index: 0;
  top: -3.1rem;
  left: -3.4rem;
  width: clamp(9rem, 12vw, 12rem);
  aspect-ratio: 11 / 9;
  opacity: 0.38;
  pointer-events: none;
  background-image: var(--aurora-gradient);
  background-size: 300% 100%;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='3 4 11 9'%3E%3Cg transform='rotate(180 8 8)'%3E%3Cpath d='M12 12a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1h-1.388c0-.351.021-.703.062-1.054.062-.372.166-.703.31-.992.145-.29.331-.517.559-.683.227-.186.516-.279.868-.279V3c-.579 0-1.085.124-1.52.372a3.322 3.322 0 0 0-1.085.992 4.92 4.92 0 0 0-.62 1.458A7.712 7.712 0 0 0 8 7.558V11a1 1 0 0 0 1 1h3Zm-6 0a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1H4.612c0-.351.021-.703.062-1.054.062-.372.166-.703.31-.992.145-.29.331-.517.559-.683.227-.186.516-.279.868-.279V3c-.579 0-1.085.124-1.52.372a3.322 3.322 0 0 0-1.085.992 4.92 4.92 0 0 0-.62 1.458A7.712 7.712 0 0 0 2 7.558V11a1 1 0 0 0 1 1h3Z'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='3 4 11 9'%3E%3Cg transform='rotate(180 8 8)'%3E%3Cpath d='M12 12a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1h-1.388c0-.351.021-.703.062-1.054.062-.372.166-.703.31-.992.145-.29.331-.517.559-.683.227-.186.516-.279.868-.279V3c-.579 0-1.085.124-1.52.372a3.322 3.322 0 0 0-1.085.992 4.92 4.92 0 0 0-.62 1.458A7.712 7.712 0 0 0 8 7.558V11a1 1 0 0 0 1 1h3Zm-6 0a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1H4.612c0-.351.021-.703.062-1.054.062-.372.166-.703.31-.992.145-.29.331-.517.559-.683.227-.186.516-.279.868-.279V3c-.579 0-1.085.124-1.52.372a3.322 3.322 0 0 0-1.085.992 4.92 4.92 0 0 0-.62 1.458A7.712 7.712 0 0 0 2 7.558V11a1 1 0 0 0 1 1h3Z'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain;
  animation: voices-aurora 16s ease-in-out infinite alternate;
}
@keyframes voices-aurora {
  from { background-position: 0% 0; }
  to { background-position: 100% 0; }
}
.voices__deck {
  position: relative;
  display: grid;
}
.voice {
  position: relative;
  z-index: 1;
  grid-area: 1 / 1;
  margin: 0;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
  pointer-events: none;
}
.voice.is-active {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.voice__quote {
  margin: 0;
  max-width: 58rem;
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.55;
  color: var(--ink);
  text-wrap: pretty;
}
.voice__quote p {
  margin: 0;
}
/* the case studies' own rating mark, same glyph, same aurora orange */
.voice__stars {
  display: flex;
  gap: 0.15em;
  color: var(--aurora-orange);
  margin-top: var(--sp-4);
}
.voice__stars svg {
  width: 1.15rem;
  height: 1.15rem;
}
.voice__by {
  margin-top: var(--sp-3);
  display: grid;
  gap: 0.25rem;
  justify-items: start;
}
.voice__by b {
  font-size: var(--step-0);
  font-weight: 700;
}
.voice__by span {
  font-size: var(--step--1);
  color: var(--ink-secondary);
  /* the long titles (committee chairs earn them) wrap inside the measure */
  max-width: 52rem;
}
.voices__controls {
  /* one source of truth for the rotation, exactly as the hero does it:
     the dot fill animates for this long and voices.js advances when it
     ends. Longer than the hero's 9s — these are paragraphs, not slides. */
  --voices-interval: 14000ms;
  margin-top: var(--sp-6);
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-3);
}
.voices__dot[aria-current="true"] .voices__dot-fill {
  animation: voices-progress var(--voices-interval) linear forwards;
}
.voices__controls[data-running="false"] .voices__dot-fill {
  animation-play-state: paused;
}
@keyframes voices-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .voices__glyph {
    animation: none;
  }
}
@media (max-width: 860px) {
  .voices {
    padding: var(--sp-6) 0 var(--sp-7);
  }
  .voices__glyph {
    top: -1.6rem;
    left: -1.1rem;
    width: 7rem;
  }
  /* Arrows stay on a phone — they are the obvious control on a touch
     screen. The full row (6 dots + 2 arrows + pause) measured 344px
     against 327px of usable width, so the dot pill gives back what the
     arrows need: tighter padding, a tighter gap, a shorter active
     track. Centered in the window rather than hung on the text edge. */
  .voices__controls {
    justify-content: center;
    gap: var(--sp-2);
  }
  .voices__dots {
    gap: 0.35rem;
    padding: 0 0.7rem;
  }
  .voices__dot[aria-current="true"] {
    width: 2.2rem;
  }
  /* The dots stay 9px here, deliberately. Six 24px targets plus two
     arrows plus pause cannot fit 327px of phone, and every way of
     forcing it either inflates the dot pill until the arrows are
     squeezed to 21px or grows the timer fill along with the hit box.
     The arrows and pause are the 38px targets on a phone; the dots are
     the supplementary jump-to affordance beside them. */
}

/* ------------------------------------------------------------
   Phone compositing budget, part two.

   These live at the end of the sheet on purpose: the sections they
   trim are declared further down than the main phone block, so the
   same rules stated up there lost the cascade to their own defaults
   and measured as no-ops.

   A blurred field costs roughly the square of its radius. Halving
   both the radius and the box keeps the same impression of light on
   a screen this size for about a quarter of the work.
   ------------------------------------------------------------ */
@media (max-width: 860px) {
  .voices__bloom,
  .curated__bloom {
    filter: blur(60px);
    width: 34rem;
  }
  .story__glow i {
    filter: blur(55px);
  }
  /* the artist photograph's ring and shadow are cheap, but its own
     stacking layer plus the bloom underneath is not — the shadow goes,
     the ring stays */
  .artist__frame {
    box-shadow: none;
  }
}

/* ============================================================
   THE PRESENTATION CARD
   Sits in the about band on a landing page, between the
   statement and the case studies. Not a poster of the deck —
   the actual opening slide, built from the same parts at a
   twelfth the size and still moving, so the click that opens
   the deck is the card growing rather than a new screen.

   Desktop only, by intent: the deck is a widescreen stage with
   a camera in it, and there is no honest version of that on a
   phone. The band reads fine without it.
   ============================================================ */
.pcard {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: stretch;
  gap: clamp(1.5rem, 2.4vw, 2.75rem);
  width: 100%;
  margin-top: var(--sp-7);
  padding: clamp(1.25rem, 1.8vw, 1.75rem);
  border: 0;
  border-radius: var(--r-card);
  background: color-mix(in srgb, var(--bg-soft) 70%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: var(--shadow-card);
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.pcard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(315deg, var(--edge-far), var(--edge-mid) 50%, var(--edge-near));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.pcard:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.pcard[hidden] { display: none; }

/* the miniature: black surround, capped frame, the employer's own two
   colors behind it — the deck's whole geometry at card scale */
.pcard__stage {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: var(--r-thumb);
  overflow: hidden;
  background: #000000;
  isolation: isolate;
  /* the frame is the reference for everything drawn inside it, so the
     slide's type scales with the card rather than with the page */
  container-type: inline-size;
}
/* The employer's primary color is the one they own; the accent is the
   second note. Two fields of equal size read as neither, so the primary
   gets the larger, more opaque field across the copy side and the accent
   is a smaller wash tucked behind the wall, keeping the frame from going
   monochrome at the corner.

   The tints, not the raw hexes. A brand whose primary is a near-black
   plum and whose accent is a bright orange makes no primary bloom at all
   from the raw values — the orange wins on luminance whatever area it is
   given, which is exactly how this read as "too much accent". The hero
   above already floors both colors' saturation and lightness into
   --tint1 and --tint3 for the same reason; the card borrows that work so
   the two fields agree instead of the card inventing its own. */
.pcard__glow {
  position: absolute;
  inset: -30%;
  border-radius: 50%;
  background: radial-gradient(ellipse 82% 86% at 30% 40%, var(--tint1, var(--accent, var(--aurora-violet))), transparent 70%),
    radial-gradient(ellipse 42% 40% at 84% 78%, color-mix(in srgb, var(--tint3, var(--accent2, var(--aurora-cyan))) 60%, transparent), transparent 66%);
  filter: blur(28px);
  opacity: 0.62;
  transition: opacity 0.6s var(--ease);
}
.pcard:hover .pcard__glow { opacity: 0.78; }

/* The copy sits in its own column and the wall is no longer beside it in
   a grid — it hangs off the frame instead, the way the hero's does, so
   the two are layered rather than partitioned. */
.pcard__mini {
  position: absolute;
  inset: 8%;
  display: grid;
  align-items: center;
}
.pcard__copy {
  position: relative;
  z-index: 1;
  max-width: 48%;
}
.pcard__mark {
  display: block;
  width: 34%;
  aspect-ratio: 1;
  margin-bottom: 8%;
  border-radius: 0.45rem;
  overflow: hidden;
  background: #ffffff;
}
.pcard__mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The position this page was built for, under the mark and aligned to it,
   at slide scale rather than caption scale — this is the line that says
   what the deck is for, and the miniature is a picture of a slide, not a
   row of UI. Sized in container units off the frame, so it holds the same
   proportion whether the card is 680px wide or the portal's 270px panel
   is showing the same slide. */
.pcard__role {
  display: block;
  margin-top: 10%;
  font-size: 5cqw;
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.02em;
  color: #ffffff;
  text-align: left;
  text-wrap: balance;
}
.pcard__role[hidden] { display: none; }
/* Off the right edge and clipped by the frame, exactly as the hero's wall
   runs off the right of the viewport — the panel-in-a-grid version read
   as a thumbnail pasted beside the copy rather than as scenery the slide
   is standing in front of. It fades to nothing toward the copy instead of
   vignetting, which is the hero's own mask. */
.pcard__wall {
  position: absolute;
  top: -16%;
  bottom: -16%;
  right: -18%;
  width: 72%;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 2%, #000 40%);
  mask-image: linear-gradient(90deg, transparent 2%, #000 40%);
}
/* three columns, drifting at their own rates — the hero's wall recomposed
   for this frame rather than scaled down from it */
.pcard__plane {
  position: absolute;
  inset: -10% 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 5%;
}
.pcard__col {
  display: grid;
  gap: 8%;
  align-content: start;
  transform: translateY(var(--offset, 0%));
  animation: pcard-drift var(--drift, 24s) linear infinite alternate;
}
.pcard__col img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 0.3rem;
}
@keyframes pcard-drift {
  from { transform: translateY(var(--offset, 0%)); }
  to { transform: translateY(calc(var(--offset, 0%) - 12%)); }
}
@media (prefers-reduced-motion: reduce) {
  .pcard__col { animation: none; }
}

.pcard__body {
  display: grid;
  align-content: center;
  gap: var(--sp-3);
  padding-right: clamp(0.5rem, 1vw, 1rem);
}
.pcard__title {
  font-size: var(--step-2);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* Greeting over name, each on its own line. Spans rather than a break
   because the title sits inside a button; `block` does the same job.
   Balance keeps a long employer name from leaving one orphaned word on
   a third line at the phone width. */
.pcard__hi,
.pcard__who {
  display: block;
  text-wrap: balance;
}
.pcard__who:empty { display: none; }
/* The hero lede's step rather than the label step the card's small type
   uses: this line is the whole pitch now that the card no longer lists
   what the deck contains, and at label size it read as a footnote to a
   title with nothing under it. */
.pcard__note {
  max-width: 34ch;
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--ink-secondary);
}
.pcard__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  justify-self: start;
  margin-top: var(--sp-2);
  padding: 0.8em 1.6em;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.pcard__cta svg {
  width: 1rem;
  height: 1rem;
}

/* The card is the deck's first frame, so opening it should look like the
   frame coming forward rather than a page swapping. It scales up and
   fades as the deck fades in behind it — the two crossing is what sells
   the card as having become the stage. */
.pcard.is-launching {
  animation: pcard-launch 0.55s var(--ease) forwards;
}
@keyframes pcard-launch {
  to {
    transform: scale(1.06);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pcard.is-launching { animation-duration: 0.01ms; }
  .pcard:hover { transform: none; }
}

/* No deck below desktop: a camera moving through a widescreen stage has
   no honest small-screen form, and a shrunken one would be worse than
   its absence. */
@media (max-width: 1023px) {
  .pcard { display: none !important; }
}

/* ------------------------------------------------------------------
   Figure stage (opportunity trees, journey, principles)

   One ground for the three drawn figures of the acquisition-channel
   record, so they read as one exhibit. The stage is the password gate's
   arrangement turned into a figure: aurora light drifting on the soft
   ground, and every node — a tree problem, a journey block, a principle
   card — a pane of fogged glass over it, edged with the site's gradient
   ring. The glass is doing a job here (the light behind it is the only
   color on the stage) rather than decorating.
   ------------------------------------------------------------------ */
.stage {
  position: relative;
  margin: 0;
  padding: var(--sp-6) var(--sp-5);
  background: var(--bg-soft);
  border-radius: var(--r-card);
  overflow: hidden;
  isolation: isolate;
}
.stage > * { position: relative; z-index: 2; }
.stage__aurora {
  position: absolute;
  inset: -30% -20%;
  z-index: 1;
  pointer-events: none;
}
.stage__aurora::before,
.stage__aurora::after {
  content: "";
  position: absolute;
  width: 46%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.42;
}
.stage__aurora::before {
  background: radial-gradient(circle, var(--tint1, var(--aurora-violet)), transparent 65%);
  top: 10%;
  left: 12%;
  animation: aurora-drift-a 26s ease-in-out infinite alternate;
}
.stage__aurora::after {
  background: radial-gradient(circle, var(--tint3, var(--aurora-pink)), transparent 65%);
  bottom: 8%;
  right: 12%;
  animation: aurora-drift-b 30s ease-in-out infinite alternate;
}
.stage__caption {
  display: flex;
  gap: 0.6rem;
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--glass-border);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.stage__caption b {
  color: var(--ink);
  white-space: nowrap;
}

/* the material every node shares: fogged glass, the gradient ring
   (painted by the shared ::after rule above), and the card lift on hover */
.glassnode {
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border-radius: var(--r-thumb);
  box-shadow: var(--shadow-card);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.glassnode:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-card-hover);
}
.glassnode--root {
  font-size: var(--step-1);
  font-weight: 800;
  letter-spacing: -0.02em;
}
.glassnode--quiet {
  background: var(--glass);
  box-shadow: none;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-secondary);
}
.glassnode--quiet:hover { transform: none; }

@media (max-width: 860px) {
  .stage { padding: var(--sp-5) var(--sp-4); }
}
@media (prefers-reduced-motion: reduce) {
  .stage__aurora::before,
  .stage__aurora::after { animation: none; }
  .glassnode,
  .glassnode:hover { transform: none; }
}

/* ------------------------------------------------------------------
   Opportunity tree (scripts/opportunity-tree.mjs)

   The connectors are the layout. Crown: outcome over measure with a stem
   between. A bar runs across the branch row from the first column's
   center to the last, with a stub down to each problem; under each
   problem a spine drops on the left with a tick out to every leaf. On a
   phone the bar becomes one trunk down the left margin and the stubs
   turn to meet it — the same tree, stood on end.
   ------------------------------------------------------------------ */
.optree {
  --line: var(--edge-far);
  --line-w: 2px;
  --gap: 2.25rem;
  /* The gap between branch columns and the drop from a problem to its
     first leaf. Both are spanned by a drawn connector — the bar across
     the top of the branches, the spine down a column — so the drawing
     reads them rather than repeating them. */
  --branch-gap: var(--sp-5);
  --leaf-lead: 0.9rem;
  /* branch count; the module sets it inline, three by default */
  --cols: 3;
}
.optree__crown {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding-bottom: var(--gap);
  text-align: center;
}
.optree__outcome {
  padding: var(--sp-3) var(--sp-5);
}
.optree__measure {
  position: relative;
  margin-top: var(--gap);
  padding: 0.6rem var(--sp-3);
}
/* stem: outcome → measure */
.optree__measure::before,
/* stem: measure → bar */
.optree__crown::after {
  content: "";
  position: absolute;
  left: 50%;
  width: var(--line-w);
  height: var(--gap);
  margin-left: calc(var(--line-w) / -2);
  background: var(--line);
  border-radius: 1px;
}
.optree__measure::before { top: calc(-1 * var(--gap)); }
.optree__crown::after { bottom: 0; }

.optree__branches {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: var(--branch-gap);
  padding: var(--gap) 0 0;
  margin: 0;
  list-style: none;
}
/* the bar: from the first column's center to the last. The columns are
   (100% - two gaps) / 3 wide, so a center is a sixth of that, not of the
   row — measured from the row it lands a third of a gap short and the
   outer stubs miss it. */
.optree__branches::before {
  content: "";
  position: absolute;
  top: 0;
  left: calc((100% - (var(--cols) - 1) * var(--branch-gap)) / (2 * var(--cols)));
  right: calc((100% - (var(--cols) - 1) * var(--branch-gap)) / (2 * var(--cols)));
  height: var(--line-w);
  background: var(--line);
  border-radius: 1px;
}
.optree__branch {
  position: relative;
}
/* stub: bar → problem */
.optree__branch::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--gap));
  left: 50%;
  width: var(--line-w);
  height: var(--gap);
  margin-left: calc(var(--line-w) / -2);
  background: var(--line);
  border-radius: 1px;
}
.optree__problem {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.45;
  text-wrap: pretty;
}
/* A problem stated as a head over its support: the head keeps the node's
   weight and the support steps down to body weight, tinted from the ink
   rather than set in the secondary gray, because the node sits on the
   aurora. A tree whose problems are single sentences has no head and
   prints as before. */
.optree__problem:has(.optree__problem-head) {
  font-weight: 500;
  color: color-mix(in srgb, var(--ink) 80%, transparent);
}
.optree__problem-head {
  display: block;
  margin-bottom: 0.3rem;
  font-weight: 700;
  color: var(--ink);
  text-wrap: balance;
}
.optree__leaves {
  position: relative;
  display: grid;
  gap: var(--leaf-gap, 0.45rem);
  margin: var(--leaf-lead) 0 0;
  padding: 0 0 0 1.5rem;
  list-style: none;
}
/* spine: down from the problem, past every leaf */
.optree__leaves::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--leaf-lead));
  bottom: 0.95rem;
  left: 0.55rem;
  width: var(--line-w);
  background: var(--line);
  border-radius: 1px;
}
.optree__leaf {
  position: relative;
  justify-self: start;
  padding: 0.35rem 0.75rem;
  border-radius: var(--r-pill);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
}
/* tick: spine → leaf */
.optree__leaf::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -0.95rem;
  width: 0.95rem;
  height: var(--line-w);
  margin-top: calc(var(--line-w) / -2);
  background: var(--line);
  border-radius: 1px;
}
/* what the product does */
.optree__leaf--build {
  background: var(--ink);
  color: var(--bg);
}
/* what the bank offers — the other half of the bet */
.optree__leaf--offer {
  background: color-mix(in srgb, var(--aurora-pink) 14%, var(--bg));
  color: var(--ink);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--aurora-pink) 45%, transparent);
}
.optree__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: var(--sp-5) 0 0;
  padding: 0;
  list-style: none;
}
.optree__legend .optree__leaf::before { display: none; }

/* four across is too tight below a wide desktop, and a two-by-two fold
   would leave the second row's stubs pointing at the first row's leaves,
   so a four-branch tree takes the phone's trunk-and-tick layout early */
@media (max-width: 1100px) {
  .optree[style*="--cols:4"] .optree__crown { align-items: flex-start; text-align: left; padding-left: 1.6rem; }
  .optree[style*="--cols:4"] .optree__measure::before,
  .optree[style*="--cols:4"] .optree__crown::after { left: 0.55rem; margin-left: 0; }
  .optree[style*="--cols:4"] .optree__crown::after { bottom: 0; }
  .optree[style*="--cols:4"] .optree__branches {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
    padding: 0 0 0 1.6rem;
  }
  .optree[style*="--cols:4"] .optree__branches::before {
    top: 0;
    bottom: 1.4rem;
    left: 0.55rem;
    right: auto;
    width: var(--line-w);
    height: auto;
  }
  .optree[style*="--cols:4"] .optree__branch::before {
    top: 1.4rem;
    left: -1.05rem;
    width: 1.05rem;
    height: var(--line-w);
    margin: 0;
  }
}
@media (max-width: 860px) {
  .optree__crown { align-items: flex-start; text-align: left; padding-left: 1.6rem; }
  .optree__measure::before,
  .optree__crown::after { left: 0.55rem; margin-left: 0; }
  .optree__crown::after { bottom: 0; }
  .optree__branches {
    grid-template-columns: 1fr;
    gap: var(--sp-4);
    padding: 0 0 0 1.6rem;
  }
  /* the bar becomes the trunk */
  .optree__branches::before {
    top: 0;
    bottom: 1.4rem;
    left: 0.55rem;
    right: auto;
    width: var(--line-w);
    height: auto;
  }
  /* the stub turns to meet it */
  .optree__branch::before {
    top: 1.4rem;
    left: -1.05rem;
    width: 1.05rem;
    height: var(--line-w);
    margin: 0;
  }
}

/* ------------------------------------------------------------------
   Journey timeline (scripts/journey-timeline.mjs)

   The board's strip as a rail: seven step cards running off the right
   of the screen, an arrow in every gap, and under them the empathy
   line with a face at every arrival, riding the same seven columns so
   a face stays under its card however far the strip scrolls. The
   strip is one grid, two rows, and the viewport is the hypothesis
   rail's: widened to the screen with the --edge and --view flip.js
   measures, never 100vw, with the site's carousel controls under the
   faces. Cards stretch to the tallest, so the line under them is
   straight.
   ------------------------------------------------------------------ */
.journey {
  --jgap: var(--sp-4);
  --jpad: var(--sp-5);
  --edge: 0px;
  --view: 72rem;
  /* two cards to the container, less the gap between, capped so the
     bullets keep a readable measure on a wide screen */
  --card-w: min(38rem, calc((var(--view) - 2 * var(--edge) - var(--jgap)) / 2));
  min-width: 0;
}
.journey__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-5);
  max-width: 16ch;
  text-wrap: balance;
}
.journey__viewport {
  contain: inline-size;
  margin-inline: calc(-1 * var(--edge));
  margin-block: calc(-1 * var(--jpad));
  padding: var(--jpad) var(--edge);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--edge);
  scrollbar-width: none;
  outline-offset: -2px;
}
.journey__viewport::-webkit-scrollbar {
  display: none;
}
.journey__viewport:focus-visible {
  outline: 2px solid var(--aurora-blue);
}
/* one grid for the cards and the line under them: seven columns of one
   width, the cards on the first row and the faces on the second */
.journey__strip {
  display: grid;
  grid-template-columns: repeat(7, var(--card-w));
  grid-template-rows: auto 8.5rem;
  column-gap: var(--jgap);
  row-gap: var(--sp-4);
  width: max-content;
}
.journey__cards {
  display: contents;
  margin: 0;
  padding: 0;
  list-style: none;
}
.journey__cell {
  position: relative;
  grid-row: 1;
  grid-column: var(--c);
  display: grid;
  min-width: 0;
  scroll-snap-align: start;
}
.journey__card {
  display: grid;
  align-content: start;
  justify-items: start;
  gap: var(--sp-3);
  height: 100%;
  padding: var(--sp-5);
  border-radius: var(--r-card);
  min-width: 0;
}
.journey__head {
  font-size: var(--step-2);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  max-width: 18ch;
  text-wrap: balance;
}
.journey__tag {
  margin-bottom: var(--sp-2);
}
.journey__items {
  display: grid;
  gap: 0.55rem;
  margin: 0;
  padding-left: 1.2rem;
  font-size: var(--step-0);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.journey__items li::marker {
  color: var(--edge-far);
}
.journey__value {
  margin-top: var(--sp-2);
  font-size: var(--step-0);
  font-weight: 700;
  line-height: 1.4;
  color: var(--ink);
  text-wrap: pretty;
}
/* the arrow to the next card, drawn in the gap: the shaft on ::after,
   the head on ::before, both on the cell, never on the card, whose
   ::after is the shared gradient ring. White, with a hairline of ink
   around it so it stays a line on the light ground and not only on the
   dark one. */
/* the arrow sits on the title line rather than at mid-height: the cards
   stretch to the tallest, and the middle of a short step is empty */
.journey__cell:not(:last-child)::after {
  content: "";
  position: absolute;
  top: calc(var(--sp-5) + 1.1rem);
  right: calc(-1 * var(--jgap));
  width: var(--jgap);
  height: 2px;
  margin-top: -1px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(10, 10, 12, 0.14);
  z-index: 4;
}
.journey__cell:not(:last-child)::before {
  content: "";
  position: absolute;
  top: calc(var(--sp-5) + 1.1rem);
  right: calc(-1 * var(--jgap) + 2px);
  width: 0.5rem;
  height: 0.5rem;
  margin-top: -0.3rem;
  border-top: 2px solid #fff;
  border-right: 2px solid #fff;
  transform: rotate(45deg);
  filter: drop-shadow(0 0 1px rgba(10, 10, 12, 0.35));
  z-index: 5;
}

/* the second row: the empathy rails and the faces that ride them. Three
   lines at the three heights the faces sit: a solid one through the
   delighted, a solid one through the one moment of friction, and a
   dashed one halfway between them through the watchful. */
.journey__rail {
  grid-row: 2;
  grid-column: 1 / -1;
  align-self: start;
  height: 2px;
  border-radius: 1px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(10, 10, 12, 0.14);
  /* the rails sit behind the faces: the two solid ones at 60%, the dashed
     one at 35%, so the faces stay the event and the lines the ground */
  opacity: 0.6;
  /* the face is 2.75rem tall and sits at --y from the row top; the rail
     crosses its center */
  margin-top: calc(var(--y) + 1.375rem - 1px);
}
.journey__rail--high { --y: 0.85rem; }
.journey__rail--low  { --y: 4.4rem; }
.journey__rail--even {
  --y: 2.6rem;
  background: repeating-linear-gradient(90deg, #fff 0 0.6rem, transparent 0.6rem 1.1rem);
  box-shadow: none;
  opacity: 0.35;
}
.journey__moods {
  grid-row: 2;
  grid-column: var(--c);
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
  height: 100%;
}
.journey__moods--pair { justify-content: space-between; }
.journey__mood {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  width: 5.5rem;
  /* the face sits on the card's left edge, where the arrival is */
  margin-left: -1.25rem;
  /* the face rides the rail: high sits on it, even hangs off it, low
     drops well beneath — the board's three heights */
  transform: translateY(var(--y));
}
.journey__moods--pair .journey__mood:last-child { margin-left: 0; margin-right: -1.25rem; }
.journey__mood--high { --y: 0.85rem; }
.journey__mood--even { --y: 2.6rem; }
.journey__mood--low { --y: 4.4rem; }
.journey__face {
  position: relative;
  z-index: 2;
  width: 2.75rem;
  height: 2.75rem;
  /* the features are always dark, whatever the theme — the same ink as
     the labels beneath, so a white grin never lands on amber */
  color: #0a0a0c;
  filter: drop-shadow(0 6px 14px rgba(10, 10, 12, 0.18));
}
.journey__face circle:first-child { stroke: none; }
.journey__face--high circle:first-child { fill: var(--mood-high); }
.journey__face--even circle:first-child { fill: var(--mood-even); }
.journey__face--low circle:first-child { fill: var(--mood-low); }

.journey__mood-label {
  padding: 0.3rem 0.65rem;
  border-radius: var(--r-pill);
  font-size: 0.6875rem;
  font-weight: 700;
  line-height: 1.2;
  white-space: nowrap;
  color: #0a0a0c;
}
.journey__mood--high .journey__mood-label { background: color-mix(in srgb, var(--mood-high) 82%, #fff); }
.journey__mood--even .journey__mood-label { background: color-mix(in srgb, var(--mood-even) 82%, #fff); }
.journey__mood--low .journey__mood-label  { background: color-mix(in srgb, var(--mood-low) 70%, #fff); }
.journey__controls {
  margin-top: var(--sp-5);
}

@media (max-width: 860px) {
  .journey {
    --jgap: var(--sp-3);
    --card-w: calc(var(--view) - 2 * var(--edge) - var(--sp-5));
  }
  .journey__card {
    padding: var(--sp-4);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  .journey__head {
    font-size: var(--step-1);
  }
  /* the arrow has no gap to live in on a phone: the next card is the
     next screen */
  .journey__cell:not(:last-child)::before,
  .journey__cell:not(:last-child)::after {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .journey__viewport { scroll-behavior: auto; }
}
[data-theme="dark"] .journey__face { filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.5)); }

/* ------------------------------------------------------------------
   Principles (scripts/principles-card.mjs)

   Four glass cards over the aurora — mark, name, definition, and two
   short rows: the library's example and where it works in Cash Preview.

   The cards share their rows. The grid hands each card four row tracks
   and the card lays itself out on them as a subgrid, so the mark, the
   name, the definition and the rows under the rule sit on one line
   across the set — the rule is at one height however long the
   definition above it runs. A card that set its own rows had its rule
   wherever its definition ended, and four rules at four heights read
   as four cards that happened to be near each other.
   ------------------------------------------------------------------ */
/* two halves at the cards' own gap, so the lede's left edge is the third
   card's left edge */
.principles__head {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: end;
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
}
.principles__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  max-width: 16ch;
  text-wrap: balance;
}
.principles__lede {
  max-width: 60ch;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.principles__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.principles__card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 4;
  /* a subgrid inherits its parent's gap, and the grid's card gap between
     a mark and its name is not a distance the card wants; the elements
     keep their own margins instead */
  row-gap: 0;
  align-content: start;
  padding: var(--sp-4);
  border-radius: var(--r-card);
  min-width: 0;
}
.principles__mark {
  width: 2rem;
  height: 2rem;
  color: var(--ink);
}
.principles__name {
  margin-top: var(--sp-3);
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.principles__def {
  margin-top: var(--sp-2);
  font-size: 0.9375rem;
  line-height: 1.5;
  text-wrap: pretty;
}
.principles__rows {
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--glass-border);
  display: grid;
  gap: var(--sp-3);
}
.principles__rows dt {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.principles__rows dd {
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.principles__rows div:last-child dd { color: var(--ink); }

@media (max-width: 1023px) {
  .principles__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .principles__head { grid-template-columns: minmax(0, 1fr); align-items: start; }
}
@media (max-width: 640px) {
  .principles__grid { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------------
   Process flow (scripts/process-flow.mjs)

   A record's stages, in order, as an ordered list of glass nodes with a
   rule running between them. The connector belongs to the nodes rather
   than to a drawing: each step but the first paints the rule and its
   arrowhead across the gap on its own leading edge, so the figure reflows
   with the words and the arrows can simply turn when the row folds into a
   trunk on a phone.

   Nothing here takes color. The rule, the heads and the step numbers are
   ink, because a stage that looks different from its neighbors is making
   a claim — and the only claim this figure makes is the order. When a
   record's stages do divide into two kinds, the two node materials below
   carry it, in the same ink-against-tinted-glass pair the opportunity
   tree uses for its leaves.
   ------------------------------------------------------------------ */
.pflow {
  --line: var(--edge-far);
  --line-w: 2px;
  --arrow: 0.5rem;
  --colgap: clamp(1.25rem, 2.6vw, 2.75rem);
  /* the generator sets the real count inline */
  --steps: 4;
}
.pflow__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-5);
  margin-bottom: var(--sp-5);
}
.pflow__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  text-wrap: balance;
}
.pflow__steps {
  display: grid;
  grid-template-columns: repeat(var(--steps), minmax(0, 1fr));
  /* stretch, not start: every node is the height of the tallest, which is
     what puts all of their midlines on one line for the connector to run
     along — and stops a two-word stage sitting higher than its neighbors */
  align-items: stretch;
  gap: var(--colgap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.pflow__step {
  position: relative;
  display: grid;
  min-width: 0;
}
.pflow__node {
  display: grid;
  align-content: start;
  gap: 0.4rem;
  height: 100%;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}
.pflow__no {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-secondary);
}
.pflow__name {
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  text-wrap: pretty;
}
.pflow__note {
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}

/* the rule, across the gap on the step's own leading edge */
.pflow__step + .pflow__step::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 100%;
  width: var(--colgap);
  height: var(--line-w);
  margin-top: calc(var(--line-w) / -2);
  background: var(--line);
  border-radius: 1px;
}
/* and its head: two strokes of the same rule, turned to meet the node */
.pflow__step + .pflow__step::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 100%;
  width: var(--arrow);
  height: var(--arrow);
  border-top: var(--line-w) solid var(--line);
  border-right: var(--line-w) solid var(--line);
  transform: translate(50%, -50%) rotate(45deg);
}

/* the two materials, when a record's stages divide — the tree's pair */
.pflow__node--do {
  background: var(--ink);
  color: var(--bg);
}
.pflow__node--do .pflow__no,
.pflow__node--do .pflow__note {
  color: color-mix(in srgb, var(--bg) 72%, var(--ink));
}
.pflow__node--auto {
  background: color-mix(in srgb, var(--aurora-cyan) 12%, var(--bg-soft));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--aurora-cyan) 40%, transparent);
}
.pflow__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.pflow__key {
  padding: 0.35rem 0.75rem;
  border-radius: var(--r-pill);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
}
.pflow__key--do {
  background: var(--ink);
  color: var(--bg);
}
.pflow__key--auto {
  background: color-mix(in srgb, var(--aurora-cyan) 12%, var(--bg-soft));
  color: var(--ink);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--aurora-cyan) 40%, transparent);
}

/* Five stages across is comfortable on a wide desktop and tight below it,
   and a wrapped row would leave the last arrow pointing at nothing. So the
   row becomes a trunk early: one column, the rule running down the left of
   the nodes, every arrowhead turned a quarter to point down it. */
@media (max-width: 900px) {
  .pflow__steps {
    grid-template-columns: minmax(0, 1fr);
    gap: 2.25rem;
    padding-left: 1.75rem;
  }
  .pflow__step + .pflow__step::before {
    top: auto;
    bottom: 100%;
    right: auto;
    left: -1.75rem;
    width: var(--line-w);
    height: 2.25rem;
    margin: 0 0 0 calc(var(--line-w) / -2);
  }
  .pflow__step + .pflow__step::after {
    top: auto;
    bottom: 100%;
    right: auto;
    left: -1.75rem;
    transform: translate(-50%, 50%) rotate(135deg);
  }
}

/* ------------------------------------------------------------------
   Assumptions matrix (scripts/assumptions-matrix.mjs)

   Two axes crossing at the middle of a plot, four quadrants, and a pile
   of stickies where the risk is. The top two quadrants take a wash of the
   sentiment scale — green where we are sure and it matters, amber where
   we are not — because that is exactly what the two colors already mean
   on the empathy rail three figures down. Stickies are placed by
   percentage so the cluster keeps its shape at every width, and they
   arrive one at a time on the shared reveal stagger.

   Every sticky is one of the workshop's three headings, and the heading
   is its colour: desirability in the site's violet, viability in its
   blue, feasibility in the sentiment green. The three chips over the plot
   are the same three colours and are toggles — matrix-filter.js shows
   and hides by heading — so the pile can be read one concern at a time.
   The colours are paper, not light: a sticky is the one flat fill on the
   site, because it is a picture of a sticky, and it keeps dark ink in
   both themes for the same reason.
   ------------------------------------------------------------------ */
.amatrix,
.plan {
  --cat-desirability: var(--aurora-violet);
  --cat-viability: var(--aurora-blue);
  --cat-feasibility: var(--mood-high);
}
/* the paper every sticky on the site is cut from: a pastel of its own
   colour (pink unless told otherwise), dark ink, a small radius and a
   soft drop, in both themes */
.sticky {
  background: color-mix(in srgb, var(--sticky, var(--aurora-pink)) 36%, #fff);
  color: #0a0a0c;
  border-radius: 0.35rem;
  box-shadow: 0 6px 18px rgba(10, 10, 12, 0.16);
  font-weight: 600;
  line-height: 1.35;
}
/* the header row: the title and the lede on the left, the three chips
   sitting on the same baseline at the right, so the filter reads as part
   of the heading rather than as a toolbar over the plot */
.amatrix__head {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  align-items: end;
  gap: var(--sp-4) var(--sp-6);
  margin-bottom: var(--sp-6);
}
.amatrix__copy {
  min-width: 0;
}
.amatrix__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  max-width: 16ch;
  text-wrap: balance;
}
.amatrix__lede {
  max-width: 60ch;
  margin-top: var(--sp-3);
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.amatrix__filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* a chip in the paper of the stickies it names, a dot of the full colour
   as its swatch; released, it goes to glass with the dot dimmed, so the
   dot still says which colour would come back */
.amatrix__chip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.55rem 1rem 0.55rem 0.8rem;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--cat) 36%, #fff);
  color: #0a0a0c;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--t-micro) var(--ease), color var(--t-micro) var(--ease), transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.amatrix__swatch {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--cat);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7);
  transition: opacity var(--t-micro) var(--ease);
}
.amatrix__chip[aria-pressed="false"] {
  background: var(--glass);
  color: var(--ink-secondary);
}
.amatrix__chip[aria-pressed="false"] .amatrix__swatch {
  opacity: 0.45;
  box-shadow: none;
}
.amatrix__chip:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-card);
}
.amatrix__chip:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 3px;
}

/* the board carries the margins the axis labels need and is the box the
   callout is positioned against — so on a phone the callout can simply
   follow the plot in flow instead of hanging off it */
.amatrix__board {
  position: relative;
  /* the axis words sit outside the plot; the margins are the room they
     need — "Uncertain" is the long one, so the right side gets more */
  margin: 0 5rem 0 3.5rem;
}
.amatrix__plot {
  position: relative;
  /* squarer than it was: forty stickies want the height, and the board
     they came off is nearer 5:4 than 16:10 */
  aspect-ratio: 4 / 3;
  /* the stickies size from the plot, not from the root: a pile placed by
     percentage only keeps its shape if the notes scale with the board */
  container-type: inline-size;
  border-radius: var(--r-thumb);
  background: var(--glass);
  box-shadow: inset 0 0 0 1px var(--glass-border);
  overflow: visible;
}
.amatrix__quad {
  position: absolute;
  top: 0;
  width: 50%;
  height: 50%;
  pointer-events: none;
}
.amatrix__quad--tl {
  left: 0;
  border-top-left-radius: inherit;
  background: color-mix(in srgb, var(--mood-high) 16%, transparent);
}
.amatrix__quad--tr {
  right: 0;
  border-top-right-radius: inherit;
  background: color-mix(in srgb, var(--mood-even) 22%, transparent);
}
.amatrix__axis {
  position: absolute;
  background: var(--ink);
  border-radius: 1px;
}
.amatrix__axis--x {
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
}
.amatrix__axis--y {
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
}
.amatrix__label {
  position: absolute;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-secondary);
  white-space: nowrap;
}
.amatrix__label--top    { left: 50%; top: -1.6rem; transform: translateX(-50%); }
.amatrix__label--bottom { left: 50%; bottom: -1.6rem; transform: translateX(-50%); }
.amatrix__label--left   { left: -0.9rem; top: 50%; transform: translate(-100%, -50%); }
.amatrix__label--right  { right: -0.9rem; top: 50%; transform: translate(100%, -50%); }

.amatrix__stickies {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.amatrix__sticky {
  position: absolute;
  /* a tenth of the plot wide, which is what a sticky was on the board;
     the height, the padding and the type follow the same measure, so the
     pile is the same pile at every width and the last row of the top
     quadrants never grows down across the axis */
  --w: 9.6cqi;
  /* placed by percentage, but never past the plot's edge */
  left: min(var(--x), calc(100% - var(--w)));
  top: min(var(--y), calc(100% - 6cqi));
  width: var(--w);
  min-height: 6cqi;
  padding: 0.45cqi 0.5cqi;
  /* the smallest type on the site: a sticky is read up close or on
     hover, and forty of them at label size would be a wall of words */
  font-size: clamp(0.5rem, 0.8cqi, 0.7rem);
  /* six lines of the longest note come to 6cqi with this leading, which
     is a row's height: a long note fills its slot and stops there */
  line-height: 1.25;
  transform: rotate(var(--r));
  transform-origin: 50% 20%;
  transition: opacity var(--t-reveal) var(--ease), transform var(--t-reveal) var(--ease), box-shadow var(--t-micro) var(--ease);
  transition-delay: calc(var(--i, 0) * 45ms);
}
/* the reveal's rise, kept together with the sticky's own tilt */
.amatrix__sticky.reveal { transform: translateY(12px) rotate(var(--r)); }
.amatrix__sticky.reveal.is-visible { transform: rotate(var(--r)); }
.amatrix__sticky:hover {
  z-index: 3;
  box-shadow: 0 14px 30px rgba(10, 10, 12, 0.22);
}
/* A sticky whose heading is released: faded and shrunk in place, never
   removed, so what is left keeps the position the workshop gave it. Stated
   under the list so it outranks the band's stand-down of .reveal, which
   would otherwise take the transition with it. */
.amatrix__stickies .amatrix__sticky.reveal.is-visible {
  transition: opacity var(--t-micro) var(--ease), transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.amatrix__stickies .amatrix__sticky.is-off,
.amatrix__stickies .amatrix__sticky.reveal.is-visible.is-off {
  opacity: 0;
  transform: rotate(var(--r)) scale(0.9);
  pointer-events: none;
}
/* Under the plot, not on it: pinned to the bottom-right corner it covered
   the stickies that landed there, which are the whole reason the corner
   is worth reading. A paragraph now, clear of the axis word, and the
   caption follows it. */
.amatrix__note {
  max-width: 60ch;
  margin: calc(1.6rem + var(--sp-5)) 0 0;
  font-size: var(--step-0);
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink);
  text-wrap: pretty;
}

@media (max-width: 860px) {
  .amatrix__head { grid-template-columns: minmax(0, 1fr); align-items: start; }
  .amatrix__filters { justify-content: flex-start; }
  .amatrix__board { margin: 0 var(--sp-2) 0 var(--sp-4); }
  .amatrix__plot { aspect-ratio: 3 / 4; }
  /* Forty stickies on a phone-wide plot cannot all be read at once, and
     the chips are the way to read them: one heading at a time. The notes
     keep a legible floor and a quarter of the plot each, so a heading on
     its own lays out clear and the whole pile reads as the pile it is. */
  .amatrix__sticky { --w: 26cqi; min-height: 0; padding: 0.3rem 0.35rem; font-size: 0.5rem; line-height: 1.25; top: min(var(--y), calc(100% - 2.5rem)); }
  .amatrix__sticky:hover { z-index: 3; }
  .amatrix__note {
    max-width: none;
    margin-top: calc(1.6rem + var(--sp-4));
  }
  .amatrix__label--left  { transform: translate(-100%, -50%) rotate(-90deg); transform-origin: right center; left: -0.4rem; }
  .amatrix__label--right { transform: translate(100%, -50%) rotate(90deg); transform-origin: left center; right: -0.4rem; }
}
@media (prefers-reduced-motion: reduce) {
  .amatrix__sticky,
  .amatrix__sticky.reveal,
  .amatrix__sticky.reveal.is-visible { transform: rotate(var(--r)); }
  .amatrix__stickies .amatrix__sticky.is-off { transform: rotate(var(--r)); }
  .amatrix__chip:hover { transform: none; }
}

/* ============================================================
   CASE-STUDY BLOCKS RESTAGED FROM THE DECK

   The deposit-growth deck grew a set of layouts the article never
   had — chapters paired on one line, a chapter beside a picture,
   the approach with its methods orbiting the stage, the discovery
   work as a row of cards, the research quotes as a wall, the
   findings counted, the member's loop as four frames, hypotheses
   as cards turned over, decisions as a ledger, the ad formats as a
   wall, and the results with the product beside them. These are
   those layouts on the page. scripts/blocks.mjs draws the markup
   from the record, and the deck reads the same markup back to
   build its slides, so an edit lands on both surfaces at once.

   Every block sizes type from the site's --step-* ramp (a page
   scrolls, so width alone is the right measure here — the deck's
   min(vw, vh) ramp is for a frame that cannot), keeps accent as
   light and structure as ink, and folds to one column at 860px.
   Repeated items are scroll reveals in a .reveal-group; the block
   root reveals with them.
   ============================================================ */

/* ------------------------------------------------------------
   A chapter inside a block. The block owns the container and the
   rhythm, so the chapter gives up both; the .chapter shape itself
   stays, because that is what the deck keys its slides off.
   ------------------------------------------------------------ */
.chapter--in {
  padding-block: 0;
}
.chapter--in .chapter__body {
  max-width: none;
}

/* ------------------------------------------------------------
   Pair — two chapters on one line, top-aligned. The deck's opening
   move: Overview beside Context. Two columns of one container come
   to about 56ch each, which is a full measure rather than the
   deck's 44ch, because a page has the height to spend.
   ------------------------------------------------------------ */
.pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-7);
  align-items: start;
  padding-block: var(--sp-6);
}
.pair .chapter__body {
  max-width: 56ch;
}
@media (max-width: 1100px) {
  .pair {
    column-gap: var(--sp-6);
  }
}
@media (max-width: 860px) {
  .pair {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ------------------------------------------------------------
   Aside — one long chapter with its title standing beside the
   body, held as the body scrolls. For a chapter the deck gives a
   stage of its own because it outgrew a column: on the page it
   takes the full width and the title keeps it company.
   ------------------------------------------------------------ */
.chapter--aside {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--sp-4) var(--sp-7);
  align-items: start;
}
.chapter--aside .chapter__aside {
  position: sticky;
  top: calc(var(--topbar-h, 5.5rem) + var(--sp-4));
}
.chapter--aside .chapter__title {
  margin-bottom: 0;
  max-width: 12ch;
  text-wrap: balance;
}
.chapter--aside .chapter__body {
  max-width: var(--measure);
}
@media (max-width: 860px) {
  .chapter--aside {
    grid-template-columns: minmax(0, 1fr);
  }
  .chapter--aside .chapter__aside {
    position: static;
  }
  .chapter--aside .chapter__title {
    max-width: none;
    margin-bottom: var(--sp-2);
  }
}

/* ------------------------------------------------------------
   Split — a chapter beside a picture. The deck's My Role: the
   scene the study is about takes half the frame and the words
   stack beside it. The panel is near-square on purpose; the still
   is 16:9 and cover trims it, held to the left so the table reads
   from its near end rather than centring on the empty middle.
   ------------------------------------------------------------ */
.split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-7);
  align-items: center;
  padding-block: var(--sp-6);
}
.split__figure {
  position: relative;
  margin: 0;
  aspect-ratio: 5 / 4;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
}
.split__figure img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 14% 50%;
}
.split__copy {
  min-width: 0;
}
.split__copy .chapter__body {
  max-width: 52ch;
}
@media (max-width: 860px) {
  .split {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }
  .split__figure {
    aspect-ratio: 16 / 10;
  }
}

/* ------------------------------------------------------------
   Orbit — the approach, drawn rather than set. The chapter on the
   left; on the right, the six things the looking consisted of,
   orbiting the bank's own cat on a stage that stays put while the
   chapter scrolls past it.

   The cards lie on a twelve-by-twelve grid over the stage rather
   than at hand-placed percentages, so the scatter is the same
   scatter at every width. Half pass behind him and half in front,
   which is the only reason there is a cat rather than a list. The
   list is a placement layer and carries no z-index of its own: one
   would seal every card inside it and the whole set would paint
   under him however high a card climbed.
   ------------------------------------------------------------ */
.orbit {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-7);
  align-items: start;
  padding-block: var(--sp-6);
}
.orbit__copy {
  min-width: 0;
}
.orbit__copy .chapter__body {
  max-width: 52ch;
}
.orbit__stage {
  /* sticky is a positioning scheme, so it is also the containing block
     for the cat and the cards */
  position: sticky;
  top: calc(var(--topbar-h, 5.5rem) + var(--sp-4));
  aspect-ratio: 1;
  border-radius: var(--r-card);
  overflow: hidden;
  isolation: isolate;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  min-width: 0;
}
/* big, and sat to the right of his own half: his silhouette starts past
   where the left-hand cards end their words, so he can be large without
   crossing a sentence */
.orbit__cat {
  position: absolute;
  inset: 8% 6% 6% 30%;
  z-index: 2;
}
.orbit__cat img {
  width: 100%;
  height: 100%;
  /* a cut-out, so contain leaves the stage showing rather than bars */
  object-fit: contain;
  object-position: 50% 100%;
  filter: drop-shadow(0 1rem 2.4rem rgba(10, 10, 12, 0.22));
}
.orbit__cards {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-template-rows: repeat(12, minmax(0, 1fr));
  gap: 0.4rem;
  margin: 0;
  padding: var(--sp-4);
  list-style: none;
  pointer-events: none;
}
.orbit__card {
  position: relative;
  grid-column: var(--col) / span var(--span);
  grid-row: var(--row);
  align-self: center;
  display: grid;
  justify-items: start;
  gap: 0.5rem;
  padding: 0.9rem 1rem;
  border-radius: var(--r-thumb);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: var(--shadow-card);
  pointer-events: auto;
  min-width: 0;
}
/* a card that runs behind him keeps its words clear of him: the last
   third is padding rather than text, so the tuck reads as depth instead
   of as a sentence with its end bitten off */
.orbit__card--back {
  z-index: 1;
  padding-right: 30%;
}
.orbit__card--front {
  z-index: 3;
}
.orbit__mark {
  display: block;
  width: 1.35rem;
  aspect-ratio: 1;
  color: var(--ink);
  opacity: 0.9;
}
.orbit__mark svg {
  width: 100%;
  height: 100%;
}
.orbit__label {
  font-size: var(--step--1);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
  text-wrap: pretty;
}
@media (max-width: 1100px) {
  .orbit__card {
    padding: 0.7rem 0.8rem;
    gap: 0.35rem;
  }
  .orbit__card--back {
    padding-right: 26%;
  }
}
/* On a phone the stage is a column: the cat first at a fixed height,
   then the six cards as a plain two-up grid. Six cards over a cat in a
   375px square would land on each other, and a card behind him has
   nothing to hide behind at that size. */
@media (max-width: 860px) {
  .orbit {
    grid-template-columns: minmax(0, 1fr);
  }
  .orbit__stage {
    position: relative;
    top: auto;
    aspect-ratio: auto;
    display: grid;
    gap: var(--sp-4);
    padding: var(--sp-4);
  }
  .orbit__cat {
    position: relative;
    inset: auto;
    height: 14rem;
  }
  .orbit__cards {
    position: static;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: none;
    grid-auto-rows: auto;
    gap: var(--sp-3);
    padding: 0;
  }
  .orbit__card,
  .orbit__card--back {
    grid-column: auto;
    grid-row: auto;
    padding: 0.9rem 1rem;
    z-index: 2;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
}

/* ------------------------------------------------------------
   Orbit, as a ring — the savings study's loop. Six square glass
   cards ride an ellipse around the whale; orbit.js places them
   from the properties set here. The whale bobs on its own clock.

   The ellipse is wide and low, the way react-bits draws its
   default, and it sits below the whale's centre so the far arc
   crosses his lower body: a card on that arc passes behind him
   and comes round in front. The radii are a share of the width,
   so the shape holds on the deck's stage as well as this square.
   ------------------------------------------------------------ */
.orbit__stage--ring {
  --period: 60s;
  --rx: 40;
  --ry: 16;
  --cy: 58;
}
/* Narrower than the ellipse by a good margin, and sat on the far arc:
   his centre is the arc's own height, so the two far-arc nodes show
   either side of him with their inner edges tucked behind his body. */
.orbit__whale {
  position: absolute;
  left: 50%;
  top: 42%;
  width: 42%;
  /* the middle of the nodes' 1 to 21: the far half sorts under him */
  z-index: 11;
  transform: translate(-50%, -50%);
  animation: orbit-bob 7s ease-in-out infinite;
  pointer-events: none;
}
/* The bob rides the outer box and the shake and pop ride this one, so
   the three compose instead of the last one winning. */
.orbit__whale-body {
  position: relative;
}
.orbit__whale img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 1rem 2.2rem rgba(10, 10, 12, 0.24));
  transition: opacity 0.12s linear;
}
/* the gold skin over the grey, shown by the class the script toggles at
   the bottom of the pop; its glow is the one Figma gives the gold art */
.orbit__whale-gold {
  position: absolute;
  inset: 0;
  opacity: 0;
  filter: drop-shadow(0 0.75rem 1.3rem rgba(239, 150, 0, 0.35));
}
.orbit__whale.is-gold .orbit__whale-gold {
  opacity: 1;
}
.orbit__whale.is-gold .orbit__whale-grey {
  opacity: 0;
}
.orbit__whale.is-shaking .orbit__whale-body {
  animation: orbit-shake 0.9s linear both;
}
.orbit__whale.is-popping .orbit__whale-body {
  animation: orbit-pop 0.9s cubic-bezier(0.2, 0.9, 0.3, 1.35) both;
}
/* a tremor that builds: small at first, wide by the end, so the pop
   reads as the thing he was working up to */
@keyframes orbit-shake {
  0%,
  100% {
    transform: translate(0, 0) rotate(0);
  }
  10% {
    transform: translate(-1%, 0.5%) rotate(-1.5deg);
  }
  20% {
    transform: translate(1%, -0.5%) rotate(1.5deg);
  }
  30% {
    transform: translate(-1.5%, 1%) rotate(-2.5deg);
  }
  40% {
    transform: translate(1.5%, -1%) rotate(2.5deg);
  }
  50% {
    transform: translate(-2%, 1%) rotate(-3.5deg);
  }
  60% {
    transform: translate(2.5%, -1.5%) rotate(3.5deg);
  }
  70% {
    transform: translate(-3%, 1.5%) rotate(-4.5deg);
  }
  80% {
    transform: translate(3%, -2%) rotate(4.5deg);
  }
  90% {
    transform: translate(-3.5%, 2%) rotate(-5deg);
  }
}
/* squash, then burst past size, then settle: the skin swaps at the
   squash, so what grows back is the other whale */
@keyframes orbit-pop {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(0.8);
  }
  62% {
    transform: scale(1.22);
  }
  100% {
    transform: scale(1);
  }
}
/* The sparkles: the script lays them at the whale's centre, each with
   its own angle, distance, size and delay, and one class sends them all
   outward at once. Two rotates: the first aims the star, the translate
   carries it, the last spins it as it goes. */
.orbit__stars {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.orbit__star {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--s, 18px);
  aspect-ratio: 1;
  margin: calc(var(--s, 18px) / -2) 0 0 calc(var(--s, 18px) / -2);
  opacity: 0;
  transform: rotate(var(--a, 0deg)) translate(0) scale(0.2);
}
.orbit__star svg {
  display: block;
  width: 100%;
  height: 100%;
}
.orbit__stars.is-burst .orbit__star {
  animation: orbit-star 1.15s cubic-bezier(0.15, 0.75, 0.2, 1) var(--dl, 0s) both;
}
@keyframes orbit-star {
  0% {
    opacity: 0;
    transform: rotate(var(--a, 0deg)) translate(0) scale(0.2) rotate(0);
  }
  12% {
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: rotate(var(--a, 0deg)) translate(var(--d, 120px)) scale(1) rotate(200deg);
  }
}
/* a slow bob with a little sway in it, and a lean into each rise */
@keyframes orbit-bob {
  0%,
  100% {
    transform: translate(-50%, -50%) rotate(-2deg);
  }
  30% {
    transform: translate(-47%, -44%) rotate(1deg);
  }
  55% {
    transform: translate(-51%, -54%) rotate(3deg);
  }
  80% {
    transform: translate(-53%, -46%) rotate(-1deg);
  }
}
.orbit__ring {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  /* a placement layer, flat: the nodes carry the depth */
  pointer-events: none;
}
.orbit__node {
  position: absolute;
  left: 0;
  top: 0;
  width: 22%;
  aspect-ratio: 1;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 0.55rem;
  padding: 0.6rem;
  border-radius: var(--r-thumb);
  text-align: center;
  /* the ride is inline, one frame at a time; a transition on it would
     smear every frame into the next, and the hover lift is not for a
     card nobody can reach */
  transition: none;
  will-change: transform;
}
.orbit__node:hover {
  transform: none;
  box-shadow: var(--shadow-card);
}
.orbit__glyph {
  display: block;
  width: 2.4rem;
  aspect-ratio: 1;
  color: var(--ink);
}
.orbit__glyph svg {
  width: 100%;
  height: 100%;
}
.orbit__node .orbit__label {
  font-size: clamp(0.72rem, 0.95vw, 0.9rem);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
  white-space: nowrap;
  color: var(--ink);
}
@media (max-width: 1100px) {
  .orbit__glyph {
    width: 2rem;
  }
}
/* the phone gets the same six as a grid under the whale, and the script
   reads the mode and stands down */
@media (max-width: 860px) {
  .orbit__stage--ring {
    --orbit-mode: grid;
  }
  .orbit__whale {
    position: relative;
    left: auto;
    top: auto;
    width: 62%;
    margin-inline: auto;
    transform: none;
    animation: none;
  }
  .orbit__ring {
    position: static;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-3);
  }
  .orbit__node {
    position: relative;
    width: auto;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  .orbit__node .orbit__label {
    white-space: normal;
    text-wrap: balance;
  }
}
@media (prefers-reduced-motion: reduce) {
  .orbit__whale,
  .orbit__whale.is-shaking .orbit__whale-body,
  .orbit__whale.is-popping .orbit__whale-body,
  .orbit__stars.is-burst .orbit__star {
    animation: none;
  }
}

/* ------------------------------------------------------------
   Track — the discovery work, on one line. The stages as cards on
   the figure stage, each with its mark and a ghosted count sunk
   into the corner. The <ol> carries the order; the count is a
   motif. Nothing here takes colour: five cards in a row already
   say "in this order".
   ------------------------------------------------------------ */
.track,
.findings,
.storyboard,
.hypo,
.bento,
.attest {
  padding-block: var(--sp-6);
}
.track__title,
.feedback__title,
.findings__title,
.storyboard__title,
.hypo__title,
.bento__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-5);
  text-wrap: balance;
}
.track__steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.track__step {
  display: grid;
  min-width: 0;
}
.track__card {
  position: relative;
  display: grid;
  /* content sits at the top of every card, so the row of names reads as
     one line whatever each note runs to */
  align-content: start;
  gap: 0.45rem;
  height: 100%;
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  /* the count is set larger than the card on purpose and clipped by it */
  overflow: hidden;
  min-width: 0;
}
.track__ghost {
  position: absolute;
  top: -0.5rem;
  right: 0.6rem;
  z-index: 0;
  font-size: clamp(3.5rem, 6vw, 5.5rem);
  font-weight: 800;
  line-height: 0.86;
  letter-spacing: -0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  opacity: 0.07;
  pointer-events: none;
  user-select: none;
}
/* a chip, not a second card: a tinted disc with no ring, so the stage
   reads as one surface */
.track__icon {
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 3.2rem;
  height: 3.2rem;
  margin-bottom: 0.4rem;
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: var(--ink);
}
.track__icon svg {
  width: 60%;
  height: 60%;
}
.track__name,
.track__note {
  position: relative;
  z-index: 2;
  margin: 0;
}
.track__name {
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.track__note {
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
/* The approach as a process: the chapter's title and one paragraph as a
   head row, then the moves as the track's own cards, four across and two
   deep. The title keeps the chapter's data attribute for the deck and
   the track's class for the band, and gives up its bottom margin to the
   row, which owns the distance to the cards. */
.track--process .track__head {
  display: grid;
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
}
.track--process .track__head .track__title,
.band .track--process .track__head .track__title {
  max-width: 14ch;
  margin-bottom: 0;
}
.track--process .track__head .chapter__body {
  max-width: 60ch;
}
.track--process .track__steps {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 1100px) {
  .track--process .track__steps {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .track--process .track__steps {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ------------------------------------------------------------
   Feedback — what the research heard, in the room's own words. A
   portrait on the left with the persona pinned to it, held as the
   wall scrolls; the quotes on the right, two across. A quote can
   run two rows deep or take the whole width, and auto-placement
   fills in around them, so the stagger is a consequence of the
   sizes rather than an arrangement that breaks when a line is
   added.
   ------------------------------------------------------------ */
/* One height for both halves. The row is as tall as the wall of quotes,
   or the portrait's floor when the wall is shorter, and each half is
   stretched to it: a long wall pulls the portrait down to its foot, and a
   short one has its quote cards grow to meet the portrait. The portrait
   used to ride sticky beside a taller wall; now it is the wall's height,
   and there is nothing for it to ride.

   The portrait carries no aspect-ratio here. Stretched in one axis, an
   item with a ratio takes its other axis from the stretch — the portrait
   came out as wide as the row was tall and ran across the quotes. Its
   floor is a length instead, and the picture covers whatever box it gets. */
.feedback {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--sp-6);
  align-items: stretch;
  padding-block: var(--sp-6);
}
.feedback__portrait {
  position: relative;
  margin: 0;
  min-height: 32rem;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
}
.feedback__portrait img,
.feedback__portrait video {
  display: block;
  width: 100%;
  height: 100%;
  /* cover, never contain: a contained figure in a tall panel paints bars */
  object-fit: cover;
  object-position: 50% 18%;
}
/* the persona, on the picture rather than beside it — a caption pill on a
   figure, which is a pattern the site already owns, not a card in a card */
.feedback__persona {
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 3;
  display: grid;
  justify-items: start;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border-radius: var(--r-thumb);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: var(--shadow-card);
}
/* The card reads as a small dossier: who, then the one number, then the
   traits as chips, then a rule and the modifiers under their own label.
   The name and the age are the two things set large — everything else
   is a chip at the site's fixed tag size, so the card scans in two
   beats and the tags read as tags rather than as a sentence. */
.feedback__persona-name {
  display: block;
  font-size: var(--step-2);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--ink);
  text-wrap: balance;
}
.feedback__persona-age {
  display: block;
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.feedback__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* the rule: one hairline of the card's own edge, the full width */
.feedback__rule {
  display: block;
  justify-self: stretch;
  height: 1px;
  margin-block: var(--sp-1);
  background: var(--glass-border);
}
.feedback__persona-sub {
  display: block;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ink-secondary);
}
.feedback__body {
  min-width: 0;
  display: grid;
  /* the wall's track is 1fr, whose minimum is its content: it takes any
     extra height the portrait leaves and can never be shorter than the
     quotes it holds */
  grid-template-rows: auto 1fr;
}
.feedback__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* auto rows, never fractional ones: a fractional row in a box that is
     too short for its text shrinks under the text and the cards pile up.
     Auto rows are at least their content, and the grid's own stretch
     shares any spare height between them when the portrait is the taller
     half. */
  grid-auto-rows: minmax(5.5rem, auto);
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.feedback__voice {
  position: relative;
  display: grid;
  align-content: center;
  padding: var(--sp-4);
  border-radius: var(--r-thumb);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  min-width: 0;
}
/* the short shout, across the wall */
.feedback__voice--feature {
  grid-column: 1 / -1;
}
/* two rows deep, which offsets the column beside it by half a card */
.feedback__voice--tall {
  grid-row: span 2;
}
.feedback__mark {
  position: absolute;
  top: 0.1rem;
  left: 0.6rem;
  z-index: 0;
  font-size: clamp(3rem, 5vw, 4.5rem);
  font-weight: 800;
  line-height: 1;
  color: var(--ink);
  opacity: 0.08;
  pointer-events: none;
  user-select: none;
}
/* The verdict, a thumb in the top-right corner: a disc in the ledger's
   own sentiment scale, so up and down mean here exactly what yes and no
   mean there. The text keeps clear of it with padding on that side
   rather than the disc floating over the words. */
.feedback__verdict {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 1.9rem;
  aspect-ratio: 1;
  border-radius: 50%;
}
.feedback__verdict svg {
  width: 58%;
  height: 58%;
}
.feedback__verdict--up {
  background: color-mix(in srgb, var(--mood-high) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-high) 48%, transparent);
  color: var(--mood-high);
}
.feedback__verdict--down {
  background: color-mix(in srgb, var(--mood-low) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-low) 48%, transparent);
  color: var(--mood-low);
}
.feedback__voice--judged .feedback__text {
  padding-right: 2.2rem;
}
.feedback__text {
  position: relative;
  z-index: 2;
  margin: 0;
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: -0.01em;
  text-wrap: pretty;
}
.feedback__voice--feature .feedback__text {
  font-size: var(--step-2);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
@media (max-width: 1100px) {
  .feedback {
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    gap: var(--sp-5);
  }
}
@media (max-width: 860px) {
  .feedback {
    grid-template-columns: minmax(0, 1fr);
  }
  /* On a phone the card is a dossier's worth of words, and laid over a
     4:3 crop it would leave no picture. It sits under the footage
     instead, and the panel becomes a card holding both. */
  .feedback__portrait {
    position: relative;
    top: auto;
    aspect-ratio: auto;
    display: grid;
    gap: var(--sp-3);
    padding-bottom: var(--sp-3);
  }
  .feedback__portrait img,
  .feedback__portrait video {
    aspect-ratio: 4 / 3;
    height: auto;
    object-position: 50% 0;
  }
  .feedback__persona {
    position: static;
    margin-inline: var(--sp-3);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
}
@media (max-width: 640px) {
  .feedback__grid {
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: auto;
  }
  .feedback__voice--tall {
    grid-row: auto;
  }
  .feedback__voice--feature .feedback__text {
    font-size: var(--step-1);
  }
}

/* ------------------------------------------------------------
   Findings — the same research, counted. The sample on the left:
   one headline count over the three ways it was taken. The
   findings on the right, two across and three down. Every card
   centres its value over its line, so the set reads as stamps
   rather than a table with a ragged edge. Values are ink, never a
   gradient, and count up through metrics.js like the results.
   ------------------------------------------------------------ */
.findings__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
}
.findings__sample {
  display: grid;
  grid-template-rows: minmax(0, 1.25fr) auto;
  gap: var(--sp-4);
  min-width: 0;
}
.findings__methods {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
  min-width: 0;
}
.findings__results {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
  min-width: 0;
}
.finding {
  position: relative;
  display: grid;
  align-content: center;
  justify-items: center;
  text-align: center;
  gap: 0.35rem;
  padding: var(--sp-4) var(--sp-3);
  min-width: 0;
  min-height: 7.5rem;
}
.finding__value.stat__value {
  font-size: var(--step-3);
  letter-spacing: -0.04em;
  line-height: 1.02;
}
.finding--hero .finding__value.stat__value {
  font-size: var(--step-5);
  letter-spacing: -0.05em;
}
.finding__label {
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink-secondary);
  text-wrap: balance;
}
.finding--hero .finding__label {
  font-size: var(--step-1);
  color: var(--ink);
}
@media (max-width: 860px) {
  .findings__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .findings__results {
    grid-template-rows: none;
  }
}
@media (max-width: 640px) {
  .finding {
    padding: var(--sp-3) var(--sp-2);
    min-height: 6rem;
  }
  .finding__value.stat__value {
    font-size: var(--step-2);
  }
  .finding__label {
    font-size: var(--step--1);
  }
}

/* ------------------------------------------------------------
   Storyboard — the loop a member actually runs, as frames in a
   row. Not a flow diagram: the argument is how ordinary the loop
   is, and boxes with arrows would make it look designed. No cards;
   the picture carries the frame and the words sit under it in the
   open, so the row reads as one strip.
   ------------------------------------------------------------ */
.storyboard__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}
.storyboard__cell {
  min-width: 0;
}
.storyboard__frame {
  display: grid;
  gap: var(--sp-3);
  margin: 0;
}
.storyboard__shot {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
}
.storyboard__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.storyboard__head {
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.25;
}
.storyboard__note {
  margin-top: 0.25rem;
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
/* The header row of a storyboard with tracks: the title on the left,
   the control on the far right, both on one baseline. The title's own
   bottom margin moves onto the bar so the strip below keeps its distance. */
.storyboard__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}
/* stated at the band's weight too: the band sets the title's margin at
   two classes deep, and the bar needs it gone or the pill sits half a
   margin below the words */
.storyboard__bar .storyboard__title,
.band .storyboard__bar .storyboard__title {
  margin-bottom: 0;
}
/* Both strips occupy the same grid cell, so the section is always as
   tall as the taller of them: a switch changes what is showing, never
   the height of the band, which used to jump by a line or two of copy.
   The one not showing is kept in layout but invisible and inert, which
   also takes it out of the accessibility tree the way display: none did. */
.storyboard__panels {
  display: grid;
}
.storyboard__panel {
  grid-area: 1 / 1;
}
.storyboard__panel.is-off {
  visibility: hidden;
  pointer-events: none;
}
/* The segmented control: the site's glass pill with the lit segment in
   ink, the way the carousel's live dot is ink — one place, one meaning.
   Nothing takes accent: the control says which strip is up, not what to
   feel about it. */
.segmented {
  display: inline-flex;
  padding: 0.25rem;
  border-radius: var(--r-pill);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: inset 0 0 0 1px var(--glass-border), var(--shadow-card);
}
.segmented__tab {
  padding: 0.6rem 1.15rem;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink-secondary);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: -0.01em;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease);
}
.segmented__tab:hover {
  color: var(--ink);
}
.segmented__tab[aria-selected="true"] {
  background: var(--ink);
  color: var(--bg);
}
.segmented__tab:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}
/* The same switch, for any block: a bar with the title on the left and
   the control on the right, and the panels stacked in one grid cell so
   the block is always as tall as the tallest of them. A switch changes
   what is showing, never the height of the band. The one not showing is
   kept in layout, invisible and inert. The storyboard keeps its own
   names for the same shape; this is the shape for the next block. */
.tabbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
}
.tabbar > h2,
.band .tabbar > h2 {
  margin-bottom: 0;
}
.tabpanels {
  display: grid;
}
.tabpanel {
  grid-area: 1 / 1;
  min-width: 0;
}
.tabpanel.is-off {
  visibility: hidden;
  pointer-events: none;
}
/* the opportunity map: the three trees, one showing, behind one header */
.optrees {
  padding-block: var(--sp-6);
}
.optrees__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  max-width: 16ch;
  text-wrap: balance;
}
.optrees__lede {
  max-width: 60ch;
  margin: 0 0 var(--sp-6);
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.optrees .figure-block {
  padding-block: 0;
}
@media (max-width: 1100px) {
  .storyboard__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .storyboard__grid {
    gap: var(--sp-4);
  }
  .storyboard__shot {
    border-radius: var(--r-thumb);
  }
  /* on a phone the control drops under the title and takes the width,
     each label an equal share */
  .storyboard__bar,
  .tabbar {
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-3);
  }
  .segmented {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }
  .segmented__tab {
    text-align: center;
  }
}

/* ------------------------------------------------------------
   Hypotheses — two cards, turned over. The only block that hides
   half of itself on purpose: a claim and its result live on the two
   faces of one card, and the card has to be turned to get from one
   to the other, so the reader meets the question before the
   answer. flip.js turns it.

   The rotation is real — a Y-axis flip, both faces present the
   whole time, backfaces hidden — and each card carries its own
   perspective, so it pivots about its own centre the way a card in
   a hand does. Visibility swaps at the midpoint on top of
   backface-visibility: backfaces stop the far side being drawn,
   not being clicked or read aloud.

   The front is the site's soft card with the study's own light
   behind a transparent cut-out of the product. The back is pinned
   dark in both themes: the front is a picture and the back is a
   statement, and one flat ground is what makes the two sides read
   as one card.
   ------------------------------------------------------------ */
.hypo__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5);
}
.hypo__card {
  position: relative;
  aspect-ratio: 1;
  min-height: 28rem;
  /* the card's own vanishing point, at its own centre */
  perspective: 1600px;
  min-width: 0;
}
.hypo__inner {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* symmetric, so the card is exactly edge-on when the faces swap */
  transition: transform 0.82s cubic-bezier(0.76, 0, 0.24, 1);
}
.hypo__card.is-flipped .hypo__inner {
  transform: rotateY(180deg);
}
.hypo__face {
  --face-pad: var(--sp-5);
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-columns: minmax(0, 1fr);
  padding: var(--face-pad);
  border-radius: var(--r-card);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* half the turn: the far face stops taking clicks and stops being read
     aloud exactly as it goes edge-on */
  visibility: visible;
  transition: visibility 0s linear 0.41s;
}
/* the ground is the study's own light: the artwork is a transparent
   cut-out of a phone, and whatever sits behind it IS the card */
.hypo__face--front {
  background:
    radial-gradient(120% 95% at 16% 8%, color-mix(in srgb, var(--tint1, var(--aurora-violet)) 34%, transparent), transparent 68%),
    radial-gradient(115% 100% at 92% 96%, color-mix(in srgb, var(--tint3, var(--aurora-cyan)) 30%, transparent), transparent 70%),
    var(--bg-soft);
  color: var(--ink);
}
.hypo__face--back {
  /* the dark palette, whole, so the ring and the quote rule invert with
     it — never gated on [data-theme], which this face ignores */
  --bg: #0a0a0c;
  --bg-soft: #151518;
  --ink: #f5f5f7;
  --ink-secondary: #a5a5ae;
  --glass-border: rgba(245, 245, 247, 0.1);
  --edge-far: rgba(245, 245, 247, 0.16);
  --edge-mid: rgba(245, 245, 247, 0.05);
  --edge-near: rgba(245, 245, 247, 0.36);
  transform: rotateY(180deg);
  visibility: hidden;
  background: #0d0d10;
  color: var(--ink);
}
.hypo__card.is-flipped .hypo__face--front {
  visibility: hidden;
}
.hypo__card.is-flipped .hypo__face--back {
  visibility: visible;
}
/* The picture, under the words rather than behind them. It used to
   start at a fixed inset roughly the height of a two-line claim, which
   held until a claim ran to six lines and the last two landed on the
   handset's own headline. Now it is the grid's own second row, from
   wherever the claim ends to the foot of the card, with a breath of air
   between: a longer claim gets a shorter picture, never a covered one.
   The negative margins hand back the face's padding on three sides so
   the artwork still runs to the edges. */
.hypo__art {
  position: relative;
  z-index: 0;
  grid-row: 2 / 4;
  grid-column: 1;
  margin: var(--sp-4) calc(-1 * var(--face-pad)) calc(-1 * var(--face-pad));
  min-height: 0;
}
.hypo__art img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* contain: a cut-out, so what fills the space beside it is the card's
     own light and there are no bars to paint; bottom-anchored because the
     handset is cropped at the foot of its own frame */
  object-fit: contain;
  object-position: 50% 100%;
}
/* the scrim the words sit on: a little weight under the claim, more
   under the control, the middle left alone — sized as lengths, so the
   cover travels with the words rather than with the card */
.hypo__face--front::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image:
    linear-gradient(180deg, color-mix(in srgb, var(--bg-soft) 92%, transparent) 0%, color-mix(in srgb, var(--bg-soft) 60%, transparent) 55%, transparent 100%),
    linear-gradient(0deg, color-mix(in srgb, var(--bg-soft) 80%, transparent) 0%, transparent 100%);
  background-size: 100% clamp(9rem, 38%, 15rem), 100% clamp(6rem, 24%, 11rem);
  background-position: top, bottom;
  background-repeat: no-repeat;
  pointer-events: none;
}
.hypo__body {
  position: relative;
  z-index: 2;
  grid-row: 1;
  grid-column: 1;
  display: grid;
  gap: var(--sp-2);
  align-content: start;
  min-width: 0;
}
.hypo__kicker {
  display: flex;
  align-items: center;
  gap: 0.5em;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
/* the sentiment scale, the one place a green is allowed to mean true */
.hypo__mark {
  display: grid;
  place-items: center;
  width: 1.5em;
  aspect-ratio: 1;
  border-radius: 50%;
  background: color-mix(in srgb, var(--mood-high) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-high) 48%, transparent);
  color: var(--mood-high);
}
.hypo__mark svg {
  width: 62%;
  height: 62%;
}
/* a claim the room split on: the amber dash of the scale's middle */
.hypo__mark--neutral {
  background: color-mix(in srgb, var(--mood-even) 22%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-even) 50%, transparent);
  color: var(--mood-even);
}
.hypo__head {
  font-size: var(--step-2);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.03em;
  max-width: 20ch;
  text-wrap: balance;
  color: var(--ink);
}
/* the result runs longer than the claim it answers, and it is the only
   face carrying two blocks of words, so it takes a measure of its own */
.hypo__face--back .hypo__head {
  max-width: 26ch;
  font-size: var(--step-1);
}
.hypo__quote {
  margin: var(--sp-3) 0 0;
  padding-left: var(--sp-3);
  border-left: 2px solid var(--edge-near);
  max-width: 44ch;
  font-size: var(--step-0);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
/* The back face's words take the two rows above the control and scroll
   inside them when they run long: a result with two quotes and a note
   ran past the foot of a phone-height card, and an overflow clip cut the
   control off with it. On a desktop card the words fit and nothing moves. */
.hypo__face--back .hypo__body {
  grid-row: 1 / 3;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 0.25rem;
}
/* A note under the quotes: what the split verdict led to. A plain plate
   on the dark face, not a second card — no ring, no shadow — with the
   head set as a label so the note reads as an aside to the result. */
.hypo__note {
  margin-top: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  max-width: 44ch;
  border-radius: var(--r-thumb);
  background: rgba(255, 255, 255, 0.07);
}
.hypo__note-head {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.hypo__note p {
  font-size: var(--step-0);
  line-height: 1.45;
  color: var(--ink);
  text-wrap: pretty;
}
/* the control, in the far corner from the words */
.hypo__turn {
  position: relative;
  z-index: 3;
  grid-row: 3;
  grid-column: 1;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.65rem 1.1rem;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: inset 0 0 0 1px var(--glass-border), var(--shadow-card);
  color: var(--ink);
  font: inherit;
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: -0.01em;
  cursor: pointer;
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.hypo__face--back .hypo__turn {
  background: rgba(255, 255, 255, 0.1);
  box-shadow: inset 0 0 0 1px var(--edge-near);
}

/* ------------------------------------------------------------
   The rail — more hypotheses than a pair. The cards keep their
   size (two to the container's width) and run off the right
   edge of the screen; scrolling carries them left, off the
   left edge, and the site's carousel controls sit under them.

   The viewport is widened to the screen with negative margins
   and given the difference back as padding, so the first card
   starts on the container's own left edge while the scroller
   itself reaches both edges of the window. It scrolls natively
   and snaps to each card; the arrows and dots only steer it.
   The vertical padding is room for the card's shadow and for
   its turn — a flipping card leans out of its own box, and an
   overflow clip would shave the corners off mid-turn.

   --edge and --view are measured by flip.js, not derived from
   100vw: on a desktop with a visible scrollbar 100vw is wider
   than the page, and a bleed set from it hands the whole page a
   horizontal scroll of half a scrollbar. Before the script runs
   the fallbacks give two cards inside the container, no bleed.
   ------------------------------------------------------------ */
.hypo--rail {
  --hypo-gap: var(--sp-5);
  --hypo-pad: var(--sp-6);
  --edge: 0px;
  --view: 72rem;
  /* The rail's width is the cards', which is fine inside the scroller and
     fatal outside it: a grid item's automatic minimum is its content, and a
     scroll container's content still counts, so on a phone the section
     came out 1408px wide, the controls were centred somewhere off the
     right of the screen, and the rail's own scroll ran out at the third
     card. The section takes the track's width and nothing else. */
  min-width: 0;
}
.hypo__viewport {
  /* and the scroller contributes no width of its own, ever */
  contain: inline-size;
  margin-inline: calc(-1 * var(--edge));
  margin-block: calc(-1 * var(--hypo-pad));
  padding: var(--hypo-pad) var(--edge);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--edge);
  scrollbar-width: none;
  outline-offset: -2px;
}
.hypo__viewport::-webkit-scrollbar {
  display: none;
}
.hypo__rail {
  display: flex;
  gap: var(--hypo-gap);
  /* the rail is as wide as its cards; the viewport's padding is the
     margin either side, and flex would otherwise swallow the right one */
  width: max-content;
}
.hypo__rail .hypo__card {
  flex: 0 0 auto;
  /* two to the container: half its width, less half the gap between */
  width: calc((var(--view) - 2 * var(--edge) - var(--hypo-gap)) / 2);
  max-width: 36rem;
  /* taller than the pair layout's square: these claims run to six lines,
     and the picture under the longest still wants a phone's worth of room */
  aspect-ratio: 4 / 5;
  scroll-snap-align: start;
}
.hypo__controls {
  margin-top: var(--sp-5);
}
.carousel__arrow[disabled] {
  opacity: 0.35;
  cursor: default;
  transform: none;
}
@media (max-width: 860px) {
  .hypo__rail .hypo__card {
    width: calc(var(--view) - 2 * var(--edge) - var(--sp-5));
  }
}
@media (prefers-reduced-motion: reduce) {
  .hypo__viewport {
    scroll-behavior: auto;
  }
}
.hypo__turn svg {
  width: 1.1em;
  height: 1.1em;
  flex: none;
  transition: transform 0.42s cubic-bezier(0.24, 0.72, 0.18, 1);
}
.hypo__turn:hover,
.hypo__turn:focus-visible {
  transform: translateY(-2px);
  box-shadow: inset 0 0 0 1px var(--glass-border), var(--shadow-card-hover);
}
/* the glyph turns with the card it is about to turn */
.hypo__turn:hover svg,
.hypo__turn:focus-visible svg {
  transform: rotate(-180deg);
}
@media (max-width: 860px) {
  .hypo__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .hypo__card {
    aspect-ratio: auto;
    height: 30rem;
  }
  .hypo__face {
    padding: var(--sp-4);
  }
  .hypo__head {
    font-size: var(--step-1);
  }
  .hypo__turn {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  /* the same two states, arrived at without the turn */
  .hypo__inner,
  .hypo__face,
  .hypo__turn,
  .hypo__turn svg {
    transition: none;
  }
  .hypo__turn:hover svg,
  .hypo__turn:focus-visible svg {
    transform: none;
  }
}

/* ------------------------------------------------------------
   Ledger — the decisions beside their verdicts. The chapter on the
   left, opened by a lede that says none of what the cards say; on
   the right, each call as one line with a mark against it, kept or
   given up, held beside the argument as it scrolls. The two marks
   are the site's sentiment scale, the one place colour means good
   and bad; the row itself stays neutral, because a row tinted green
   would say the same thing twice and turn a ledger into a
   scorecard.

   The chapter is display: contents, so its head and body take grid
   cells of their own while the DOM keeps the .chapter shape the deck
   reads. On a phone the three re-order: lede, verdicts, argument.
   ------------------------------------------------------------ */
.ledger {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* No row gap. The title and the body are two grid rows here, and a
     row gap on top of the title s own bottom margin put twice the space
     under this chapter s header that every other chapter carries; the
     margin alone is the site s heading-to-body distance. */
  gap: 0 var(--sp-7);
  /* The verdicts span both rows, and a spanning item hands its height to
     the rows it crosses. With two auto rows that share went partly to the
     first, which pushed the argument 47px down from its own header. A
     flexible second row takes the whole of it, and the first row stays
     exactly the header s height. */
  grid-template-rows: auto 1fr;
  align-items: start;
  padding-block: var(--sp-6);
}
.ledger .chapter--in {
  display: contents;
}
.ledger__lead {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
}
.ledger .chapter__body {
  grid-column: 1;
  grid-row: 2;
  max-width: 52ch;
}
.ledger__lede {
  margin-top: var(--sp-3);
  margin-bottom: var(--sp-5);
  max-width: 44ch;
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--ink);
  text-wrap: pretty;
}
.ledger__list {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: start;
  position: sticky;
  top: calc(var(--topbar-h, 5.5rem) + var(--sp-4));
  display: grid;
  align-content: start;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;
}
.ledger__item {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-thumb);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  min-width: 0;
}
.ledger__mark {
  display: grid;
  place-items: center;
  width: 1.9rem;
  aspect-ratio: 1;
  border-radius: 50%;
}
.ledger__mark svg {
  width: 62%;
  height: 62%;
}
.ledger__item--yes .ledger__mark {
  background: color-mix(in srgb, var(--mood-high) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-high) 48%, transparent);
  color: var(--mood-high);
}
.ledger__item--no .ledger__mark {
  background: color-mix(in srgb, var(--mood-low) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-low) 48%, transparent);
  color: var(--mood-low);
}
.ledger__verdict {
  margin: 0;
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.3;
  text-wrap: pretty;
}
@media (max-width: 860px) {
  .ledger {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
  }
  .ledger__lead {
    grid-column: auto;
    grid-row: auto;
    order: 1;
  }
  .ledger__list {
    grid-column: auto;
    grid-row: auto;
    position: static;
    order: 2;
    /* the row gap is gone from the desktop grid, so the list carries its
       own breathing room between the header and the argument here */
    margin-block: var(--sp-2) var(--sp-5);
  }
  .ledger .chapter__body {
    grid-column: auto;
    grid-row: auto;
    order: 3;
  }
}

/* ------------------------------------------------------------
   Bento — every surface an offer was allowed to live on, on one
   wall. Twelve columns, because twelve divides into the halves,
   thirds and quarters this set needs, and the spans do the
   ranking: the largest surface takes the largest cell before any
   label says so. Every tile is the same two rows — the picture,
   then its name — so the labels in a row sit on one line however
   tall the artwork above them. Contained, not covered: the artwork
   is a transparent cut-out, so what fills the space beside a narrow
   one is the tile it sits on.
   ------------------------------------------------------------ */
.bento__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: 17rem;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.bento__cell {
  grid-column: span var(--span, 4);
  min-width: 0;
  min-height: 0;
}
.bento__tile {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--sp-2);
  height: 100%;
  margin: 0;
  padding: var(--sp-3);
  border-radius: var(--r-card);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  min-height: 0;
}
.bento__shot {
  position: relative;
  min-height: 0;
}
.bento__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* the artwork is light and floats; a soft drop keeps a cut-out from
     looking cut out */
  filter: drop-shadow(0 0.6rem 1.4rem rgba(10, 10, 12, 0.18));
}
.bento__label {
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.02em;
  text-align: center;
  text-wrap: balance;
}
/* Labels above, artwork on the foot. The tile keeps its padding at the
   top and sides for the words, and the picture takes the padding back on
   three sides so it runs to the tile's edges and bottom, centred and
   contained: these are compositions with their own ground, and a caption
   under them would land on the art. Taller rows, so a phone in the
   picture is still a phone. */
.bento--above .bento__grid {
  grid-auto-rows: 27rem;
}
.bento--above .bento__tile {
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--sp-3);
  padding-bottom: 0;
}
.bento--above .bento__label {
  grid-row: 1;
}
.bento--above .bento__shot {
  grid-row: 2;
  margin-inline: calc(-1 * var(--sp-3));
}
.bento--above .bento__shot img {
  object-position: 50% 100%;
  filter: none;
}
@media (max-width: 1100px) {
  .bento__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 15rem;
  }
  .bento--above .bento__grid {
    grid-auto-rows: 24rem;
  }
  .bento__cell {
    grid-column: auto;
  }
  /* the largest surface keeps the whole first row */
  .bento__cell:first-child {
    grid-column: 1 / -1;
  }
}
@media (max-width: 640px) {
  .bento__grid {
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: 13rem;
  }
  .bento--above .bento__grid {
    grid-auto-rows: 20rem;
  }
  .bento__cell:first-child {
    grid-column: auto;
  }
}

/* ------------------------------------------------------------
   Results with a scene — the product beside its numbers. The scene
   takes the left of the card at full bleed; the words and the
   numbers take the right. The image is positioned so the panel,
   not the picture, decides the card's height, and cover trims a
   tall board export held slightly high because the handset sits
   above its middle.
   ------------------------------------------------------------ */
.results__card--scene {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  padding: 0;
  min-height: 34rem;
}
.results__scene {
  position: relative;
  min-width: 0;
  overflow: hidden;
}
.results__scene img,
.results__scene video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 42%;
}
@media (prefers-reduced-motion: reduce) {
  .results__scene video {
    display: none;
  }
}
.results__panel {
  position: relative;
  z-index: 2;
  display: grid;
  align-content: center;
  padding: var(--sp-6) var(--sp-5);
  min-width: 0;
}
.results__card--scene .results__narrative {
  max-width: 60ch;
}
.results__card--scene .outcome__metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-4);
  margin-top: var(--sp-5);
}
@media (max-width: 860px) {
  .results__card--scene {
    grid-template-columns: minmax(0, 1fr);
    min-height: 0;
  }
  .results__scene {
    aspect-ratio: 4 / 3;
  }
  .results__panel {
    padding: var(--sp-5) var(--sp-4);
  }
  .results__card--scene .outcome__metrics {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 640px) {
  .results__card--scene .outcome__metrics {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ------------------------------------------------------------
   Attest — the testimonial standing on its own, where the results
   card has given its width to the scene. No card and no glass: the
   words stand on the page the way the hero's copy does, and the
   quote paints in with the heading sweep.
   ------------------------------------------------------------ */
/* the block centers as one column; everything inside it stays on its
   shared left edge, which is the about page's arrangement exactly */
.attest__stage {
  position: relative;
  max-width: 72rem;
  margin-inline: auto;
}
.attest__figure {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: start;
  margin: 0;
  text-align: left;
}
.attest .testimonial__stars {
  justify-content: flex-start;
  margin-top: var(--sp-4);
}
/* the case-study testimonial's own size and weight, which is the site's
   answer for a quote this long */
.attest__quote {
  margin: 0;
  max-width: 58rem;
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.35;
  color: var(--ink);
  text-wrap: pretty;
}
.attest__by {
  display: grid;
  gap: 0.25rem;
  justify-items: start;
  margin-top: var(--sp-3);
}
.attest__by b {
  font-weight: 700;
  font-size: var(--step-0);
}
.attest__by span {
  font-size: var(--step--1);
  color: var(--ink-secondary);
}

/* ------------------------------------------------------------
   Plan — the research plan, as a list of steps beside the step
   that is open. The list on the left: eight glass cards, each a
   mark beside a name over a line, the open one lit in ink the
   way the segmented control lights its segment. The panel on the
   right: the step's goal, what it consisted of, and the decision
   point that closes it, marked with a question in a ring because
   it is the one line that is not a thing to do but a thing to
   answer. Every panel sits in the same grid cell, so the block is
   the height of the longest step and opening a shorter one never
   moves the page.

   Specifics are highlighted the way the workshop board highlights
   them, in the paper of a sticky; where the board pinned a note
   beside a step, the note sits on the panel's corner as a sticky.
   The decision point takes the amber of the matrix's top-right
   quadrant: the same colour, the same meaning — this needs
   thought before anything continues.
   ------------------------------------------------------------ */
.plan {
  padding-block: var(--sp-6);
}
/* the lead row and the grid share one pair of columns at one gap, so the
   lede sits over the panel it introduces with the same left edge */
.plan__lead {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: end;
  gap: var(--sp-4) var(--sp-5);
  margin-bottom: var(--sp-6);
}
.plan__title-main {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  max-width: 14ch;
  text-wrap: balance;
}
.plan__lede {
  max-width: 60ch;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.plan__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-5);
  /* stretch: the open panel runs the full height of the list beside it,
     and the stacked panels inside it fill that height in turn */
  align-items: stretch;
}
.plan__list {
  display: grid;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.plan__item {
  min-width: 0;
}
.plan__tab {
  display: grid;
  grid-template-columns: 3.2rem minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  border-radius: var(--r-thumb);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease), background-color var(--t-micro) var(--ease), color var(--t-micro) var(--ease);
}
/* the open step: ink, the way the lit segment of a segmented control
   is ink — one place, one meaning */
.plan__tab[aria-selected="true"] {
  background: var(--ink);
  color: var(--bg);
}
.plan__tab:focus-visible {
  outline: 2px solid var(--aurora-blue);
  outline-offset: 3px;
}
.plan__icon {
  display: grid;
  place-items: center;
  width: 3.2rem;
  height: 3.2rem;
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: currentColor;
}
.plan__tab[aria-selected="true"] .plan__icon {
  background: color-mix(in srgb, var(--bg) 14%, transparent);
}
.plan__icon svg {
  width: 60%;
  height: 60%;
}
.plan__words {
  display: grid;
  gap: 0.1rem;
  min-width: 0;
}
.plan__no {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  opacity: 0.55;
}
.plan__head {
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.plan__sub {
  font-size: var(--step--1);
  line-height: 1.35;
  color: var(--ink-secondary);
}
.plan__tab[aria-selected="true"] .plan__sub {
  color: color-mix(in srgb, var(--bg) 72%, transparent);
}
.plan__panel {
  padding: var(--sp-5);
  border-radius: var(--r-card);
  background: var(--bg-soft);
  box-shadow: var(--shadow-card);
}
.plan__kicker {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.plan__title {
  margin-top: 0.35rem;
  /* room on the right for the sticky pinned to the corner */
  padding-right: 9.5rem;
  font-size: var(--step-2);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  max-width: 28ch;
  text-wrap: balance;
}
.plan__goal {
  max-width: 60ch;
  margin-top: var(--sp-3);
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.45;
  text-wrap: pretty;
}
.plan__items {
  display: grid;
  gap: 0.55rem;
  max-width: 64ch;
  margin: var(--sp-3) 0 0;
  padding-left: 1.2rem;
  font-size: var(--step-0);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.plan__items .plan__items {
  max-width: none;
  margin-top: 0.45rem;
  gap: 0.35rem;
}
.plan__items li::marker {
  color: var(--edge-far);
}
/* a specific, in the paper of a sticky: the line the reader is meant to
   take away, marked the way the board marks it */
.plan__hl {
  padding: 0.04em 0.32em;
  border-radius: 0.3em;
  background: color-mix(in srgb, var(--aurora-pink) 30%, #fff);
  color: #0a0a0c;
  font-weight: 600;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.plan__decision {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  max-width: 64ch;
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--mood-even) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mood-even) 40%, transparent);
  font-size: var(--step-0);
  line-height: 1.45;
  text-wrap: pretty;
}
.plan__decision b {
  font-weight: 800;
}
.plan__q {
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  margin-top: 0.05rem;
  color: var(--ink);
}
.plan__q svg {
  width: 100%;
  height: 100%;
}
.plan__sticky {
  position: absolute;
  top: var(--sp-4);
  right: var(--sp-4);
  z-index: 2;
  width: 8rem;
  padding: 0.6rem 0.65rem;
  font-size: 0.6875rem;
  transform: rotate(3deg);
}
@media (max-width: 860px) {
  .plan__lead {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }
  .plan__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  /* on a phone the list is a swiped row: eight cards stacked would put
     the open step a screen away from the card that opened it */
  .plan__list {
    display: flex;
    gap: var(--sp-3);
    margin-inline: calc(-1 * var(--sp-4));
    padding-inline: var(--sp-4);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--sp-4);
    scrollbar-width: none;
  }
  .plan__item {
    flex: 0 0 15rem;
    scroll-snap-align: start;
  }
  .plan__tab {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  .plan__tab[aria-selected="true"] {
    background: var(--ink);
  }
  .plan__panel {
    padding: var(--sp-4);
  }
  .plan__title {
    padding-right: 0;
  }
  .plan__sticky {
    position: static;
    display: inline-block;
    margin-top: var(--sp-3);
    transform: rotate(-1.5deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .plan__tab,
  .plan__tab:hover {
    transform: none;
  }
}

/* ------------------------------------------------------------
   Decisions bento — the calls as five glass cards of five shapes
   on twelve columns: one tall, one wide, two square, one banner.
   Each is a mark, a head in bold and the reasoning under it. The
   shapes are set by the record (--span, --rows), so a study with
   four calls or six can lay its own board. The chapter shape
   wraps the grid for the deck; the page prints no lede.
   ------------------------------------------------------------ */
.dbento {
  padding-block: var(--sp-6);
}
.dbento .chapter__body {
  max-width: none;
  color: var(--ink);
}
.dbento__grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: minmax(12rem, auto);
  grid-auto-flow: dense;
  gap: var(--sp-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.dbento__card {
  grid-column: span var(--span, 4);
  grid-row: span var(--rows, 1);
  display: grid;
  align-content: start;
  gap: var(--sp-3);
  padding: var(--sp-5);
  border-radius: var(--r-card);
  min-width: 0;
}
/* the banner reads left to right: the mark and the head on one side, the
   reasoning beside them */
.dbento__card[style*="--span:12"] {
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  align-items: center;
  column-gap: var(--sp-6);
}
.dbento__card[style*="--span:12"] .dbento__icon {
  grid-column: 1;
}
.dbento__card[style*="--span:12"] .dbento__head {
  grid-column: 1;
}
.dbento__card[style*="--span:12"] .dbento__text {
  grid-column: 2;
  grid-row: 1 / span 2;
}
.dbento__icon {
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: var(--ink);
}
.dbento__icon svg {
  width: 58%;
  height: 58%;
}
.dbento__head {
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  max-width: 24ch;
  text-wrap: balance;
}
.dbento__text {
  font-size: var(--step-0);
  line-height: 1.5;
  max-width: 52ch;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
/* the tall card sets its head large: it has the height to spend */
.dbento__card[style*="--rows:2"] .dbento__head {
  font-size: var(--step-2);
  letter-spacing: -0.03em;
}
@media (max-width: 1100px) {
  .dbento__grid {
    grid-auto-rows: auto;
  }
  .dbento__card,
  .dbento__card[style*="--span:12"] {
    grid-column: span 6;
    grid-row: auto;
    grid-template-columns: minmax(0, 1fr);
  }
  .dbento__card[style*="--span:12"] .dbento__text {
    grid-column: 1;
    grid-row: auto;
  }
  .dbento__card[style*="--rows:2"] .dbento__head {
    font-size: var(--step-1);
  }
}
@media (max-width: 640px) {
  .dbento__card,
  .dbento__card[style*="--span:12"] {
    grid-column: span 12;
  }
  .dbento__card {
    padding: var(--sp-4);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
}

/* ------------------------------------------------------------
   Outcomes — what the research found, one card per step, on a
   rail. The hypothesis rail's mechanism with a wider card: the
   viewport is the screen's width (measured by flip.js, never
   100vw), the cards keep their width and run off the right edge,
   and the site's carousel controls sit under them. Each card is
   a mark beside a head, the findings as bullets with the
   specifics in sticky paper, and the board's own notes stacked
   on the right as stickies. Cards stretch to the tallest, so the
   rail is one height however long a step's findings run.
   ------------------------------------------------------------ */
.outcomes {
  --oc-gap: var(--sp-5);
  --oc-pad: var(--sp-5);
  --edge: 0px;
  --view: 72rem;
  padding-block: var(--sp-6);
  min-width: 0;
}
.outcomes__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-5);
  text-wrap: balance;
}
.outcomes__lede {
  max-width: 60ch;
  margin: calc(-1 * var(--sp-3)) 0 var(--sp-6);
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.outcomes__viewport {
  contain: inline-size;
  margin-inline: calc(-1 * var(--edge));
  margin-block: calc(-1 * var(--oc-pad));
  padding: var(--oc-pad) var(--edge);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--edge);
  scrollbar-width: none;
  outline-offset: -2px;
}
.outcomes__viewport::-webkit-scrollbar {
  display: none;
}
.outcomes__viewport:focus-visible {
  outline: 2px solid var(--aurora-blue);
}
.outcomes__rail {
  display: flex;
  align-items: stretch;
  gap: var(--oc-gap);
  width: max-content;
  margin: 0;
  padding: 0;
  list-style: none;
}
.outcomes__card {
  flex: 0 0 auto;
  /* wider than half the container, so the next card shows its edge
     off the right of the screen and the rail reads as a rail */
  width: calc((var(--view) - 2 * var(--edge)) * 0.64);
  max-width: 58rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 11rem;
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-5) var(--sp-5) var(--sp-5);
  border-radius: var(--r-card);
  scroll-snap-align: start;
  min-width: 0;
}
.outcomes__body {
  min-width: 0;
}
.outcomes__head {
  display: grid;
  grid-template-columns: 4rem minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}
.outcomes__icon {
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: var(--ink);
}
.outcomes__icon svg {
  width: 56%;
  height: 56%;
}
.outcomes__name {
  font-size: var(--step-2);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
.outcomes__items {
  display: grid;
  gap: 0.55rem;
  margin: 0;
  padding-left: 1.2rem;
  font-size: var(--step-0);
  line-height: 1.5;
  color: var(--ink-secondary);
  text-wrap: pretty;
}
.outcomes__items li::marker {
  color: var(--edge-far);
}
/* the board's notes, stacked down the right with the same lean the
   board gave them, each a little further over than the one above */
.outcomes__notes {
  display: grid;
  align-content: start;
  gap: var(--sp-3);
  margin: 0;
  padding: var(--sp-2) 0 0;
  list-style: none;
}
.outcomes__sticky {
  width: 10rem;
  padding: 0.7rem 0.75rem;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.3;
  transform: rotate(calc(-2deg + var(--n, 0) * 3deg)) translateX(calc(var(--n, 0) * 0.35rem));
}
.outcomes__controls {
  margin-top: var(--sp-5);
}
@media (max-width: 1100px) {
  .outcomes__card {
    width: calc((var(--view) - 2 * var(--edge)) * 0.82);
  }
}
@media (max-width: 860px) {
  .outcomes__card {
    width: calc(var(--view) - 2 * var(--edge) - var(--sp-5));
    grid-template-columns: minmax(0, 1fr);
    padding: var(--sp-4);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  .outcomes__head {
    grid-template-columns: 3.2rem minmax(0, 1fr);
    gap: var(--sp-3);
  }
  .outcomes__icon {
    width: 3.2rem;
    height: 3.2rem;
  }
  .outcomes__name {
    font-size: var(--step-1);
  }
  /* the notes as a row under the findings, no lean */
  .outcomes__notes {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: var(--sp-2);
  }
  .outcomes__sticky {
    width: auto;
    transform: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .outcomes__viewport {
    scroll-behavior: auto;
  }
}

/* ------------------------------------------------------------
   Value props — what a person gets for each move. Two halves:
   the board's list of features and principles on the left, and
   on the right the conversion as three glass cards down a line,
   the free user, the connected user, the held-account user. Under
   both, the feature table, with the connected tier's blue and the
   held tier's green on the cards, on the column heads and on the
   checks, so a colour in a cell points at a card above it. The
   blue is the site's own; the green is the sentiment scale's, the
   one place a green means "yes" — which is what a full check is.
   ------------------------------------------------------------ */
.vprops {
  --tier-connected: var(--aurora-blue);
  --tier-held: var(--mood-high);
  padding-block: var(--sp-6);
}
.vprops__title {
  font-size: var(--step-2);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--sp-5);
  max-width: 16ch;
  text-wrap: balance;
}
.vprops__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-7);
  align-items: start;
}
.vprops__head {
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-bottom: var(--sp-4);
  text-wrap: balance;
}
.vprops__lead {
  max-width: 52ch;
  margin-bottom: var(--sp-3);
  font-size: var(--step-0);
  font-weight: 700;
  color: var(--ink);
  text-wrap: pretty;
}
.vprops__list {
  display: grid;
  gap: 0.6rem;
  margin: 0;
  padding-left: 1.2rem;
  font-size: var(--step-0);
  line-height: 1.45;
  color: var(--ink);
}
.vprops__list li::marker {
  color: var(--edge-far);
}
.vprops__list li {
  display: list-item;
}
.vprops__tag {
  margin-left: 0.6rem;
  vertical-align: 0.1em;
}
.vprops__sub {
  margin: var(--sp-5) 0 var(--sp-3);
  font-size: var(--step-0);
  font-weight: 800;
  letter-spacing: -0.01em;
}
.vprops__list--plain {
  color: var(--ink-secondary);
}
/* the conversion: three cards down a line, a step drawn between each on
   the item, never on the card, whose ::after is the ring */
.vprops__tiers {
  display: grid;
  gap: 2.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.vprops__tier {
  position: relative;
}
.vprops__tier + .vprops__tier::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -2.6rem;
  width: 2px;
  height: 2.6rem;
  margin-left: -1px;
  background: var(--tier-line, var(--edge-far));
  border-radius: 1px;
}
.vprops__tier + .vprops__tier::after {
  content: "";
  position: absolute;
  left: 50%;
  top: -0.85rem;
  width: 0.55rem;
  height: 0.55rem;
  margin-left: -0.35rem;
  border-right: 2px solid var(--tier-line, var(--edge-far));
  border-bottom: 2px solid var(--tier-line, var(--edge-far));
  transform: rotate(45deg);
  z-index: 2;
}
.vprops__tier--connected { --tier: var(--tier-connected); --tier-line: color-mix(in srgb, var(--tier-connected) 60%, transparent); }
.vprops__tier--held { --tier: var(--tier-held); --tier-line: color-mix(in srgb, var(--tier-held) 60%, transparent); }
.vprops__card {
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-card);
}
/* the tinted tiers wear their colour as light in the glass and as the
   ring's own hue, never as a fill */
.vprops__tier--connected .vprops__card,
.vprops__tier--held .vprops__card {
  background:
    radial-gradient(120% 120% at 0% 50%, color-mix(in srgb, var(--tier) 22%, transparent), transparent 70%),
    var(--glass-strong);
}
.vprops__tier--connected .vprops__card::after,
.vprops__tier--held .vprops__card::after {
  background: linear-gradient(315deg, color-mix(in srgb, var(--tier) 55%, transparent), color-mix(in srgb, var(--tier) 15%, transparent) 50%, color-mix(in srgb, var(--tier) 75%, transparent));
}
.vprops__icon {
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  border-radius: var(--r-thumb);
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: var(--ink);
}
.vprops__tier--connected .vprops__icon,
.vprops__tier--held .vprops__icon {
  background: color-mix(in srgb, var(--tier) 18%, transparent);
  color: var(--tier);
}
.vprops__icon svg {
  width: 58%;
  height: 58%;
}
.vprops__words {
  min-width: 0;
}
.vprops__tier-name {
  font-size: var(--step-1);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
}
.vprops__tier-sub {
  margin-top: 0.2rem;
  font-size: var(--step--1);
  color: var(--ink-secondary);
}
/* the table: a glass panel with the rows ruled inside it, the panel's
   edge the only structural line, and a sideways scroller on a phone */
.vprops__tablewrap {
  margin-top: var(--sp-7);
}
.vprops__head--table {
  max-width: 40ch;
}
.vprops__scroll {
  overflow-x: auto;
  border-radius: var(--r-card);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: inset 0 0 0 1px var(--glass-border), var(--shadow-card);
  scrollbar-width: thin;
}
.vprops__table {
  width: 100%;
  min-width: 44rem;
  border-collapse: collapse;
  font-size: var(--step-0);
}
.vprops__table th,
.vprops__table td {
  padding: 0.85rem var(--sp-4);
  text-align: left;
  vertical-align: middle;
  border-top: 1px solid var(--glass-border);
}
.vprops__table thead th {
  border-top: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-secondary);
}
.vprops__table tbody th {
  font-weight: 600;
  color: var(--ink);
}
.vprops__table th:not(:first-child),
.vprops__table td {
  width: 17%;
}
/* the column heads carry the tier's dot: the same colour as the card */
.vprops__dot {
  display: inline-block;
  width: 0.6rem;
  height: 0.6rem;
  margin-right: 0.5rem;
  border-radius: 50%;
  background: var(--edge-far);
  vertical-align: 0.05em;
}
.vprops__col--connected .vprops__dot { background: var(--tier-connected); }
.vprops__col--held .vprops__dot { background: var(--tier-held); }
.vprops__col--connected { color: var(--ink); }
.vprops__col--held { color: var(--ink); }
.vprops__mark {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 600;
  white-space: nowrap;
}
.vprops__mark--view {
  color: var(--ink-secondary);
  font-weight: 500;
}
.vprops__check {
  display: grid;
  place-items: center;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 50%;
}
.vprops__check svg {
  width: 62%;
  height: 62%;
}
.vprops__mark--full .vprops__check {
  background: color-mix(in srgb, var(--tier-held) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tier-held) 48%, transparent);
  color: var(--tier-held);
}
.vprops__mark--start .vprops__check {
  background: color-mix(in srgb, var(--tier-connected) 20%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tier-connected) 48%, transparent);
  color: var(--tier-connected);
}
@media (max-width: 860px) {
  .vprops__grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .vprops__card {
    padding: var(--sp-4);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .vprops__scroll {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
  }
  .vprops__table th,
  .vprops__table td {
    padding: 0.7rem var(--sp-3);
  }
}

/* ============================================================
   BANDS — a case study as a run of full-width sections

   A record that sets `layout.bands` stops being a column of
   blocks and becomes what a product page is: one full-bleed
   ground after another, alternating soft and page, with a single
   pinned-dark band for the results. Each band carries the study's
   own light — two blooms in the hero's sampled tints — and the
   stages the blocks used to draw for themselves (the track's, the
   findings', the orbit's) dissolve into the band, so the cat sits
   on the section rather than on a card inside it.

   Everything inside keeps the container it had: the margins are
   the site's, the rhythm is the band's. Headers step up to the
   display ramp because a band is a chapter and reads from across
   the room; the hero stays the only --step-5 on the page.

   Where the browser can drive an animation from the scroll
   position (Chrome, Edge, Safari), a band's arrival is the scroll
   itself: the blooms travel against it, the header settles in as
   the band enters, the cards rise in order, the cat and the
   portrait float against their copy. Firefox and phones get the
   site's reveal instead, which is the same page standing still.
   ============================================================ */
.band {
  position: relative;
  padding-block: clamp(5rem, 9vw, 8.5rem);
  /* clip, never hidden: hidden makes the band a scroll container and
     unpins every sticky stage inside it */
  overflow: clip;
  isolation: isolate;
  color: var(--ink);
}
.band--soft {
  background: var(--bg-soft);
}
.band--page {
  background: var(--bg);
}
/* The bands pinned dark in both themes: the feature band, and any band
   with a shader ground. The whole set is redeclared, never gated on
   [data-theme], so the ring, the glass and the shadows invert with the
   ground — the same move the about band makes. */
.band--feature,
.band--fx {
  --bg: #0a0a0c;
  --bg-soft: #151518;
  --ink: #f5f5f7;
  --ink-secondary: #a5a5ae;
  --glass: rgba(21, 21, 24, 0.6);
  --glass-strong: rgba(21, 21, 24, 0.8);
  --glass-border: rgba(245, 245, 247, 0.1);
  --shadow-card: 0 12px 40px rgba(0, 0, 0, 0.5);
  --shadow-card-hover: 0 24px 60px rgba(0, 0, 0, 0.6);
  --shadow-pop: 0 32px 80px rgba(0, 0, 0, 0.7);
  --edge-far: rgba(245, 245, 247, 0.16);
  --edge-mid: rgba(245, 245, 247, 0.05);
  --edge-near: rgba(245, 245, 247, 0.36);
  background: var(--bg);
  color-scheme: dark;
}
.band__inner {
  position: relative;
  z-index: 1;
  display: grid;
  gap: clamp(3rem, 6vw, 5.5rem);
}
/* the blocks carried their own block padding for the column layout; the
   band owns the rhythm now */
.band__inner > * {
  padding-block: 0;
}
.band .figure-inline {
  margin-block: 0;
}

/* ------------------------------ the light ------------------------- */
.band__bloom {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.band__bloom i {
  position: absolute;
  width: min(64rem, 80vw);
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(110px);
  opacity: 0.26;
}
.band__bloom i:nth-child(1) {
  top: -30%;
  left: -18%;
  background: radial-gradient(circle, var(--tint1, var(--aurora-violet)), transparent 66%);
  animation: aurora-drift-a 34s ease-in-out infinite alternate;
}
.band__bloom i:nth-child(2) {
  bottom: -35%;
  right: -16%;
  width: min(52rem, 70vw);
  background: radial-gradient(circle, var(--tint3, var(--aurora-pink)), transparent 66%);
  animation: aurora-drift-b 38s ease-in-out infinite alternate;
}
/* every other band swaps its pair, so the light walks down the page
   rather than sitting in the same two corners of every section */
.band:nth-of-type(even) .band__bloom i:nth-child(1) {
  left: auto;
  right: -18%;
  background: radial-gradient(circle, var(--tint2, var(--aurora-cyan)), transparent 66%);
}
.band:nth-of-type(even) .band__bloom i:nth-child(2) {
  right: auto;
  left: -16%;
}
.band--page .band__bloom i {
  opacity: 0.16;
}
.band--feature .band__bloom i {
  opacity: 0.5;
}
/* A shader ground: the canvas band-fx.js draws on is the band's whole
   light, filling it edge to edge under the content. The ground under the
   canvas is the shader's own black, so the frame before the first paint
   and a device with no WebGL both read as the same dark band. */
.band--fx {
  background: #06030f;
}
.band__fx {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* A scrim over the ground, so the header and the cards sit on night
   rather than on the light. Weighted to the top over the waves, where the
   words are and where the haze is brightest, easing off toward the foot
   where the cards carry their own glass; an even night over the pillar,
   whose light stands in the middle of the band behind everything. */
.band--fx::before {
  content: "";
  position: absolute;
  inset: 0;
  /* one above the canvas, which follows it in the tree and would paint
     over it at the same level; the content sits at one as well and comes
     later, so it stays on top */
  z-index: 1;
  background: linear-gradient(180deg, rgba(6, 3, 15, 0.72) 0%, rgba(6, 3, 15, 0.4) 40%, rgba(6, 3, 15, 0.22) 100%);
  pointer-events: none;
}
.band--fx[data-fx="light-pillar"]::before {
  background: linear-gradient(180deg, rgba(6, 3, 15, 0.66) 0%, rgba(6, 3, 15, 0.5) 45%, rgba(6, 3, 15, 0.6) 100%);
}
/* the library screens the pillar over its ground */
.band--fx[data-fx="light-pillar"] .band__fx {
  mix-blend-mode: screen;
}
/* a lede on a shader ground reads in the band's ink, which is white on
   every one of them, rather than the secondary grey the ledes elsewhere
   sit in: grey on a moving violet haze fell under contrast */
.band--fx .principles__lede,
.band--fx .amatrix__lede,
.band--fx .plan__lede,
.band--fx .optrees__lede,
.band--fx .outcomes__lede,
.band--fx .chapter__body {
  color: var(--ink);
}
/* A band whose ground is footage. The poster and the clip fill it edge
   to edge under the content, covered and held to the middle; the clip
   plays once and holds, and under reduced motion only the poster shows.
   The footage is light, so the band keeps the reader's theme for its
   cards and holds only the header and the lede in dark ink, because
   those two sit on the film itself. */
.band--film {
  background: var(--bg);
}
.band__film {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.band__poster,
.band__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
}
.band--film .principles__title,
.band--film .principles__lede,
.band--film .track__title,
.band--film .chapter__title {
  color: #0a0a0c;
}
@media (prefers-reduced-motion: reduce) {
  .band__video {
    display: none;
  }
}
/* the fogged glass over a moving ground: a little more ground in the
   glass, so the words stay on a surface rather than on the streaks */
.band--fx .glassnode {
  background: rgba(21, 21, 24, 0.66);
}
.band--fx .track__icon,
.band--fx .plan__icon {
  background: rgba(245, 245, 247, 0.08);
}

/* ------------------------ the stages dissolve --------------------- */
/* the filmed track has no panel to dissolve, and this rule would take its
   container padding with it — which ran its cards to the stage edge and
   moved where the canoe "arrives" to the last frame of the footage */
.band .track:not(.track--film) .track__stage,
.band .findings__stage {
  padding: 0;
  border-radius: 0;
  overflow: visible;
  background: transparent;
  box-shadow: none;
}
.band .orbit__stage {
  border-radius: 0;
  overflow: visible;
  background: transparent;
  box-shadow: none;
}
.band .track__stage > .stage__aurora,
.band .findings__stage > .stage__aurora,
.band .orbit__stage > .stage__aurora {
  display: none;
}
.band .orbit__cards {
  padding: 0;
}

/* A drawn figure keeps its stage on a column page, where the panel is what
   gives it a ground. On a band the band is the ground, and a panel inside
   it is exactly the card-inside-a-section the bands exist to remove — so
   the stage dissolves and the figure stands on the section, its caption
   rule and its nodes as they were. */
.band .figure-block .stage {
  padding: 0;
  border-radius: 0;
  overflow: visible;
  background: transparent;
  box-shadow: none;
}
.band .figure-block .stage > .stage__aurora {
  display: none;
}

/* ------------------------------ the headers ----------------------- */
.band .track__title,
.band .feedback__title,
.band .findings__title,
.band .storyboard__title,
.band .hypo__title,
.band .bento__title,
.band .amatrix__title,
.band .principles__title,
.band .optrees__title,
.band .plan__title-main,
.band .outcomes__title,
.band .journey__title,
.band .vprops__title,
.band .dbento .chapter__title {
  max-width: 16ch;
  margin-bottom: var(--sp-6);
  font-size: var(--step-4);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.band .chapter__title {
  max-width: 14ch;
  margin-bottom: var(--sp-5);
  font-size: var(--step-3);
  line-height: 1.04;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.band .chapter--aside .chapter__title {
  max-width: 12ch;
  margin-bottom: 0;
}
/* Every chapter reads at body size. A one-paragraph chapter used to step
   up to --step-1 as a lede, which held while every chapter on the page was
   one paragraph; the moment a record paired a one-paragraph Overview with
   a three-paragraph Context the two halves of the same row ran at 19px
   and 16px, and the rule read as a mistake rather than as emphasis. The
   chapter title carries the hierarchy; the paragraphs share one size. */
.band .ledger__lede {
  font-weight: 500;
}
/* the heads that sit in a row beside their lede own no margin of their
   own: the row does */
.band .amatrix__head .amatrix__title,
.band .principles__head .principles__title,
.band .plan__lead .plan__title-main {
  margin-bottom: 0;
}
/* The pair's headers keep their own length. The 14ch cap is right for a
   one-word chapter title and wrong for "Context & Constraints", which it
   broke across two lines in a column with room for it. */
.band .pair .chapter__title {
  max-width: none;
}
.band .results__title {
  font-size: var(--step-3);
  letter-spacing: -0.035em;
}
/* The drawn mark, tucked against the top-left corner of the quote.

   Both offsets are bounded rather than generous, and that is the whole
   design of them. The band clips, and the stage runs the full width of
   its container once the viewport drops under about 1200px — so a mark
   hanging 4rem to the left had nothing to hang over and lost half of
   itself to the clip. 1rem is inside the container padding at every
   width. Upward it is held under the 3rem floor of the band gap above,
   because the results card paints its own ground and a mark reaching
   into it is simply cut in half. */
.band .attest .voices__glyph {
  width: clamp(8rem, 10vw, 11rem);
  top: -2rem;
  left: -1rem;
}

/* --------------------- every card answers the pointer ------------- */
.feedback__voice,
.ledger__item,
.orbit__card {
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.feedback__voice:hover,
.ledger__item:hover,
.orbit__card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.bento__tile,
.storyboard__shot,
.split__figure {
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease);
}
.bento__tile:hover,
.storyboard__shot:hover,
.split__figure:hover {
  box-shadow: var(--shadow-card-hover);
}
.storyboard__shot img,
.split__figure img,
.bento__shot img {
  transition: transform 0.5s var(--ease);
}
.storyboard__shot:hover img,
.split__figure:hover img {
  transform: scale(1.03);
}
.bento__tile:hover .bento__shot img {
  transform: translateY(-3px) scale(1.02);
}
/* the picture tiles lean toward the pointer like the work grid; tilt.js
   publishes the angles and the shared transform in the tilt block above
   composes them with the lift */
@media (hover: hover) and (min-width: 861px) {
  .bento__tile:hover,
  .storyboard__shot:hover,
  .split__figure:hover {
    --lift: -4px;
  }
}
@media (max-width: 860px) {
  .bento__tile:hover,
  .storyboard__shot:hover,
  .split__figure:hover {
    transform: translateY(-4px);
  }
}

/* ------------------------------ the rail -------------------------- */
/* The about page's scroller, fixed to the page's right edge with a tick
   per band. Desktop only: on a phone the bands are the whole width and a
   rail would sit on the words. */
.rail--page {
  position: fixed;
  top: 0;
  bottom: 0;
  right: clamp(0.9rem, 1.6vw, 1.6rem);
  z-index: 70;
  display: none;
  transition: opacity var(--t-micro) var(--ease);
}
@media (min-width: 1200px) {
  .rail--page {
    display: flex;
  }
}
/* before the first band the rail is over the hero and has nothing to
   point at yet */
.rail--page.is-off {
  opacity: 0.55;
}
/* over the pinned-dark band the ticks would be ink on ink, so the rail
   takes the dark set while it is there */
.rail--page[data-over="feature"] {
  --ink: #f5f5f7;
  --bg-soft: #151518;
  --glass-strong: rgba(21, 21, 24, 0.8);
  --glass-border: rgba(245, 245, 247, 0.1);
}
.rail__arrow:disabled {
  opacity: 0.35;
  cursor: default;
  transform: none;
}
.rail__arrow:disabled:hover {
  background: var(--glass-strong);
}

/* ------------------------ phones and stillness -------------------- */
@media (max-width: 860px) {
  .band {
    padding-block: clamp(3.5rem, 12vw, 5rem);
  }
  .band__inner {
    gap: var(--sp-6);
  }
  .band__bloom i {
    width: 34rem;
    filter: blur(60px);
  }
  .band__bloom i:nth-child(2) {
    width: 28rem;
  }
  .band .attest__quote {
    font-size: var(--step-2);
  }
  /* the prototype embed is desktop-only, and a band with nothing in it
     would be a blank stripe on a phone */
  .band:has(> .band__inner > .protoembed:only-child) {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .band__bloom i {
    animation: none;
  }
}

/* ------------------------ the scroll drives arrival --------------- */
/* Progressive: every declaration below is inside @supports, so a browser
   without scroll-driven animation keeps the site's reveal and the bands
   stand still. Off on phones (a blurred layer moving on every frame is
   the compositing budget) and under reduced motion. Positions animate
   through `translate` and `scale`, never `transform`, so a card's hover
   lift — which is a transform — composes with its arrival instead of
   being overwritten by a filled animation. */
@keyframes band-lift {
  from { translate: 0 12%; }
  to { translate: 0 -12%; }
}
@keyframes band-head {
  from { opacity: 0; translate: 0 28px; scale: 0.96; }
  to { opacity: 1; translate: 0 0; scale: 1; }
}
@keyframes band-rise {
  from { opacity: 0; translate: 0 32px; scale: 0.97; }
  to { opacity: 1; translate: 0 0; scale: 1; }
}
@keyframes band-float {
  from { translate: 0 6%; }
  to { translate: 0 -6%; }
}
@keyframes band-cover {
  from { translate: 0 -5%; scale: 1.12; }
  to { translate: 0 5%; scale: 1.12; }
}
@supports (animation-timeline: view()) {
  @media (min-width: 861px) and (prefers-reduced-motion: no-preference) {
    .band {
      view-timeline-name: --band;
      view-timeline-axis: block;
    }
    /* the reveal stands down inside a band; the scroll is the entrance */
    .band .reveal {
      opacity: 1;
      transform: none;
      transition: none;
    }
    .band__bloom {
      animation: band-lift linear both;
      animation-timeline: --band;
      animation-range: entry 0% exit 100%;
    }
    .band .track__title,
    .band .feedback__title,
    .band .findings__title,
    .band .storyboard__title,
    .band .hypo__title,
    .band .bento__title,
    .band .optrees__title,
    .band .plan__title-main,
    .band .outcomes__title,
    .band .journey__title,
    .band .vprops__title,
    .band .vprops__scroll,
    .band .dbento .chapter__title,
    .band .plan__panel,
    .band .chapter__title,
    .band .results__card,
    .band .attest__figure,
    .band .protoembed__frame,
    .band .figure-inline,
    .band .figure-block > * {
      animation: band-head linear both;
      animation-timeline: view();
      animation-range: entry 5% entry 45%;
    }
    /* the sets arrive one card at a time, each starting a little later
       than the one before it — the stagger is the index the markup carries */
    .band .track__step,
    .band .feedback__voice,
    .band .finding,
    .band .storyboard__cell,
    .band .hypo__card,
    .band .ledger__item,
    .band .bento__cell,
    .band .plan__item,
    .band .dbento__card,
    .band .outcomes__card,
    .band .journey__cell,
    .band .vprops__tier,
    .band .orbit__card {
      animation: band-rise linear both;
      animation-timeline: view();
      animation-range: entry calc(var(--i, 0) * 5%) entry calc(38% + var(--i, 0) * 5%);
    }
    /* the pinned artwork floats against the words scrolling past it */
    .band .orbit__cat img {
      animation: band-float linear both;
      animation-timeline: --band;
      animation-range: entry 0% exit 100%;
    }
    .band .feedback__portrait img,
    .band .feedback__portrait video,
    .band .split__figure img,
    .band .results__scene img {
      animation: band-cover linear both;
      animation-timeline: --band;
      animation-range: entry 0% exit 100%;
    }
  }
}

/* ------------------------------------------------------------
   The discovery band, filmed.

   A record whose discovery block names a frame set gets footage
   behind its cards, scrubbed by the scroll the way the about
   page's astronaut is. The band grows tall and the stage pins for
   the length of it, so the reader's scroll through the band is the
   film's timeline and a slow read is a slow film. The band's height
   is set by the script from the number of frames it will show,
   because the run ends where the cards end and that is a different
   frame on every width.

   The frame is anchored to the bottom of the stage: the scene's
   ground sits on the section's ground, and the header and the cards
   sit in the sky. The poster is a real image under the canvas and
   is the whole of the treatment on a phone, under reduced motion,
   and until the first frame lands.

   Nothing is laid over the footage. The header is ink on the sky —
   pinned to ink in both themes, because the sky is the same off-white
   in both and the words have to hold their contrast on it alone —
   and the cards are the glassnodes they already were, real glass,
   since this is the page and not the deck's 3D rail.
   ------------------------------------------------------------ */
.track--film {
  position: relative;
  isolation: isolate;
  min-height: 90vh;
  min-height: 90dvh;
  display: grid;
  align-content: center;
  overflow: hidden;
}
/* the band that holds a film gives up its own padding and rhythm: the
   stage owns both, and the footage runs to every edge */
.band:has(> .band__inner > .track--film) {
  padding-block: 0;
}
.band:has(> .band__inner > .track--film) > .band__inner {
  display: block;
}
.band:has(> .band__inner > .track--film) > .band__bloom {
  display: none;
}
/* Pinned. The band is tall and the stage rides the viewport for all of
   it, which is what turns scroll distance into film time. The height
   lives on the inner rather than the band, because a sticky child is
   bounded by its containing block, and a grid item's block would
   otherwise be exactly the stage's own height with nowhere to travel.
   The script sets the real height from the run; this is the floor it
   starts from. The stage is nine tenths of the screen, and it sits in
   the middle of it, so the band's own ground shows above and below like
   a letterbox as the film plays. */
.band.is-film > .band__inner {
  min-height: 300vh;
}
.track--film.is-enhanced {
  position: sticky;
  top: 5vh;
  top: 5dvh;
  height: 90vh;
  height: 90dvh;
  min-height: 0;
}

.track__film {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--bg);
}
.track__poster,
.track__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* anchored to the foot: the frame's ground on the section's ground */
  object-position: 50% 100%;
}
.track__canvas {
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.track__canvas.is-live {
  opacity: 1;
}

/* The words, centred in the stage as one block — centred on the sky
   rather than on the frame. The mountain rises from the left edge to a
   quarter of the way down the picture, and a title centred on the whole
   frame put its first word on that navy; the extra foot of padding lifts
   the block so the title clears the ridge and the cards, which carry
   their own glass, are what pass in front of it. */
.track--film .track__stage {
  position: relative;
  z-index: 1;
  padding-block: var(--sp-6) calc(var(--sp-6) + 20vh);
}
/* ink on the sky in both themes: the picture does not change with the
   theme, so neither can the one thing set directly on it */
.track--film .track__title {
  color: #0a0a0c;
}
/* Five in a row, always, on the stage: a second row would push the cards
   into the ground of the scene, and the film only runs where the stage is
   wide enough for one. The cards are the glassnodes they were, with the
   blur turned up a notch because the ground behind them moves. */
.track--film .track__steps {
  grid-template-columns: repeat(var(--steps, 5), minmax(0, 1fr));
}
.track--film .track__card {
  -webkit-backdrop-filter: blur(28px) saturate(1.5);
  backdrop-filter: blur(28px) saturate(1.5);
}
/* the count is sunk into moving light now, and a touch more of it keeps
   the motif from vanishing on a bright frame */
.track--film .track__ghost {
  opacity: 0.1;
}

/* a static stage — a phone, reduced motion, a canvas that never came —
   keeps the poster as its ground and lets the cards wrap as they need */
.track--film:not(.is-enhanced) .track__steps {
  grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
}
/* The video band: the footage fills the stage like a frame would, anchored
   to the foot so the water stays on the ground of the section, and plays
   on its own. The stage takes its natural height — there is no run to
   scroll through — and above 1100px the cards keep their one row, which
   is the only width the film variant ever showed them in. */
.track--video .track__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 100%;
}
.track--video .track__stage {
  padding-block: var(--sp-6) calc(var(--sp-6) + 14vh);
}
/* stated at the film's own weight: the static-film rule above is three
   classes deep and would otherwise wrap these into auto-fit */
@media (min-width: 1101px) {
  .track--film.track--video .track__steps {
    grid-template-columns: repeat(var(--steps, 5), minmax(0, 1fr));
  }
}
@media (prefers-reduced-motion: reduce) {
  .track--video .track__video {
    display: none;
  }
}
@media (max-width: 860px) {
  .track--film {
    min-height: 0;
  }
  .track--film .track__stage {
    padding-block: var(--sp-7);
  }
  .track--film .track__card {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: color-mix(in srgb, var(--bg-soft) 92%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .track__canvas {
    transition: none;
  }
}
