/* Ben Golden, personal site. One stylesheet, no build step.

   Look: "Signal". Dark ground with grain and scanlines, condensed sentence-case display,
   transit-signage vocabulary, analog degradation. Keyed to the Service Changes pieces on
   purpose, so the case studies wear the art project's clothes.

   The full system, the reasoning, and the checks to run are in ../../../DESIGN.md.
   Read it before changing colour or type here.

   Two things that will bite if you skip the doc:
     1. Colour variants are derived per surface. --orange fails as small text on the dark
        ground; that is what --orange-lift is for. Type on a red fill is ink, not paper.
     2. Display leading is 1.06 because at 0.95 the leading was shorter than the ink and
        descenders collided with the line below.

   Motion: everything moves at 12 fps on desktop. See the MOTION section at the end. */

:root {
  /* ---- Surfaces ---- */
  --ground: #121217;      /* the page. Lifted off absolute black so grain reads as
                             texture rather than noise. */
  --panel: #16161c;       /* diagram and log panels, one step up from the ground */
  --paper: #f1efe8;       /* light surface: slabs, the "currently" block */
  --surface: #1a1a21;     /* media backing while a video or image loads */
  --line: #2a2a31;        /* hairlines and section rules on the ground */
  --line-paper: #c9c6bc;  /* the same, on a paper surface */

  /* ---- Text ---- */
  --text: #f1efe8;        /* body on the ground */
  --text-2: #a9a7a1;      /* secondary on the ground */
  --ink: #0b0b0c;         /* body on paper */
  --ink-2: #3a3a38;       /* secondary on paper */

  /* ---- Accents. See DESIGN.md §1. Red is the signage voice, cyan is machine
         readout. Every variant clears AA on the surface it is used on. ---- */
  --orange: #FF7A1A;      /* signage fills: bands, slab rules, chips, plates.
                             Ink on this fill reads 7.54:1, where the old red
                             managed only 4.24 and quietly failed its own bar. */
  --orange-lift: #FF8A33; /* signage text on the ground        7.95:1 */
  --orange-ink: #A85100;  /* signage text on paper             4.75:1. Reads closer
                             to tobacco than to orange, and cannot be helped: at
                             4.5:1 on this paper there is no orange left. Large
                             signage type on paper may use #DA6A00 at the 3:1 bar. */
  --teal: #0FBBBD;        /* readout text and diagram strokes  7.88:1 */
  --teal-ink: #0A7879;    /* readout text on paper             4.59:1 */

  --display: "Archivo", "Arial Narrow", Impact, sans-serif;
  --serif-text: "Newsreader", "Iowan Old Style", Georgia, serif;
  --mono: "Martian Mono", "SFMono-Regular", Consolas, monospace;

  --measure: 38rem;
  --margin-col: 13rem;
  --wrap: 72rem;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --tile-h: clamp(220px, 34vw, 460px);
  --sy: 0;   /* scroll position, quantised, set by site.js at 12 fps */
  --vel: 0;  /* smoothed scroll speed 0..1, set by site.js */
}

/* ---- Reset, base ---- */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  color-scheme: dark;
  font-family: var(--serif-text);
  font-size: 1.125rem;
  line-height: 1.55;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img, video { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration-color: var(--orange); text-decoration-thickness: 2px; text-underline-offset: 0.16em; }
a:hover { color: var(--orange-lift); }
:focus-visible { outline: 3px solid var(--teal); outline-offset: 3px; }
hr { border: 0; border-top: 1px solid var(--line); margin: 3rem 0; }
strong { font-weight: 600; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 var(--gutter); }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* Anything sitting on a light surface flips the text tokens rather than restating
   every colour. Put .on-paper on the section, not on each element. */
.on-paper { background: var(--paper); color: var(--ink); }
.on-paper { --text: var(--ink); --text-2: var(--ink-2); --line: var(--line-paper); }
.on-paper a { color: inherit; }
.on-paper a:hover { color: var(--orange-ink); }

/* ---- Grain and scanlines ----
   The finish on the Service Changes pieces, applied to the page. Grain overlays at low
   opacity; scanlines are a 3px repeating gradient. Both are pointer-events:none and sit
   above content but below the timecode. */
body::after {
  content: ""; position: fixed; inset: 0; z-index: 9990; pointer-events: none;
  mix-blend-mode: overlay; opacity: 0.38;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.42'/></svg>");
  background-size: 160px 160px;
}
body::before {
  content: ""; position: fixed; inset: 0; z-index: 9989; pointer-events: none;
  opacity: 0.34;
  background: repeating-linear-gradient(180deg, rgba(0,0,0,.22) 0 1px, transparent 1px 3px);
}

/* ---- Type ---- */

h1, h2, h3, h4 { margin: 0; line-height: 1.12; text-wrap: balance; }
/* Display headings are sentence case. Caps are reserved for signage: the wordmark, the
   nav, the eyebrow chips, table labels. Anything read as a sentence is not caps.
   Leading is 1.06 because sentence case has descenders and ascenders to clear; at the
   old 0.95 the leading was shorter than the ink and lines physically collided. */
h1, h2, .leadins h2, .index-list h2 {
  font-family: var(--display);
  font-variation-settings: "wdth" 70;
  font-weight: 900;
  letter-spacing: -0.015em;
  /* Condensed faces narrow the word space along with the glyphs, so at wdth 70 and
     weight 900 two short words run together: Ben read "Four pieces" as one word. The
     fix is word space, not tracking. Tracking would loosen the letters inside each
     word, which is the opposite of what display type at this width wants. */
  word-spacing: 0.07em;
  line-height: 1.06;
}
/* The wordmark stays caps: two words, scanned not read. Caps want positive tracking. */
.wordmark {
  font-family: var(--display);
  font-variation-settings: "wdth" 72;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 0.95;
}
h1 { font-size: clamp(2.6rem, 6.4vw, 5.6rem); }
h2 { font-size: clamp(1.7rem, 3vw, 2.5rem); }
/* Tracking is optical, so it cannot be one value across a 2.6rem to 5.6rem range. The
   -0.015em above is tuned for h1; at h2 size the same relative value closes up pairs
   like the "r p" in "Four pieces", which is what Ben caught. Big type wants tighter
   tracking, smaller type wants looser. */
h2, .leadins h2, .index-list h2 { letter-spacing: -0.004em; }
h3 { font-family: var(--serif-text); font-size: 1.3rem; font-weight: 500; }
p { margin: 0 0 1.15em; }
p:last-child { margin-bottom: 0; }
ul, ol { padding-left: 1.2em; margin: 0 0 1.15em; }
li { margin-bottom: 0.35em; }

.eyebrow {
  display: inline-block; width: fit-content;
  /* Ink on the orange fill, 7.54:1 at this size. The old red fill managed only
     4.24 and had to fall back to paper type on a darkened red, which is why this
     rule used to reach for --orange-ink. Black on orange is also the signage
     convention the whole system is quoting. */
  background: var(--orange); color: var(--ink);
  font-family: var(--mono);
  font-variation-settings: "wdth" 85;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.28em 0.55em 0.22em;
  margin: 0 0 0.9rem;
}
.eyebrow a { color: inherit; text-decoration: none; }
.eyebrow a:hover { text-decoration: underline; color: inherit; }

/* text-wrap: pretty to kill widows. Ben flagged a lede ending on a dangling "at";
   the last line was one word wide. Inert in browsers that do not support it. */
.lede { font-size: 1.3rem; line-height: 1.45; color: var(--text); max-width: var(--measure);
        text-wrap: pretty; }
.mono { font-family: var(--mono); font-variation-settings: "wdth" 85; font-size: 0.8rem; letter-spacing: 0.01em; }

/* Machine readout: the cyan half of the rule. Numbers, dates, counts, specs, tool
   names. Not prose, and not titles. See DESIGN.md §1. */
.readout {
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.78rem; letter-spacing: 0.01em; color: var(--teal);
}
.on-paper .readout { color: var(--teal-ink); }

/* ---- Header ---- */

.site-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 2rem;
  padding: 1.6rem 0 1.2rem;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}
.wordmark { font-size: 1.15rem; letter-spacing: 0.02em; text-decoration: none; color: var(--text); }
.wordmark:hover { color: var(--text); }
.site-nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 1.5rem; flex-wrap: wrap; }
.site-nav a {
  text-decoration: none; color: var(--text-2); padding-bottom: 0.15rem;
  font-family: var(--display); font-variation-settings: "wdth" 90;
  text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.08em; font-weight: 600;
}
.site-nav a:hover { color: var(--text); }
.site-nav a[aria-current="page"],
.site-nav a[aria-current="true"] { color: var(--text); border-bottom: 3px solid var(--orange); }

/* ---- Footer ---- */

.site-foot {
  margin-top: 6rem;
  padding: 2.5rem 0 3rem;
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
  font-size: 1rem;
  color: var(--text-2);
}
.site-foot h2 { font-family: var(--serif-text); font-size: 1rem; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--text); margin-bottom: 0.6rem; }
.site-foot ul { list-style: none; margin: 0; padding: 0; }
.site-foot li { margin-bottom: 0.3rem; }
.site-foot p { margin-bottom: 0.4rem; }
.site-foot .foot-id { grid-column: 1 / -1; }
.site-foot .foot-id p { margin: 0; text-wrap: nowrap; }
@media (max-width: 46rem) { .site-foot .foot-id p { text-wrap: wrap; } }
/* The copyright line is fine print. It is neither signage nor readout, so per DESIGN.md
   it takes neither accent and stays on --text-2, which measures 7.76:1 on the ground and
   leaves room to drop the size without approaching the 4.5:1 small-text bar. The rule
   needs three classes to outrank `.site-foot .foot-id p`, which zeroes the margin. */
.site-foot .foot-id .copyright { margin-top: 0.55rem; font-size: 0.82rem; color: var(--text-2); }
@media (max-width: 40rem) { .site-foot { grid-template-columns: 1fr; } }

/* ---- Full bleed ----
   Lets a block span the viewport from inside a .wrap without restructuring the page.
   Inside a wrap the margins pull it out to the viewport edges; where there is no wrap
   they compute to zero, so the same rule is safe in both layouts. body has
   overflow-x:hidden, which absorbs the scrollbar width that 100vw would otherwise add. */
.masthead, .hero, .slab, .band { margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); }

/* ---- Masthead: type over a darkened ground ----
   The ground can be a still or a muted looping video. Never a 100vh wall except on a
   case study. The chromatic split is driven by --vel, so it smears while you scroll and
   resolves when you stop. */
.masthead { position: relative; overflow: hidden; padding: clamp(3.5rem, 13vh, 8rem) 0 clamp(2.5rem, 8vh, 5rem); }

/* Hero framing holds at every viewport width. Added September 19, 2026.
   The masthead is full-bleed, but its height came only from padding and content, so a
   wider window widened the box without deepening it. background-size: cover then scaled
   the image to the new width and cropped the overflow vertically, which at 4K closed the
   frame down to torsos. Tying a MINIMUM height to viewport WIDTH holds the box at a
   constant ratio, so cover crops by the same amount at 1080p, 1440p and 4K.
     - min-height, not height: a narrow or short window still falls back to content height
       and nothing gets clipped.
     - 33vw comes from the framing Ben approved: the 16:9 source shows about 58% of its
       height in that crop, and 0.58 / 1.777 = 0.326.
     - The 75vh ceiling is for ultrawide (21:9), where 33vw would exceed the screen.
   Content is centred because the extra height would otherwise pool below the text. */
.masthead, .hero {
  min-height: min(33vw, 75vh);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Home keeps its portrait-beside-text layout and gains a ground behind it, rather
   than becoming a masthead. The video is there for colour and movement, not to be
   looked at, so it sits under the same veil everything else does. */
.hero { position: relative; overflow: hidden; }
.masthead .ground, .hero .ground {
  position: absolute; inset: -14% 0; overflow: hidden;
  background-size: cover; background-position: center;
  filter: grayscale(0.72) contrast(1.05) brightness(0.38);
  transform: translateY(calc((var(--p, 0) - 0.5) * 16vh)) scale(1.1);
}
/* A ground tuned for a high-key source. The default filter assumes a dark photograph
   and only has to hold it down; a mostly-white image put through brightness(0.38)
   becomes flat mid-grey instead, because there is nothing dark in it to begin with.
   So: desaturate harder, push contrast to keep the geometry legible, and take the
   brightness much lower. Motif's ground is the only one of these on the site. */
/* Two ground variants now, and the reason is worth stating once: the default filter
   assumes a normally exposed photograph and only has to hold it down. It fails in both
   directions. A high-key source put through brightness(0.38) goes flat mid-grey because
   there is nothing dark in it to hold; an already-dark source goes to mush because
   there is nothing left to take away.

   --light is for the bright end. Motif's stock render. */
.masthead .ground--light {
  filter: grayscale(0.88) contrast(1.5) brightness(0.17);
}
/* --lift is for the dark end. About's night shoot, where the default crushed the fire,
   the graffiti and the crew into one black field. Desaturates much less, because the
   colour in that frame is the thing worth having. */
.masthead .ground--lift {
  /* Retuned from 0.78 once Ben graded the source frame himself. The variant exists
     because the *ungraded* night still went to mush at the default 0.38; a lifted
     source needs less help, and at 0.78 the headline lost its separation. */
  filter: grayscale(0.3) contrast(1.05) brightness(0.66);
}
.masthead .ground video, .hero .ground video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.masthead .veil, .hero .veil { position: absolute; inset: 0; background: radial-gradient(110% 80% at 30% 45%, rgba(18,18,23,.45), rgba(18,18,23,.97)); }
.masthead .wrap, .hero .wrap { position: relative; }
.masthead h1 {
  /* Four layers, not two: a wide pair that also blurs as it goes, and a tight
     pair that keeps an edge inside the smear. Amplitude doubled from 6px. The
     fringe stays red/cyan whatever the accents are, because that is what chroma
     error physically is; it is the artifact's own voice, not the palette's. */
  text-shadow:
    calc(var(--vel, 0) *  -9px) calc(var(--vel, 0) * -1px) calc(var(--vel, 0) * 2.2px) rgba(255,0,64,.82),
    calc(var(--vel, 0) *   9px) calc(var(--vel, 0) *  1px) calc(var(--vel, 0) * 2.2px) rgba(0,224,255,.82),
    calc(var(--vel, 0) * -3.5px) 0 0 rgba(255,0,64,.5),
    calc(var(--vel, 0) *  3.5px) 0 0 rgba(0,224,255,.5);
}

/* ---- Signage slab: the notice ----
   Works as a section header, a materials list, and a "currently" block. Label column is
   signage, value column is a sentence or a readout. */
.slab {
  background: var(--paper); color: var(--ink);
  --text: var(--ink); --text-2: var(--ink-2); --line: var(--line-paper);
  padding: clamp(2.2rem, 6vh, 4rem) 0;
  border-top: 8px solid var(--orange); border-bottom: 8px solid var(--orange);
}
.slab h2 { color: var(--ink); }
.slab .slab-tag { color: var(--orange-ink); }
.slab .rows {
  display: grid; grid-template-columns: auto 1fr; gap: 0.55rem 1.6rem;
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.78rem; letter-spacing: 0.01em; line-height: 1.6;
}
.slab .rows b {
  font-weight: 600; color: var(--orange-ink); text-transform: uppercase;
  letter-spacing: 0.08em; font-size: 0.66rem; white-space: nowrap;
}
.slab .rows span { color: var(--ink-2); }
@media (max-width: 40rem) {
  .slab .rows { grid-template-columns: 1fr; gap: 0.15rem 0; }
  .slab .rows b { margin-top: 0.7rem; }
}

/* ---- Band: one sentence on solid red ----
   Ink type, which is both higher contrast (4.24 vs 4.03) and the signage convention.
   Never put type over a picture: every image on this site already contains type. */
.band { background: var(--orange); color: var(--ink); padding: clamp(1.6rem, 5vh, 3rem) 0; }
.band p {
  font-family: var(--display); font-variation-settings: "wdth" 72; font-weight: 900;
  font-size: clamp(1.4rem, 4vw, 3rem); line-height: 1.06; letter-spacing: -0.015em;
  margin: 0; max-width: 30ch; text-wrap: balance;
}

/* ---- Home ---- */

.hero { padding: clamp(3.5rem, 10vh, 7rem) 0 clamp(2.5rem, 7vh, 4.5rem); }
/* Less desaturation than a masthead ground. Ben asked for the video to add some
   colour, and grayscale(0.72) takes almost all of it out. Brightness stays low so
   the headline and the portrait still carry. */
.hero .ground { filter: grayscale(0.3) contrast(1.05) brightness(0.52); }
.hero h1 { font-size: clamp(2.4rem, 5.4vw, 5rem); margin-bottom: 1.4rem; }
.hero .lede { max-width: 44rem; font-size: clamp(1.15rem, 1.6vw, 1.35rem); color: var(--text); }
.hero .lede br { display: block; content: ""; margin-top: 0.55rem; }
.hero .role { margin-top: 1.6rem; color: var(--text-2); font-size: 1rem; }

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(9.5rem, 19vw, 16.5rem);
  gap: clamp(1.1rem, 2.4vw, 2rem);
  align-items: start;
}
.hero-portrait { width: 100%; height: auto; display: block; border-radius: 50%; }
@media (max-width: 46rem) {
  .hero-grid { grid-template-columns: 1fr; gap: 1.8rem; }
  .hero-portrait { max-width: 9rem; }
}

.leadins { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.leadins > li {
  display: grid;
  grid-template-columns: var(--margin-col) minmax(0, 1fr);
  gap: 2rem;
  padding: 2.2rem 0;
  border-bottom: 1px solid var(--line);
  margin: 0;
}
.leadins .eyebrow { margin: 0.2rem 0 0; align-self: start; justify-self: start; }
.leadins h2 { margin-bottom: 0.5rem; }
.leadins h2 a { text-decoration: none; }
.leadins h2 a:hover { text-decoration: underline; }
/* :not(.eyebrow) on every container paragraph rule below. .eyebrow is a <p>, so a
   selector like ".leadins p" outspecifies ".eyebrow" and repaints the label text,
   which turned the case study index labels the wrong colour on their own background. */
.leadins p:not(.eyebrow) { max-width: var(--measure); color: var(--text); }
.leadins ul.sub { list-style: none; padding: 0; margin: 0.9rem 0 0; }
.leadins ul.sub li { margin-bottom: 0.25rem; color: var(--text-2); }
.leadins ul.sub a { color: var(--text); }

/* ---- Page head ---- */

.page-head { padding: clamp(2.5rem, 6vw, 4.5rem) 0 2.5rem; }
.page-head h1 { max-width: 48rem; margin-bottom: 1.2rem; }
.page-head .lede { margin-top: 0; color: var(--text); }
/* Production metadata: the label is signage, the value is readout. */
.meta {
  list-style: none; padding: 0.7rem 0; margin: 1.8rem 0 0;
  display: flex; flex-wrap: wrap; gap: 0.5rem 2.5rem;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.75rem; color: var(--teal); letter-spacing: 0.01em;
}
.meta li { margin: 0; }
.meta b { font-weight: 600; color: var(--orange-lift); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.66rem; }

/* ---- Marginalia layout for long reads ---- */

.sec {
  display: grid;
  grid-template-columns: var(--margin-col) minmax(0, var(--measure));
  gap: 1rem 2.5rem;
  padding: 2.4rem 0;
  border-top: 1px solid var(--line);
}
.sec:first-of-type { padding-top: 2.4rem; }
.sec-margin { grid-column: 1; }
.sec-main { grid-column: 2; }
.sec-margin .eyebrow { margin-bottom: 0.6rem; }
.sec-margin h2 { font-family: var(--serif-text); text-transform: none; letter-spacing: 0; font-size: 1.15rem; font-weight: 500; line-height: 1.3; }
.sec-main p { color: var(--text-2); }
.aside {
  font-size: 0.95rem; line-height: 1.5; color: var(--text-2);
  margin-top: 1rem; padding-top: 0.8rem; border-top: 1px solid var(--line);
}
.sec .fig, .sec .placeholder--wide { grid-column: 1 / -1; }
/* A figure that stays in the reading column so its left edge lines up with the
   paragraph, rather than hanging into the margin. Ben's call for About: at the full
   marginalia width the two squares dominated the page and their left edge disagreed
   with the text above them. Declared after the span rule so it wins the tie. */
.sec > .fig--inset { grid-column: 2; }
.fig--inset img, .fig--inset video { max-width: 27rem; }
.sec .sec-main + .fig { margin-top: 1.5rem; }

@media (max-width: 56rem) {
  .sec > .fig--inset { grid-column: 1; }
  /* minmax(0,1fr), not 1fr: a bare 1fr track has min-width:auto, so the <pre> in .log
     forces the grid wider than the viewport and the whole page scrolls sideways. */
  .sec, .leadins > li { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
  .sec-margin, .sec-main { grid-column: 1; }
  .aside { border-top: 0; padding-top: 0; margin-top: 0.25rem; margin-bottom: 0.75rem; }
  :root { --margin-col: 1fr; }
}

/* A full-width section that only needs vertical rhythm, no margin column. */
.sec-plain { padding: clamp(3rem, 9vh, 6.5rem) 0; }

/* ---- Long-read devices ----
   Added for About, which had two copy-plus-figure rows, an orphaned paragraph and a
   table that did not cohere. These are the two things a magazine feature has that the
   page was missing: something that interrupts the column, and a chart. */

/* A pull quote. Spans the whole marginalia grid so it breaks the measure rather than
   sitting inside it, which is the entire point of the device. Serif, because it is a
   sentence being read again rather than a label, so none of the accent roles apply. */
.pull {
  grid-column: 1 / -1;
  margin: 1.6rem 0 0.4rem;
  padding-left: 1.2rem;
  border-left: 3px solid var(--orange);
  max-width: 42rem;
  font-family: var(--serif-text);
  font-size: clamp(1.3rem, 2.3vw, 1.85rem);
  line-height: 1.22;
  color: var(--text);
  text-wrap: balance;
}

/* Employment as a chart rather than a table, so the arc in the headline is something a
   reader can see instead of add up. Bars are positioned on a shared 2021-to-now axis,
   which is what makes the overlap legible: the Libera bar runs under everything else.

   The bar is aria-hidden and purely decorative. The dates sit beside it as text and
   carry the same information, so a row still reads with CSS off, in a screen reader,
   or if --from/--to ever go undefined. An undefined custom property in `left` would
   compute to invalid and the bar would simply not paint, which the audit now catches
   but which should not lose information either way. */
.timeline { list-style: none; margin: 0.5rem 0 0; padding: 0; grid-column: 1 / -1; }
.tl-row {
  display: grid;
  grid-template-columns: minmax(6.5rem, 10rem) minmax(0, 1fr) minmax(7.5rem, 11rem);
  gap: 0.35rem 1.2rem; align-items: center;
}
.timeline li { padding: 0.65rem 0; border-top: 1px solid var(--line); margin: 0; }
.timeline li:last-of-type { border-bottom: 0; border-top: 0; padding-top: 0; }
/* The axis is the last li but is not a record, so the closing rule belongs to the
   employer above it. */
.timeline li:nth-last-of-type(2) { border-bottom: 1px solid var(--line); }
.timeline b {
  font-family: var(--display); font-variation-settings: "wdth" 84; font-weight: 700;
  font-size: 1.02rem; letter-spacing: 0; word-spacing: normal; color: var(--text);
}
.tl-track { position: relative; height: 0.45rem; background: var(--panel); }
.tl-bar {
  position: absolute; top: 0; bottom: 0;
  left: var(--from); right: calc(100% - var(--to));
  background: var(--teal);
  /* A one or two month stint is ~1.5% of the axis; floor it so the dash still paints. */
  min-width: 4px;
}
.tl-when {
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.68rem; color: var(--teal); letter-spacing: 0.01em;
}
.tl-axis { margin-top: 0.5rem; }
/* A line that trails the chart. .timeline spans the whole grid, so without an
   explicit column this lands under the margin label instead of the text. */
/* Aligned to the chart above it, not to the reading measure: the chart spans the whole
   grid, so a note indented to column 2 reads as a mistake rather than a hierarchy. */
.sec > .tl-note { grid-column: 1 / -1; margin: 1.4rem 0 0; color: var(--text-2); }
.tl-axis .tl-ticks {
  display: flex; justify-content: space-between;
  font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.08em;
  color: var(--text-2);
}
@media (max-width: 46rem) {
  /* Stack the row: name, then dates, then the bar full width underneath. The bar is
     last because it is the decorative part and should not separate a label from its
     value. */
  .tl-row { grid-template-columns: minmax(0, 1fr) auto; }
  .tl-row .tl-track { grid-column: 1 / -1; order: 3; }
  .tl-row .tl-when { text-align: right; }
}

/* ---- Figures, tables ---- */

.fig { margin: 1.5rem 0 0; }
/* Captions carry a small caps label, then the sentence underneath. The label is
   signage, so caps and positive tracking are right there; the sentence is read, so it
   is not caps. A caption that is only a title is label alone, with no trailing gap. */
.fig figcaption {
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.72rem; color: var(--text-2); margin-top: 0.7rem; line-height: 1.65;
  letter-spacing: 0.01em;
}
.fig figcaption b {
  display: block; font-size: 0.64rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--orange-lift); margin-bottom: 0.35rem;
}
.fig figcaption b:only-child { margin-bottom: 0; }
.fig img, .fig video { width: 100%; height: auto; background: var(--surface); }
/* Square and vertical media get a width cap. .fig video is width:100%, so a 1:1 cut
   in a full-width column renders as a giant square. */
.fig.r-1x1 video, .fig.r-1x1 img { max-width: 30rem; }
.fig.r-9x16 video, .fig.r-9x16 img { max-width: 20rem; }
.fig.r-3x4 video, .fig.r-3x4 img { max-width: 26rem; }
/* The three spots as one block: square Out of the Box at left, the two 16:9 cuts
   stacked at right. The right column's height is two 16:9s (1.125 x its width) plus a
   row gap AND a caption block, so a plain fr ratio leaves it hanging below the square.
   Solving for it: R = (100% - 2*gap - caption) / 2.125, and the square takes the rest. */
.spot-grid {
  --g: 0.6rem;
  --cap: 1.75rem; /* figcaption margin-top 0.7rem + one line at 0.7rem x 1.5 */
  display: grid;
  gap: var(--g);
  align-items: start;
  grid-template-columns:
    calc(100% - var(--g) - (100% - 2 * var(--g) - var(--cap)) / 2.125)
    calc((100% - 2 * var(--g) - var(--cap)) / 2.125);
}
.spot-grid .spot { margin: 0; min-width: 0; }
.spot-grid .spot-lead { grid-row: 1 / 3; }
@media (max-width: 46rem) {
  .spot-grid { grid-template-columns: minmax(0, 1fr); }
  .spot-grid .spot-lead { grid-row: auto; }
}
.fig.compare { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.fig.compare figcaption { grid-column: 1 / -1; }
@media (max-width: 40rem) { .fig.compare { grid-template-columns: 1fr; } }
.fig.passes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; }
.fig.passes figcaption { grid-column: 1 / -1; }

.data {
  border-collapse: collapse; width: 100%; max-width: var(--measure);
  border-top: 1px solid var(--line);
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.8rem; line-height: 1.4; margin: 0.5rem 0 1.2rem;
  color: var(--teal);
}
.data th, .data td { text-align: left; padding: 0.55rem 0.8rem 0.55rem 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.data th { font-weight: 600; color: var(--orange-lift); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; }
.data td:last-child, .data th:last-child { padding-right: 0; }
.data--wide { max-width: none; }

/* ---- Machine output: logs and headline stats ---- */

.log {
  background: var(--panel);
  color: var(--text-2);
  border: 1px solid var(--line);
  font-family: var(--mono); font-variation-settings: "wdth" 85;
  font-size: 0.66rem; line-height: 1.8;
  padding: 1.1rem 1.2rem;
  margin: 0;
  overflow-x: auto;
  white-space: pre;
  tab-size: 2;
}
.log .pass { color: var(--text); }
.log .skip { color: var(--teal); }      /* the skips are the point of the block */
.log .dim  { color: #808089; }   /* 4.60:1 on --panel; #6e6e77 was 3.57 */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 1.4rem 2rem;
  margin: 1.6rem 0 1.2rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
  max-width: var(--measure);
}
.stats--wide { max-width: none; }
.stat b {
  display: block;
  font-family: var(--display);
  font-variation-settings: "wdth" 70;
  font-weight: 900;
  font-size: clamp(2.1rem, 4.4vw, 3.2rem);
  line-height: 1;
  letter-spacing: -0.005em;
  font-feature-settings: "tnum" 1;
}
.stat span {
  display: block;
  font-family: var(--mono); font-variation-settings: "wdth" 85;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-2);
  margin-top: 0.55rem;
  line-height: 1.4;
}
.stat--accent b { color: var(--teal); }
/* Both of these are the *fallback* for the ramp below, so both must sit above the
   @supports block. `.board .n.is-hot` used to be declared down in the board section
   instead, after the ramp and at identical specificity, so it won and painted flat
   teal over the clip. The gradient was applied and invisible. */
.board .n.is-hot { color: var(--teal); }

/* ---- The teal-to-white ramp ----
   Only on readout big enough to carry it. The ramp is held flat for the first
   42% and breaks late, so it reads as a decision; a ramp that starts at the cap
   just looks like the glyph ran out of ink. Contrast along it runs 7.88:1 at the
   teal end to 18.67:1 at the white, so every part of it clears AA on the ground.
   Guarded because the fallback for unsupported background-clip is invisible text. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .board .n.is-hot, .stat--accent b {
    background-image: linear-gradient(180deg, var(--teal) 0%, var(--teal) 42%, #ffffff 100%);
    -webkit-background-clip: text; background-clip: text;
    color: transparent;
  }
}

/* ---- Board: figure, description, production fact ----
   Numerals stay paper because at that scale their job is mass, not colour. Facts are
   readout. Descriptions are plain language, so neither. */
.board { list-style: none; margin: 1.6rem 0 0; padding: 0; }
.board .line {
  position: relative; margin: 0;
  display: grid; grid-template-columns: auto 1fr auto; gap: 1.5rem; align-items: baseline;
  border-bottom: 1px solid var(--line); padding: 0.85rem 0;
}
.board .line::before {
  content: ""; position: absolute; left: -0.9rem; top: 1.15rem;
  width: 3px; height: 1.1rem; background: var(--teal); opacity: 0.45;
}
.board .line.is-hot::before { opacity: 1; }
.board .n {
  font-family: var(--display); font-variation-settings: "wdth" 70; font-weight: 900;
  font-size: clamp(2.2rem, 6vw, 4.6rem); line-height: 1; letter-spacing: -0.005em;
  min-width: 4.5ch; font-feature-settings: "tnum" 1;
}
.board .l { color: var(--text-2); }
.board .t { color: var(--teal); font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.12em; }
@media (max-width: 46rem) {
  .board .line { grid-template-columns: auto 1fr; gap: 0.8rem; }
  .board .t { grid-column: 1 / -1; }
}

/* ---- Diagrams: inline SVG on an oscilloscope grid ----
   Wires carry a slow dash so the thing reads as a system with something moving in it.
   Undefined custom properties fail silently here: stroke computes to none and the
   element vanishes. Check that every var() resolves after any regeneration. */
.diagram { width: 100%; height: auto; background: var(--ground); border: 1px solid var(--line); display: block; }
/* The ramp lives in each page's <defs> as #flow. Presentation attributes on the
   lines lose to this, so the markup keeps its plain stroke as a fallback for any
   context where the gradient does not resolve. */
.diagram .wire { stroke: url(#flow); stroke-width: 1.7; }
.diagram .n { font-family: var(--mono); font-variation-settings: "wdth" 85; font-size: 12px; font-weight: 600; letter-spacing: 0.06em; }
.diagram .s { font-family: var(--mono); font-variation-settings: "wdth" 92; font-size: 9.5px; letter-spacing: 0.02em; }
.diagram .i { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.14em; }

/* ---- Placeholders. Not designed. Listed in TODO.md, gone before launch. ---- */

.placeholder {
  border: 1px dashed var(--orange);
  background: rgba(255, 122, 26, 0.08);
  padding: 1rem 1.1rem;
  margin: 1.5rem 0;
  font-family: var(--mono); font-variation-settings: "wdth" 85;
  font-size: 0.78rem; line-height: 1.5; color: var(--text-2);
}
.placeholder::before {
  content: "PLACEHOLDER";
  display: block; font-weight: 600; letter-spacing: 0.08em; color: var(--orange-lift); margin-bottom: 0.35rem;
}
.placeholder--media { display: grid; place-content: center; text-align: center; aspect-ratio: 16 / 9; min-height: 8rem; max-height: 20rem; }
.placeholder--media.ratio-9x16 { aspect-ratio: 9 / 16; max-width: 18rem; }
.placeholder--media.ratio-1x1 { aspect-ratio: 1; max-width: 22rem; }
.placeholder--inline { display: inline; padding: 0.1em 0.4em; margin: 0; }
.placeholder--inline::before { content: none; }

/* ---- Case studies index ---- */

.index-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.index-list li {
  display: grid;
  grid-template-columns: var(--margin-col) minmax(0, 1fr) clamp(11rem, 21vw, 19rem);
  gap: 1rem 2.5rem;
  padding: 2rem 0; border-bottom: 1px solid var(--line); margin: 0;
}
.index-list h2 { margin-bottom: 0.5rem; }
.index-list h2 a { text-decoration: none; }
.index-list h2 a:hover { text-decoration: underline; }
.index-list p:not(.eyebrow) { max-width: var(--measure); color: var(--text); }
.index-list .eyebrow { margin: 0.2rem 0 0; align-self: start; justify-self: start; }

/* ---- Case study index thumbnails ----
   A third column, deliberately narrow. Ben's call after seeing three layouts built:
   these images are not all strong enough to hold half a composition unaltered, so they
   work as an accent rather than the centre of attention, and they get darkened to sit
   in the system rather than on top of it.

   Each image previews the masthead of the page it links to, so the thing you click is
   the thing you land on. That is why no new assets were needed.

   The wrapping anchor is aria-hidden and out of the tab order: the headline above it is
   already a link to the same place, and a second tab stop to the same destination is
   noise for anyone using a keyboard or a screen reader. */
.index-list li { align-items: start; }
.index-list .cs-link { display: block; text-decoration: none; grid-column: 3; }
.index-list .cs-img {
  display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
  object-fit: cover; background: var(--surface);
  filter: brightness(0.78) contrast(1.08) saturate(0.92);
}
/* The Motif tile is a near-white lockup rather than a photograph. The darkening above
   is tuned to stop photographs competing with the type; applied to a white card it just
   makes it dingy grey, and the card is still the brightest thing in the grid either way.
   So: hold the brightness and take the weight down with contrast instead. */
.index-list .cs-img--flat { filter: brightness(0.92) contrast(0.88) saturate(0.9); }

@media (prefers-reduced-motion: no-preference) {
  .index-list .cs-img { transition: filter 0.25s steps(3, end); }
  .index-list .cs-link:hover .cs-img--flat { filter: none; }
  .index-list .cs-link:hover .cs-img { filter: brightness(1) contrast(1.06) saturate(1.02); }
}
@media (max-width: 56rem) {
  .index-list li { grid-template-columns: minmax(0, 1fr); gap: 0.5rem; }
  .index-list .cs-link { grid-column: 1; margin-top: 0.9rem; max-width: 26rem; }
}

/* ---- Work page ---- */

.pieces { list-style: none; margin: 0; padding: 0; }
.pieces li { padding: 2rem 0; border-top: 1px solid var(--line); margin: 0; display: grid; grid-template-columns: var(--margin-col) minmax(0, 1fr); gap: 1rem 2.5rem; }
.pieces .eyebrow { margin: 0.2rem 0 0; align-self: start; justify-self: start; }
.pieces h3 { margin-bottom: 0.4rem; }
.pieces p:not(.eyebrow) { max-width: var(--measure); color: var(--text-2); }
@media (max-width: 56rem) { .pieces li { grid-template-columns: minmax(0, 1fr); gap: 0.5rem; } }

/* ---- Service Changes: ragged contact sheet ----
   The pieces are the page. Tiles run in a 12-column grid at uneven spans rather than a
   polite even one, the title sits on a signage plate, and the index is readout. */
.sheet {
  list-style: none; margin: 2.5rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(12, 1fr);
  gap: clamp(0.6rem, 1.4vw, 1.1rem);
}
.sheet li { margin: 0; position: relative; }
.sheet a { display: block; text-decoration: none; color: inherit; }
.sheet .tile { position: relative; overflow: hidden; background: var(--surface); }
.sheet .tile video, .sheet .tile img { width: 100%; height: 100%; object-fit: cover; filter: contrast(1.06) saturate(1.04); }
/* object-fit: cover centre-crops, so a wide photograph in a square tile loses both
   edges. Where the subject is off-centre, move the crop rather than the image. */
.sheet .tile img.pos-left { object-position: left center; }
.sheet li.r-9x16 .tile { aspect-ratio: 9 / 16; }
.sheet li.r-1x1 .tile { aspect-ratio: 1; }
.sheet .plate {
  position: absolute; left: 0; bottom: 0; z-index: 2;
  background: var(--orange); color: var(--ink);
  font-family: var(--display); font-variation-settings: "wdth" 76; font-weight: 900;
  /* Floor kept at 1.2rem for rhythm, not for contrast: ink on --orange reads
     7.54:1 and would clear the small-text 4.5 bar at any size. It was 4.24:1 and
     genuinely floor-dependent when this fill was red. */
  font-size: clamp(1.2rem, 1.5vw, 1.45rem); letter-spacing: -0.005em; line-height: 1;
  padding: 0.5em 0.7em 0.45em;
}
.sheet .num {
  position: absolute; right: 0.55rem; top: 0.45rem; z-index: 2;
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.12em;
  color: var(--teal);
  /* A scrim, not a text-shadow. The shadow alone vanished over the bright corner of a
     photograph, and the contrast audit cannot see it: that check compares text against
     its computed background, which here is an image it cannot sample. */
  background: rgba(11,11,12,.62); padding: 0.15rem 0.34rem; border-radius: 1px;
}
/* The status line under a log-index tile ("Five pieces, ongoing"). Readout, because it
   is a count and a date rather than a title; the entry's name is on the plate above it. */
.sheet .title {
  display: block; margin-top: 0.6rem;
  font-family: var(--mono); font-variation-settings: "wdth" 85;
  font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-2);
}
.sheet a:hover .title { color: var(--orange-lift); }
.sheet li:nth-child(1), .sheet li:nth-child(2), .sheet li:nth-child(3) { grid-column: span 4; }
.sheet li:nth-child(4) { grid-column: span 5; }
.sheet li:nth-child(5) { grid-column: span 7; }
@media (max-width: 60rem) {
  .sheet { grid-template-columns: repeat(6, 1fr); }
  .sheet li:nth-child(1), .sheet li:nth-child(2) { grid-column: span 3; }
  .sheet li:nth-child(3) { grid-column: span 6; }
  .sheet li:nth-child(4), .sheet li:nth-child(5) { grid-column: span 3; }
}
@media (max-width: 36rem) {
  .sheet { grid-template-columns: 1fr; }
  .sheet li { grid-column: 1 / -1 !important; }
}

/* ---- Even sheet variants, for the Personal work log ----
   The composition above is bespoke to Service Changes: five tiles at hand-set uneven
   spans. A log entry holds a fixed set that wants an even grid instead, so these
   respan every child.

   The `:nth-child(n)` is load-bearing, not decoration. It matches every child, and it
   puts a third class-level component in the selector, so these beat the `span 4/5/7`
   rules above no matter where the blocks sit relative to each other. Without it the
   two sets tie at 0,2,1 and the winner depends on source order. The 36rem rule stays
   `!important`, so one column on a phone still wins over all of this. */
.sheet.sheet--1up { grid-template-columns: 1fr; max-width: 44rem; margin-inline: auto; }
.sheet.sheet--1up > li:nth-child(n) { grid-column: 1 / -1; }
.sheet.sheet--2up > li:nth-child(n) { grid-column: span 6; }
.sheet.sheet--3up > li:nth-child(n) { grid-column: span 4; }
.sheet.sheet--4up > li:nth-child(n) { grid-column: span 3; }
/* Mixed orientations in one set leave ragged rows. Let them: a row sized to its tallest
   tile and stretching the others would crop photographs to fit a grid. */
.sheet--2up, .sheet--3up, .sheet--4up { align-items: start; }
@media (max-width: 60rem) {
  .sheet.sheet--2up > li:nth-child(n),
  .sheet.sheet--3up > li:nth-child(n),
  .sheet.sheet--4up > li:nth-child(n) { grid-column: span 3; }
}

/* Ratios carried by the log sets. 4x7 is Slow Boat's native 1536x2688. */
.sheet li.r-3x2 .tile { aspect-ratio: 3 / 2; }
.sheet li.r-2x3 .tile { aspect-ratio: 2 / 3; }
.sheet li.r-4x7 .tile { aspect-ratio: 4 / 7; }

/* A log set is a series, not a set of links, so most tiles carry no anchor. Keep the
   plate and index legible without the hover affordances an anchor would imply. */
.sheet--static .tile { cursor: default; }

/* ---- Piece detail ---- */

.piece { display: grid; grid-template-columns: minmax(0, 1fr) minmax(14rem, 22rem); gap: 2.5rem; align-items: start; padding-top: 2rem; }
.piece .media { background: var(--surface); }
.piece .media video, .piece .media img { width: 100%; }
.piece .media.r-9x16 { max-width: 30rem; }
.piece .media.r-1x1 { max-width: 40rem; }
.piece dl { margin: 0; font-family: var(--mono); font-variation-settings: "wdth" 92; font-size: 0.78rem; line-height: 1.5; }
.piece dt { color: var(--orange-lift); margin-top: 0.9rem; text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; font-weight: 600; }
.piece dt:first-child { margin-top: 0; }
.piece dd { margin: 0.1rem 0 0; color: var(--teal); }
.piece .note { font-family: var(--serif-text); font-size: 1rem; line-height: 1.5; margin-top: 1.4rem; padding-top: 1rem; border-top: 1px solid var(--line); color: var(--text-2); }
.piece-nav { display: flex; justify-content: space-between; gap: 1rem; margin-top: 3rem; padding-top: 1.2rem; border-top: 1px solid var(--line); font-size: 0.95rem; }
.piece-nav a { text-decoration: none; color: var(--text-2); }
.piece-nav a:hover { color: var(--orange-lift); }
@media (max-width: 56rem) { .piece { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; } }

/* ---- About ---- */

.prose { max-width: var(--measure); }
.prose p { color: var(--text-2); }
/* .fig carries a top margin only, because on a case study page it is the last thing in
   its section. Inside .prose a paragraph can follow it, so it needs room underneath. */
.prose .fig { margin-bottom: 1.6rem; }
/* The container has to span the wrap for the rows to have anywhere to put a picture,
   so every text block inside re-asserts --measure rather than inheriting it. Without
   this the copy column eats the whole grid and the image renders at zero width. */
.prose--wide { max-width: none; }
.prose--wide > p,
.prose--wide > h2,
.prose--wide > ul,
.prose--wide > ol,
.prose--wide > table { max-width: var(--measure); }
.about-row { display: grid; grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr); gap: 2.5rem; align-items: start; }
.about-row + .about-row { margin-top: 1.9rem; }
.about-row:last-of-type { margin-bottom: 1.9rem; }
.about-copy { max-width: var(--measure); }
.about-row .fig { margin: 0; }
@media (max-width: 60rem) {
  .about-row { grid-template-columns: minmax(0, 1fr); gap: 0.9rem; }
  .about-row .fig { max-width: 24rem; }
}
.prose h2 { font-family: var(--serif-text); text-transform: none; letter-spacing: 0; font-size: 1.15rem; font-weight: 500; margin: 2.2rem 0 0.7rem; color: var(--text); }

/* Label/value rows. Used on both surfaces, so the paper case is scoped explicitly:
   colouring it only for the dark ground produced nine contrast failures. */
.roles {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 2.5rem;
  font-family: var(--mono); font-variation-settings: "wdth" 92;
  font-size: 0.78rem; letter-spacing: 0.01em;
}
.roles div { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1.4rem; border-top: 1px solid var(--line); padding: 0.8rem 0; }
.roles b { color: var(--orange-lift); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.66rem; font-weight: 600; white-space: nowrap; }
.roles span { color: var(--teal); }
.on-paper .roles b, .slab .roles b { color: var(--orange-ink); }
.on-paper .roles span, .slab .roles span { color: var(--teal-ink); }
@media (max-width: 52rem) { .roles { grid-template-columns: 1fr; } }

/* ---- Timecode burn-in (created by site.js, desktop only) ---- */

.tc {
  position: fixed; right: var(--gutter); bottom: 1.1rem; z-index: 9991;
  /* Readout, not signage: the timecode states a machine fact, so it is teal by
     the rule in DESIGN.md §1. It had been an orange chip, which the old red was
     dim enough to disguise and the brighter orange is not. Ink on the fill,
     8.30:1, same construction as the eyebrow chip. */
  background: var(--teal); color: var(--ink);
  font-family: var(--mono); font-variation-settings: "wdth" 85;
  font-size: 0.72rem; font-weight: 600; letter-spacing: 0.1em;
  padding: 0.35em 0.6em 0.3em;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* ---- MOTION ----
   Everything here runs at 12 frames per second, on purpose. Reveals and hovers use
   steps() so they jump through five or six poses instead of gliding. Scroll-linked
   movement reads CSS variables that site.js updates twelve times a second.

   Nothing may depend on motion to become readable. reduced-motion parks every tracked
   element at --p 0.5, reveals everything, and stops the loop. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
  .tc { display: none; }
  .masthead h1 { text-shadow: none; }
}

@media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {
  /* Load sequence: the headline prints down the page, then the lede and role appear. */
  @keyframes print { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 -10% 0); } }
  @keyframes appear { from { opacity: 0; } to { opacity: 1; } }
  .js .hero h1 { clip-path: inset(0 0 -10% 0); animation: print 0.55s steps(7, end) 0.1s both; }
  .js .hero .lede { animation: appear 0.25s steps(3, end) 0.6s both; }
  .js .hero .role, .js .hero + .leadins { animation: appear 0.25s steps(3, end) 0.85s both; }
  .js .page-head h1 { animation: print 0.45s steps(6, end) 0.05s both; }
  .js .page-head .lede, .js .page-head .meta { animation: appear 0.25s steps(3, end) 0.45s both; }

  /* Scroll reveals: site.js adds .rv to targets and .in when they enter the viewport. */
  .js .rv { opacity: 0; transform: translateY(20px); transition: opacity 0.4s steps(5, end), transform 0.4s steps(5, end); }
  .js .rv.in { opacity: 1; transform: translateY(0); }
  .js .fig.rv img, .js .fig.rv video { transform: scale(1.05); transition: transform 0.55s steps(6, end); }
  .js .fig.rv.in img, .js .fig.rv.in video { transform: scale(1); }

  /* Scroll-linked drift. --sy is quantised, so this jumps in 12 fps increments by itself. */
  .js .hero h1 { translate: 0 calc(var(--sy) * -0.14px); }
  .js .page-head h1 { translate: 0 calc(var(--sy) * -0.08px); }
  /* No drift on .eyebrow. --sy is absolute scroll, so on a long page the offset keeps
     growing and section eyebrows end up sitting on top of the figure above them. */

  /* Diagram wires. */
  .diagram .flow { stroke-dasharray: 7 6; animation: flow 1.6s linear infinite; }
  @keyframes flow { to { stroke-dashoffset: -13; } }

  /* Hovers step too. */
  .site-nav a, .piece-nav a, .leadins h2 a, .index-list h2 a { transition: color 0.2s steps(3, end); }
  .sheet .tile video, .sheet .tile img { transition: transform 0.25s steps(3, end), filter 0.25s steps(3, end); }
  .sheet a:hover .tile video, .sheet a:hover .tile img { transform: scale(1.04); filter: contrast(1.15) saturate(1.15); }
  /* The masthead fringe, fired by the pointer instead of by scroll speed. Display
     type only: below about 1.2rem a 3px split stops reading as an effect and starts
     reading as a rendering fault, which is why the nav, the piece nav and the legacy
     .sheet .title are all left out. Stepped, like everything else here. */
  .index-list h2 a, .leadins h2 a, .sheet .plate {
    transition: text-shadow 0.12s steps(3, end), color 0.2s steps(3, end);
  }
  .index-list h2 a:hover, .leadins h2 a:hover, .sheet a:hover .plate {
    /* Hold the text colour. The global a:hover repaints links --orange-lift,
       and orange type under a red fringe goes muddy and loses the split. The
       aberration is the whole hover signal here; the underline stays for the
       affordance. */
    color: var(--text);
    text-shadow: -2.2px 0 0 rgba(255,0,64,.8), 2.2px 0 0 rgba(0,224,255,.8);
  }
  .sheet a:hover .plate { color: var(--ink); }

  .eyebrow { transition: background-color 0.2s steps(2, end); }
  a:hover .eyebrow, .leadins > li:hover .eyebrow, .index-list li:hover .eyebrow { background: var(--orange-lift); }
}
