/* ==========================================================================
   STYLES — layout and components. Everything references tokens.css.
   ========================================================================== */

@import url('tokens.css');

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

html { -webkit-text-size-adjust: 100%; }

body {
  min-height: 100svh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  transition: background var(--dur-base), color var(--dur-base);
}

img, canvas, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input { font: inherit; color: inherit; }

/* Focus is gold everywhere. The dark inner ring means it survives on top of
   the canvas layers as well as on flat ground. */
:focus-visible {
  outline: 2px solid var(--data);
  outline-offset: 3px;
  border-radius: var(--radius);
  box-shadow: 0 0 0 3px var(--bg);
}
:focus:not(:focus-visible) { outline: none; }

.skip {
  position: absolute; top: var(--s-8); left: var(--s-8); z-index: 200;
  padding: var(--s-12) var(--s-16);
  background: var(--bg); border: 1px solid var(--line);
  color: var(--ink); text-decoration: none;
  font-family: var(--font-mono); font-size: var(--step--1);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip:focus { transform: translateY(0); }

/* Screen-reader-only. This was referenced from app.js and never defined, which
   is why "(opens in a new tab)" rendered visibly and pushed the build cards
   past their grid track. */
.visually-hidden:not(:focus):not(:active) {
  clip-path: inset(50%);
  block-size: 1px; inline-size: 1px;
  overflow: hidden; position: absolute; white-space: nowrap;
}

/* ---------- type primitives ---------- */
.display {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.08;
  color: var(--ink);
}
.label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
}
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--low); }

/* ---------- shell ---------- */
.app { position: relative; min-height: 100svh; }
[hidden] { display: none !important; }

/* ---------- landing ---------- */
.landing { position: relative; min-height: 100svh; overflow: hidden; }

.landing__canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0;
  transition: opacity var(--dur-fade) var(--ease-out);
}
.landing__canvas.is-in { opacity: 1; }

.landing__inner {
  position: relative; z-index: 2;
  min-height: 100svh;
  display: flex; flex-direction: column;
  padding: var(--s-24) clamp(var(--s-20), 4vw, var(--s-40));
  /* Keep the sentence clear of the Explore zone on the right. */
  padding-inline-end: clamp(16rem, 30vw, 26rem);
  pointer-events: none;
}
.landing__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-16); flex-wrap: wrap;
}
.landing__row--foot { align-items: flex-end; }
.landing__sentence {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--step-3);
  line-height: 1.07;
  letter-spacing: -0.022em;
  max-width: 18ch;
  margin: auto 0;
}
.landing__controls { display: flex; align-items: center; gap: var(--s-16); pointer-events: auto; }
.landing__controls .muted { color: var(--mid); }
.landing .link, .landing .seg { pointer-events: auto; }

/* ---------- explore: middle right, sphere turning behind it ----------
   Merges the two requests: the transparent rectangle and hairline from the
   rail version, sitting in front of the slowly rotating point-sphere from the
   Codrops entry screen. The zone positions and sizes both; the button is the
   only part that takes a click.
   ------------------------------------------------------------------------- */
.explore-zone {
  position: absolute;
  /* Pulled in off the right edge — it is the subject, not a corner control. */
  inset-inline-end: clamp(2rem, 11vw, 9rem);
  inset-block-start: 50%;
  transform: translateY(-50%);
  z-index: 4;
  inline-size: clamp(18rem, 36vw, 31rem);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  pointer-events: none;
}

/* Knocks the grain field back under the sphere so the network has contrast to
   work against. Radial, so there is no visible edge. */
.explore-zone::before {
  content: '';
  position: absolute; inset: -12%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--bg) calc(var(--sphere-veil) * 100%), transparent) 0%,
    color-mix(in srgb, var(--bg) calc(var(--sphere-veil) * 60%), transparent) 52%,
    transparent 72%);
  pointer-events: none;
}

/* The sphere fills the zone and sits behind the block. */
.explore-zone__sphere {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
}

.explore {
  position: relative;
  z-index: 2;
  pointer-events: auto;
  display: grid;
  justify-items: center;
  gap: var(--s-8);
  padding: var(--s-16);
  background: transparent;
  border: 0;
  transition: transform var(--dur-base) var(--ease-out);
}
.explore:hover { transform: scale(1.03); }
.explore:active { transform: scale(0.98); }

/* A gold bar under the word — the device from the reference, where a solid
   label sits beneath the display type over the network. */
.explore__line {
  order: 3;
  inline-size: clamp(6rem, 11vw, 9rem);
  block-size: 3px;
  background: var(--seam);
  transition: inline-size var(--dur-base) var(--ease-out);
}
.explore:hover .explore__line { inline-size: clamp(8rem, 14vw, 12rem); }


.explore__word {
  font-family: var(--font-mono);
  font-size: clamp(1.3rem, 2.6vw, 2.1rem);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  /* the tracking adds a trailing gap; pull it back so the word optically centres */
  text-indent: 0.24em;
  color: var(--ink);
  transition: color var(--dur-fast);
}
.explore__arrow {
  order: 4;
  font-family: var(--font-mono);
  font-size: 1.2rem; line-height: 1;
  color: var(--data);
  transition: transform var(--dur-base) var(--ease-out);
}
.explore:hover .explore__word, .explore:focus-visible .explore__word { color: var(--data); }
.explore:hover .explore__arrow, .explore:focus-visible .explore__arrow { transform: translateY(4px); }

/* ---------- entry transition ---------- */
.curtain {
  position: fixed; inset: 0; z-index: 60;
  background: var(--seam); pointer-events: none;
  transform: scale(0); transform-origin: 88% 50%; border-radius: 50%;
}
.curtain.is-in  { animation: curtain-in  280ms var(--ease-in-out) forwards; }
.curtain.is-out { animation: curtain-out 260ms var(--ease-out) forwards; }
@keyframes curtain-in  { from { transform: scale(0); border-radius: 50%; }
                         to   { transform: scale(2.8); border-radius: 0; } }
@keyframes curtain-out { from { transform: scale(2.8); opacity: 1; }
                         to   { transform: scale(2.8); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .curtain.is-in, .curtain.is-out { animation: none; transform: scale(0); }
}

/* ---------- controls ---------- */
.link {
  font-family: var(--font-mono); font-size: var(--step--2);
  text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--ink); text-decoration: none;
}
.link--data { color: var(--data); }
.link:hover { color: var(--data); }

.seg {
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--control-line); border-radius: var(--radius);
  background: var(--control-bg); overflow: hidden;
}
.seg button {
  display: inline-flex; align-items: center; gap: var(--s-6);
  padding: var(--s-8) var(--s-12);
  /* Not 10px/400. Contrast was never the problem here — optical weight was.
     Uppercase mono at 10px with 0.13em tracking is thin, and light-on-dark
     always reads thinner than the same weight dark-on-light. Bigger, heavier,
     and tracked tighter so the letters cohere into a word. */
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  /* No opacity. Opacity multiplies against whatever is behind the control,
     so the same value lands differently in each theme and over the moving
     canvas. Full ink for the unselected half: 17.5:1 dark, 15.8:1 light. */
  color: var(--ink);
  transition: background var(--dur-fast), color var(--dur-fast);
}
/* A divider, so it reads as one control with two halves rather than as two
   separate things that happen to be adjacent. */
/* A state indicator that does not depend on fill contrast. Even if the
   background fails to paint — stale cache, forced colours, a printer — the
   filled square still says which option is active. Belt and braces on a
   control that has now failed twice. */
.seg button::before {
  content: ''; width: 8px; height: 8px; flex: 0 0 8px;
  border: 1px solid currentColor; background: transparent;
}
.seg button[aria-pressed='true']::before { background: currentColor; }
.seg button + button { border-left: 1px solid var(--control-line); }
.seg button:hover { background: var(--wash); }
.seg button[aria-pressed='true'] { background: var(--control-active-bg); color: var(--control-active-ink); }

/* Literal fallbacks. Same values as the tokens above, no var() indirection. */
[data-theme='dark'] .seg button { color: #f0eef7; }
[data-theme='dark'] .seg button[aria-pressed='true'] { background: #ffc93c; color: #06070d; }
[data-theme='light'] .seg button { color: #14110e; }
[data-theme='light'] .seg button[aria-pressed='true'] { background: #6b4a00; color: #edebe6; }

/* Forced-colours mode (Windows high contrast) throws away every colour above.
   ButtonText / Highlight are the system keywords that survive it. */
@media (forced-colors: active) {
  .seg button { color: ButtonText; border-color: ButtonText; }
  .seg button[aria-pressed='true'] { background: Highlight; color: HighlightText; forced-color-adjust: none; }
  .seg button::before { border-color: ButtonText; }
}

.range { -webkit-appearance: none; appearance: none; width: 88px; height: 14px; background: transparent; cursor: ew-resize; }
/* Explicit, not currentColor: an <input> does not inherit colour, so this
   track resolved to the UA default black and vanished on a dark page. */
.range::-webkit-slider-runnable-track { height: 2px; background: var(--line-strong); }
.range::-webkit-slider-thumb { -webkit-appearance: none; width: 9px; height: 9px; border-radius: 50%; background: var(--data); margin-top: -4px; }
.range::-moz-range-track { height: 2px; background: var(--line-strong); }
.range::-moz-range-thumb { width: 9px; height: 9px; border: 0; border-radius: 50%; background: var(--data); }

/* ---------- work: rail + content ---------- */
.work { display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); min-height: 100svh; }

.rail {
  /* Stays put while the content column scrolls. Its own overflow means a long
     nav still reaches its end on a short viewport. */
  position: sticky;
  inset-block-start: 0;
  align-self: start;
  block-size: 100svh;
  overflow-y: auto;
  border-right: 1px solid var(--line);
  padding: 0 var(--s-8) var(--s-20) var(--s-16);
  display: flex; flex-direction: column;
}
.rail__mark { font-family: var(--font-display); font-weight: 600; font-size: 1.25rem; line-height: 1.1; padding-left: var(--s-8); text-align: left; }
/* Fixed-height head block. Its bottom edge is where .rail__nav's border sits,
   and the band is the same height, so the hairline and the gold seam land on
   one line regardless of how many lines the title runs to. */
/* One group, not two things pinned to opposite ends of a tall box. The block
   settles just above centre, which reads as deliberate where space-between
   read as a gap nobody chose. */
.rail__head {
  block-size: var(--head-h);
  display: flex; flex-direction: column;
  align-items: flex-start; justify-content: center;
  gap: var(--s-16);
  padding-block-end: var(--s-12);
}
.rail__mark { line-height: 1.06; letter-spacing: -0.015em; }
.rail__controls { margin-top: 0; }
.rail__nav { margin-top: 0; border-top: 1px solid var(--line-strong); padding-top: var(--s-8); }
.rail__foot { margin-top: auto; padding-top: var(--s-16); border-top: 1px solid var(--line); display: grid; gap: var(--s-12); justify-items: start; }

.nav {
  display: flex; align-items: center; gap: var(--s-8);
  width: 100%; padding: var(--s-8) var(--s-12);
  border-left: 2px solid transparent; text-align: left;
  font-family: var(--font-mono); font-size: 0.875rem;
  color: var(--mid);
  transition: color var(--dur-fast), background var(--dur-fast), border-color var(--dur-fast);
}
.nav__num { color: var(--violet-lift); }
.nav__dot { width: 5px; height: 5px; border-radius: 50%; background: var(--line); flex: 0 0 5px; }
.nav:hover { color: var(--ink); background: var(--wash); border-left-color: var(--violet); }
.nav[aria-current='true'] { color: var(--ink); border-left-color: var(--violet); background: linear-gradient(90deg, var(--wash-strong), transparent); }
.nav[aria-current='true'] .nav__dot { background: var(--data); }
.nav--spaced { margin-top: var(--s-12); }

.content { min-width: 0; }

/* ---------- band: fluid dye + gold seam ---------- */
.band {
  position: sticky;
  inset-block-start: 0;
  z-index: 5;
  /* min-height, not height: a two-line Source Serif title grows the band
     instead of being cut off at the bottom. The canvas tracks the real size. */
  block-size: var(--head-h);
  overflow: hidden;
  background: var(--bg);
  display: grid;
}
.band__canvas { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity var(--dur-fade) var(--ease-out); }
.band__canvas.is-in { opacity: 1; }
.band__inner {
  position: relative; z-index: 2;
  block-size: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  /* Same max-width and same left padding as .pane, and centred the same way,
     so the title's left edge tracks the body text at every viewport. */
  max-width: var(--content-max);
  margin-inline: auto;
  inline-size: 100%;
  padding: var(--s-16) var(--s-24) var(--s-16) var(--col-pad);
  /* Reserve the seam's height so descenders never sit under it. */
  padding-block-end: calc(var(--s-16) + 20px);
  pointer-events: none;
}
.band__num { color: var(--data); }
/* Two lines, never three. A third line pushed descenders into the gold seam
   and grew the sticky header past what a laptop viewport can spare. */
.band__title {
  font-size: var(--step-2); line-height: 1.12; margin-top: var(--s-6);
  max-width: 28ch; text-wrap: balance;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.band__seam { position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 18px; z-index: 3; }
.band__seam polygon { fill: var(--seam); }

/* ---------- pane ---------- */
.pane {
  /* Asymmetric on purpose: the gap to the rail wants to be larger than the
     gap to the right edge, or the column looks pinned to the menu. */
  padding-block: var(--s-32) var(--s-56);
  padding-inline-start: var(--col-pad);
  padding-inline-end: clamp(var(--s-24), 4vw, var(--s-56));
  animation: pane-in var(--dur-slow) var(--ease-out);
  /* One reading column. Charts and galleries fill it; text stops at
     --measure inside it. This is what lines the left edges up. */
  /* Centred, not pinned left. On a 1900px monitor a left-aligned column leaves
     every pixel of slack on one side, and that asymmetry is what reads as
     broken — not the empty space itself. Below this width the auto margins
     collapse to nothing and it stays left-aligned against the rail. */
  max-width: var(--content-max);
  margin-inline: auto;
}
/* Consistent vertical rhythm between every block in a pane. */
.pane > * + * { margin-block-start: var(--s-20); }
.pane > .group + .group { margin-block-start: var(--s-32); }
@keyframes pane-in { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }

.lede { font-size: var(--step-1); line-height: 1.45; color: var(--ink); max-width: 44ch; }
/* Justified, with hyphenation on. Justification without hyphens opens rivers
   of white space at this measure; the two only work as a pair. The document is
   lang="en-GB", which is what gives the browser a hyphenation dictionary. */
.prose p {
  color: var(--mid);
  max-width: var(--measure);
  margin-bottom: var(--s-16);
  line-height: 1.68;
  text-align: justify;
  hyphens: auto;
  -webkit-hyphens: auto;
}
.item__body p, .cv__summary { text-align: justify; hyphens: auto; -webkit-hyphens: auto; }

/* Short strings justify badly — the last line stretches to fill. Keep these ragged. */
.lede, .figure__caption, .stat__label, .cv__note, .features__desc { text-align: left; hyphens: manual; }

.stats { display: flex; flex-wrap: wrap; gap: var(--s-16) var(--s-32); }
.stat { max-width: 21ch; }
.stat__value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 1.4375rem; line-height: 1.1; color: var(--data); }
.stat__label { font-size: var(--step--1); color: var(--mid); margin-top: var(--s-4); line-height: 1.35; }
/* Provenance, always visible. A number without a source is decoration. */
.stat__source { display: block; font-family: var(--font-mono); font-size: 10px; color: var(--low); margin-top: var(--s-4); }

.chip-forthcoming {
  display: inline-flex; align-items: center; gap: var(--s-6);
  padding: 2px 7px; border: 1px dashed var(--violet-lift); border-radius: var(--radius);
  font-family: var(--font-mono); font-size: 10px;
  text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--mid); white-space: nowrap;
}

/* ---------- figures ---------- */
.figure { }
.figure__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-12); padding-bottom: var(--s-8); border-bottom: 1px solid var(--line-strong); }
.figure__title { color: var(--ink); font-weight: 400; }
.figure__provenance { color: var(--violet-lift); border: 1px solid var(--line); border-radius: 2px; padding: 3px 6px; }
.figure__frame {
  margin-top: var(--s-12); aspect-ratio: 16 / 9;
  background-color: var(--panel);
  background-image: radial-gradient(circle at 1px 1px, var(--line) 1px, transparent 0);
  background-size: 22px 22px;
  border: 1px solid var(--line); border-radius: 4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s-6);
}
/* Full width of the frame above it. A caption set narrower than its own figure
   reads as a stray paragraph rather than as part of the exhibit. */
.figure__caption {
  margin-top: var(--s-12);
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--mid);
}

/* ---------- sequence explorable ---------- */
.seq { }
.seq__events { display: flex; flex-wrap: wrap; gap: var(--s-6); }
.event {
  font-family: var(--font-mono); font-size: var(--step--2);
  padding: var(--s-6) var(--s-8);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: var(--mid);
  transition: border-color var(--dur-fast), color var(--dur-fast), transform var(--dur-fast);
}
.event:hover { border-color: var(--data); color: var(--ink); transform: translateY(-2px); }
.event--risk { border-left: 3px solid var(--data); }
.seq__chart { margin-top: var(--s-16); background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: var(--s-12); }
.seq__head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-12); }
.seq__final { font-family: var(--font-mono); font-size: 1.25rem; color: var(--data); }
.seq__svg { width: 100%; height: auto; aspect-ratio: 320 / 96; margin-top: var(--s-8); display: block; }
.seq__svg .grid { stroke: var(--line-strong); }
.seq__svg .area { fill: var(--wash-strong); }
.seq__svg .line { fill: none; stroke: var(--data); stroke-width: 2; stroke-linejoin: round; }
.seq__svg .node { fill: var(--bg); stroke: var(--data); stroke-width: 1.4; }

/* ---------- cv ---------- */
.cv__section { margin-top: var(--s-32); padding-bottom: var(--s-8); border-bottom: 1px solid var(--line-strong); color: var(--violet-lift); }
.cv__entry { margin-top: var(--s-16); }
.cv__head {
  display: flex; justify-content: space-between; gap: var(--s-8) var(--s-24);
  flex-wrap: wrap; align-items: baseline;
}
.cv__head > div:first-child { flex: 1 1 22rem; min-width: 0; }
.cv__dates { flex: 0 0 auto; max-width: 20rem; text-align: end; }
.cv__role { font-size: 0.9375rem; font-weight: 600; color: var(--ink); }
.cv__org { font-size: var(--step--1); color: var(--violet-lift); }
.cv__dates { font-family: var(--font-mono); font-size: var(--step--2); color: var(--low); }
.cv__summary { margin-top: var(--s-8); font-size: var(--step--1); line-height: 1.6; color: var(--mid); max-width: var(--measure); }
.tags { display: flex; flex-wrap: wrap; gap: var(--s-6); margin-top: var(--s-16);
  list-style: none; padding: 0; }
.tags li { list-style: none; }
.tag { font-family: var(--font-mono); font-size: var(--step--2); color: var(--mid); border: 1px solid var(--line); border-radius: 2px; padding: 3px 7px; }

/* ---------- mobile menu toggle and quick bar (phone only) ----------
   Declared here at full strength and switched on inside the media query, so
   nothing about them can reach a desktop viewport. */
.rail__menu { display: none; }

.quickbar {
  display: none;
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 40;
  align-items: stretch;
  background: var(--control-bg);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-block-start: 1px solid var(--line-strong);
  padding-block-end: env(safe-area-inset-bottom);
}
.quickbar__item {
  flex: 1 1 0; min-inline-size: 0;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-6);
  min-block-size: var(--quickbar-h);
  padding: 0 var(--s-8);
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink); text-decoration: none; text-align: center;
  border-inline-start: 1px solid var(--line);
}
.quickbar__item:first-child { border-inline-start: 0; }
.quickbar__item--primary { color: var(--data); }
.quickbar__item:active { background: var(--wash); }

/* ==========================================================================
   MOBILE
   Most traffic to this site arrives from a tap on a phone. Treated as a
   first-class layout rather than as the desktop one squeezed.
   ========================================================================== */

@media (max-width: 48rem) {
  :root {
    --band-h: 6.5rem;
    --step-3: clamp(1.9rem, 8.5vw, 2.6rem);
    --rail-w: 100%;
    /* The rail head and the band share this on desktop so that the hairline
       under the theme toggle and the gold seam land on one baseline. There is
       no rail beside the content on a phone, so both size to their contents
       instead of to a fixed 156px block — which is what made the header eat a
       quarter of the screen. */
    --head-h: auto;
    --quickbar-h: 3.5rem;
  }

  /* ------------------------------------------------------------------------
     LANDING — one screen, no scrolling.
     Name at the top, sentence and sphere as one centred group, controls along
     the bottom. .landing__inner drops out of the box tree so its three
     children and the explore zone can be ordered as a single column; the
     explore zone used to sit below a 100svh block, i.e. off the bottom of the
     screen, which is why EXPLORE was nowhere to be found.
     ---------------------------------------------------------------------- */
  .landing {
    display: flex;
    flex-direction: column;
    min-block-size: 100svh;
    gap: var(--s-12);
    padding: calc(var(--s-16) + env(safe-area-inset-top)) var(--s-16)
             calc(var(--s-16) + env(safe-area-inset-bottom));
    /* The field reads the pointer stream; the browser must not claim it as a
       pan gesture on a screen that does not scroll. */
    touch-action: manipulation;
  }
  .landing__inner { display: contents; }

  .landing__row { order: 1; padding: 0; }
  .landing__sentence {
    order: 2;
    margin: auto 0 0;
    max-width: none;
    text-wrap: balance;
  }
  .explore-zone { order: 3; }
  .landing__row--foot { order: 4; margin-block-start: auto; }

  /* The zone must stay a positioned box. As `position: static` the sphere
     canvas and the veil behind it both resolved against .landing instead —
     a full-screen wash of --bg over the grain (which is where the sand went)
     and a sphere drawn at viewport size behind the type. */
  .explore-zone {
    position: relative;
    inset: auto;
    transform: none;
    inline-size: min(70vw, clamp(11rem, 32svh, 17rem));
    margin: 0 auto;
    pointer-events: none;
  }
  .explore { padding: var(--s-16) var(--s-24); }
  .explore__word { font-size: 1.35rem; }
  .explore__line { inline-size: 7.5rem; }
  .explore:active .explore__line { inline-size: 9.5rem; }

  /* Grain slider and theme toggle share one line, hard against the bottom
     edge — a stacked pair of controls read as leftover furniture. */
  .landing__row--foot {
    flex-direction: row; align-items: center; justify-content: space-between;
    gap: var(--s-12);
  }
  .landing__controls {
    flex: 1 1 auto; flex-wrap: nowrap;
    justify-content: space-between; gap: var(--s-12);
  }
  .landing__controls .label { font-size: 10px; }
  .range { width: auto; flex: 1 1 4rem; min-width: 3.5rem; max-width: 7.5rem; }

  /* 44px tall, tighter tracking, equal halves. The desktop proportions are
     drawn for a pointer; under a thumb they were both small and loose. */
  .seg { border-radius: 999px; }
  .seg button {
    flex: 1 1 0; justify-content: center;
    padding: 0 var(--s-12); min-block-size: 42px;
    font-size: 11px; letter-spacing: 0.06em;
  }
  .seg button::before { width: 7px; height: 7px; flex-basis: 7px; }

  /* ------------------------------------------------------------------------
     WORK — sticky top bar, menu behind one button.
     .work was a single-column grid, which scoped the rail's sticky positioning
     to its own grid row: the header slid away with the first screenful and
     never came back. As a block, the sticky box is the whole page.
     ---------------------------------------------------------------------- */
  .work { display: block; }

  .rail {
    position: sticky; inset-block-start: 0; z-index: 20;
    display: block;
    block-size: auto; overflow: visible;
    border-right: 0; border-bottom: 1px solid var(--line-strong);
    padding: 0 var(--s-16);
    padding-block-start: env(safe-area-inset-top);
    background: var(--bg);
  }
  .rail__head {
    block-size: auto; min-block-size: 3.75rem;
    flex-direction: row; align-items: center; justify-content: space-between;
    gap: var(--s-12);
    padding-block: var(--s-8);
  }
  .rail__mark {
    padding-left: 0; font-size: 1.0625rem; line-height: 1.15;
    flex: 0 1 auto; min-inline-size: 0;
  }
  .rail__mark br { display: none; }
  .rail__controls {
    display: flex; align-items: center; gap: var(--s-8);
    flex: 0 0 auto;
  }

  .rail__menu {
    display: inline-flex; align-items: center; gap: var(--s-8);
    min-block-size: 42px; max-inline-size: 9.5rem;
    padding: 0 var(--s-12);
    border: 1px solid var(--control-line); border-radius: 999px;
    background: var(--control-bg);
    font-family: var(--font-mono); font-size: 11px; font-weight: 500;
    text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--ink);
  }
  .rail__menu-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rail__menu-caret { color: var(--data); transition: transform var(--dur-fast) var(--ease-out); }
  .rail__menu[aria-expanded='true'] { background: var(--control-active-bg); color: var(--control-active-ink); border-color: transparent; }
  .rail__menu[aria-expanded='true'] .rail__menu-caret { transform: rotate(180deg); color: inherit; }
  /* Three bars, drawn rather than shipped as an icon font. */
  .rail__menu-bars {
    inline-size: 13px; block-size: 9px; flex: 0 0 13px;
    border-block-start: 2px solid currentColor;
    border-block-end: 2px solid currentColor;
    position: relative;
  }
  .rail__menu-bars::after {
    content: ''; position: absolute; inset-inline: 0; inset-block-start: 3px;
    border-block-start: 2px solid currentColor;
  }

  /* All eight sections at once, in two columns, over the content rather than
     pushing it down. A horizontal scroller showed three and a half of them and
     gave no sign that the rest existed. */
  .rail__nav {
    display: none;
    position: absolute; inset-inline: 0; inset-block-start: 100%;
    z-index: 30;
    margin: 0; padding: var(--s-8);
    border-top: 0; border-bottom: 1px solid var(--line-strong);
    background: var(--bg);
    box-shadow: 0 22px 46px -22px rgba(0, 0, 0, 0.55);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-4);
    max-block-size: calc(100svh - 8rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  :root.menu-open .rail__nav { display: grid; animation: menu-in var(--dur-base) var(--ease-out); }
  @keyframes menu-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

  .nav {
    inline-size: auto; min-inline-size: 0;
    padding: var(--s-12) var(--s-8);
    font-size: 0.8125rem;
    border-left: 0;
    border: 1px solid transparent; border-radius: var(--radius);
  }
  .nav > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .nav:hover { border-left: 0; }
  .nav[aria-current='true'] {
    border-left: 0; background: var(--wash);
    border-color: var(--violet); color: var(--ink);
  }
  .nav--spaced { margin-top: 0; }

  /* Dims the page behind an open menu and gives the tap-to-dismiss somewhere
     to land. */
  :root.menu-open .work::before {
    content: ''; position: fixed; inset: 0; z-index: 10;
    background: color-mix(in srgb, var(--bg) 62%, transparent);
  }

  /* The band sticks too, directly under the top bar. --bar-h is measured from
     the bar itself in app.js, because its height moves with the safe-area
     inset and with the wordmark wrapping — a hard-coded offset leaves either a
     hairline gap or an overlap on some device. The 1px pull closes the seam
     against rounding. */
  .band {
    position: sticky;
    inset-block-start: calc(var(--bar-h, 3.75rem) - 1px);
    z-index: 5;
    block-size: auto; min-block-size: 5rem;
  }
  .band__inner {
    padding: var(--s-12) var(--s-16); padding-inline-start: var(--col-pad);
    padding-block-end: calc(var(--s-8) + 13px);
  }
  .band__title { font-size: 1.2rem; max-width: none; -webkit-line-clamp: 3; line-clamp: 3; }
  .band__seam { height: 13px; }

  /* Clears the quick bar, so the last paragraph is not sitting under it. */
  .pane {
    padding: var(--s-16) var(--s-16)
             calc(var(--s-32) + var(--quickbar-h) + env(safe-area-inset-bottom));
    padding-inline-start: var(--col-pad);
  }

  .quickbar { display: flex; }

  /* One column everywhere that was a grid. */
  .builds { grid-template-columns: minmax(0, 1fr); }
  .gallery { grid-template-columns: minmax(0, 1fr); }
  .features__row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .stats { gap: var(--s-16) var(--s-24); }
  .stat { max-width: none; }

  /* Explorables */
  .dag__svg { min-width: 560px; }
  .px__bar .range { width: 100%; max-width: 240px; }
  .seq__events { gap: var(--s-8); }
  .event { padding: var(--s-8) var(--s-12); }   /* fatter touch target */

  /* CV */
  .cv__head { gap: var(--s-4); }
  .cv__dates { text-align: start; }
  .features__name { color: var(--violet-lift); }
}

/* Very small phones. Three controls share the top bar; at 360px they only fit
   on one line if each gives up a little. The wordmark keeps its width and the
   controls shed padding, because a name that breaks in half at one width and
   not at another looks like an accident. */
@media (max-width: 26rem) {
  .explore-zone { inline-size: min(72vw, clamp(10rem, 30svh, 14rem)); }
  .builds__card { padding: var(--s-12); }
  .rail__mark { font-size: 1rem; white-space: nowrap; }
  .rail__controls { gap: var(--s-6); }
  .seg button { padding: 0 var(--s-8); font-size: 10px; letter-spacing: 0.04em; gap: var(--s-4); }
  .rail__menu { max-inline-size: 7rem; padding: 0 var(--s-8); gap: var(--s-6); font-size: 10px; }
  .quickbar__item { font-size: 11px; letter-spacing: 0.04em; }
}

/* Short landscape phones — the landing must not need scrolling to reach the
   Explore square. */
@media (max-height: 34rem) and (orientation: landscape) {
  .explore-zone { inline-size: clamp(8rem, 30svh, 15rem); }
  .landing { padding-block: var(--s-12); gap: var(--s-8); }
  .rail__nav { max-block-size: calc(100svh - 5rem); }
}

/* Coarse pointers get no hover-dependent affordances. */
@media (hover: none) {
  .gallery__item img { opacity: 1; }
  .dag.is-focused .dag-node rect,
  .dag.is-focused .dag-node text { opacity: 0.4; }
}

@media print {
  .landing, .band, .rail__foot, .skip { display: none !important; }
  body { background: #fff; color: #000; }
}

/* ==========================================================================
   EXPLORABLES
   ========================================================================== */

/* A figure frame holding a live explorable does not need a fixed ratio or the
   graph-paper placeholder fill. */
.figure__frame--live {
  aspect-ratio: auto;
  display: block;
  background-image: none;
  padding: var(--s-16);
}

.explorable__bar {
  display: flex; align-items: center; gap: var(--s-12);
  flex-wrap: wrap; margin-bottom: var(--s-12);
}

/* ---------- 02 · dependency graph ---------- */
.dag__bar { display: flex; align-items: center; gap: var(--s-16); margin-bottom: var(--s-12); flex-wrap: wrap; }
.dag__run {
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em;
  padding: var(--s-6) var(--s-16);
  border: 1px solid var(--control-line); border-radius: var(--radius);
  background: var(--control-bg); color: var(--ink);
  transition: background var(--dur-fast), color var(--dur-fast);
}
.dag__run:hover { background: var(--control-active-bg); color: var(--control-active-ink); }
.dag__scroll { overflow-x: auto; scrollbar-width: thin; }
.dag { max-width: 52rem; }
.dag__svg { width: 100%; min-width: 620px; height: auto; display: block; }

.dag-edge {
  fill: none; stroke: var(--line-strong); stroke-width: 1.4;
  transition: stroke var(--dur-fast), opacity var(--dur-fast);
}
.dag__svg .dag-node rect {
  fill: var(--panel); stroke: var(--line); stroke-width: 1;
  transition: fill var(--dur-fast), stroke var(--dur-fast);
}
.dag__svg .dag-node text {
  font-family: var(--font-mono); font-size: 10.5px; fill: var(--mid);
  pointer-events: none; transition: fill var(--dur-fast);
}
.dag-node { cursor: pointer; }
.dag-node:focus { outline: none; }
.dag-node:focus-visible rect { stroke: var(--data); stroke-width: 2; }

/* Dim everything that is not in the lineage, rather than brightening what is —
   the eye finds the surviving path faster than it finds an added highlight. */
.dag.is-focused .dag-edge { opacity: 0.2; }
.dag.is-focused .dag-node rect { opacity: 0.28; }
.dag.is-focused .dag-node text { opacity: 0.28; }
.dag.is-focused .dag-node.is-lit rect,
.dag.is-focused .dag-node.is-lit text { opacity: 1; }
.dag.is-focused .dag-edge.is-lit { opacity: 1; stroke: var(--violet-lift); stroke-width: 1.6; }
.dag-node.is-lit rect { stroke: var(--violet-lift); }
.dag-node.is-lit text { fill: var(--ink); }
.dag-node.is-self rect { fill: var(--violet); stroke: var(--violet-lift); }
.dag-node.is-self text { fill: #fff; }

.dag-node.is-running rect { fill: var(--data); stroke: var(--data); }
.dag-node.is-running text { fill: var(--bg); }

/* ---------- 04 · pricing ---------- */
.px__bar { display: flex; align-items: center; gap: var(--s-12); flex-wrap: wrap; margin-bottom: var(--s-8); }
.px__bar .range { width: 150px; }
#px-mode { color: var(--data); }
.px__svg { width: 100%; height: auto; display: block; }
.px__grid line { stroke: var(--line-strong); }
.px__flat rect { fill: none; stroke: var(--violet-lift); stroke-width: 1; opacity: 0.55; }
.px__dyn rect { fill: var(--data); opacity: 0.85; transition: none; }
.px__axis text { font-family: var(--font-mono); font-size: 10px; fill: var(--low); }
.px__axis .px__note { fill: var(--low); }
.px__out { display: flex; gap: var(--s-32); flex-wrap: wrap; margin-top: var(--s-12); }
.px__out > div { display: grid; gap: 2px; }
.px__num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 1.25rem; color: var(--data); }
.px__num--ref { color: var(--violet-lift); }

/* ---------- 05 · sequence chips, legibility ---------- */
/* Same lesson as the theme toggle: at this size the fault is optical weight,
   not contrast ratio. 11px/400 in --mid reads as faint however well it
   measures. */
.event { font-size: 12px; font-weight: 500; color: var(--ink); }

/* ==========================================================================
   BUILDS · GROUPS · GALLERY
   Added for the expanded content structure: four named systems, sub-segments
   within a section, and the Flightdeck screenshots.
   ========================================================================== */

/* ---------- the four named systems ---------- */
.builds {
  display: grid; gap: var(--s-8);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  padding: 0; list-style: none;
}
.builds__item { display: grid; min-width: 0; }
.builds__card {
  flex: 1; min-width: 0; display: grid; gap: var(--s-4); align-content: start;
  padding: var(--s-12) var(--s-16); text-align: left;
  border: 1px solid var(--line); border-left: 2px solid var(--violet);
  border-radius: var(--radius); background: var(--panel);
  transition: border-color var(--dur-fast), background var(--dur-fast), transform var(--dur-fast);
}
.builds__card:hover { background: var(--wash); border-left-color: var(--violet-lift); transform: translateY(-2px); }
.builds__name { font-family: var(--font-mono); font-size: 15px; font-weight: 500; color: var(--ink); }
.builds__kind { color: var(--violet-lift); }
.builds__blurb { font-size: var(--step--1); line-height: 1.45; color: var(--mid); }
/* In normal flow, below the blurb. Absolutely positioning it over the card
   header is what collided with the title. */
.builds__ext {
  display: inline-block; margin-top: var(--s-8);
  color: var(--data); text-decoration: none;
}
.builds__ext:hover { text-decoration: underline; text-underline-offset: 0.25em; }

/* ---------- sub-segments ---------- */
.group { }
.group__label {
  color: var(--violet-lift);
  padding-bottom: var(--s-6);
  border-bottom: 1px solid var(--line-strong);
}
.item { margin-top: var(--s-16); padding-top: var(--s-16); border-top: 1px solid var(--line); }
.group .item:first-of-type { border-top: 0; padding-top: var(--s-8); }
.item__head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--s-8) var(--s-16); }
.item__head > .item__title { flex: 1 1 20rem; min-width: 0; }
.item__meta { flex: 0 1 auto; min-width: 0; text-align: end; }
.item__title { font-size: 1rem; font-weight: 600; color: var(--ink); line-height: 1.3; }
.item__meta { font-family: var(--font-mono); font-size: var(--step--2); color: var(--mid); }

/* Citations. Full width of the column, left aligned, and allowed to break —
   these run to 200 characters and must never set their own width. */
.item__cite {
  margin-top: var(--s-6);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  line-height: 1.55;
  color: var(--mid);
  max-width: 100%;
  overflow-wrap: anywhere;
  text-align: left;
  hyphens: manual;
}
.item__body { margin-top: var(--s-8); }
.item__body p { margin-bottom: var(--s-12); }
.item__metrics { margin-top: var(--s-12); }
.item__link { margin-top: var(--s-12); }
.item__link a { color: var(--data); text-decoration: none; }
.item__link a:hover { text-decoration: underline; text-underline-offset: 0.25em; }

/* ---------- feature list ---------- */
.features { margin-top: var(--s-16); display: grid; gap: var(--s-8); }
.features__row { display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: var(--s-12); align-items: baseline; }
.features__name { font-family: var(--font-mono); font-size: var(--step--2); color: var(--ink); }
.features__desc { font-size: var(--step--1); line-height: 1.5; color: var(--mid); }
@media (max-width: 40rem) { .features__row { grid-template-columns: minmax(0, 1fr); gap: 2px; } }

/* ---------- screenshots ---------- */
.gallery {
  display: grid; gap: var(--s-12); margin-top: var(--s-16);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.gallery__item { margin: 0; }
.gallery__item img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: var(--radius);
  /* Screenshots of a dark UI on a light page look like holes. Knock them back
     a touch and let hover restore them. */
  opacity: 0.92; transition: opacity var(--dur-fast), border-color var(--dur-fast);
}
.gallery__item img:hover { opacity: 1; border-color: var(--violet-lift); }
.gallery__caption { margin-top: var(--s-6); font-family: var(--font-mono); font-size: var(--step--2); color: var(--mid); }

/* ---------- cv additions ---------- */
.cv__bullets { margin-top: var(--s-8); padding-inline-start: var(--s-20); }
.cv__bullets li { font-size: var(--step--1); line-height: 1.6; color: var(--mid); margin-bottom: var(--s-4); }
.cv__bullets li::marker { color: var(--violet-lift); }
.cv__line { margin-top: var(--s-12); }
.cv__item { font-size: 0.875rem; color: var(--ink); }
.cv__item a { color: var(--ink); }
.cv__note { font-size: var(--step--1); color: var(--mid); margin-top: 2px; max-width: var(--measure); }
.skillgroup { margin-top: var(--s-16); }
.skillgroup__label { color: var(--low); }

/* ==========================================================================
   PRINT — people print CVs. Without this they get a black page out of a
   shared office printer. Strips the chrome, the grain and the dark ground;
   prints the CV as plain black-on-white with visible link targets.
   ========================================================================== */
@media print {
  :root, [data-theme] {
    --bg: #fff; --ink: #000; --mid: #333; --dim: #555;
    --line: #ccc; --gold: #000; --violet: #000; --copper: #000;
  }
  html, body { background: #fff !important; color: #000 !important; }
  .grain, .landing, .rail, .explore-zone, .curtain, .seg, .range,
  .landing__controls, .figure, .gallery, .skip { display: none !important; }
  .work { display: block !important; }
  .stage, .work__main { padding: 0 !important; margin: 0 !important; max-width: none !important; }
  * { box-shadow: none !important; text-shadow: none !important; animation: none !important; transition: none !important; }
  a { color: #000 !important; text-decoration: underline; }
  /* Expand link targets so a printed CV stays actionable. */
  .cv__contact a[href^="http"]::after,
  .cv__item a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; color: #444; }
  .cv__entry, .cv__line, .skillgroup { page-break-inside: avoid; }
  h1, h2, h3, h4 { page-break-after: avoid; }
  .tag { border: 1px solid #999 !important; color: #000 !important; }
}
