/* ============================================================
   Portfolio v3 — "Instrument"
   Direction locked in docs/design-direction.md
   Primary reference: GT Planar (engineered utility, void canvas,
   hairline instrumentation, 0px radius, kinetic generated media)
   Borrowed: Charlie (full-bleed accent statement surface),
             Pipe + Cron (molten orange as the sole accent on black),
             Elvina Prasad (black/white inversion + section rhythm),
             Max Yinger (monospace metadata layer)
   ============================================================ */

/* ---------- 1. Tokens ---------- */

:root {
  /* Surfaces — GT Planar surface ladder, accent-adapted */
  --void: #000000;
  --surface-1: #0a0a0a;
  --surface-2: #140c07; /* recessed surface, tinted toward the accent */
  --paper: #ffffff;

  /* Ink */
  --ink-hi: #ffffff;
  --ink-mid: #949494; /* GT Planar Subtle Gray — muted text */
  --ink-meta: #808080; /* Pipe Slate Text — tertiary text, clears AA on black */
  --ink-lo: #606060; /* GT Planar Border Gray — hairlines only, never text */

  /* Accent — single, sitting between Cron's Action Orange #ff4700 and
     GT Planar's Indicator Orange #ff8a00. Carries Charlie's
     statement-surface role: full-bleed plane + active state + marker.
     7.34:1 on black, both directions.
     Never body text. Never white text on it (Pipe's explicit rule);
     everything sitting on the accent is --void at 0.7 alpha or higher. */
  --signal: #ff6a24;

  /* The accent carried toward white, 45 percent. One use only: the light
     that runs the accent route on the case diagrams. It is never a surface,
     never a fill behind reading copy and never carries text, which is why it
     has no row in the contrast audit. If it ever gets a second use, it needs
     one. */
  --signal-hot: #ffb08a;

  /* Inverted scope (paper sections) */
  --inv-ink-hi: #000000;
  --inv-ink-mid: #606060; /* 6.29:1 on paper */
  --inv-ink-meta: #6e6e6e; /* 5.10:1 on paper */
  --inv-rule: #c8c8c8; /* hairlines only, never text */

  /* Type */
  --font-display: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  --font-ui: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --text-caption: 0.6875rem; /* 11px */
  --text-body-sm: 0.875rem; /* 14px */
  --text-body: 1rem; /* 16px */
  --text-prose: 1.125rem; /* 18px */
  --text-subheading: 1.5rem; /* 24px */
  --text-heading: clamp(2.25rem, 4.4vw, 3.625rem); /* →58px */
  --text-heading-lg: clamp(3rem, 7vw, 5.375rem); /* →86px */
  --text-display-sm: clamp(4rem, 13vw, 9.125rem); /* →146px */
  --text-display: clamp(4.5rem, 20vw, 17.125rem); /* →274px */

  /* Rhythm — GT Planar owns internal density, Elvina owns section gap */
  --u: 4px;
  --gap-el: 8px;
  --pad-card: 24px;
  --gap-section: clamp(88px, 11vw, 168px);
  --page-max: 1440px;
  --gutter: clamp(20px, 4vw, 56px);

  /* Form — 0px is the default. 9999px is reserved for tags only. */
  --radius: 0px;
  --radius-tag: 9999px;
  --hair: 1px;

  /* Motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 180ms;
  --dur: 420ms;
  --dur-slow: 900ms;
}

/* ---------- 2. Reset ---------- */

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

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

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background: var(--void);
  color: var(--ink-hi);
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: 1.15;
  letter-spacing: 0.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img,
svg,
canvas {
  display: block;
  max-width: 100%;
}

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

h1,
h2,
h3,
h4,
p,
figure,
blockquote,
ul,
ol {
  margin: 0;
  padding: 0;
}

ul,
ol {
  list-style: none;
}

::selection {
  background: var(--signal);
  color: var(--void);
}

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

/* WCAG 1.4.11 asks for 3:1 between the focus indicator and what it sits
   on. The accent measures 2.86:1 against paper, which the direction doc
   already records as a forbidden pairing, so on the inverted section the
   ring switches to a black core with the accent as its outer band. Both
   halves are legible on white and the indicator keeps its identity. */
.work :focus-visible {
  outline-color: var(--void);
  box-shadow: 0 0 0 6px var(--signal);
}

/* ---------- 3. Primitives ---------- */

.shell {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.mono {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.43;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mid);
}

.mono--hi {
  color: var(--ink-hi);
}
.mono--signal {
  color: var(--signal);
}

.display {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 0.8;
  letter-spacing: -0.05em;
  text-wrap: balance;
}

.heading {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-heading);
  line-height: 0.95;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.prose {
  font-size: var(--text-prose);
  line-height: 1.55;
  letter-spacing: 0.005em;
  color: var(--ink-mid);
  max-width: 62ch;
  text-wrap: pretty;
}

.prose strong {
  color: var(--ink-hi);
  font-weight: 500;
}

/* The site's memorable token: an open question, marked not hidden. */
.q {
  font-family: var(--font-mono);
  color: var(--signal);
  font-weight: 500;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* Rules — hairline instrumentation */
.rule {
  border: 0;
  border-top: var(--hair) solid var(--ink-lo);
  width: 100%;
}

/* Plain button row. The hero has its own right-aligned variant because it
   sits in the second column of a two column footer; this is the default. */
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Buttons — GT Planar: 0 radius, 1px border, no fill by default */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 13px 20px;
  border: var(--hair) solid var(--ink-hi);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-hi);
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  line-height: 1.15;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.btn:hover,
.btn:focus-visible {
  background: var(--paper);
  color: var(--void);
}

.btn--signal {
  border-color: var(--signal);
  color: var(--signal);
}

.btn--signal:hover,
.btn--signal:focus-visible {
  background: var(--signal);
  color: var(--void);
}

.btn__arrow {
  transition: transform var(--dur-fast) var(--ease-out);
}

.btn:hover .btn__arrow {
  transform: translateX(4px);
}

/* Tags — the one place a 9999px radius is allowed */
.tag {
  display: inline-block;
  padding: 5px 12px;
  border: var(--hair) solid var(--ink-lo);
  border-radius: var(--radius-tag);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mid);
}

/* Section header — number + label, instrument style */
.sec-head {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding-block: 14px;
  border-top: var(--hair) solid var(--ink-lo);
  margin-bottom: 56px;
}

.sec-head__no {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  color: var(--signal);
}

.sec-head__label {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mid);
}

.sec-head__note {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-meta);
}

section {
  scroll-margin-top: 96px;
}

/* ---------- 4. Chrome: nav + HUD ----------
   The chrome does not blend, it switches. Each surface the page
   travels through (void, paper, signal) has one legible pairing,
   so the palette never drifts off black / white / red. */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px var(--gutter);
  background: var(--void);
  color: var(--ink-hi);
  border-bottom: var(--hair) solid var(--ink-lo);
  /* No tween on background or colour. The surface behind the chrome
     changes in one frame, so a 180ms linear fade put the bar through
     intermediate pairings that measured as low as 1.14:1 and produced
     off-palette browns on a fast scroll. The comment above this block
     always said the chrome switches rather than blends; now it does. */
  transition: border-color var(--dur-fast) linear;
}

.nav.is-top {
  background: transparent;
  border-bottom-color: transparent;
}

.nav.on-paper {
  background: var(--paper);
  color: var(--inv-ink-hi);
  border-bottom-color: var(--inv-rule);
}

.nav.on-signal {
  background: var(--signal);
  color: var(--void);
  border-bottom-color: rgba(0, 0, 0, 0.4);
}

/* Black at 0.6 alpha does not clear AA on the accent plane, so the
   dimmed chrome states are lifted while the nav sits on it. */
.nav.on-signal .nav__link,
.nav.on-signal .nav__mark span {
  opacity: 0.75;
}

.nav__mark {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: inherit;
  line-height: 1.4;
}

.nav__mark b {
  font-weight: 500;
}

.nav__mark span {
  display: block;
  opacity: 0.55;
}

.nav__links {
  display: flex;
  gap: 4px;
}

.nav__link {
  padding: 7px 12px;
  border: var(--hair) solid transparent;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: inherit;
  transition: border-color var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
  opacity: 0.6;
}

.nav__link:hover,
.nav__link:focus-visible {
  opacity: 1;
  border-color: currentColor;
}

.nav__link[aria-current="true"] {
  opacity: 1;
  border-color: currentColor;
}

/* Below 700px the four labels do not fit beside the wordmark — measured,
   they overflow the bar by 13px. Hiding them was the earlier answer and it
   cost the page its only accelerator at exactly the viewport where the
   scroll is longest. So they move instead: the section list becomes a
   fixed bottom bar, in the thumb zone, where a one-handed reader can
   actually reach it. The bar is always void-surfaced rather than tracking
   the section behind it, because it sits over three different surfaces on
   one page and a chrome element that has to stay legible should not have
   to negotiate.

   Every declaration here is scoped through .nav__links: the
   (pointer: coarse) block below touches .nav__link at the same
   specificity and later in the source, so an unscoped rule loses on every
   touch device under 700px — both queries match, and order decides. */
@media (max-width: 700px) {
  .nav__links {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 70;
    justify-content: space-around;
    gap: 0;
    padding: 0 8px env(safe-area-inset-bottom);
    background: var(--void);
    color: var(--ink-hi);
    border-top: var(--hair) solid var(--ink-lo);
  }
  .nav__links .nav__link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 7px 10px;
    border: 0;
    /* Indicator on the top edge, not the bottom: at the bottom it lands on
       the home indicator and gets clipped by the safe area. */
    border-top: 2px solid transparent;
    color: var(--ink-hi);
    opacity: 0.6;
  }
  .nav__links .nav__link[aria-current="true"] {
    opacity: 1;
    border-top-color: var(--signal);
  }
  .nav__mark span {
    display: none;
  }
  /* The HUD occupied this corner and printed over body copy at ten
     measured positions. The bottom bar carries the same "where am I"
     signal, with a label instead of a number, and can be tapped. The
     rule itself lives further down, immediately after .hud is defined —
     placed here it would be overridden by the base declaration below,
     which is the same trap the nav links fell into. */
}

/* Touch pointers get 44px rows. Screen width does not tell you the input
   method: a laptop can have a touchscreen and a tablet can have a mouse. */
@media (pointer: coarse) {
  .nav__link,
  .foot__links a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    min-width: 44px;
  }
  .nav__mark {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    min-height: 44px;
  }
}

/* HUD — Max Yinger's metadata badge row, made live */
/* The HUD sits in the content column at every width, not just below
   700px, so it carries its own surface everywhere. Without it the readout
   printed over body copy at thirteen measured scroll positions, and it
   went fully invisible (1.00:1, black on black) while the colour caught
   up with the section behind it. */
.hud {
  position: fixed;
  left: calc(var(--gutter) - 8px);
  bottom: 16px;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 8px;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  background: var(--void);
  color: var(--ink-hi);
  pointer-events: none;
}

.hud.on-paper {
  background: var(--paper);
  color: var(--void);
}

.hud.on-signal {
  background: var(--signal);
  color: var(--void);
}

.hud__bar {
  position: relative;
  width: 88px;
  height: var(--hair);
}

.hud__bar::before {
  content: "";
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0.28;
}

.hud__bar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--scroll, 0) * 100%);
  background: currentColor;
}

/* 0.35 measured 3.01:1 against the void. The separator is a glyph, not
   decoration, so it has to clear AA like the rest of the readout. */
.hud__sep {
  opacity: 0.55;
}

/* Black at 0.55 on the accent plane is 3.41:1, and the HUD sits on that
   plane for 620px of continuous scroll. The nav already carries this
   lift for its own dimmed states; the separator was missed. */
.hud.on-signal .hud__sep {
  opacity: 0.75;
}

/* Below 700px the bottom bar owns this corner: it carries the same "where
   am I" signal with a tappable label instead of a number, and the readout
   was printing over body copy at ten measured positions. The page also has
   to stop short of the bar or the last footer row hides underneath. */
@media (max-width: 700px) {
  .hud {
    display: none;
  }
  body {
    padding-bottom: calc(44px + env(safe-area-inset-bottom));
  }
}

/* ---------- 5. Hero ---------- */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-bottom: clamp(64px, 9vh, 120px);
  padding-top: 120px;
  overflow: hidden;
}

.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

.hero__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: radial-gradient(
      120% 90% at 50% 100%,
      rgba(0, 0, 0, 0.92) 0%,
      rgba(0, 0, 0, 0.4) 55%,
      rgba(0, 0, 0, 0) 100%
    ),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 32%);
  pointer-events: none;
}

.hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
}

.hero__eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  padding-bottom: 20px;
  border-bottom: var(--hair) solid var(--ink-lo);
  margin-bottom: 28px;
}

.hero__eyebrow .dot {
  width: 7px;
  height: 7px;
  background: var(--signal);
  flex: none;
}

.hero__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-display-sm);
  line-height: 0.84;
  letter-spacing: -0.05em;
  margin-bottom: 32px;
  text-wrap: balance;
}

/* Descenders need room, or the mask reveal clips them. */
.hero__line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}

.hero__line > span {
  display: block;
}

.hero__foot {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: 32px 48px;
  align-items: end;
  padding-top: 28px;
  border-top: var(--hair) solid var(--ink-lo);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-self: end;
  justify-content: flex-end;
}

@media (max-width: 860px) {
  .hero__foot {
    grid-template-columns: 1fr;
  }
  .hero__actions {
    justify-content: flex-start;
  }
  .hero {
    padding-top: 88px;
  }
}

/* A full svh hero plus 120px of top padding put the paragraph and both
   buttons below the fold at 640x480 — a 1280px desktop at 200% zoom. No
   arrangement fits three lines of 83px display type, a paragraph and two
   buttons into 480px, so on a short viewport the headline gives up the
   size rather than the reader giving up the action. */
@media (max-width: 900px) and (max-height: 640px) {
  .hero {
    min-height: 0;
    padding-top: 52px;
    padding-bottom: 12px;
  }
  .hero__title {
    font-size: clamp(2.25rem, 8vw, 4rem);
  }
  .hero__eyebrow {
    margin-bottom: 12px;
  }
}

/* ---------- 6. Capability grid ---------- */

.caps {
  padding-block: var(--gap-section) 0;
}

.caps__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-top: var(--hair) solid var(--ink-lo);
  border-left: var(--hair) solid var(--ink-lo);
}

.cap {
  position: relative;
  padding: var(--pad-card);
  border-right: var(--hair) solid var(--ink-lo);
  border-bottom: var(--hair) solid var(--ink-lo);
  min-height: 236px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.cap__no {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  color: var(--signal);
}

.cap__title {
  font-family: var(--font-display);
  font-size: var(--text-subheading);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.01em;
}

.cap__body {
  font-size: var(--text-body-sm);
  line-height: 1.5;
  color: var(--ink-mid);
  margin-top: auto;
}

@media (max-width: 860px) {
  .caps__grid {
    grid-template-columns: 1fr;
  }
  .cap {
    min-height: 0;
  }
}

/* ---------- 7. Red tape — Charlie's full-bleed statement surface ---------- */

.tape {
  margin-top: var(--gap-section);
  background: var(--signal);
  color: var(--void);
  padding-block: clamp(48px, 8vw, 96px);
  overflow: hidden;
}

.tape__track {
  display: flex;
  width: max-content;
  will-change: transform;
}

.tape__row {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.5rem, 7.6vw, 6.5rem);
  line-height: 0.9;
  letter-spacing: -0.045em;
  white-space: nowrap;
  text-transform: uppercase;
  padding-right: 0.35em;
}

/* Separators stay quieter than the words but must still clear AA
   for large text against the accent plane. */
.tape__row span {
  opacity: 0.55;
}

.tape__statement {
  margin-top: clamp(32px, 5vw, 64px);
  padding-top: 24px;
  border-top: var(--hair) solid rgba(0, 0, 0, 0.45);
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 20px 48px;
}

.tape__statement .mono {
  color: rgba(0, 0, 0, 0.72);
}

.tape__statement p {
  font-size: clamp(1.125rem, 2.2vw, 1.75rem);
  line-height: 1.25;
  letter-spacing: -0.015em;
  font-weight: 500;
  max-width: 30ch;
  text-wrap: pretty;
}

@media (max-width: 860px) {
  .tape__statement {
    grid-template-columns: 1fr;
  }
}

/* ---------- 8. Work — the inversion (Elvina) ---------- */

.work {
  background: var(--paper);
  color: var(--inv-ink-hi);
  padding-block: var(--gap-section);
  position: relative;
}

.work .sec-head {
  border-top-color: var(--inv-rule);
}

/* The accent measures 2.86:1 on paper and the direction doc lists that
   pairing as forbidden, so inside the inverted section the number takes
   the top of the inverted ink ladder instead. Number darkest, label mid,
   note meta: the same hierarchy the void sections carry. */
.work .sec-head__no {
  color: var(--inv-ink-hi);
}

.work .sec-head__label {
  color: var(--inv-ink-mid);
}

.work .sec-head__note {
  color: var(--inv-ink-meta);
}

.work__list {
  border-top: var(--hair) solid var(--inv-ink-hi);
}

.work-row {
  position: relative;
  display: block;
  border-bottom: var(--hair) solid var(--inv-rule);
  padding-block: clamp(28px, 3.4vw, 44px);
  overflow: hidden;
  transition: color var(--dur) var(--ease-out);
}

.work-row__sweep {
  position: absolute;
  inset: 0;
  background: var(--signal);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform var(--dur) var(--ease-out);
  z-index: 0;
}

.work-row__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 56px minmax(0, 1.05fr) minmax(0, 1fr) 200px;
  gap: 24px 32px;
  align-items: center;
}

.work-row__no {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  color: var(--inv-ink-mid);
  transition: color var(--dur) var(--ease-out);
}

.work-row__name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.25rem, 5.2vw, 4.25rem);
  line-height: 0.9;
  letter-spacing: -0.045em;
}

.work-row__kicker {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.5;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--inv-ink-mid);
  margin-bottom: 12px;
  font-weight: 400;
  transition: color var(--dur) var(--ease-out);
}

.work-row__body {
  display: grid;
  gap: 16px;
  justify-items: start;
}

.work-row__desc {
  font-size: var(--text-body-sm);
  line-height: 1.5;
  color: var(--inv-ink-mid);
  max-width: 44ch;
  transition: color var(--dur) var(--ease-out);
}

/* The row is a link, and before this the only thing saying so was a hover
   sweep. Hover never fires on a phone, so three of the most important
   links on the site read as headings to every touch visitor. */
.work-row__go {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--inv-ink-hi);
  border-bottom: var(--hair) solid currentColor;
  padding-bottom: 4px;
  transition: color var(--dur) var(--ease-out);
}

.work-row__go i {
  display: inline-block;
  font-style: normal;
  transition: transform var(--dur-fast) var(--ease-out);
}

.work-row:hover .work-row__go i {
  transform: translateX(4px);
}

/* A real frame from the project's own files, not a drawing of one. The
   source images are 400x232, exactly twice the slot, so the browser has a
   second pixel to spend on dense screens. `cover` only ever matters at the
   two smaller widths, where the slot ratio drifts from the file's. */
.work-row__viz {
  justify-self: end;
  display: block;
  width: 200px;
  height: 116px;
  object-fit: cover;
  object-position: left top;
  border: var(--hair) solid var(--inv-rule);
  transition: border-color var(--dur) var(--ease-out);
}

.work-row:hover .work-row__sweep,
.work-row:focus-visible .work-row__sweep {
  transform: scaleY(1);
}

.work-row:hover,
.work-row:focus-visible {
  color: var(--void);
}

.work-row:hover .work-row__no,
.work-row:hover .work-row__kicker,
.work-row:hover .work-row__desc,
.work-row:focus-visible .work-row__no,
.work-row:focus-visible .work-row__kicker,
.work-row:focus-visible .work-row__desc {
  color: rgba(0, 0, 0, 0.7);
}

.work-row:hover .work-row__go,
.work-row:focus-visible .work-row__go {
  color: var(--void);
}

.work-row:hover .work-row__viz,
.work-row:focus-visible .work-row__viz {
  border-color: rgba(0, 0, 0, 0.5);
}

@media (max-width: 1080px) {
  .work-row__grid {
    grid-template-columns: 40px minmax(0, 1fr) 160px;
  }
  /* The description and its "Case study" affordance share one cell, so the
     placement rule follows the wrapper, not the paragraph inside it. */
  .work-row__body {
    grid-column: 2;
  }
  .work-row__desc {
    max-width: 100%;
  }
  .work-row__viz {
    grid-row: 1 / span 2;
    grid-column: 3;
    width: 160px;
    height: 94px;
    align-self: center;
  }
}

@media (max-width: 700px) {
  .work-row__grid {
    grid-template-columns: 32px minmax(0, 1fr);
  }
  /* The slot stops being a fixed box here and becomes the image's own
     shape, so a screen shown at 260px is never cropped to fit a ratio the
     file does not have. */
  .work-row__viz {
    grid-row: auto;
    grid-column: 2;
    justify-self: start;
    width: 100%;
    max-width: 260px;
    height: auto;
  }
}

/* ---------- 9. Process ---------- */

.process {
  padding-block: var(--gap-section);
}

.process__lead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 32px 64px;
  margin-bottom: 64px;
}

@media (max-width: 860px) {
  .process__lead {
    grid-template-columns: 1fr;
  }
}

.stages {
  border-top: var(--hair) solid var(--ink-lo);
}

.stage {
  display: grid;
  grid-template-columns: 56px minmax(0, 220px) minmax(0, 1fr) 96px;
  gap: 16px 32px;
  align-items: baseline;
  padding-block: 16px;
  border-bottom: var(--hair) solid var(--ink-lo);
  transition: background var(--dur-fast) var(--ease-out);
}

.stage:hover {
  background: var(--surface-2);
}

.stage__no {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  color: var(--ink-meta);
}

.stage__name {
  font-size: var(--text-body);
  font-weight: 500;
  letter-spacing: -0.005em;
}

.stage__what {
  font-size: var(--text-body-sm);
  line-height: 1.5;
  color: var(--ink-mid);
}

.stage__out {
  justify-self: end;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-meta);
  white-space: nowrap;
}

@media (max-width: 860px) {
  .stage {
    grid-template-columns: 40px minmax(0, 1fr);
  }
  .stage__what {
    grid-column: 2;
  }
  .stage__out {
    grid-column: 2;
    justify-self: start;
  }
}

/* Callout — the [?] rule */
.callout {
  margin-top: 48px;
  border: var(--hair) solid var(--signal);
  background: var(--surface-2);
  padding: var(--pad-card);
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 20px 32px;
  align-items: start;
}

@media (max-width: 700px) {
  .callout {
    grid-template-columns: 1fr;
  }
}

/* ---------- 10. About ---------- */

.about {
  padding-block: var(--gap-section);
}

.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 360px) minmax(0, 1fr);
  gap: 40px 64px;
  align-items: start;
}

@media (max-width: 860px) {
  .about__grid {
    grid-template-columns: 1fr;
  }
}

/* Portrait — 4:5, subject cut out of its background.

   The first version was greyscale inside a hairline box, and it read as a
   headstone: a pale rectangular slab, a frontal face, a neutral expression,
   a frame around it. The slab was the problem, not the grey. With the
   background gone the subject stands on the page's own void, the same field
   the hero runs, and the only edge left is the rule under his shoulders. */
.portrait {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--void);
  border-bottom: var(--hair) solid var(--ink-lo);
  overflow: hidden;
}

/* The hero's instrument field, at a third of the density. It sits behind
   the cutout and pauses itself whenever the About section is off screen. */
.portrait__field {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* <picture> exists only to pick a file format, so it is taken out of the
   layout entirely and the img stays a direct child of the frame. */
.portrait picture {
  display: contents;
}

/* Colour, not grey. A rim from upper camera-left, on the same side as the
   key light baked into the photo, so the accent reads as a light falling on
   him rather than as a border drawn around him. drop-shadow follows the
   alpha silhouette, which is the whole reason the cutout is worth its
   extra 20 KB. */
.portrait img {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 12%;
  filter: drop-shadow(-2px -1px 0 rgba(255, 106, 36, 0.9));
}

.about__facts {
  margin-top: 40px;
  border-top: var(--hair) solid var(--ink-lo);
}

.fact {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  padding-block: 12px;
  border-bottom: var(--hair) solid var(--ink-lo);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.fact dt {
  color: var(--ink-meta);
}

.fact dd {
  margin: 0;
  color: var(--ink-hi);
  text-align: right;
}

.fact dd.is-open {
  color: var(--signal);
}

/* ---------- 11. Contact ---------- */

.contact {
  padding-block: var(--gap-section);
  border-top: var(--hair) solid var(--ink-lo);
}

.contact__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-display-sm);
  line-height: 0.84;
  letter-spacing: -0.05em;
  margin-bottom: 40px;
}

.contact__actions {
  margin-top: 32px;
}

.contact__facts {
  margin: 0 0 32px;
  border-top: var(--hair) solid var(--ink-lo);
}

.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 32px 64px;
  align-items: start;
}

@media (max-width: 860px) {
  .contact__grid {
    grid-template-columns: 1fr;
  }
}

.links {
  border-top: var(--hair) solid var(--ink-lo);
}

.link-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-block: 18px;
  border-bottom: var(--hair) solid var(--ink-lo);
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  transition: transform var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

/* Was padding-inline, which relaid out the row's contents on every hover.
   Every other hover on the site changes colour or fill; this one moves. */
.link-row:hover,
.link-row:focus-visible {
  color: var(--signal);
  transform: translateX(12px);
}

.link-row span:last-child {
  color: var(--ink-meta);
  transition: color var(--dur-fast) var(--ease-out);
}

.link-row:hover span:last-child {
  color: var(--signal);
}

/* ---------- 11b. The external-link mark ---------- */

/* This was the character U+2197. The JetBrains Mono subset Google serves
   does not carry it, so every one of the thirty-five marks on the site fell
   back to the system monospace: a different weight, a different baseline and
   a visibly different design sitting inside a line of the real font.

   So it is drawn. Two hairlines at the same weight as everything else on the
   page, sized in `em` so they track whatever type they follow, and painted in
   `currentColor` so they inherit the accent on hover with the label beside
   them. The shaft is one rotated rule; the head is two edges of a square. */
.ext {
  display: inline-block;
  position: relative;
  width: 0.58em;
  height: 0.58em;
  margin-left: 0.5em;
  vertical-align: 0.02em;
  color: inherit;
}

.ext::before {
  content: "";
  position: absolute;
  left: 0.02em;
  bottom: 0.02em;
  width: 0.76em;
  height: var(--hair);
  background: currentColor;
  transform-origin: left bottom;
  transform: rotate(-45deg);
}

.ext::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 0.34em;
  height: 0.34em;
  border-top: var(--hair) solid currentColor;
  border-right: var(--hair) solid currentColor;
}

/* The back mark, same problem and same answer. U+2190 fails the subset
   check exactly like U+2197 did, and it sat in the nav of all three case
   studies and the 404, at the top left of the page, next to the name. The
   shaft is a full-width rule and the head is two edges of a square pinned
   to the shaft's left end and turned 45 degrees, so the corner points the
   way the link goes. */
.back {
  display: inline-block;
  position: relative;
  width: 0.72em;
  height: 0.58em;
  margin-right: 0.55em;
  vertical-align: 0.04em;
  color: inherit;
}

.back::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--hair);
  background: currentColor;
}

.back::after {
  content: "";
  position: absolute;
  left: 0;
  top: calc(50% - 0.3em);
  width: 0.3em;
  height: 0.3em;
  border-left: var(--hair) solid currentColor;
  border-bottom: var(--hair) solid currentColor;
  transform-origin: left bottom;
  transform: rotate(45deg);
}

/* And the forward mark, on the seven buttons and work rows that still
   carried U+2192. Same failure: the subset does not have it either, so the
   arrow on the site's primary call to action was rendering in a different
   typeface from the words beside it. The head hangs off the shaft's right
   end; nothing here touches the element's own transform, so the four pixel
   nudge on hover survives untouched. */
.fwd {
  display: inline-block;
  position: relative;
  width: 0.72em;
  height: 0.58em;
  vertical-align: 0.04em;
  color: inherit;
}

.fwd::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: var(--hair);
  background: currentColor;
}

.fwd::after {
  content: "";
  position: absolute;
  right: 0;
  top: calc(50% - 0.3em);
  width: 0.3em;
  height: 0.3em;
  border-right: var(--hair) solid currentColor;
  border-bottom: var(--hair) solid currentColor;
  transform-origin: right bottom;
  transform: rotate(-45deg);
}

/* ---------- 12. Footer ---------- */

/* The footer is the guaranteed route. Whatever the viewport does to the
   nav, and wherever the visitor stopped reading, the way to write to me
   is at the end of the page. */
.foot {
  padding-block: 24px 56px;
  border-top: var(--hair) solid var(--ink-lo);
  display: grid;
  gap: 18px;
}

.foot__row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 32px;
  justify-content: space-between;
  align-items: baseline;
}

.foot__links {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 20px;
}

.foot a {
  color: var(--ink-mid);
  transition: color var(--dur-fast) var(--ease-out);
}

.foot a:hover,
.foot a:focus-visible {
  color: var(--signal);
}

.foot a.mono--signal {
  color: var(--signal);
}

/* ---------- 13. Case study pages ---------- */

.case-hero {
  padding-top: 140px;
  padding-bottom: clamp(48px, 7vw, 88px);
  border-bottom: var(--hair) solid var(--ink-lo);
}

.case-hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 28px;
}

.case-hero__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-display-sm);
  line-height: 0.84;
  letter-spacing: -0.05em;
  margin-bottom: 24px;
}

.case-hero__sub {
  font-size: clamp(1.125rem, 2.1vw, 1.625rem);
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--ink-hi);
  max-width: 34ch;
  font-weight: 500;
  text-wrap: pretty;
}

.case-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-left: var(--hair) solid var(--ink-lo);
  margin-top: 48px;
}

.case-facts > div {
  padding: 16px 20px;
  border-right: var(--hair) solid var(--ink-lo);
  border-top: var(--hair) solid var(--ink-lo);
  border-bottom: var(--hair) solid var(--ink-lo);
}

.case-facts dt {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-meta);
  margin-bottom: 8px;
}

.case-facts dd {
  margin: 0;
  font-size: var(--text-body-sm);
  line-height: 1.4;
  color: var(--ink-hi);
}

@media (max-width: 860px) {
  .case-facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.case-body {
  padding-block: var(--gap-section);
}

.case-block {
  display: grid;
  grid-template-columns: minmax(0, 220px) minmax(0, 1fr);
  gap: 24px 64px;
  padding-block: clamp(40px, 5vw, 72px);
  border-bottom: var(--hair) solid var(--ink-lo);
  align-items: start;
}

.case-block:first-of-type {
  border-top: var(--hair) solid var(--ink-lo);
}

@media (max-width: 860px) {
  .case-block {
    grid-template-columns: 1fr;
  }
}

.case-block__label {
  position: sticky;
  top: 88px;
  display: flex;
  gap: 12px;
  align-items: baseline;
}

@media (max-width: 860px) {
  .case-block__label {
    position: static;
  }
}

.case-block__h {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  line-height: 1;
  letter-spacing: -0.03em;
  margin-bottom: 20px;
  text-wrap: balance;
}

.case-block .prose + .prose {
  margin-top: 18px;
}

.pull {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.75rem, 4vw, 3rem);
  line-height: 0.98;
  letter-spacing: -0.04em;
  color: var(--ink-hi);
  margin-block: 32px;
  padding-left: 20px;
  border-left: 3px solid var(--signal);
  max-width: 22ch;
  text-wrap: balance;
}

.dec-list {
  margin-top: 24px;
  border-top: var(--hair) solid var(--ink-lo);
}

.dec {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 8px 24px;
  padding-block: 18px;
  border-bottom: var(--hair) solid var(--ink-lo);
}

.dec__id {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  letter-spacing: 0.05em;
  color: var(--signal);
  padding-top: 3px;
}

.dec__t {
  font-size: var(--text-body);
  font-weight: 500;
  line-height: 1.4;
  margin-bottom: 6px;
  letter-spacing: -0.005em;
}

/* Measured at 121 characters per line against a 62ch cap everywhere else
   on the site. The decision ledger is the densest reading on these pages;
   it was the only body text without a measure. */
.dec__w {
  font-size: var(--text-body-sm);
  line-height: 1.5;
  color: var(--ink-mid);
  max-width: 74ch;
}

.case-viz {
  margin-top: 32px;
  border: var(--hair) solid var(--ink-lo);
  background: var(--surface-1);
  padding: clamp(20px, 3vw, 36px);
}

.case-viz svg {
  width: 100%;
  height: auto;
}

/* Below the breakpoint the diagram scrolls instead of shrinking. A
   900-unit schematic with nineteen labels cannot be read at 293px wide,
   and scaling the labels up to compensate would swamp the drawing.

   The `min-width: 0` chain is load-bearing: a grid item defaults to
   `min-width: auto`, which resolves to min-content, so the 820px floor on
   the SVG propagated all the way up and pushed the whole column past the
   viewport. Body-level `overflow-x: hidden` then hid the scrollbar and
   simply clipped the running text instead. */
@media (max-width: 860px) {
  .case-block > * {
    min-width: 0;
  }
  .case-viz {
    overflow-x: auto;
    min-width: 0;
  }
  .case-viz svg {
    min-width: 820px;
  }
  .case-viz figcaption {
    position: sticky;
    left: 0;
  }
}

.case-viz figcaption {
  margin-top: 20px;
  padding-top: 14px;
  border-top: var(--hair) solid var(--ink-lo);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.6;
  max-width: 80ch;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-meta);
}

.case-viz .viz-stroke {
  stroke: var(--ink-hi);
}
.case-viz .viz-dim {
  stroke: var(--ink-lo);
}
.case-viz .viz-fill {
  fill: var(--ink-hi);
}
.case-viz .viz-accent {
  stroke: var(--signal);
}
.case-viz .viz-accent-fill {
  fill: var(--signal);
}
/* A knockout plate: it hides the line running under a label so the rule does
   not strike through the type. It takes the figure's own surface from the
   token rather than a literal, because a hardcoded #000 against a #0a0a0a
   panel is a rectangle you can see, and in print it would be a black bar. */
.case-viz .viz-plate {
  fill: var(--surface-1);
}
/* 8 user units in a 900-unit viewBox rendered 293px wide is 2.6px of
   actual type. The label size is set for the diagram's own coordinate
   space, so it has to be read back through the render scale: 10.5 units
   lands at 11px on a desktop column, and below 860px the figure scrolls
   against a floor width rather than shrinking the type into a smear. */
.case-viz .viz-label {
  fill: var(--ink-mid);
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.055em;
  text-transform: uppercase;
}
.case-viz .viz-label--hi {
  fill: var(--ink-hi);
}

/* ---------- Screens ---------- */

/* Frames captured from the project's own deployed files. The figure carries
   the same hairline box and surface as the schematic above it, so a screen
   and a diagram read as two views of one file rather than two kinds of asset.

   The box belongs to the figure and nothing else. The screenshot inside it
   has no border of its own: two hairlines a few pixels apart, with a strip of
   surface between them that separated nothing, read as a smudge rather than
   as structure. One rectangle per figure. */
.case-screens {
  display: grid;
  gap: clamp(24px, 3.5vw, 44px);
  margin-top: clamp(28px, 4vw, 44px);
}

.case-shot {
  margin: 0;
  border: var(--hair) solid var(--ink-lo);
  background: var(--surface-1);
  padding: clamp(14px, 2vw, 24px);
}

/* The frames are windows, not posters. A ceiling on the height keeps three
   screens to one screenful of scroll each and gives the block a rhythm the
   source files do not share: they render from 301 to 752 pixels tall. What
   falls below the cut is the continuation of a list, never a decision, and
   the artifact links open the file itself. */
.case-shot img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 620px;
  object-fit: cover;
  object-position: left top;
}

/* Below the breakpoint the frame scrolls rather than shrinking, on the same
   reasoning as the schematic above it: a 1140 px application screen at 326 px
   wide is a smear, and the whole argument of this block is that the interface
   can be read. The floor is the width the narrowest of these files was drawn
   at. The `min-width: 0` chain in the .case-viz block above covers this one
   too, since both sit in the same grid child. */
@media (max-width: 860px) {
  .case-shot {
    overflow-x: auto;
    min-width: 0;
  }
  .case-shot img {
    min-width: 720px;
  }
  .case-shot figcaption {
    position: sticky;
    left: 0;
  }
}

.case-shot figcaption {
  margin-top: 20px;
  padding-top: 14px;
  border-top: var(--hair) solid var(--ink-lo);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.6;
  max-width: 80ch;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-meta);
}

/* The first words of a caption say which layer the frame comes from. A
   coloured screen and a grey prototype page are different claims and the
   reader is told which one they are looking at before they judge it. */
.case-shot__layer {
  color: var(--ink-hi);
}

.case-next {
  border-top: var(--hair) solid var(--ink-lo);
  padding-block: clamp(48px, 6vw, 88px);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: end;
  gap: 24px;
}

/* Reading the next case is one option. Writing to me is the other, and
   until now this row only offered the first, three cases in a closed loop. */
.case-next__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.case-next__name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.5rem, 6vw, 5rem);
  line-height: 0.9;
  letter-spacing: -0.045em;
  margin-top: 12px;
}

/* ---------- 13b. The 404 ---------- */

/* Every other page on the site is taller than the window, so the footer
   lands at the bottom on its own. This one is not, and a footer floating
   two thirds down with black underneath looks like the page failed to
   finish loading, which on a 404 is the exact wrong reading. Opt-in rather
   than global: the home page sizes its canvas against the body and does not
   want a flex context imposed on it. */
.page-min {
  min-height: 100vh;
  min-height: 100svh; /* the tall value counts browser chrome that is not there */
  display: flex;
  flex-direction: column;
}

.page-min > main {
  flex: 1 0 auto;
}

/* This page borrows the case hero's vertical rhythm and none of its
   structure. A case hero can afford a single left column because the facts
   grid underneath spans the page; the 404 has nothing underneath, so the
   same shape left the right two thirds of the screen black and the page
   read as broken rather than as a page about something being broken. */
.err {
  padding-top: 140px;
  padding-bottom: clamp(56px, 8vw, 104px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
  gap: clamp(36px, 5vw, 72px);
  align-items: start;
}

.err__title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 0.3em;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-heading-lg);
  line-height: 0.92;
  letter-spacing: -0.045em;
  margin: 0 0 20px;
}

.err__sub {
  font-size: clamp(1.0625rem, 1.7vw, 1.375rem);
  line-height: 1.35;
  letter-spacing: -0.012em;
  color: var(--ink-mid);
  max-width: 38ch;
  margin: 0 0 36px;
  text-wrap: pretty;
}

.err__panel {
  border: var(--hair) solid var(--ink-lo);
  background: var(--surface-1);
  padding: clamp(18px, 2.2vw, 26px);
  display: grid;
  gap: clamp(20px, 2.4vw, 28px);
}

.err__readout {
  display: grid;
  gap: 10px;
  padding-bottom: clamp(20px, 2.4vw, 28px);
  border-bottom: var(--hair) solid var(--ink-lo);
}

/* The path is whatever the visitor typed, so it is set to wrap anywhere
   rather than to overflow: a mistyped address is often a long one, and a
   readout that runs off its own panel is not a readout. */
.err__path {
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  line-height: 1.45;
  letter-spacing: 0.02em;
  color: var(--signal);
  overflow-wrap: anywhere;
}

.err__index {
  display: grid;
  gap: 12px;
}

.err__index .link-row {
  padding-block: 15px;
  gap: 16px;
}

.err__index .link-row span:last-child {
  font-size: var(--text-caption);
  text-align: right;
}

/* One column, in source order. Reversing it with `order` so the exits come
   first would leave the tab sequence running up the page while the eye runs
   down it, and the sentence being skipped past is two lines long. */
@media (max-width: 900px) {
  .err {
    grid-template-columns: minmax(0, 1fr);
    padding-top: 120px;
  }
}

@media (max-width: 520px) {
  .err__index .link-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  .err__index .link-row span:last-child {
    text-align: left;
  }
}

/* ---------- 14. Motion (opt-in via .js-motion) ---------- */

.js-motion .reveal > * {
  opacity: 0;
  transform: translateY(18px);
}

.js-motion .reveal.is-in > * {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

.js-motion .reveal.is-in > *:nth-child(2) {
  transition-delay: 70ms;
}
.js-motion .reveal.is-in > *:nth-child(3) {
  transition-delay: 140ms;
}
.js-motion .reveal.is-in > *:nth-child(4) {
  transition-delay: 210ms;
}
.js-motion .reveal.is-in > *:nth-child(5) {
  transition-delay: 280ms;
}
.js-motion .reveal.is-in > *:nth-child(6) {
  transition-delay: 350ms;
}

/* Case diagram build. The classes and the dash length are set by site.js
   from each shape's own geometry, so a diagram with the file removed, or
   with the measurement unavailable, is simply the finished drawing. */
.js-motion .case-viz .viz-draw {
  stroke-dasharray: var(--draw);
  stroke-dashoffset: var(--draw);
}

.js-motion .case-block.is-in .case-viz .viz-draw {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--draw-dur, 600ms) var(--ease-out)
    var(--draw-delay, 0ms);
}

.js-motion .case-viz .viz-fade {
  opacity: 0;
}

.js-motion .case-block.is-in .case-viz .viz-fade {
  opacity: 1;
  transition: opacity var(--dur) var(--ease-out) var(--draw-delay, 0ms);
}

/* The travelling light. Two strokes make it read as light rather than as a
   moving line: a wide one at low alpha for the halo and a thin bright one
   for the core, both on copies of the contour so the drawn shape underneath
   is never touched. No blur filter and no gradient: the glow is built the
   same way everything else on this site is, out of strokes.

   `--signal-hot` is the accent mixed toward white. It exists only here, as
   light on a diagram. It is never a surface and never carries text, so it
   sits outside the contrast audit by construction rather than by exception. */
.viz-pulses {
  pointer-events: none;
}

.viz-pulse {
  fill: none;
  stroke-linecap: round;
}

.viz-pulse--halo {
  stroke: var(--signal);
  stroke-width: 5;
  opacity: 0.18;
}

.viz-pulse--core {
  stroke: var(--signal-hot);
  stroke-width: 1.6;
  opacity: 0.95;
}

/* Hero type build */
.js-motion .hero__line > span {
  transform: translateY(105%);
}

.js-motion .hero.is-ready .hero__line > span {
  transform: none;
  transition: transform 1100ms var(--ease-out);
}

.js-motion .hero.is-ready .hero__line:nth-child(2) > span {
  transition-delay: 90ms;
}
.js-motion .hero.is-ready .hero__line:nth-child(3) > span {
  transition-delay: 180ms;
}

.js-motion .hero__eyebrow,
.js-motion .hero__foot {
  opacity: 0;
}

.js-motion .hero.is-ready .hero__eyebrow {
  opacity: 1;
  transition: opacity 700ms var(--ease-out) 120ms;
}

.js-motion .hero.is-ready .hero__foot {
  opacity: 1;
  transition: opacity 700ms var(--ease-out) 460ms;
}

/* The [?] token scans */
.js-motion .q--live {
  animation: qscan 3.2s var(--ease-in-out) infinite;
}

@keyframes qscan {
  0%,
  72%,
  100% {
    opacity: 1;
  }
  78% {
    opacity: 0.25;
  }
  84% {
    opacity: 1;
  }
  90% {
    opacity: 0.4;
  }
}

/* Work section: the inversion wipes in. Kept to a sliver on purpose;
   a deep clip reads as an empty black block, not as a transition. */
.js-motion .work {
  clip-path: inset(calc((1 - var(--enter, 1)) * 8%) 0 0 0);
}

/* Case study section markers slide */
.js-motion .case-block__label {
  opacity: 0;
  transform: translateX(-10px);
}

.js-motion .case-block.is-in .case-block__label {
  opacity: 1;
  transform: none;
  transition: opacity 600ms var(--ease-out), transform 600ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .js-motion .reveal > *,
  .js-motion .hero__line > span,
  .js-motion .hero__eyebrow,
  .js-motion .hero__foot,
  .js-motion .case-block__label {
    opacity: 1;
    transform: none;
  }

  /* site.js does not arm the diagrams under reduce, so these should never
     match. They are here because a stylesheet that only works when the
     script agrees with it is one refactor away from a blank diagram. */
  .js-motion .case-viz .viz-draw {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }
  /* site.js never builds the pulse group under reduce, so this should never
     match either. It is the same belt-and-braces as the line above. */
  .viz-pulses {
    display: none;
  }
  .js-motion .case-viz .viz-fade {
    opacity: 1;
  }
  .js-motion .work {
    clip-path: none;
  }
  /* These four escaped the gate. The work-row sweep is the worst of them:
     a 420ms full-row fill that fires on :focus, which is exactly what a
     keyboard user triggers on the three most important links on the page.
     State still changes — it just arrives at once instead of travelling. */
  .work-row,
  .work-row__sweep,
  .work-row__no,
  .work-row__kicker,
  .work-row__desc,
  .work-row__go,
  .work-row__viz,
  .link-row,
  .btn,
  .btn__arrow,
  .stage {
    transition: none;
  }
  .link-row:hover,
  .link-row:focus-visible {
    transform: none;
  }
  .work-row:hover .work-row__go i,
  .btn:hover .btn__arrow {
    transform: none;
  }
  .js-motion .q--live {
    animation: none;
  }
}

/* ---------- 15. Utility ---------- */

/* The attribute has to beat any display the element also carries, or a
   `hidden` flex or grid container stays on screen. This is the whole reason
   the 404 readout can be markup that the script reveals rather than markup
   the script creates. */
[hidden] {
  display: none !important;
}

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

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--paper);
  color: var(--void);
  padding: 14px 16px;
  font-family: var(--font-mono);
  font-size: var(--text-body-sm);
  text-transform: uppercase;
}

.skip:focus {
  left: 8px;
  top: 8px;
}

/* ---------- 16. Print ----------
   A hiring panel saves the page to PDF. Chrome drops backgrounds by
   default, so white type on a void surface printed as blank paper. This
   inverts the world rather than trying to force the ink: black on white,
   chrome removed, and every external href printed after its link text so
   the artifact trail survives on paper. */

@media print {
  html,
  body {
    background: #fff;
    color: #000;
  }

  .nav,
  .hud,
  .hero__canvas,
  .hero__veil,
  .portrait__field,
  .skip,
  .foot__links,
  .case-next,
  .work-row__sweep {
    display: none !important;
  }

  .hero {
    min-height: 0;
    padding-top: 0;
  }

  /* The marquee is a moving object with no print equivalent; the
     statement beside it carries the same claim in one line. */
  .tape__track {
    display: none;
  }

  .tape {
    background: #fff;
    color: #000;
    border-block: 1px solid #000;
  }

  .work {
    clip-path: none !important;
  }

  main,
  section,
  .shell {
    max-width: none;
    padding-inline: 0;
  }

  /* Motion leaves everything at opacity 0 until it is scrolled into view,
     and print never scrolls. */
  .reveal > *,
  .hero__line > span,
  .hero__eyebrow,
  .hero__foot,
  .case-block__label {
    opacity: 1 !important;
    transform: none !important;
  }

  a {
    color: #000;
    text-decoration: underline;
  }

  .link-row[href^="http"]::after,
  .foot a[href^="http"]::after,
  .case-artifacts a::after {
    content: " (" attr(href) ")";
    font-family: var(--font-mono);
    font-size: 9pt;
    word-break: break-all;
  }

  .prose,
  .dec__w,
  .case-viz figcaption,
  .case-shot figcaption {
    color: #000;
  }

  /* The knockout plate takes the sheet's colour, not the screen's, or it
     prints as a black bar across the middle of the diagram. */
  .case-viz .viz-plate {
    fill: #fff;
  }

  /* A light frozen mid-route prints as a stray dash across the drawing. */
  .viz-pulses {
    display: none !important;
  }

  /* The screens are the part of a case study a printed panel copy is worth
     carrying, so they print. Each keeps its caption on the same sheet. */
  .case-shot,
  .case-block,
  .work-row,
  .stage {
    break-inside: avoid;
  }

  .case-shot img {
    max-height: 60vh;
    object-fit: contain;
    object-position: left top;
  }

  h1,
  h2,
  .contact__title,
  .case-hero__title {
    break-after: avoid;
  }
}
