/* Austra Cukura — portfolio
   Editorial workshop. Warm oat paper, heavy serif display,
   monospace labels, a single deep-green accent. Made, not generated. */

:root {
  --background: #F4EFE4;  /* warm oat paper */
  --surface:    #ECE4D3;
  --ink:        #1E1A12;  /* warm near-black */
  --ink-muted:  #6B6149;  /* warm taupe — AA on paper */
  --neutral:    #4A4334;  /* mid tone — decks, demoted credits */
  --rule:       #DBD1BD;
  --rule-strong:#8A7E60;  /* interactive control borders — ≥3:1, WCAG 1.4.11 */
  --accent:     #262017;
  --pop:        #255A3E; /* deep green — identity, links, live markers, the chosen option */
  --paper-white: #FCFBF8; /* warm near-white for UI surfaces — never pure #FFF */

  /* Case accent. There used to be a "each case study carries its own colour
     world" system: every case page set --case on its own <body>. All four set
     the SAME lighter green (#2E6B4A), so the system did nothing except put a
     second green on a site that already had one — the essays' diagrams drifted
     to the lighter value while the page chrome around them stayed --pop.
     Removed 16 Aug 2026 on Anna's ruling ("the darkest green throughout"):
     the per-page overrides are gone and the accent is defined once, here.
     The var names stay because ~80 rules across the case pages use them. */
  --case:       var(--pop);
  --case-tint:  #E4F0EA;  /* pale wash of the accent — chips, dots, disabled pills */

  --radius-sm:   4px;    /* thumbnails, small inset frames        */
  --radius:      6px;    /* default — figures, demos, cards        */
  --radius-lg:  16px;    /* large landing tiles (Anna, 10 Aug 2026) */
  --radius-pill: 999px;  /* segmented controls, note pills, dot    */
  --rule-soft:  #E6DDCB;

  /* ── Motion language ──────────────────────────────────────────────
     One curve, two speeds — borrowed from Linear's coherence. State
     changes (colour, border, background) register fast on --dur-1;
     positional reveals (arrows, underline draws) ease in on --dur-2.
     Everything shares --ease, so the whole site moves as one material. */
  --ease:  cubic-bezier(0.22, 1, 0.36, 1);
  --dur-1: 120ms;   /* hover / state feedback — crisp   */
  --dur-2: 240ms;   /* reveals, draws, fades — smooth   */

  /* ── Display tracking ─────────────────────────────────────────────
     One systematic scale: letter-spacing tightens as size grows and
     relaxes toward 0 at reading sizes — monotonic with type size,
     not per-element magic numbers.                                    */
  --ls-display: -0.025em;  /* hero wordmark / display XL */
  --ls-h1:      -0.022em;  /* case-study H1             */
  --ls-h2:      -0.018em;  /* section H2 · pull quotes  */
  --ls-h3:      -0.012em;  /* index titles · H3         */

  /* Label tracking — one value, and that value is zero (12 Aug 2026).
     Spreading letters apart is an UPPERCASE technique: capitals are
     drawn tight and need the air. Lowercase already carries the fit the
     type designer drew, and pushing it apart breaks the word-shape the
     eye actually reads. This site sets every label in lowercase, so the
     right amount is none. Previously ten hand-picked values (0.01 →
     0.1em) across 24 rules; now one knob. If a label ever goes
     uppercase, it gets its own token — it does not borrow this one. */
  --ls-label: 0;

  /* Two typefaces only — a serif for display, a sans for everything else.
     Labels use the sans (lowercase, untracked) rather than a third family.
     --font-label is deliberately NOT called --font-mono: it resolves to
     the sans, and the old name invited someone to swap a real monospace
     in and quietly make it three families.
     It had done exactly that anyway: --font-label named Geist, no
     @font-face ever loaded it, so every label on the site fell through to
     -apple-system. Three sans faces in one band head (Archivo lede, system
     labels, Newsreader title) is the font party Anna called on 13 Aug 2026.
     --font-label now IS --font-body: one serif, one sans, no third. */
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-body:    "Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --font-label:   "Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;

  /* Type scale — fluid. Each step grows with the viewport so the page
     scales up on large monitors instead of stranding tiny fixed text.
     min (small phones) · fluid middle · max (large desktop). */
  --fs-micro: 12px;  /* captions / micro, fixed at the 12 floor from 22 Aug 2026 (was clamp 10.5 → 12; 10.5 on phones was the smallest text on the site)
     Starts at the 10.5 label size used inside the SVG diagrams, so a caption
     under a picture and a label inside a drawing start from the same place.
     Fluid from there, on the same 390→1280 calibration and the same ~1.15
     growth as the steps below, so it keeps pace with the type around it
     instead of staying pinned while everything else grows (Anna, 12 Aug 2026).
     This also tracks the diagrams rather than fighting them: their SVGs are
     width:100%, so their 10.5 labels scale up with the drawing on a wide
     screen. A fixed caption would have fallen behind them.
     The 10.5 floor sits below the usual platform minimums (Material 12sp, iOS
     11pt). Line-height 1.7 is what carries it. Nothing goes smaller. */
  --fs-label: clamp(13px, 12.3px + 0.18vw, 15px);   /* labels / captions  13 → 15 */
  --fs-small: clamp(16px, 15.12px + 0.22vw, 18px);  /* secondary / meta   16 → 18
     Was 15 → 17, which put it 1.13× above --fs-label and only 1.18× below
     --fs-body — too close to either side to read as its own step. 16 → 18
     restores the ~1.2 ratio the rest of the scale runs on (12 Aug 2026). */
  --fs-body:  clamp(16px, 15.12px + 0.22vw, 18px);  /* body               16 → 18 (Anna, 22 Aug 2026: 18px is the body size, site-wide; one rung with --fs-small) */
  --fs-lede:  clamp(19px, 16px + 0.78vw, 25px);     /* lede / stand       19 → 25 */

  /* ── Vertical-spacing system ──────────────────────────────────────
     One scale, used for page rhythm on every page. Component internals
     (form fields, dense demo UIs) keep their own tight values; this
     governs the gaps you read DOWN the page.

     Bottom end (4 / 12) + usage rules borrowed from Linear's measured
     scale (xxs 4 · xs 8 · sm 12 · md 16 · lg 24 · xl 32 · xxl 48 ·
     section 96). Top end (64 / 128 / 160) kept for editorial air —
     a long-form portfolio breathes more than a dense product tool.
     Rules: section rhythm anchors on 96; headings take space ABOVE,
     not below; reading column stays ~68–73ch.
     4 · 8 · 12 · 16 · 24 · 32 · 48 · 64 · 96 · 128 · 160               */
  --space-3xs:  4px;   /* Linear xxs — caption / label micro-gaps */
  --space-2xs:  8px;   /* Linear xs  — tightest stack             */
  --space-xs:  12px;   /* Linear sm  — caption-to-image           */
  --space-sm:  16px;   /* Linear md                               */
  --space-md:  24px;   /* Linear lg  — paragraph rhythm           */
  --space-lg:  32px;   /* Linear xl  — block padding              */
  --space-xl:  48px;   /* Linear xxl — block separation           */
  --space-2xl: 64px;   /* sub-section                             */
  --space-3xl: 96px;   /* Linear section — dominant section gap   */
  --space-4xl: 128px;  /* page top / bottom                       */
  --space-5xl: 160px;  /* reserved — largest editorial break      */

  /* ── Vertical rhythm — the ROLE layer (Anna, 12 Aug 2026) ─────────
     The scale above is the palette: it says which numbers exist, not
     which one to use. That gap is why the pages drifted — every block
     picked its own value and the page ended up with 10, 14, 18, 20, 22,
     26, 28 and 40 in it, and with gaps that grew as the blocks got
     smaller (About ran 24 · 64 · 96 · 96).

     This layer is the system: seven named jobs, one value each, no
     choice left to make at the call site. Every vertical gap you read
     DOWN a page uses one of these. Nothing else.

       --v-hair     8   a label and its value; the tightest real pair
       --v-tight   12   a caption and the thing it describes
       --v-para    16   paragraph to paragraph; item to item in a list
       --v-group   24   a heading to its first line; two related blocks
       --v-block   32   block to block inside one section
       --v-sub     48   sub-section break; a figure sitting in the flow
       --v-section 64   major section to major section
       --v-page    96   the page frame, top and bottom

     Ladder: each step is 1.33–1.5× the one below, so adjacent levels
     read as different without shouting. Use CONSECUTIVE steps for
     consecutive levels — skipping (16 straight to 64) is what "not
     proportional" looks like.

     Two standing rules:
     1. Headings take their space ABOVE (--v-sub / --v-section) and
        --v-group below. A heading belongs to the text under it.
     2. Proximity encodes relation: the gap inside a group is always
        smaller than the gap around it. If it isn't, the group is
        reading as loose parts.

     Component internals — control padding, tap targets, dense demo UI —
     are NOT page rhythm and keep their own values. This governs the
     page, not the widgets.                                            */
  --v-hair:     var(--space-2xs);   /*  8 */
  --v-tight:    var(--space-xs);    /* 12 */
  --v-para:     var(--space-sm);    /* 16 */
  --v-group:    var(--space-md);    /* 24 */
  --v-block:    var(--space-lg);    /* 32 */
  --v-sub:      var(--space-xl);    /* 48 */
  --v-section:  var(--space-2xl);   /* 64 */
  --v-page:     var(--space-3xl);   /* 96 */

  /* ── Horizontal system (council-approved) ──────────────────────
     One fluid gutter for every section — never steps, never lands
     off-grid, never narrower than the old 18px floor. Breathes from
     ~600px up to ~1000px, then holds at 32 inside the 1280 cap.
     One shared column-gap so the copy/metric tension rhymes hero→band. */
  --gutter:  clamp(24px, 2vw + 12px, 32px);
  --gap-col: var(--space-xl);   /* 48 — hero + band-intro share this */

  /* Standard section vertical padding — fluid, anchored on Linear's 96. */
  --section: clamp(64px, 8vw, 96px);
}

/* ════════════════════════════════════════════════════════════════
   SELF-HOSTED FONTS — Fraunces (display, chosen 22 Aug 2026 over Newsreader) + Archivo (body & labels).
   The fraunces-*.woff2 and geist-*.woff2 files in /fonts/ are from the
   earlier pairing and are no longer referenced by any @font-face.
   Latin-subset .woff2 in /fonts/. url() is relative to this stylesheet,
   so the paths resolve from every page (root and /projects/) and locally.
   font-display: swap — text is never invisible while a font loads.
   ════════════════════════════════════════════════════════════════ */
@font-face { font-family: "Fraunces"; src: url("fonts/fraunces-var.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Fraunces"; src: url("fonts/fraunces-var-ext.woff2") format("woff2"); font-weight: 100 900; font-style: normal; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Fraunces"; src: url("fonts/fraunces-var-italic.woff2") format("woff2"); font-weight: 100 900; font-style: italic; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Fraunces"; src: url("fonts/fraunces-var-italic-ext.woff2") format("woff2"); font-weight: 100 900; font-style: italic; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Archivo"; src: url("fonts/archivo-var.woff2") format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Archivo"; src: url("fonts/archivo-var-ext.woff2") format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Archivo"; src: url("fonts/archivo-var-italic.woff2") format("woff2"); font-weight: 400 700; font-style: italic; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Archivo"; src: url("fonts/archivo-var-italic-ext.woff2") format("woff2"); font-weight: 400 700; font-style: italic; font-display: swap; unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
/* Fraunces axes (22 Aug 2026): SOFT rounds the terminals towards paper,
   WONK turns on the leaning "a" forms. Weight and optical size stay on the
   normal properties; Fraunces is heavier than Newsreader at the same value,
   so every display weight sits one step lower than it used to. */
h1, h2, .nav-mark, .pull, .figure, .stat .n, .ucite blockquote, .cs-next a { font-variation-settings: "SOFT" 40, "WONK" 1; }

* { margin: 0; padding: 0; box-sizing: border-box; }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

html { overflow-x: clip; }

body {
  /* Paper grain — feTurbulence fractal noise at ~4.5 % opacity tiled
     as a background-image layer. Sits under all child elements so it
     never overlays screenshots or images. mix-blend-mode not needed here
     because the SVG rect opacity handles the weight. */
  background-color: var(--background);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='250'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.68' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='250' height='250' filter='url(%23n)' opacity='0.045'/%3E%3C/svg%3E");
  background-size: 250px 250px;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
  max-width: 100%;
}


.wrap {
  max-width: 1280px;
  margin-inline: auto;
  padding-inline: max(var(--gutter), env(safe-area-inset-left))
                  max(var(--gutter), env(safe-area-inset-right));
}

/* ---------- Nav ---------- */

nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--background);
  border-bottom: 1px solid var(--rule);
}

.nav-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-block: var(--v-tight);   /* 12 — the nav bar is page furniture, so it sits on the scale too */
}

.nav-mark {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 44px;
  line-height: 1;
  text-decoration: none;
  color: var(--ink);
  letter-spacing: -0.02em;
  border-radius: 8px;
}

.nav-mark:focus-visible {
  outline: 2px solid var(--pop);
  outline-offset: 4px;
  border-radius: 8px;
}

/* The wordmark was the one link on the site with no hover answer — every other
   link on every page turns green, this one sat inert. The dot is already green,
   so the letter joins it rather than the whole mark changing colour at once.
   (Anna, 13 Aug 2026: hover states everywhere there is a hyperlink.) */
.nav-mark { transition: color var(--dur-1) var(--ease); position: relative; }
.nav-mark .dot { color: var(--pop); }
.nav-mark:hover { color: var(--pop); }
/* The mark's glyph box measures 20 x 30 — it is the only route home on a
   phone, and it was the smallest tap target on the site. The hit area is
   grown to 44 x 44 with a centred pseudo-element rather than by padding
   the link, so the mark does not move a single pixel. (16 Aug 2026) */
.nav-mark::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

/* Shared confetti easter-egg layer (home logo + About link). Motion is in confetti.js. */
.confetti-layer { position: fixed; inset: 0; pointer-events: none; z-index: 99999; overflow: hidden; }
.confetti-layer i { position: absolute; top: 0; left: 0; display: block; border-radius: 1px; will-change: transform; }

.nav-links a {
  font-family: var(--font-label);
  font-size: var(--fs-small);
  letter-spacing: var(--ls-label);
  text-transform: none;
  text-decoration: none;
  color: var(--ink-muted);
  margin-left: 32px;
  padding: 12px 0;
  display: inline-block;
  transition: color var(--dur-1) var(--ease);
}

.nav-links a:hover { color: var(--pop); }

.nav-links a[aria-current] {
  color: var(--pop);
}   /* underline is drawn by the ::after rule further down (22 Aug 2026: stale text-decoration removed) */

/* ---------- Skip link ---------- */

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: var(--background);
  font-family: var(--font-label);
  font-size: var(--fs-label);
  padding: 12px 16px;
  z-index: 10;
  text-decoration: none;
}

.skip:focus {
  left: 0;
}

/* ---------- Hero ---------- */

.hero {
  padding-block: var(--v-section) var(--v-group);   /* 64 / 24 — lead work sits higher, less dead air */
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--gap-col);                                 /* 48 — shared */
  align-items: center;
}

.hero-text { min-width: 0; }

.hero h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(60px, 9vw, 124px);
  line-height: 0.98;
  letter-spacing: var(--ls-display);
  max-width: 11ch;
}

.hero h1 .dot { color: var(--pop); }
.hero h1 em {
  font-style: italic;
  font-weight: 600;
  color: var(--pop);
  letter-spacing: -0.03em;
}

@media (max-width: 760px) {
  .hero { grid-template-columns: 1fr; gap: var(--v-para); padding-block: var(--v-block) var(--v-sub); }            /* placeholder — hide on mobile; headline leads */
  .hero h1 { font-size: clamp(38px, 11vw, 60px); overflow-wrap: break-word; }
  .hero .stand { font-size: 18px; margin-top: var(--v-para); max-width: none; }
}

.hero .kicker {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: none;
  color: var(--ink-muted);
  margin-bottom: var(--v-group);
}

.hero .kicker::before,
.cs-header .kicker::before {
  content: "/ ";
  color: var(--pop);
  font-weight: 500;
}

.hero .stand {
  font-size: var(--fs-lede);
  line-height: 1.5;
  max-width: 34em;
  /* Tucked under the masthead (Anna, 13 Aug 2026). At 24 the lede floated as a
     separate thing; the wordmark's own line box already leaves descender space
     above it, so the measured gap reads wider than the number. Negative on
     Anna's "closer by a touch", then "reduce the gap more" (14 Aug) — eats
     part of that descender space. */
  margin-top: calc(-1 * var(--space-xs));
}

/* ---------- Index list ---------- */

.index { padding-bottom: 0; }

/* ---------- Selected work — media-forward bands ---------- */
.band {
  display: block;
  text-decoration: none;
  color: var(--ink);
  /* 32 top + 32 bottom = 64 between projects. Was --v-section both sides, i.e.
     128px, against a 32px largest-gap INSIDE a band — a 4x jump that skipped
     48, 64 and 96 and read as four pages rather than one index. 64 is double
     the largest internal gap: clearly a break, still one list (Anna, 12 Aug 2026). */
  padding: var(--v-block) 0;
}

/* First band sits closer to the hero — kills the dead gap under the standfirst. */
.index .band:first-child { padding-top: var(--v-block); }   /* 32 not 64 */

/* RULED, Anna, 17 Aug 2026: every case study is set IDENTICALLY. No lead
   band, no scale step, no "differentiate the strongest case" — she has
   ruled this repeatedly and it is final. Do not re-propose it. The four
   bands are one index, and they are equals. */

.band-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--v-para);
}
.band-head {
  display: flex;
  align-items: baseline;
  /* The four client names are a long line on a phone. Without min-width:0 the
     .sector text refuses to shrink, overflows the flex row and gets clipped —
     on a 390 screen the whole line rendered as "UBS" (13 Aug 2026). */
  flex-wrap: wrap;
  gap: 4px var(--v-para);
  min-width: 0;
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--ink-muted);
}
/* Structure, not affordance — so not green. The accent was doing six jobs
   at once (wordmark, links, arrows, focus ring, underline draw, and these
   numerals); green stops meaning "you can click this" when it also means
   "this is item three". The numerals sit with the sector line they belong
   to (Anna, 12 Aug 2026). */
.band-head .num { color: var(--ink-muted); flex: none; }
.band-head .sector { min-width: 0; }

/* CTA: sentence case, with an underline that draws in on hover and keyboard focus */
.band-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--ink-muted);
  white-space: nowrap;
  transition: color var(--dur-1) var(--ease);
}
.band-cta .t { position: relative; }
.band-cta .t::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -4px;
  height: 1.5px;
  background: var(--pop);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-2) var(--ease);
}
.band-cta .arrow {
  /* Body face, not the display serif. Fraunces earns its place on headings,
     the wordmark, drop caps and pull quotes — one job, "the page speaking".
     Rendering an arrow glyph in it is using a typeface as an icon, which is
     the fifth serif role and pure decoration (12 Aug 2026). It also drew
     differently in the Georgia fallback; the sans glyph is stable. */
  font-family: var(--font-body);
  font-size: clamp(28px, 2vw + 0.5rem, 42px);
  line-height: 1;
  color: var(--pop);
  /* Hidden until hover/focus — same as the next-case link, so a row stays calm
     until you reach for it. One arrow at a time, never a column of them. */
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}

/* Touch: no hover, so no arrow (Anna, 13 Aug 2026). The 12 Aug rule showed it
   standing on touch, reasoning that a band otherwise gave no sign it was a
   link. On a phone the whole band is the tap target and the title is the
   affordance — a permanent arrow with nothing to reveal is just decoration in
   the corner. It stays on pointer devices, where it answers the hover. */
@media (hover: none) {
  .band-cta { display: none; }
}
/* Matches the "About me" nav hover: text turns green, then the green stripe draws in */
.band:hover .band-cta, .band:focus-within .band-cta, .band:focus-visible .band-cta { color: var(--pop); }
.band:hover .band-cta .t::after, .band:focus-within .band-cta .t::after, .band:focus-visible .band-cta .t::after { transform: scaleX(1); }
/* Reveal the arrow on hover/focus only — identical behaviour to .cs-next a::after.
   On touch / no-hover devices it stays hidden, exactly as the next-case arrow does. */
.band:focus-within .band-cta .arrow,
.band:focus-visible .band-cta .arrow { opacity: 1; transform: translateX(0); }
@media (hover: hover) {
  .band:hover .band-cta .arrow { opacity: 1; transform: translateX(0); }
}

.band:focus-visible {
  outline: 2px solid var(--pop);
  outline-offset: 8px;
  border-radius: 2px;
}

/* Intro: copy on the left, the lead metric pulled out as a display figure on the right */
.band-intro {
  display: flex;
  /* baseline, not flex-end (17 Aug 2026). flex-end dropped the stat to the
     bottom of the row, where it lined up with nothing — level with the second
     line of the outcome, which is not a structural line — and read as
     orphaned in the right-hand gutter. On the baseline it shares the title's
     first baseline exactly, so the gutter becomes a spread instead of a gap.
     Alignment does the work; no rule or box needed to tie them together. */
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-col);
  margin: var(--v-group) 0 var(--v-block);
}
.band-copy { min-width: 0; }
.band h2 {
  font-family: var(--font-display);
  font-weight: 450;
  /* Bigger, and the only thing on the page that got bigger (Anna, 13 Aug 2026).
     Was 2.4vw + 0.5rem, capped 46 — 42.6px at 1440. Now ~52px there. The stat
     and the outcome stay where they are, so the step down from the title widens
     rather than everything inflating together. */
  font-size: clamp(30px, 3vw + 0.5rem, 58px);
  line-height: 1.1;
  letter-spacing: var(--ls-h3);
  /* 8, not 12: the title's own line box already carries descender space above
     the lede, so the measured gap reads larger than the token. */
  margin: 0 0 var(--space-2xs);
  max-width: 20ch;
  transition: color var(--dur-1) var(--ease);
}
/* Title turns green with the rest of the band — whole row is one hover target,
   so the title responds wherever it sits in the layout. */
.band:hover h2,
.band:focus-within h2,
.band:focus-visible h2 { color: var(--pop); }
/* the stat greens with the title — the whole tile answers the hover (Anna, 11 Aug 2026) */
.band:hover .band-lead .figure,
.band:focus-within .band-lead .figure,
.band:focus-visible .band-lead .figure { color: var(--pop); }
.band-lead .figure { transition: color var(--dur-1) var(--ease); }
/* Touch equivalent of the tile hover (Anna, 14 Aug 2026: "on mobile there is
   no hover"). No hover on a touchscreen, so the same answer fires while the
   finger is down — green title and stat, CTA underline and arrow, the image's
   gentle scale. Scoped to hover-less devices so desktop behaviour is
   untouched. */
@media (hover: none){
  .band:active h2 { color: var(--pop); }
  .band:active .band-lead .figure { color: var(--pop); }
  .band:active .band-cta { color: var(--pop); }
  .band:active .band-cta .t::after { transform: scaleX(1); }
  .band:active .band-cta .arrow { opacity: 1; transform: translateX(0); }
  .band:active .band-media .bvp img { transform: scale(1.04); }
}
/* Second read, and set like one. At --fs-lede this was 25px against a 42px
   title — 0.59, close enough that the eye met two headlines and picked
   neither, which with the 37px stat beside it made three near-equal voices
   in one block. Anna, 13 Aug 2026: "too much text and no hierarchy". Now
   --fs-body and muted: title first, outcome second, and the drop is large
   enough to be read as a decision rather than a rounding error. */
.band .outcome {
  font-family: var(--font-body);
  font-style: normal;
  font-size: var(--fs-body);
  line-height: 1.5;
  color: var(--ink-muted);
  margin: 0;
  max-width: 52ch;
}
.band-lead {
  flex: none;
  text-align: right;
  max-width: 16ch;
}
.band-lead .figure {
  display: block;
  /* Serif, on Anna's ruling (13 Aug 2026). I had moved it to the sans so the
     serif carried one job instead of two; she wants the number in Newsreader.
     Her call, and it stands — the stat and the title are the two things worth
     reading, so they share the display face and the rest of the band is sans. */
  font-family: var(--font-display);
  font-weight: 450;
  /* Sits BELOW the title at every width. Was clamp(40, 3.4vw+0.5rem, 68),
     which rendered 51.5px against a 38.7px h2 at 1280 — the number won the
     first read over "Edgefolio: fund evaluation for institutional
     investors", and the title is the more valuable sentence. Now ~0.87 of
     the h2 across the range, checked at 390 / 768 / 1280 so it never
     inverts on mobile either (12 Aug 2026).
     0.87 was still a tie: 37px beside a 42px title reads as a second
     headline, not as evidence under one. Now ~0.65 of the h2, which puts
     four clear steps in the block — title, stat, outcome, labels — instead
     of two headlines and a shout (13 Aug 2026). */
  font-size: clamp(22px, 1.4vw + 0.5rem, 30px);
  line-height: 0.95;
  letter-spacing: -0.03em;
  color: var(--ink);
  font-variant-numeric: oldstyle-nums tabular-nums;
}
.band-lead .figure-cap {
  display: block;
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--ink-muted);
  /* 4, not 12 — the caption belongs to the number above it, and at 12 it read as
     a separate line rather than the number's own label (Anna, 13 Aug 2026). The
     figure's line-height of 0.95 leaves clear space under the digits already. */
  margin-top: var(--space-3xs);
}

/* The product sits in a minimal flat browser frame on warm paper — a deliberate
   hero, matching the case-study pages. The frame opens on the screen's signature
   region (set per band via --obj), not the nav bar. */
.band-media {
  background: none;                /* no card — the browser frame sits straight on the paper */
  padding: 0;
  /* In the wrap column, not full-bleed (Anna, 24 Aug 2026: "I don't want these
     cards to be the full width anymore — the width as it's in case studies").
     The wrap's 1280 − 64 gutters = 1216, the same rail as --cs-media on the
     case-study pages, so covers here and figures there share one width. */
}

.band-media .browser {
  background: var(--paper-white);
  /* Hairline is back with the column width (24 Aug 2026): inside the column the
     screenshot's white needs an edge against the paper — the same frame the
     case-study shots carry. */
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius);
  overflow: hidden;
}
/* No drawn browser chrome. The screenshots are real product work; a hand-built
   title bar with traffic-light dots around them is the strongest "made by AI"
   tell there is, and it was reading as the frame rather than the work. The
   hairline frame above is the whole treatment now. (10 Aug 2026) */
/* ONE height, every cover (Anna, 13 Aug 2026) — now delivered by ratio, not a
   constant. The 560px constant belonged to the full-bleed era: at 1440 wide it
   matched the exports' own shape, so nothing cropped. Inside the 1216 column a
   fixed 560 cut ~16% off every composition (the 90POE table lost its left
   column mid-word). All four masters are re-exported at exactly 1840 x 710
   (24 Aug 2026), so the box takes that ratio: every cover renders complete and
   identical in height (~469px at the full column) at any viewport. The phone
   override below still wins under 760. */
.band-media .bvp {
  aspect-ratio: 1840 / 710;
  overflow: hidden;
  --obj: 50% 50%;
}
.band-media .bvp img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--obj, 50% 0%);
  transform-origin: var(--obj, 50% 0%);
  transition: transform var(--dur-2) var(--ease);
}
/* Hover zoom is gated to devices that actually hover. Ungated, a tap on a
   phone latched the 4% scale and left it stuck until the next tap
   elsewhere; the touch answer is the .band:active rule above. Keyboard
   focus keeps the zoom on every device — it is a focus signal, not a
   hover flourish. (16 Aug 2026) */
@media (hover: hover) {
  .band:hover .band-media .bvp img { transform: scale(1.04); }
}
.band:focus-within .band-media .bvp img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .band-media .bvp img, .band-cta .arrow, .band-cta .t::after { transition: none; }
  .band:hover .band-media .bvp img,
  .band:focus-within .band-media .bvp img { transform: none; }
}
@media (max-width: 760px) {
  .band { padding: var(--v-sub) 0; }
  .band-intro { flex-direction: column; align-items: flex-start; gap: var(--v-para); margin-bottom: var(--v-group); }
  /* Fluid, not pinned at 30 (Anna, 13 Aug 2026: "the edgefolio should be
     larger"). A fixed 30 sat only 1.4x above the 22px stat, so the two read as
     one weight on a phone. At 37 to 44 the title leads by the same margin it
     does on desktop. */
  .band h2 { font-size: clamp(34px, 9.5vw, 44px); max-width: none; }
  .band-lead { text-align: left; max-width: none; }
  /* 22, matching the landing page's own inline rule (index.html, 13 Aug 2026:
     "the stat steps down on mobile too"). This line said 44 for weeks but was
     always overridden there, and a reviewer reading only this file called a
     hierarchy inversion that never rendered (21 Aug 2026). One number now. */
  .band-lead .figure { font-size: 22px; }
  /* Phone exemption from the 560px constant (Anna, 14 Aug 2026, two passes:
     "responsive like the title images", then "the same vertical height as
     this" — the hero band). Tiles share the hero's exact band shape on a
     phone: 38svh capped at 360, image covering it. */
  /* aspect-ratio: auto is load-bearing: with the desktop ratio still set, the
     fixed height would resolve the box's WIDTH from the ratio (800px wide in a
     327px frame — only the left slice of the cover showed, 24 Aug 2026). */
  .band-media .bvp { aspect-ratio: auto; height: 38svh; max-height: 360px; }
}

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

/* ---------- Case study pages ---------- */

/* Case-study column is left-anchored to the page frame (aligns under the nav
   mark). Reading measure is controlled per element, not by a centred column. */
/* Case-study frame matches the landing exactly — same 1280 wrap, same gutters,
   so the wordmark and spacing never jump between pages. Content is left-aligned
   within the frame, like the landing; reading measure is set per element. */
.cs-header.wrap,
article.wrap,
.cs-next.wrap { max-width: 1280px; }

.cs-header { padding-block: var(--v-sub) var(--v-group); }   /* 48 / 24 */

/* Case branding — each case study carries its own colour world.
   Pages set --case / --case-tint on <body>; everything else inherits. */
.themed .cs-header {
  background: none;                     /* no frame — open on the paper */
  border: 0;
  border-radius: 0;
  padding-block: var(--v-sub) var(--v-group);   /* 48 / 24 — side padding inherits .wrap → aligns under nav */
  margin-top: 0;
}

.themed .cs-header .kicker { color: var(--case); }

.themed article .cs-body h2::before { color: var(--case); }

.themed .demo-bar { background: var(--surface); }

.themed .stats-hero { border-top-color: var(--case); box-shadow: none; }

@media (max-width: 640px) {
  .themed .cs-header { padding-block: var(--v-block) var(--v-para); margin-top: 0; }   /* 32 / 16 */
}

.cs-header .kicker {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: none;
  color: var(--ink-muted);
  margin-bottom: var(--v-group);
}

.cs-header h1 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(38px, 4vw + 0.6rem, 76px);
  line-height: 1.08;
  letter-spacing: var(--ls-h1);
  max-width: 22ch;
}

.cs-header .lede {
  font-size: var(--fs-lede);
  line-height: 1.5;
  max-width: 34em;          /* a deck — kept to an optimal reading measure */
  margin-top: var(--v-group);
  color: var(--neutral);
}

article { padding-block: var(--v-para) var(--v-sub); }   /* 16 / 48 — Anna, 22 Aug 2026: the footer block was double a normal footer */

.cs-body { max-width: 68ch; }

.cs-body .demo { margin-left: 0; margin-right: 0; }

.cs-body h2 {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(30px, 2.2vw + 0.6rem, 46px);
  line-height: 1.1;
  letter-spacing: var(--ls-h2);
  margin: var(--v-section) 0 var(--v-group);   /* 64 above / 24 below — calm, even section rhythm; heading hugs its text */
}

.cs-body h2:first-child { margin-top: 0; }

.cs-body p { margin-bottom: var(--v-para); max-width: 68ch; }

.cs-body strong { font-weight: 500; }   /* precise, not bold — emphasis one notch up, never 600+ in running text */

.cs-body a,
.about .meta a,
.about-writing a {
  color: var(--pop);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color var(--dur-1) var(--ease), text-decoration-thickness var(--dur-1) var(--ease);
}

.cs-body a:hover,
.about .meta a:hover,
.about-writing a:hover { color: var(--ink); text-decoration-thickness: 2px; }

figure { margin: var(--v-sub) 0; }   /* 48 */

/* Figures and screenshot slots break out of the text column and scale up on
   larger screens — aligned left with the title and stats, capped at the
   stat-band width so the whole header/body share one right edge. */
.cs-body figure,
.cs-body .shot,
.cs-body .demo{
  width: min(1216px, calc(100vw - 64px));
  max-width: none;
}
@media (max-width: 640px) {
  .cs-body figure,
  .cs-body .shot,
  .cs-body .demo{ width: 100%; }
}

figure img {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius);
}

/* One caption treatment for the whole site (Anna, 12 Aug 2026). A descriptive
   line under an image is the same object everywhere, so it gets one size, one
   gap, one colour. This had drifted to eight variants across the case studies:
   11px to 17px, gaps of 0, 10, 12, 13, 16 and 22px. Pages may still set their
   own max-width (measure is local); nothing else.
   Size is the --fs-micro floor and line-height goes up to 1.6 to carry it. The
   gap is --space-xs, the scale's own caption-to-image step. */
figcaption {
  font-family: var(--font-label);
  font-size: var(--fs-micro);
  line-height: 1.7;
  color: var(--ink-muted);
  margin-top: var(--v-tight);
  max-width: 60ch;
}

/* Attributed user quote — the one quote component, every case study */
.ucite { margin: var(--v-section) 0; border-left: 2px solid var(--case, var(--pop)); padding-left: 24px; }
.ucite blockquote { margin: 0; font-family: var(--font-display); font-weight: 400; font-style: italic; font-size: calc(var(--fs-body) * 1.6); line-height: 1.25; letter-spacing: -0.01em; color: var(--ink); max-width: 54ch; }   /* QA 22 Aug 2026: the pull quote ran to 75 characters a line at 1680+; capped at the reading measure */
/* A quote attribution is not a picture caption, so it does not take --fs-micro.
   Exactly 2px under --fs-small, tracking it at every width: 17→15 on desktop,
   15→13 on a phone (Anna, 12 Aug 2026). Line-height is the unitless 1.6, so it
   shrinks in the same proportion as the type. Gap is the sitewide 12. */
.ucite figcaption {
  font-size: calc(var(--fs-small) - 2px);
  line-height: 1.6;
  letter-spacing: var(--ls-label);
  max-width: 54ch;
}
@media (max-width: 640px) { .ucite blockquote { font-size: 22px; } }

/* The seam into "Next" had two owners: <article> ended on 96px of padding and
   .cs-next opened with another 48, so every case study put 160px of nothing
   between the last sentence and the only link out of the page. One owner now.
   The body drops its bottom padding when .cs-next follows it, and .cs-next
   carries the whole break at --v-section. 160px becomes about 80 (Anna, 12 Aug
   2026, second time raised). */
article:has(+ .cs-next) { padding-bottom: 0; }

.cs-next {
  padding-block: var(--v-sub) var(--v-sub);   /* 48 / 48 (Anna, 22 Aug 2026: standard footer) */
  display: flex;
  align-items: center;
  gap: var(--v-para);                               /* 16 */
  flex-wrap: wrap;
}

.cs-next .label {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  color: var(--ink-muted);
  white-space: nowrap;
}

.cs-next .label::before { content: "/ "; color: var(--pop); font-weight: 500; }

/* Mobile: the next-case link sits right under the story's last line —
   the desktop air above it pushed it out of sight (Anna, 11 Aug 2026) */
@media (max-width: 640px) { .cs-next { padding-block: var(--v-hair) var(--v-sub); } }

.cs-next a {
  display: flex;
  align-items: baseline;
  gap: 24px;
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: clamp(28px, 2vw + 0.5rem, 42px);
  letter-spacing: -0.015em;
  color: var(--ink);
  text-decoration: none;
  transition: color var(--dur-1) var(--ease);
}

.cs-next a:hover { color: var(--pop); }

/* ---------- Pull quotes & section numbers ---------- */

article .cs-body { counter-reset: sec; }

article .cs-body h2 { counter-increment: sec; }

article .cs-body h2::before {
  content: counter(sec, decimal-leading-zero);
  display: block;
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--pop);
  margin-bottom: var(--v-tight);
}

.pull {
  font-family: var(--font-display);
  font-weight: 450;
  font-size: clamp(28px, 2.6vw + 0.5rem, 46px);
  line-height: 1.2;
  letter-spacing: var(--ls-h2);
  max-width: 24ch;
  color: var(--ink);
  margin: var(--v-sub) 0;   /* 48 — flows with the section rhythm */
}

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

.about .cs-body h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(28px, 1.8vw + 0.6rem, 40px);
  line-height: 1.15;
  margin: var(--v-section) 0 var(--v-group);   /* 64 / 24 */
}

.about-grid {
  display: grid;
  grid-template-columns: clamp(232px, 24vw, 312px) minmax(0, 58ch);
  gap: var(--v-sub) var(--space-lg);   /* 48 down · 32 across — portrait sits close to the copy */
  align-items: start;
}

.about-aside {
  display: flex;
  flex-direction: column;
  gap: var(--v-tight);   /* 12 — the scale's own caption-to-image step */
  order: -1;
  min-width: 0;
}

@media (max-width: 820px) {
  .about-grid { grid-template-columns: 1fr; gap: var(--v-block); }
  .about-aside { max-width: 340px; order: -1; }
}

/* The testimonial runs on the same two-column spine as the intro above it, so
   the quote hangs directly under the portrait and the story sits in the text
   column. One grid down the whole page rather than two. The green rule moves
   off the figure onto the blockquote — it marks the quote, not the layout, and
   leaving it on the figure would push the whole row off the page's left edge
   and break alignment with the intro. (Anna, 12 Aug 2026.) */
.ab-cite { border-left: 0; padding-left: 0; }

.ab-cite blockquote {
  border-left: 2px solid var(--case, var(--pop));
  padding-left: var(--space-md);
  font-size: calc(var(--fs-body) * 1.25);   /* sized for the narrow column */
}

.ab-cite figcaption { margin-top: 0; }

/* ---------- Print — nothing hidden, ever ---------- */

@media print {
  .js .fade { opacity: 1 !important; transform: none !important; }
}

.about { padding-block: var(--v-section) var(--v-sub); }   /* 64 / 48 */
/* the painted header meets the nav directly, like a case study — no cream gap */
.about:has(.cs-painted) { padding-block-start: 0; }

.about h1 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(44px, 6vw, 64px);
  line-height: 1.05;
  letter-spacing: -0.015em;
  margin-bottom: var(--v-group);
}

.about .cs-body p { font-size: var(--fs-small); line-height: 1.5; }   /* Anna, 22 Aug 2026: the intro reads at the same size as the How-I-decide receipt lines, one step under body */

.about .meta {
  margin-top: var(--v-section);
  padding-top: var(--v-block);
  border-top: 1px solid var(--rule);
}

.about .meta p {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  color: var(--ink-muted);
  margin-bottom: var(--v-hair);
}

.about .meta a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.about .meta a:hover { color: var(--pop); }

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

footer { border-top: 1px solid var(--rule); }

.footer-inner {
  display: flex;
  justify-content: flex-start;
  align-items: baseline;
  padding-block: var(--space-xs);  /* 12 — one line of links, 12 above and below (Anna, 22 Aug 2026, three times: 'half the height'); the links carry no padding of their own */
  flex-wrap: wrap;
  gap: var(--v-group);             /* 24 */
}

footer a {
  color: var(--pop);
  /* One size across the whole footer (Anna, 13 Aug 2026). The links were
     inheriting body size, 20px against the copyright's 15px, so the contact
     details shouted and the row read as two different registers. */
  /* The footer links are links (Anna, 22 Aug 2026: "LinkedIn is a hyperlink, so it should be
     green and have the line below it"): green with the nav's underline held, the same dress
     as the current "About me" in the nav. The copyright stays muted. */
  position: relative;
  font-family: var(--font-label);
  font-size: var(--fs-label);
  text-decoration: none;
  padding: 0;
  line-height: 1.4;
  display: inline-block;
  transition: color var(--dur-1) var(--ease);
}

footer a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 1px;   /* tucked under the baseline, not floating below the line (Anna, 22 Aug 2026: "fix this") */
  height: 1px; background: var(--pop);
}
footer a:hover { color: var(--ink); }
footer a:hover::after { background: var(--ink); }

footer .copy {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  color: var(--ink-muted);
  margin-left: auto;
}

/* Phones: the footer reads as one compact colophon — tight stack, modest
   padding — instead of two small lines orphaned across a wide band
   (Anna, 18 Aug 2026). */
@media (max-width: 820px) {
  /* Tablets and phones alike (Anna, 22 Aug 2026: the tablet footer stacked three
     lines 44px apart and read as a hole): the two links share one row, the
     copyright sits on the same row on tablets and drops under them on phones, 12 of padding. */
  .footer-inner { flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--space-2xs) var(--space-md); padding-block: var(--space-xs); }   /* 8 of row gap so the copyright breathes when it drops under the links (Anna, 22 Aug 2026) */
  footer a { padding: 0; display: inline-flex; align-items: center; }   /* the 44px tap floor comes from the pointer: coarse rule below, touch devices only */
  /* Less dead air above the footer rule on a phone screen. */
  main { padding-bottom: var(--v-sub); }
}
@media (max-width: 640px) {
  footer .copy { margin-left: 0; flex-basis: 100%; }   /* phones: the copyright takes the line under the two links */
}

/* ---------- Motion ---------- */

/* Fade-in only when JS is confirmed present (html.js set in <head>),
   so content is never hidden if JS fails. */
@media (prefers-reduced-motion: no-preference) {
  .fade { animation: fadeUp 520ms ease-out both; }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

/* Hero stat band — numbers before prose, carried by a soft card */
.stats-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--v-block) var(--v-sub);
  max-width: none;            /* data is not reading text — fill the frame */
  margin: var(--v-group) 0 var(--v-hair);
  padding: var(--v-group) 0;
  background: none;          /* no frame — an editorial band, not a card */
  border: 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  box-shadow: none;
}

.stats-hero .stat .n {
  font-family: var(--font-display);
  font-size: clamp(38px, 3vw + 0.6rem, 66px);
  line-height: 1.05;
  letter-spacing: -0.02em;
  display: block;
}

.stats-hero .stat .d {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-muted);
  display: block;
  margin-top: var(--v-hair);
  max-width: 24ch;
}

.cs-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--v-para) var(--v-block);
  max-width: none;            /* metadata fills the frame, like the stats */
  margin-top: var(--v-group);
}

.cs-meta dt {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: none;
  color: var(--ink-muted);
  margin-bottom: var(--space-3xs);
}

.cs-meta dd {
  font-size: var(--fs-small);
  line-height: 1.4;
  color: var(--neutral);              /* demoted — reference, not narrative */
}

/* Owned replaced Role (Anna, 11 Aug 2026). A title is the one thing on these
   pages that argues her down a level; what she actually held decides is the
   Staff-level signal. So this cell alone is narrative, not reference: full ink,
   and the width to say a real sentence. Scope and Method stay demoted. */
.cs-meta .own dd { color: var(--neutral); }   /* Anna, 22 Aug 2026: the three columns read at one size and one colour, the lead line included; weight alone marks it */
/* Meta hierarchy (Anna, 22 Aug 2026): the block read as three equal grey
   paragraphs and the longest column won the eye. Owned now opens on the one
   sentence that has to land, a step heavier; Scope and Method are lists by
   nature, so they stack one item per line instead of running on. */
.cs-meta .own dd .lead { display: block; font-weight: 500; margin-bottom: var(--space-3xs); }   /* 500 = the site's one emphasis step (crit, 22 Aug 2026) */
.cs-meta dd .ln { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3xs); }
@media (min-width: 641px) {
  .cs-meta { grid-template-columns: 1.7fr 1fr 1fr; }
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--v-block);
  margin: var(--v-block) 0 var(--v-hair);
}

.stats .stat .n {
  font-family: var(--font-display);
  font-size: clamp(32px, 2vw + 0.6rem, 46px);
  line-height: 1.05;
  letter-spacing: -0.02em;
  display: block;
}

.stats .stat .d {
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--ink-muted);
  display: block;
  margin-top: var(--v-hair);
  max-width: 28ch;
}

.wrong {
  border-left: 2px solid var(--rule);
  padding-left: 24px;
  margin: var(--v-block) 0;
}

.wrong .label {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: none;
  color: var(--ink-muted);
  margin-bottom: var(--v-hair);
}

.wrong p { color: var(--ink); margin-bottom: 0; }

/* ---------- Embedded demos ---------- */

.demo {
  border: 1px solid var(--rule);
  background: var(--paper-white);
  border-radius: var(--radius);
  box-shadow: none;
  overflow: hidden;
  margin: var(--v-sub) 0;
}

.demo-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}

.demo-bar .t {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  color: var(--ink-muted);
}

.demo-bar .live {
  font-family: var(--font-label);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  color: var(--ink-muted);
  white-space: nowrap;
}

.demo-body { padding: 28px; }




/* ---------- Small screens ---------- */

@media (max-width: 640px) {
  /* gutter now handled by the fluid --gutter token on .wrap — no step needed */

  /* Nav — fits one line, never wraps */
  .nav-mark { font-size: 30px; }
  .nav-links a { margin-left: 16px; font-size: var(--fs-label); letter-spacing: var(--ls-label); white-space: nowrap; }

  .hero { padding-block: var(--v-block) var(--v-sub); }   /* 32 / 48 */

  /* Case header */
  .cs-header { padding-block: var(--v-block) var(--v-tight); }
  /* Phone hierarchy (22 Aug 2026): title 31.5 over section heads of 29 was a
     1.09 ratio, both serif — the chapter read like the book. The title stays
     at 31.5 (33 pushed "use" onto a third line on its own); the section head
     drops to 25, ratio 1.26 (desktop is 1.63). */
  .cs-header h1 { font-size: clamp(30px, 8.4vw, 42px); }
  .cs-header .lede { font-size: 18px; max-width: none; }
  .stats-hero { grid-template-columns: 1fr; gap: var(--v-para); padding: var(--v-group) 0; }
  .stats-hero .stat .n { font-size: 38px; }
  .cs-meta { grid-template-columns: 1fr 1fr; gap: 14px 20px; }
  /* Owned is the line that has to land, so on a phone it takes the full row
     rather than being squeezed into a half-column (Anna, 11 Aug 2026). */
  .cs-meta .own { grid-column: 1 / -1; }

  /* Body — full measure on small screens */
  .cs-body h2 { font-size: clamp(25px, 6.6vw, 29px); margin-top: var(--v-section); }   /* 64 on mobile; size see note above */
  .cs-body p, .pull { max-width: none; }
  .pull { font-size: 23px; margin: var(--v-sub) 0; }   /* 48 */
  .demo-body { padding: 16px; }
  .decision { padding: var(--v-group) 0; }
  .stats { gap: 20px; }
  .stats .stat .n { font-size: 32px; }

  /* Wide SVG diagrams stay legible via contained horizontal scroll */
  figure { overflow-x: auto; }
  figure svg { min-width: 480px; }

  .about { padding-block: var(--v-sub) var(--v-section); }   /* 48 / 64 on mobile */

  /* Footer: the row layout lives in the ≤820 block above (phones and tablets share it, 22 Aug 2026). */
}

/* Generous tap targets on touch devices (WCAG 2.5.5) */
@media (pointer: coarse) {
  .nav-links a, footer a, .cs-next a, .more {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .seg button { min-height: 44px; }
  .px-btn, .px-jump, .so-opt { min-height: 44px; }
  .note > summary { min-height: 44px; display: flex; align-items: center; }
}

/* Very small phones */
@media (max-width: 380px) {
  .hero h1 { font-size: clamp(32px, 10vw, 46px); }
  .cs-meta { grid-template-columns: 1fr; }
}

/* ════════════════════════════════════════════════════════════════
   ART DIRECTION — Edwardian editorial · Holland Park · warm light
   A refinement layer over the system above. Visual only.
   ════════════════════════════════════════════════════════════════ */

:root {
  --brass: #9C7B3F;          /* hairline ornament — used at whisper weight */
  --paper-deep: #EFE8D9;     /* faint depth tone */
}

/* Old-world numerals + fine ligatures across prose — instant Edwardian warmth.
   Display stats keep their own treatment below. */
body {
  font-variant-numeric: oldstyle-nums proportional-nums;
  font-feature-settings: "liga" 1, "dlig" 1, "onum" 1, "kern" 1;
  text-rendering: optimizeLegibility;
}

/* Display type: tighter, warmer, with optical softness where Fraunces allows. */
.hero h1, .cs-header h1, .cs-body h2, .pull, .cs-next a {
  font-variant-numeric: oldstyle-nums;
  font-feature-settings: "liga" 1, "onum" 1, "ss01" 1;
}

/* Section rhythm is carried by the number + generous space — no extra rules.
   (The per-heading brass hairline was removed: it fought the section number.) */

/* Stat numbers — oldstyle figures for the ledger look, tabular so the
   digits hold an even column across the band (data, not prose). */
.stats-hero .stat .n, .stats .stat .n { font-variant-numeric: oldstyle-nums tabular-nums; }

/* Links — a slow underline draw, the way fine print rewards attention. */
.cs-body a, .about .meta a, .about-writing a {
  text-decoration: none;
  background-image: linear-gradient(var(--pop), var(--pop));
  background-size: 100% 1px;
  background-repeat: no-repeat;
  /* Anchored to the bottom of the box, not to an em offset: a fixed 1.18em
     sat below Archivo's shorter content area and the rule was clipped away
     (Anna, 12 Aug 2026 — "the hyperlinks"). Padding makes the gap, so the
     rule survives any font swap. */
  padding-bottom: 3px;
  background-position: 0 100%;
  transition: background-size var(--dur-2) var(--ease), color var(--dur-1) var(--ease);
}
.cs-body a:hover, .about .meta a:hover, .about-writing a:hover {
  color: var(--pop);
  background-size: 100% 2px;
}

/* Lighter, airier rhythm — let the warmth breathe. */
.cs-body p { line-height: 1.62; }
/* Lighter than the masthead, on purpose (Anna, 13 Aug 2026: "in a lighter
   tone"). Full ink put the lede at the same weight as the name above it; muted
   lets the wordmark lead and still measures 5.3:1 on paper, past AA. */
.hero .stand { color: var(--ink-muted); }

/* ════════════════════════════════════════════════════════════════
   MOTION — advanced, restrained, 2026
   Scroll-driven reveals + View Transitions + reading progress.
   transform/opacity only · @supports-gated · reduced-motion safe.
   If unsupported, everything renders fully visible and static.
   ════════════════════════════════════════════════════════════════ */

/* Smooth cross-page transitions (multi-page View Transitions). */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: vt-out 220ms ease both; }
  ::view-transition-new(root) { animation: vt-in 320ms ease both; }
}
@keyframes vt-out { to { opacity: 0; transform: translateY(-6px); } }
@keyframes vt-in  { from { opacity: 0; transform: translateY(8px); } }

/* Scroll-driven reveal — a quiet opacity fade only. No movement, no
   clipping: content settles in place so nothing shifts or cuts the eye
   as you scroll. Calm flow, the Linear feeling. */
@keyframes rise-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes wipe-in { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    article .cs-body > h2,
    article .cs-body > p,
    .cs-meta, .pull, .wrong,
    .stats, .stats-hero {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 18%;   /* solid almost immediately — no faint, gappy zone */
    }
    .demo, figure {
      animation: wipe-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 18%;
    }
    .stats-hero .stat, .stats .stat {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 18%;
    }

    /* Reading-progress hairline in the case's own colour. */
    body.themed::after {
      content: "";
      position: fixed; top: 0; left: 0;
      height: 2px; width: 100%;
      background: var(--case, var(--pop));
      transform: scaleX(0); transform-origin: 0 50%;
      z-index: 10000;
      animation: progress linear both;
      animation-timeline: scroll(root block);
    }
  }
}
@keyframes progress { to { transform: scaleX(1); } }

/* ── Hover micro-interactions ── */

/* Nav links: a fine underline draws in from the left. */
.nav-links a { position: relative; }
.nav-links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 8px;
  height: 1px; background: var(--pop);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--dur-2) var(--ease);
}
.nav-links a:hover::after,
.nav-links a:focus-visible::after { transform: scaleX(1); }
.nav-links a[aria-current] { text-decoration: none; }
.nav-links a[aria-current]::after { transform: scaleX(1); background: var(--pop); }

/* Stat numbers lift a hair and warm to the accent on hover of their card. */
.stats-hero:hover .stat .n{ transition: color var(--dur-1) var(--ease); }

/* Next-case link — consistent with the landing index: the arrow is hidden
   until hover, pinned to the right of the column, then eases in. On touch /
   no-hover devices it's hidden entirely (there is no hover), exactly as the
   landing-row arrow is hidden on mobile. */
.cs-next a::after {
  content: "→"; flex: none; margin-left: auto; color: var(--pop);
  opacity: 0; transform: translateX(-8px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.cs-next a:focus-visible::after { opacity: 1; transform: translateX(0); }
@media (hover: hover) {
  .cs-next a:hover::after { opacity: 1; transform: translateX(0); }
}
@media (hover: none) {
  .cs-next a::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-links a::after { transition: none; }
  .cs-next a::after { transition: none; }
}

/* ════════════════════════════════════════════════════════════════
   READING CONTROLS — depth (skim / full) + director's commentary
   No-JS default: full case shown, summary + notes hidden, controls
   hidden. With JS the head script sets the depth before paint.
   ════════════════════════════════════════════════════════════════ */

.case-controls { display: none; }
html.js .case-controls {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--v-hair) var(--v-para); padding-top: var(--v-para); padding-bottom: var(--v-group);
}
.case-controls .seg-label {
  font-family: var(--font-label); font-size: var(--fs-label); letter-spacing: var(--ls-label);
  color: var(--ink-muted);
}
.case-controls .seg-label::before { content: "/ "; color: var(--case, var(--pop)); }

.seg {
  display: inline-flex; gap: 2px;
  border: 1px solid var(--rule-strong); border-radius: var(--radius-pill);
  padding: 3px; background: var(--surface);
}
.seg button {
  font-family: var(--font-label); font-size: var(--fs-label); letter-spacing: var(--ls-label);
  border: 0; background: none; color: var(--neutral);
  /* A standard segmented control (Anna, 22 Aug 2026): fixed 40px segment,
     text dead-centre via align-items + line-height 1, no vertical padding
     to drift. Both words share one size, so centre alignment is exact. */
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 40px; padding: 0 16px; line-height: 1;
  border-radius: var(--radius-pill); cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.seg button:hover { color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--case, var(--pop)); color: var(--paper-white); }
/* Two steps that both clear AA on the control surface: label in --neutral
   (7.8:1), the "60 sec" sub in full --ink-muted (4.8:1). The old 0.55
   opacity put the sub at 2.2:1 (crit panel, 21 Aug 2026). */
.seg .seg-sub { opacity: 1; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.seg button[aria-pressed="true"] .seg-sub { opacity: 0.8; color: inherit; } /* white on green, 5.7:1 */

/* (commentary uses inline <details>; styled below) */

/* ── Skim summary ── */
.case-skim { display: none; }
.case-skim.wrap { max-width: 1280px; padding-top: var(--v-hair); padding-bottom: var(--v-hair); }
html.mode-skim .case-skim { display: block; }
html.mode-skim article { display: none; }
/* Header (kicker · H1 · lede · stats · meta) stays identical in BOTH modes,
   so the Skim/Full toggle never moves when you switch — only the content
   below it swaps (summary ↔ full article). No jump. */

.skim-row { display: grid; grid-template-columns: 92px 1fr; gap: var(--v-group); padding: var(--v-group) 0; border-top: 1px solid var(--rule); }
.skim-row:first-of-type { border-top: 0; }
.skim-row .sk-k { font-family: var(--font-label); font-size: var(--fs-label); color: var(--ink-muted); padding-top: 4px; }
.skim-row .sk-v { font-size: var(--fs-body); line-height: 1.55; max-width: 58ch; }
.skim-more { font-family: var(--font-label); font-size: var(--fs-label); color: var(--ink-muted); margin-top: var(--v-group); text-align: right; }
/* 44px floor on the tap target; the underline still sits on the text, so
   the extra height is hit area, not a bigger-looking link. (16 Aug 2026) */
.skim-more button { font: inherit; color: var(--case, var(--pop)); background: none; border: 0; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; padding: 12px 0; min-height: 44px; display: inline-flex; align-items: center; }

/* ── Inline comments — click to open each one, right where it belongs.
   Warm and embedded: a darker tone of the oat paper, no accent colour. ── */
.note { margin: var(--v-group) 0; }
.note > summary {
  display: inline-flex; align-items: center; gap: 8px; width: fit-content;
  font-family: var(--font-label); font-size: var(--fs-label); color: var(--ink-muted);
  background: var(--surface);
  border-radius: var(--radius-pill); padding: 6px 14px; cursor: pointer; list-style: none;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.note > summary::-webkit-details-marker { display: none; }
.note > summary::before {
  content: ""; width: 13px; height: 13px; flex: none; background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v6A1.5 1.5 0 0 1 12.5 11H6l-3 3v-3H3.5A1.5 1.5 0 0 1 2 9.5z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v6A1.5 1.5 0 0 1 12.5 11H6l-3 3v-3H3.5A1.5 1.5 0 0 1 2 9.5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.note > summary:hover { color: var(--ink); background: var(--paper-deep, var(--surface)); }
.note[open] > summary { color: var(--ink); background: var(--rule); }
.note > summary::after {
  content: ""; width: 11px; height: 11px; flex: none;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform var(--dur-1) var(--ease);
}
.note[open] > summary::after { transform: rotate(180deg); }
.note .note-body {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 16px 20px; margin-top: var(--v-hair);
}
.note .note-body p { font-size: var(--fs-body); line-height: 1.55; margin: 0; max-width: none; color: var(--ink); }
.note .note-body p + p { margin-top: var(--v-para); }

/* About — the personal story disclosure: a tidy full-width rounded button in the
   narrow aside (the 999px pill blobs when it wraps, so square it off and wrap cleanly). */
.about .note.story { margin: var(--space-md) 0 0; }
.about .note.story > summary {
  width: 100%;
  justify-content: space-between;
  white-space: normal;
  text-align: left;
  line-height: 1.4;
  border-radius: var(--radius);
  padding: var(--space-xs) var(--space-sm);   /* 12 / 16 */
}
.about .note.story > summary::before { display: none; }
.about .note.story > summary::after { margin-left: var(--space-xs); }
.about .note.story .note-body p { font-size: var(--fs-body); }

@media (max-width: 640px) {
  html.js .case-controls { padding-left: var(--gutter); padding-right: var(--gutter); justify-content: flex-start; }
  .skim-row { grid-template-columns: 1fr; gap: 6px; }
  .skim-row .sk-v { font-size: var(--fs-body); }
}

/* ════════════════════════════════════════════════════════════════
   SCREENSHOT PLACEHOLDERS
   A <figure class="shot"> reserves the exact aspect box (zero layout
   shift) and shows the build brief. When the real screen is ready,
   delete the <div class="ph">…</div> and drop in:
     <img src="../img/<name>.webp" alt="…" width="1840" height="1150">
   Per-image ratio is set inline on .ph (style="aspect-ratio:16/10").
   Add class "phone" to the <figure> for tall app screens.
   ════════════════════════════════════════════════════════════════ */
.shot { margin: var(--space-xl) 0; }   /* 48 */
.shot .ph {
  width: 100%;
  aspect-ratio: 16 / 10;            /* default; override inline per image */
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: var(--space-xs); text-align: center; padding: var(--space-lg);   /* 12 / 32 */
  background: var(--surface);
  border: 1px dashed var(--ink-muted);
  border-radius: var(--radius);
  color: var(--ink-muted);
}
.shot img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--rule-soft); border-radius: var(--radius);
}
.shot.phone .ph, .shot.phone img { max-width: 340px; margin-inline: auto; }
.shot.phone .ph { aspect-ratio: 9 / 19; }

/* ════════════════════════════════════════════════════════════════
   Mobile crops — dense desktop shots crop to their key detail.
   A full dashboard shrunk to ~340px is unreadable; on small screens
   a .cropw wrapper becomes a window onto the part that matters.
   Desktop is untouched (display: contents — the wrapper vanishes).
   Tune per image inline: --ar window shape · --pos focus point ·
   --z extra zoom (--pos doubles as transform-origin so focus holds).
   ════════════════════════════════════════════════════════════════ */
.cropw { display: contents; }
@media (max-width: 640px) {
  /* No crop windows on a phone (Anna, 11 Aug 2026): the shot shows the FULL
     image at its natural length and runs edge to edge, no zoom, no pan.
     The --ar / --pos / --z tuning only applies on desktop composites now. */
  .cropw {
    display: block;
    margin-inline: calc(-1 * max(var(--gutter), env(safe-area-inset-left)))
                   calc(-1 * max(var(--gutter), env(safe-area-inset-right)));
  }
  .cropw img,
  .shot .cropw img {
    width: 100% !important; height: auto !important; /* beat context img rules (.skim-shots etc.) */
    transform: none;
    border: 0; border-radius: 0;
    margin: 0 !important;
  }
  .cropw + .cropw { margin-top: var(--v-para); }
  /* the rounded frame around a full shot dissolves with the crop */
  .shot .shot-full { border: 0; border-radius: 0; background: none; overflow: visible; }
}


/* ── Interactive-control focus, made explicit for cross-browser certainty (WCAG 2.4.7) ── */
.seg button:focus-visible,
.sx-type:focus-visible,
.px-btn:focus-visible {
  outline: 2px solid var(--case, var(--pop));
  outline-offset: 2px;
}

/* ════════════════════════════════════════════════════════════════
   CASE-STUDY LAYOUT — ONE LEFT AXIS  (case-study pages only)
   Every tier — kicker · H1 · lede · stats · meta · body · figures ·
   controls · next — anchors to the SAME left edge as the nav wordmark
   (the .wrap 32px gutter). Reading widths nest (text < heading < media)
   but all share that left edge: asymmetry over centring, aligned under
   the logo. Nothing is centred. The frame/nav stay 1280, so the
   wordmark never moves between pages.
   ════════════════════════════════════════════════════════════════ */
body.themed {
  --cs-text:  68ch;                              /* reading measure          */
  --cs-wide:  70rem;                             /* headings · kicker · next */
  --cs-media: min(1216px, calc(100vw - 64px));   /* figures · data · demos   */
}

/* Catch-all: NOTHING centres. Every direct child of the header and the
   body sits on the same left edge as the nav wordmark — whatever its type.
   The rules below only add reading-width caps on top of this. */
body.themed .cs-header > *,
body.themed .cs-body > *           { margin-inline: 0; }

/* Header — left-anchored tiers */
body.themed .cs-header .kicker,
body.themed .cs-header h1      { max-width: var(--cs-wide);  margin-inline: 0; }
body.themed .cs-header .lede   { max-width: 52rem;           margin-inline: 0; }
body.themed .stats-hero,
body.themed .cs-meta           { max-width: var(--cs-media); margin-inline: 0; }

/* Body — full-width column; each child left-anchors at its own width */
body.themed .cs-body           { max-width: none; }
body.themed .cs-body > p,
body.themed .cs-body > .pull,
body.themed .cs-body > .note,
body.themed .cs-body > .wrong  { max-width: var(--cs-text);  margin-inline: 0; }
body.themed .cs-body > h2      { max-width: var(--cs-wide);  margin-inline: 0; }
body.themed .cs-body > figure,
body.themed .cs-body > .shot,
body.themed .cs-body > .demo{
  width: var(--cs-media); max-width: none; margin-inline: 0;
}
@media (max-width: 640px) {
  body.themed .cs-body > figure,
  body.themed .cs-body > .shot,
  body.themed .cs-body > .demo{ width: 100%; }
}

/* Controls · skim · next — left-aligned on the same axis.
   NOTE: .case-controls and .cs-next are themselves .wrap elements that
   centre via margin:0 auto. We must NOT zero their inline margins (that
   throws them to the viewport edge on wide screens) — only left-align
   their *contents*. .skim-row is an inner child, so zeroing it is safe. */
body.themed .case-controls         { justify-content: flex-start; }
body.themed .case-skim .skim-row   { max-width: var(--cs-wide); margin-inline: 0; }
body.themed .cs-next               { justify-content: flex-start; }

/* ---------- Painted case-study headers ----------
   The Turner hero from the landing carried into every case study (Anna,
   11 Aug 2026). The painting runs full-bleed behind the header content;
   a paper wash strengthens downwards so the stats and meta rows finish
   on solid paper. painting-motion.js animates it, same attract loop. */
html { overflow-x: clip; }   /* the 100vw painted band may exceed the scrollbar-less viewport by a scrollbar's width */

/* ---------- Warm details (Anna, 11 Aug 2026) ---------- */
/* Selecting text answers in the house green */
::selection { background: var(--pop); color: var(--paper-white); }
/* Pages hand over with a soft cross-fade where the browser supports it;
   under reduced motion the transition is stripped back to a plain load */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}
.cs-painted { position: relative; isolation: isolate; }
.csp-media, .csp-wash {
  position: absolute; top: 0; bottom: 0; left: 50%;
  width: 100vw; transform: translateX(-50%);
  z-index: -1; pointer-events: none;
}
.csp-media { overflow: hidden; opacity: var(--csp-opacity, 1); }   /* --csp-opacity: how faint the painting sits behind a case-study header (Anna, 22 Aug 2026: barely there) */
.csp-media img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 45%;
}
.lp-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.csp-media.lp-on img { visibility: hidden; }
/* Accessibility treatment (measured, 11 Aug 2026): the painting is softened
   and lifted so every text colour clears WCAG AA over its worst pixel —
   blur melts small dark patches, brightness lifts the floor, the wash does
   the rest. --csp-lift lets a dark painting (90POE's Snow Storm) ask for
   more light. Scale hides the blur's soft edge. */
.csp-media img, .csp-media .lp-canvas {
  filter: blur(3px) saturate(var(--csp-sat, 0.95)) brightness(var(--csp-lift, 1.1));
  transform: scale(1.04);
}
.csp-wash {
  background: linear-gradient(to bottom,
    rgba(244, 239, 228, 0.88) 0%,
    rgba(244, 239, 228, 0.62) 26%,
    rgba(244, 239, 228, 0.66) 46%,
    rgba(244, 239, 228, 0.90) 66%,
    var(--background) 84%);
}
/* Same dissolve as the /about band — the painting never ends on a cut line */
.csp-media{
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
}
/* The /about movement, carried onto the case studies (Anna, 12 Aug 2026):
   the same cloud drift and dawn-light sweep, same attract loop. */
.csp-media::after{
  content: "";
  position: absolute;
  inset: -12%;
  pointer-events: none;
  background: radial-gradient(46% 60% at 52% 34%, #FFF5D68C 0%, #FFF5D62E 55%, transparent 100%);
  mix-blend-mode: soft-light;
  opacity: 0;
}
.js .csp-media img,
.js .tp-media img{ animation: pb-drift 16s ease-in-out infinite; }
.js .csp-media::after{ animation: pb-light 9s ease-in-out infinite; }
.hp-still .csp-media img,
.hp-still .csp-media::after{ animation-play-state: paused; }
@media (prefers-reduced-motion: reduce){
  .csp-media img, .csp-media::after{ animation: none !important; }
  .csp-media::after{ opacity: 0; }
}

/* ============================================================
   Painted band — a Turner behind a page header.
   Same treatment as the homepage hero: paper wash feathering
   from the left, attract-loop motion that stops for good on
   first interaction, off under prefers-reduced-motion.
   Used on: /about, /writing/*. The homepage keeps its own
   inline copy so this file never changes it.
   ============================================================ */
@keyframes pb-drift{
  0%   { transform: scale(1.07) translate(1.6%, 0.6%); }
  50%  { transform: scale(1.15) translate(-1.6%, -0.9%); }
  100% { transform: scale(1.07) translate(1.6%, 0.6%); }
}
@keyframes pb-light{
  0%   { transform: translateX(-18%); opacity: 0; }
  30%  { opacity: 0.7; }
  50%  { transform: translateX(0);    opacity: 0.8; }
  70%  { opacity: 0.7; }
  100% { transform: translateX(18%);  opacity: 0; }
}
.pband{
  position: relative;
  overflow: hidden;
  max-width: none;
  padding-inline: 0;
  border-radius: 0;
  display: flex;
  align-items: center;
  min-height: min(40svh, 360px);
}
.pband .pband-text{
  position: relative;
  width: 100%;
  max-width: 1280px;
  margin-inline: auto;
  padding-inline: max(var(--gutter), env(safe-area-inset-left))
                  max(var(--gutter), env(safe-area-inset-right));
  padding-block: var(--space-2xl);
}
/* The essay bands take the case studies' treatment exactly (Anna, 12 Aug 2026):
   the painting dissolves into the paper at the foot instead of ending on a cut,
   and it carries the same blur/lift that keeps the text clear of it. */
.pb-media{
  position: absolute; inset: 0;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
}
.pb-media img{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  transform-origin: 50% 40%;
  filter: blur(3px) saturate(var(--csp-sat, 0.95)) brightness(var(--csp-lift, 1.1));
}
.pb-veil{
  position: absolute;
  inset: -12%;
  pointer-events: none;
  background: radial-gradient(46% 60% at 52% 34%, #FFF5D68C 0%, #FFF5D62E 55%, transparent 100%);
  mix-blend-mode: soft-light;
  opacity: 0;
}
/* Same wash as the case studies — cream in at the top, cream back in at the
   foot, so the painting arrives and leaves rather than sitting in a block. */
.pb-wash{
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(244, 239, 228, 0.88) 0%,
    rgba(244, 239, 228, 0.62) 26%,
    rgba(244, 239, 228, 0.66) 46%,
    rgba(244, 239, 228, 0.90) 66%,
    var(--background) 84%);
}
.js .pb-media img{ animation: pb-drift 16s ease-in-out infinite; }
.js .pb-veil{ animation: pb-light 9s ease-in-out infinite; }
.hp-still .pb-media img,
.hp-still .pb-veil{ animation-play-state: paused; }
@media (prefers-reduced-motion: reduce){
  .pb-media img, .pb-veil{ animation: none !important; }
  .pb-veil{ opacity: 0; }
}
/* the band meets the nav directly, no rule between — painted case-study
   headers get the same treatment, so no rule cuts across the painting */
body:has(.pband) nav,
body:has(.cs-painted) nav{ border-bottom: 0; }
@media (max-width: 640px){
  .pband{ min-height: 0; height: 30svh; max-height: 280px; }
  .pband .pband-text{ padding-block: var(--v-block); }
  .pb-wash{ background: linear-gradient(90deg, #F4EFE4F2 0%, #F4EFE4CC 55%, #F4EFE459 100%); }
}
/* essay header sitting inside a painted band: the band owns the padding */
.pband .cs-header{ padding-block: 0; margin-top: 0; max-width: none; }
/* …but when the header IS the band's text block it keeps the 1280 column,
   so the kicker, h1 and lede line up with the body copy and the nav below */
.pband .pband-text.cs-header{ max-width: 1280px; }

/* The painting dissolves into the paper at the foot of the band — no cut
   line where the image stops (Anna, 12 Aug 2026). Masking the media layer
   keeps the text above it untouched. */
.pb-media{
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 80%, transparent 100%);
}

/* The agents link in the nav (Anna, 23 Sep 2026). Short label on phones. */
/* Nav: "AI manager" with a tiny org chart, one dot over three (23 Sep 2026).
   On hover the three agents take their handoff in turn, hard cuts only. */
.nav-links .nl-ico{ width:16px; height:16px; margin-right:6px; vertical-align:-2px; overflow:visible; }
.nav-links .nl-ico path{ fill:none; stroke:currentColor; stroke-width:1; }
.nav-links .nl-ico circle{ fill:currentColor; stroke:none; }
@keyframes nl-hand{ 0%{ visibility:hidden; } 100%{ visibility:visible; } }
@media (hover:hover) and (prefers-reduced-motion:no-preference){
  .nav-links a:hover .nl-ico .c{ animation:nl-hand 1ms steps(1) both; }
  .nav-links a:hover .nl-ico .c1{ animation-delay:120ms; }
  .nav-links a:hover .nl-ico .c2{ animation-delay:240ms; }
  .nav-links a:hover .nl-ico .c3{ animation-delay:360ms; }
}


/* ── Essays wear the landing page's mist (Anna, 23 Sep 2026: "use the same
   background as I have on my landing page"). The painting band is retired:
   the header sits on paper, and the brand mist's still frame spreads from the
   top right, exactly as it does behind the landing masthead — solid paper
   holds the text column, the colour opens rightwards and dissolves before
   the first paragraph. No drift, no blur, no veil: nothing moves. */
.pband.mistband{ overflow:visible; min-height:0; isolation:isolate; }
.pband.mistband .pband-text{ position:relative; z-index:1; padding-block:var(--space-2xl) var(--space-xl); }
.pb-mist{
  position:absolute; z-index:0; top:calc(-1 * var(--header-h, 72px)); bottom:-45%;
  left:calc(50% - 50vw); width:100vw; overflow:hidden; pointer-events:none;
  -webkit-mask-image:linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
}
.pb-mist img{ width:100%; height:100%; object-fit:cover; display:block; }
.pb-mist::after{ content:""; position:absolute; inset:0;
  background:linear-gradient(to right, var(--paper, #F4EFE4) 0%, var(--paper, #F4EFE4) 40%, rgba(244,239,228,0) 85%); }
@media (max-width:767px){
  .pb-mist::after{ background:linear-gradient(to bottom, var(--paper, #F4EFE4) 0%, var(--paper, #F4EFE4) 72%, rgba(244,239,228,0.20) 100%); }
  .pband.mistband{ height:auto; max-height:none; }
}
.pband.mistband + article{ position:relative; z-index:1; }
@media (prefers-reduced-transparency:reduce), print{ .pb-mist{ display:none; } }

/* Essay rhythm, measured in ink (23 Sep 2026). `.cs-body p` set every pull
   quote's bottom margin to 16, so the line after a pull sat at 30 while the
   line before it sat at 61; and a numbered list had no margin at all, so the
   next paragraph landed 4px under it. Both now match the paragraph step. */
.cs-body .pull{ margin:var(--v-sub) 0; }
.cs-body > ol{ margin:0 0 20px; padding-top:4px; }
@media (max-width:767px){ .cs-body .pull{ margin:var(--v-sub) 0; } }
