/* ------------------------------------------------------------------ *
 * Nyansora — layout, chrome and the scrollytelling stage
 * ------------------------------------------------------------------ */

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

html {
  /* Lenis drives scrolling; native smooth would fight it. */
  scroll-behavior: auto;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  font-feature-settings: 'ss01' 1, 'cv05' 1, 'tnum' 1;
}

h1,
h2,
h3,
p,
figure {
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
  border-radius: 3px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 200;
  padding: 10px 16px;
  background: var(--acc);
  color: var(--acc-ink);
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 500;
  transform: translateY(-160%);
  transition: transform var(--t-fast) var(--ease);
}
.skip-link:focus-visible {
  transform: none;
}

/* ------------------------------------------------------------------ *
 * Typographic primitives
 * ------------------------------------------------------------------ */

.label {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-weight: 500;
  color: var(--ink-3);
}

.display {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
  font-weight: 500;
  text-wrap: balance;
}

.h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-h);
  letter-spacing: var(--tracking-h);
  font-weight: 500;
  text-wrap: balance;
}

.dim {
  color: var(--ink-2);
}

/* ------------------------------------------------------------------ *
 * Full-frame atmospherics: vignette, edge defocus, film grain.
 * Together these are what make the canvas read as a photograph of a
 * model rather than a WebGL viewport.
 * ------------------------------------------------------------------ */

.atmos {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10;
}

.atmos--dof {
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  -webkit-mask-image: radial-gradient(
    ellipse 58% 54% at 50% 46%,
    rgba(0, 0, 0, 0) 52%,
    rgba(0, 0, 0, 1) 100%
  );
  mask-image: radial-gradient(
    ellipse 58% 54% at 50% 46%,
    rgba(0, 0, 0, 0) 52%,
    rgba(0, 0, 0, 1) 100%
  );
}

.atmos--vignette {
  background: radial-gradient(
      ellipse 76% 70% at 50% 44%,
      rgb(var(--bg-rgb) / 0) 40%,
      rgb(var(--bg-rgb) / 0.55) 78%,
      rgb(var(--bg-rgb) / 0.92) 100%
    ),
    linear-gradient(to bottom, rgb(var(--bg-rgb) / 0.7) 0%, rgb(var(--bg-rgb) / 0) 22%);
}

.atmos--grain {
  opacity: 0.05;
  mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------------------ *
 * Sticky top bar
 * ------------------------------------------------------------------ */

.bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 var(--gutter);
  background: var(--bg-glass);
  backdrop-filter: blur(22px) saturate(130%);
  -webkit-backdrop-filter: blur(22px) saturate(130%);
  /* No bottom rule: the frosted glass itself is the edge. A drawn divider
     read as a gray line cutting the header off from the page. */
  z-index: var(--z-chrome);
}

/*
 * The wordmark is the supplied lockup, not type set in Inter — the 'a' is a
 * database glyph and cannot be reproduced with a font.
 *
 * Sized by CAP HEIGHT rather than box height. The lockup's box includes the
 * descender of the 'y', which is 66 of its 257 viewBox units; sizing on the
 * box would make the letters read smaller than the number suggests and would
 * drift the moment the artwork is re-exported.
 */
.wordmark {
  --wordmark-cap: 15px;
  /* 257 viewBox units tall, of which the cap band is 191. */
  --wordmark-box: calc(var(--wordmark-cap) * 257 / 191);
  display: inline-flex;
  align-items: center;
  color: var(--ink);
  transition: color var(--t-fast) var(--ease),
    opacity var(--t-fast) var(--ease);
}
.wordmark:hover {
  opacity: 0.72;
}

.wordmark__lockup {
  display: block;
  height: var(--wordmark-box);
  width: auto;
  /* Centring the box floats the lockup high, because the descender occupies
     the lower quarter of it and carries no visual weight. Drop it by half the
     descender so the cap band sits on the optical centre line instead. */
  transform: translateY(calc(var(--wordmark-box) * 33 / 257));
}

.wordmark__type {
  fill: currentColor;
}

/* The gold survives on dark; flattening it to white would lose the one part
   of the mark that identifies it. */
.wordmark__accent {
  fill: var(--brand-gold);
}

.bar__right {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

/* Phase indicator pill — "3 · Interconnection" */
.phase-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  height: 32px;
  padding: 0 0.875rem;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: var(--tint-1);
  font-size: var(--fs-sm);
  letter-spacing: -0.005em;
  white-space: nowrap;
  transition: border-color var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease);
}

.phase-pill[data-live='true'] {
  border-color: var(--acc-glow);
  background: var(--acc-ghost);
}

.phase-pill__num {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--acc);
}

.phase-pill__sep {
  color: var(--ink-4);
}

.phase-pill__name {
  color: var(--ink);
}

.bar__link {
  font-size: var(--fs-sm);
  letter-spacing: -0.005em;
  color: var(--ink-2);
  padding: 0.375rem 0;
  border-bottom: 1px solid transparent;
  transition: color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease);
}
.bar__link:hover {
  color: var(--ink);
  border-color: var(--line-2);
}

/*
 * Menu toggle — the two-bar morph, not the three-bar-with-fading-middle one.
 * Apple's own global nav uses exactly this: two bars that rotate and slide
 * to meet dead-center as a symmetric X, nothing fading, nothing left over.
 * A third "middle" bar that fades out is the tell of an imitation.
 */
.burger {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  transition: background-color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease);
}
.burger:hover {
  background: var(--tint-3);
}
.burger:active {
  transform: scale(0.93);
}

.burger__icon {
  position: relative;
  width: 16px;
  height: 16px;
}

.burger__bar {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 1.5px;
  border-radius: 2px;
  background: var(--ink);
  /* Anchored dead-center on the icon box, then walked up/down by the same
     4px so the two bars sit symmetrically either side of the middle. */
  transform: translate(-50%, calc(-50% - 4px));
  transition: transform var(--t-mid) var(--ease);
}
.burger__bar:last-child {
  transform: translate(-50%, calc(-50% + 4px));
}

/* Open: both bars fall back to dead-center and rotate to meet as an X.
   Same offset removed, same pivot point — the two paths are mirror images
   of each other, which is what keeps the crossing point exact. */
.burger[aria-expanded='true'] .burger__bar {
  transform: translate(-50%, -50%) rotate(45deg);
}
.burger[aria-expanded='true'] .burger__bar:last-child {
  transform: translate(-50%, -50%) rotate(-45deg);
}

/* Slide-down menu */
.menu {
  position: fixed;
  inset: var(--bar-h) 0 auto 0;
  z-index: var(--z-menu);
  background: var(--bg-glass-strong);
  backdrop-filter: blur(26px);
  -webkit-backdrop-filter: blur(26px);
  border-bottom: 1px solid var(--line);
  padding: clamp(1.5rem, 4vw, 3rem) var(--gutter) clamp(2rem, 5vw, 3.5rem);
  display: grid;
  gap: 0.25rem;
  transform-origin: top;
  transition: opacity var(--t-mid) var(--ease),
    transform var(--t-mid) var(--ease), visibility var(--t-mid);
}
.menu[hidden] {
  display: grid;
}
.menu[aria-hidden='true'] {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  pointer-events: none;
}
.menu__item {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-h3);
  letter-spacing: var(--tracking-h);
  font-weight: 500;
  text-align: left;
  width: 100%;
  transition: color var(--t-fast) var(--ease);
}
.menu__item:hover {
  color: var(--acc);
}
.menu__item span {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  color: var(--ink-4);
  font-weight: 500;
  min-width: 2.5rem;
}

/* ------------------------------------------------------------------ *
 * The stage — fixed 3D canvas plus the pre-rendered fallback stills
 * ------------------------------------------------------------------ */

.stage {
  position: fixed;
  inset: 0;
  z-index: var(--z-stage);
  background:
    radial-gradient(
      ellipse 70% 55% at 46% 52%,
      var(--bg-pool) 0%,
      rgb(var(--bg-rgb) / 0) 70%
    ),
    var(--bg);
}

.stage__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The pre-rendered presentation of the same scene: two stacked buffers that
   crossfade between ambient loops and transition clips. Slightly overscanned
   so the scroll drift transform never exposes an edge. */
.stage__videos {
  position: absolute;
  inset: 0;
  overflow: hidden;
  will-change: transform;
}
.stage:not([data-mode='video']) .stage__videos {
  display: none;
}
.stage[data-mode='video'] .stage__canvas {
  display: none;
}
.stage__videos video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 240ms var(--ease);
}
.stage__videos video[data-on='true'] {
  opacity: 1;
}

/* Cross-fading still frames. Used when WebGL is unavailable and as the
   reduced-motion presentation. Identical framing to the live scene. */
.stills {
  position: absolute;
  inset: 0;
  display: none;
}
.stage[data-mode='stills'] .stills {
  display: block;
}
.stage[data-mode='stills'] .stage__canvas {
  display: none;
}
.stills__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--t-phase) var(--ease);
}
.stills__img[data-active='true'] {
  opacity: 1;
}

.stage__loader {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--ink-3);
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  transition: opacity var(--t-mid) var(--ease);
}
.stage__loader::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--acc);
  animation: pulse var(--t-loop-pulse) var(--ease) infinite;
}
.stage[data-ready='true'] .stage__loader {
  opacity: 0;
  pointer-events: none;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0.25;
    transform: scale(0.8);
  }
  50% {
    opacity: 1;
    transform: scale(1.15);
  }
}

/* ------------------------------------------------------------------ *
 * Annotation layer — DOM pins projected from 3D anchor points
 * ------------------------------------------------------------------ */

.annot {
  position: fixed;
  inset: 0;
  z-index: var(--z-annot);
  pointer-events: none;
}

.annot__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.annot__leader {
  stroke: var(--line-2);
  stroke-width: 1;
  fill: none;
  vector-effect: non-scaling-stroke;
  transition: opacity var(--t-phase) var(--ease);
}

.pin-group {
  display: contents;
}

/* A pin is a ZERO-SIZE anchor parked exactly on its projected 3D point. The
   dot and the label are absolutely positioned off it, so JavaScript only ever
   has to write one translate — no measuring, no layout thrash. */
.pin {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  pointer-events: none;
  will-change: transform, opacity;
  /* Arriving: wait for the outgoing set to clear, then come in quickly. Both
     groups fading over the same long window put two full sets of cards on the
     model at once, which is the messiest moment of the whole phase change. */
  transition: opacity 260ms var(--ease) 200ms;
}

/* Inactive groups are never projected, so their pins sit parked at the origin
   with the label hanging off the left edge. `visibility` transitions at the
   end of the fade, so it hides them for real without cutting the cross-fade. */
.pin-group[data-visible='false'] .pin {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Leaving: no delay and much faster than arriving, so the labels are gone
     before the incoming ones appear. */
  transition: opacity 160ms var(--ease), visibility 160ms;
}

.pin__dot {
  position: absolute;
  left: -4.5px;
  top: -4.5px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--acc);
  pointer-events: auto;
  box-shadow: 0 0 0 4px var(--acc-halo),
    0 0 14px 2px var(--acc-glow);
  transition: transform var(--t-fast) var(--ease);
}

.pin__body {
  position: absolute;
  left: 74px;
  /* Vertical lead is set per frame so a label near the top of the frame can
     drop below its anchor instead of sliding under the top bar. */
  top: var(--lead-y, -40px);
  transform: translateY(-50%);
  pointer-events: auto;
  display: grid;
  gap: 2px;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg-glass);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  text-align: left;
  /* Closed, the card hugs its title. The box sits in a zero-width containing
     block, so `auto` shrink-to-fits to the widest unbreakable row — the nowrap
     title. A fixed measure here would leave a slab of empty glass beside every
     short title, lying over the model; during a phase change both groups are
     on screen and those slabs are the whole picture. */
  width: auto;
  transition: border-color var(--t-fast) var(--ease),
    background-color var(--t-fast) var(--ease),
    width var(--t-mid) var(--ease);
}

/* Open, it takes a real measure so the explanation wraps to a readable line
   instead of the sliver a short title would impose. _place() reserves this
   width for collisions in every state, so growing cannot reach a neighbour. */
.pin:hover .pin__body,
.pin:focus-visible .pin__body,
.pin[data-open='true'] .pin__body,
.pin[data-measure='true'] .pin__body {
  width: 15rem;
}

.pin[data-side='left'] .pin__body {
  left: auto;
  right: 74px;
}

/* The button box has no size, so the focus ring has to live on the label. */
.pin:focus-visible {
  outline: none;
}
.pin:focus-visible .pin__body {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.pin__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* A rotated square, sized off the cap height — a marker on a drawing rather
   than a bullet in a list. */
.pin__glyph {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  background: var(--acc);
  transform: rotate(45deg);
}

.pin__title {
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: -0.012em;
  line-height: 1.25;
  white-space: nowrap;
}

.pin__detail {
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--ink-2);
  max-width: 22ch;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  /* Indented past the glyph so the card keeps one text edge. */
  padding-left: calc(7px + 0.5rem);
  transition: max-height var(--t-mid) var(--ease),
    opacity var(--t-mid) var(--ease), padding-top var(--t-mid) var(--ease);
}

.pin:hover .pin__dot,
.pin:focus-visible .pin__dot {
  transform: scale(1.35);
}
.pin:hover .pin__body,
.pin:focus-visible .pin__body,
.pin[data-open='true'] .pin__body {
  border-color: var(--acc-dim);
  background: var(--bg-glass-strong);
}
/* max-height is only here because height cannot transition from 0 to auto.
   It is measured per pin (Annotations._measure) rather than guessed, because a
   fixed clamp silently cut the longest explanations off mid-sentence. */
.pin:hover .pin__detail,
.pin:focus-visible .pin__detail,
.pin[data-open='true'] .pin__detail {
  max-height: var(--detail-h, 9rem);
  opacity: 1;
  padding-top: 0.3125rem;
}

/* The stack reserves room for whichever label opens, so an open one cannot
   reach a neighbour. Hover and keyboard focus can still land on two different
   pins at once, though, and then the one being read has to be on top. */
.pin:hover,
.pin:focus-within,
.pin[data-open='true'] {
  z-index: 1;
}

/* Annotations._measure() flips this on to read the open height, so it must
   apply the open geometry unclamped, with no transition to wait out. */
.pin[data-measure='true'] .pin__detail {
  max-height: none;
  opacity: 1;
  padding-top: 0.3125rem;
  transition: none;
}

/* ------------------------------------------------------------------ *
 * Scroll track — spacer panes that drive the phase timeline
 * ------------------------------------------------------------------ */

.track {
  position: relative;
  z-index: var(--z-content);
  pointer-events: none;
}

.pane {
  height: 150vh;
  /* Stable across the iOS URL-bar collapse, so the scroll triggers do not
     shift mid-gesture. Browsers without svh keep the line above. */
  height: 150svh;
}

.intro {
  position: relative;
  z-index: var(--z-content);
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: calc(var(--bar-h) + 6vh) var(--gutter) 22vh;
  pointer-events: none;
}

/* Mirror of .rail::before on the other side of the frame. The live scene pans
   the model clear of the headline, but the pre-rendered stills cannot — this
   keeps the hero readable on the no-WebGL fallback too. */
.intro::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to right,
    rgb(var(--bg-rgb) / 0.92) 0%,
    rgb(var(--bg-rgb) / 0.74) 30%,
    rgb(var(--bg-rgb) / 0) 62%
  );
}

.intro__inner {
  /* Sized so the display line breaks after "is", not before it. */
  max-width: 43rem;
  display: grid;
  gap: 1.5rem;
}

.intro__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
}
@media (min-width: 1081px) {
  /* Let the tagline run past the headline measure rather than break in two. */
  .intro__eyebrow {
    white-space: nowrap;
  }
}
.intro__eyebrow::before {
  content: '';
  width: 22px;
  height: 1px;
  background: var(--acc);
}

.intro__sub {
  max-width: 46ch;
  color: var(--ink-2);
  font-size: var(--fs-body);
}

/* ------------------------------------------------------------------ *
 * Caption rail — one card per phase, cross-fading in place
 * ------------------------------------------------------------------ */

.rail {
  position: fixed;
  right: var(--gutter);
  top: 50%;
  width: var(--rail-w);
  transform: translateY(-50%);
  z-index: var(--z-content);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease);
}
.rail[data-visible='true'] {
  opacity: 1;
}

/* The maquette is lit and pale; caption text laid straight over it is
   unreadable. This pulls the right edge of the frame back down to near-black
   without putting a visible panel on the page. */
.rail::before {
  content: '';
  position: absolute;
  top: -100vh;
  bottom: -100vh;
  right: calc(var(--gutter) * -1);
  left: -15vw;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to left,
    rgb(var(--bg-rgb) / 0.95) 0%,
    rgb(var(--bg-rgb) / 0.9) 36%,
    rgb(var(--bg-rgb) / 0.62) 66%,
    rgb(var(--bg-rgb) / 0) 100%
  );
}

/* Every card occupies the same grid cell, so they cross-dissolve in place
   and the rail is sized by the tallest card rather than the first one. */
.rail__stack {
  display: grid;
}

.cap {
  grid-area: 1 / 1;
  align-self: center;
  display: grid;
  gap: 1rem;
  align-content: start;
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity var(--t-phase) var(--ease),
    transform var(--t-phase) var(--ease), visibility var(--t-phase);
}

.cap[data-active='true'] {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
}

.cap__head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}

.cap__rule {
  flex: 1;
  height: 1px;
  background: var(--line);
}

.cap__title {
  font-size: var(--fs-h3);
  line-height: 1.2;
  letter-spacing: var(--tracking-h);
  font-weight: 500;
  text-wrap: balance;
}

.cap__body {
  color: var(--ink-2);
  font-size: var(--fs-body);
  text-wrap: pretty;
}

.cap__stat {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--line);
}
.cap__stat b {
  font-size: 1.9375rem;
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.cap__stat i {
  font-style: normal;
  font-size: var(--fs-sm);
  color: var(--acc);
}
.cap__stat em {
  font-style: normal;
  font-size: var(--fs-sm);
  color: var(--ink-3);
  margin-left: auto;
  text-align: right;
}

/* ------------------------------------------------------------------ *
 * Phase rail — small dot nav, and the keyboard route between phases
 * ------------------------------------------------------------------ */

.dots {
  position: fixed;
  left: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  z-index: var(--z-content);
  display: grid;
  gap: 0.5rem;
  opacity: 0;
  transition: opacity var(--t-mid) var(--ease);
}
.dots[data-visible='true'] {
  opacity: 1;
}

.dot {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 4px 0;
}
.dot::before {
  content: '';
  width: 18px;
  height: 1.5px;
  background: var(--ink-4);
  border-radius: 2px;
  transition: width var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease);
}
.dot__label {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-4);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
  white-space: nowrap;
}
.dot:hover .dot__label,
.dot:focus-visible .dot__label {
  opacity: 1;
  transform: none;
  color: var(--ink-2);
}
.dot[aria-current='true']::before {
  width: 30px;
  background: var(--acc);
}

/* The active dot deliberately does NOT pin its label open: the top bar already
   names the phase, and a permanent label here collides with left-side pins. */
.dot[aria-current='true']:hover .dot__label,
.dot[aria-current='true']:focus-visible .dot__label {
  color: var(--ink);
}

/* ------------------------------------------------------------------ *
 * Floating chrome — scroll hint and next-phase CTA
 * ------------------------------------------------------------------ */

.hint {
  position: fixed;
  left: var(--gutter);
  bottom: clamp(1.25rem, 3vh, 2rem);
  z-index: var(--z-content);
  display: flex;
  align-items: center;
  gap: 0.625rem;
  color: var(--ink-3);
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  transition: opacity var(--t-mid) var(--ease);
}
.hint[data-visible='false'] {
  opacity: 0;
  pointer-events: none;
}
.hint__bar {
  position: relative;
  width: 34px;
  height: 1px;
  background: var(--line-2);
  overflow: hidden;
}
.hint__bar::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--acc);
  transform: translateX(-100%);
  animation: sweep var(--t-loop-sweep) var(--ease) infinite;
}
@keyframes sweep {
  0% {
    transform: translateX(-100%);
  }
  55%,
  100% {
    transform: translateX(100%);
  }
}

.cta {
  position: fixed;
  right: var(--gutter);
  bottom: clamp(1.25rem, 3vh, 2rem);
  z-index: var(--z-chrome);
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  height: 44px;
  padding: 0 1.125rem;
  border-radius: var(--r-pill);
  border: 1px solid var(--line-2);
  background: var(--bg-glass-strong);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: -0.008em;
  transition: opacity var(--t-mid) var(--ease),
    transform var(--t-mid) var(--ease), border-color var(--t-fast) var(--ease),
    background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.cta:hover {
  border-color: var(--acc);
  color: var(--acc);
}
.cta[data-visible='false'] {
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
}
.cta__arrow {
  transition: transform var(--t-fast) var(--ease);
}
.cta:hover .cta__arrow {
  transform: translateY(2px);
}
.cta--end .cta__arrow {
  transform: rotate(-90deg);
}
.cta--end:hover .cta__arrow {
  transform: rotate(-90deg) translateY(2px);
}

/* ------------------------------------------------------------------ *
 * Mobile legend — pins collapse into a tappable list
 * ------------------------------------------------------------------ */

/* The legend is the same pin nodes, reparented and restyled as chips. */
.legend {
  display: none;
}

.pin-group--legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4375rem;
}
.pin-group--legend[data-visible='false'] {
  display: none;
}

.pin-group--legend .pin {
  position: static;
  width: auto;
  height: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  /* Shrinkable and capped, or a long label pushes the chip off the screen. */
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  padding: 0.4375rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--tint-2);
  pointer-events: auto;
  transform: none !important;
  opacity: 1;
  transition: border-color var(--t-fast) var(--ease),
    background-color var(--t-fast) var(--ease);
}

.pin-group--legend .pin[aria-expanded='true'] {
  flex-basis: 100%;
  border-color: var(--acc-dim);
  background: var(--acc-ghost);
}

/* The chip already carries the diamond marker; the anchor dot exists to sit on
   the model, and there is no model under a legend chip. */
.pin-group--legend .pin__dot {
  display: none;
}

.pin-group--legend .pin__body {
  position: static;
  transform: none;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  /* A chip is sized by the flex row it sits in, not by the floating card's
     fixed measure. */
  width: auto;
}

.pin-group--legend .pin__title {
  font-size: var(--fs-xs);
  white-space: normal;
}

/* Collapsed detail still contributes its intrinsic width, which stretches
   every chip to full width. Take it out of layout until it is opened. */
.pin-group--legend .pin__detail {
  display: none;
  font-size: var(--fs-sm);
  max-height: none;
  opacity: 1;
}
.pin-group--legend .pin[aria-expanded='true'] .pin__detail {
  display: block;
}

/* The floating card's open state (15rem measure, glass fill) outranks the
   legend resets on specificity, so inside a chip it painted a dark slab
   behind the text. Re-assert the chip look at matching specificity. */
.pin-group--legend .pin:hover .pin__body,
.pin-group--legend .pin:focus-visible .pin__body,
.pin-group--legend .pin[data-open='true'] .pin__body,
.pin-group--legend .pin[data-measure='true'] .pin__body {
  width: auto;
  background: none;
  border-color: transparent;
}

.pin-group--legend .pin:focus-visible .pin__body {
  outline: none;
}
/* Fingers, not cursors: Apple's 44pt floor for anything tappable. Desktop
   keeps the tighter chip; only coarse pointers pay the extra height. */
@media (pointer: coarse) {
  .pin-group--legend .pin {
    min-height: 44px;
  }
  .dot {
    padding: 10px 0;
  }
}

.pin-group--legend .pin:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ *
 * Outro
 * ------------------------------------------------------------------ */

.outro {
  position: relative;
  z-index: var(--z-content);
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  align-content: center;
  gap: 2.5rem;
  padding: 16vh var(--gutter) 0;
  background: linear-gradient(
    to bottom,
    rgb(var(--bg-rgb) / 0) 0%,
    rgb(var(--bg-rgb) / 0.88) 26%,
    var(--bg) 55%
  );
}

.outro__inner {
  display: grid;
  gap: 1.75rem;
  max-width: 30ch;
}

.outro__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  height: 46px;
  padding: 0 1.375rem;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 500;
  border: 1px solid transparent;
  transition: background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
    transform var(--t-fast) var(--ease);
}
.btn:hover {
  transform: translateY(-1px);
}
.btn--primary {
  background: var(--acc);
  color: var(--acc-ink);
}
/* Brightened rather than recoloured — the system has exactly one accent, and
   a second hex here would quietly make it two. */
.btn--primary:hover {
  filter: brightness(1.12);
}
.btn--ghost {
  border-color: var(--line-2);
  color: var(--ink);
}
.btn--ghost:hover {
  border-color: var(--ink-3);
}

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-top: 3rem;
}
.fact {
  background: var(--bg);
  padding: 1.5rem 1.25rem;
  display: grid;
  gap: 0.375rem;
}
.fact b {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.fact span {
  font-size: var(--fs-sm);
  color: var(--ink-3);
}

.foot {
  position: relative;
  z-index: var(--z-content);
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding: 2rem var(--gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--fs-sm);
  color: var(--ink-3);
}
.foot nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}
.foot a:hover {
  color: var(--ink);
}

/* ------------------------------------------------------------------ *
 * Responsive — under 1080px the rail becomes a bottom sheet and the
 * annotation pins collapse into a tappable legend.
 * ------------------------------------------------------------------ */

@media (max-width: 1080px) {
  :root {
    --rail-w: auto;
  }

  .bar__link {
    display: none;
  }

  .wordmark {
    --wordmark-cap: 13px;
  }

  .phase-pill {
    height: 28px;
    padding: 0 0.6875rem;
    font-size: var(--fs-xs);
  }

  .annot {
    display: none;
  }

  .dots {
    display: none;
  }

  .atmos--dof {
    -webkit-mask-image: radial-gradient(
      ellipse 76% 34% at 50% 34%,
      rgba(0, 0, 0, 0) 55%,
      rgba(0, 0, 0, 1) 100%
    );
    mask-image: radial-gradient(
      ellipse 76% 34% at 50% 34%,
      rgba(0, 0, 0, 0) 55%,
      rgba(0, 0, 0, 1) 100%
    );
  }

  /* On a phone the maquette has to live in the top third and get out of the
     way. Contain rather than cover, parked high, with a scrim under it so the
     headline sits on near-black instead of on gravel. */
  /* Give the maquette the top 62% at full bleed and fade it out before the
     caption sheet. Contain would letterbox it down to a postage stamp on a
     tall phone. */
  .stills__img {
    height: 62vh;
    object-fit: cover;
    object-position: 50% 42%;
    -webkit-mask-image: linear-gradient(to bottom, #000 68%, transparent 97%);
    mask-image: linear-gradient(to bottom, #000 68%, transparent 97%);
  }

  .intro {
    align-content: end;
    padding-bottom: 15vh;
  }

  .intro::before {
    content: '';
    position: absolute;
    inset: 32% 0 0 0;
    z-index: -1;
    pointer-events: none;
    background: linear-gradient(
      to bottom,
      rgb(var(--bg-rgb) / 0) 0%,
      rgb(var(--bg-rgb) / 0.82) 32%,
      var(--bg) 72%
    );
  }

  .pane {
    height: 130vh;
  }

  .rail::before {
    display: none;
  }

  .rail {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    transform: none;
    /* Room at the foot for the fixed CTA to sit over. */
    padding: 3.5rem var(--gutter) calc(4.75rem + env(safe-area-inset-bottom));
    background: linear-gradient(
      to top,
      var(--bg) 42%,
      rgb(var(--bg-rgb) / 0.92) 68%,
      rgb(var(--bg-rgb) / 0) 100%
    );
  }

  .cap {
    gap: 0.75rem;
  }

  .cap__body {
    font-size: 0.9375rem;
  }

  .cap__stat b {
    font-size: 1.375rem;
  }

  .legend {
    display: block;
    padding-bottom: 0.25rem;
  }

  /* Stays fixed — going static would drop it out of the sticky chrome and
     into the document flow just above the footer. */
  .cta {
    left: var(--gutter);
    right: var(--gutter);
    bottom: calc(1rem + env(safe-area-inset-bottom));
    justify-content: center;
    height: 46px;
  }
}

/* Short viewports — small phones and phone landscape. The caption sheet will
   happily eat the whole screen and leave no diorama, which defeats the point
   of the page, so everything in it gets tighter rather than taller. */
@media (max-width: 1080px) and (max-height: 720px) {
  .stills__img {
    height: 48vh;
  }

  /* A shorter sheet compresses the scrim, so the phase label ends up in the
     transparent part of it. Bring the opaque stop up. */
  .rail {
    padding-top: 2rem;
    background: linear-gradient(
      to top,
      var(--bg) 56%,
      rgb(var(--bg-rgb) / 0.94) 82%,
      rgb(var(--bg-rgb) / 0) 100%
    );
  }

  .cap {
    gap: 0.5rem;
  }

  .cap__title {
    font-size: 1.0625rem;
  }

  .cap__body {
    font-size: var(--fs-sm);
    line-height: 1.45;
  }

  .cap__stat {
    padding-top: 0.5rem;
  }

  .cap__stat b {
    font-size: 1.125rem;
  }

  .pin-group--legend {
    gap: 0.3125rem;
  }

  .pin-group--legend .pin {
    padding: 0.3125rem 0.625rem;
  }

  .legend {
    padding-bottom: 0;
  }
}

/* Phone landscape: the caption goes to two columns so it stays short. */
@media (max-width: 1080px) and (orientation: landscape) {
  .rail {
    padding-bottom: calc(3.75rem + env(safe-area-inset-bottom));
  }

  /* Every child is placed explicitly — auto-placement puts the stat row and
     the legend in the same cell and they draw on top of each other. */
  .cap {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    grid-template-rows: auto auto 1fr;
    column-gap: 2rem;
    row-gap: 0.5rem;
    align-items: start;
  }

  .cap__head {
    grid-area: 1 / 1;
  }
  .cap__title {
    grid-area: 2 / 1;
  }
  .cap__body {
    grid-area: 3 / 1;
  }
  .cap__stat {
    grid-area: 2 / 2;
    border-top: 0;
    padding-top: 0;
  }
  .legend {
    grid-area: 3 / 2;
  }
}

/* ------------------------------------------------------------------ *
 * Reduced motion — discrete phase steps, no continuous animation.
 * The scene still renders; it simply stops moving between beats.
 * ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .cap {
    transform: none;
  }

  .hint__bar::after {
    transform: translateX(0);
    animation: none;
  }

  .stage__loader::before {
    animation: none;
    opacity: 1;
  }
}
