/* ============================================================
   Portfolio site
   ------------------------------------------------------------
   Every value here comes from tokens.css. If a raw hex or a
   stray px shows up below, it is a bug, not a shortcut.

   Instrument Sans is variable on two axes: wght 400 to 700 and
   wdth 75 to 100. The brief caps weight at 500, so the width
   axis does the work that bold usually does. Meta, eyebrows and
   years run semi-condensed or condensed. Nothing else does.
   ============================================================ */

@font-face {
  font-family: 'Instrument Sans';
  src: url('../fonts/InstrumentSans-var.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-stretch: 75% 100%;
  font-style: normal;
  font-display: swap;
}

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg-page);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  font-weight: var(--weight-regular);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 { margin: 0; font-weight: var(--weight-medium); }
p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img { display: block; max-width: 100%; height: auto; }

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

:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

/* Available to a screen reader, absent from the page. Used for table captions
   where the figure caption beside them already carries the visible label. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute; left: var(--space-md); top: calc(var(--space-md) * -4);
  padding: var(--space-sm) var(--space-md);
  background: var(--accent-primary); color: var(--text-on-accent);
  border-radius: var(--radius-sm); z-index: 100;
  transition: top var(--dur-fast) var(--ease-out);
}
.skip:focus { top: var(--space-md); }

.wrap {
  max-width: var(--max-content);
  margin: 0 auto;
  padding-inline: var(--space-xl);
}


/* --- Shared type ------------------------------------------- */

.eyebrow {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;                 /* semi-condensed */
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
}

.display {
  font-size: var(--text-display);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}

.question {
  font-size: clamp(30px, 4.4vw, 48px);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  font-weight: var(--weight-regular);
}
.question-lg { font-size: clamp(34px, 5.6vw, 62px); }

.sec-intro {
  font-size: var(--text-body-lg);
  color: var(--text-secondary);
  max-width: 52ch;
}

.inline-link {
  color: var(--accent-primary);
  border-bottom: 1px solid color-mix(in srgb, var(--accent-primary) 32%, transparent);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.inline-link:hover { border-bottom-color: var(--accent-primary); }


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

.nav {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--bg-page) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-base) var(--ease-out);
}
.nav.stuck { border-bottom-color: var(--border-default); }

.nav-in {
  max-width: var(--max-content); margin: 0 auto;
  padding: var(--space-md) var(--space-xl);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-lg);
}

.brand { display: flex; align-items: center; gap: 11px; }
.brand .mark { color: var(--text-primary); }
.wordmark {
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-heading);
}

.nav-links { display: flex; align-items: center; gap: var(--space-lg); }
.nav-links a {
  font-size: var(--text-small);
  color: var(--text-secondary);
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.nav-links a:hover { color: var(--text-primary); border-bottom-color: var(--accent-primary); }
.nav-cv { color: var(--accent-primary) !important; }


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

.hero { padding: var(--space-3xl) 0 var(--space-2xl); }
.hero .eyebrow { margin-bottom: var(--space-lg); }
.hero .display { max-width: 15ch; }

/* The headline and the King Protea, on the same template as .hero-body, so
   the drawing and the Now readout share a right edge. Bottom aligned: the
   drawing sits on the hairline that starts the body row rather than floating
   beside the headline, which is what stops it reading as a background. */
.hero-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
  gap: var(--space-2xl);
  align-items: end;
}

/* The drawing sits bottom right in a square the width of the Now readout
   below it, so the two share a right edge. Square rather than the drawing's
   own 1024 by 1400, because at full portrait height it stands 410 tall and
   pushes the Now readout under the fold on a 1280 by 800 laptop. Sizing off
   the column instead of a height keeps the edge without a magic number.

   The ink is written into the SVG by tools/optimise-drawing.py, not applied
   here, because the export is black on transparent and would be invisible on
   this page. The value it writes is --c-neutral-100. If the palette moves, or
   light mode ever happens, rebuild the asset rather than restyling it. */
.hero-art {
  aspect-ratio: 1;
  align-self: end;
  width: 100%;
  height: auto;
  object-fit: contain;
  object-position: bottom right;
}

/* Two columns: the positioning, and what she is doing right now. Top aligned,
   because the border above gives both columns a shared baseline and that is
   what makes them read as a pair rather than as two adjacent things. */
.hero-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
  gap: var(--space-2xl);
  align-items: start;
  margin-top: var(--space-2xl);
  padding-top: var(--space-2xl);
  border-top: 1px solid var(--border-default);
}
.lede {
  font-size: var(--text-body-lg);
  color: var(--text-secondary);
  max-width: var(--max-prose);
}

/* Names on their own line, because nobody scanning reads to the end of a
   paragraph to find out where you have worked. */
.hero-clients {
  margin-top: var(--space-lg);
  font-size: var(--text-small);
  font-stretch: 87.5%;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-primary);
}

/* Location and right to work, directly under the client row. Same size and
   tracking so the two read as one block, secondary colour so the clients
   stay the louder of the pair. Who she has worked for is the claim; where
   she is, is the qualifier. */
.hero-where {
  margin-top: var(--space-sm);
  font-size: var(--text-small);
  font-stretch: 87.5%;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* The mark defaults to its finished, straight state. The crooked path is
   only swapped in by JS at the moment it is about to animate, so no JS and
   reduced motion both land on a correct static mark rather than a wonky one. */
.mark-lg { color: var(--text-primary); flex-shrink: 0; }
#crooked { display: none; }
.mark-lg.is-animating #crooked { display: inline; }
.mark-lg.is-animating #straight { display: none; }

/* --- The intro ------------------------------------------------

   Two and a bit seconds, once per session, on the front door only.

     0         the veil is already up, the page renders behind it
     0-380     the mark fades in
     450-1350  the coral arm straightens
     1350-1900 it holds, straight, so the eye registers what happened
     1900-2060 the mark leaves
     1960-2280 the veil lifts

   The straighten is 0.45s delay and 0.9s duration because that is what the
   brand guide specifies. An earlier build compressed it to 180/520 to hit a
   load budget, which made it hurried, and then cut it short on load, which
   made it invisible. The hold afterwards is the part people forget: an
   animation that leaves the instant it resolves reads as a glitch, because
   there is no moment where the finished state is simply true.

   Do not add a cut-short on `load`. A build did, and `load` fires around
   150ms on a fast connection, so the veil lifted before the arm had begun to
   move and the signature was never once seen. The animation is the point, so
   it is allowed to finish. That time, paid once per session, is the price
   of the idea that carries the whole positioning being seen at all.

   All of it is CSS, so it works with JavaScript off. The script only does the
   things that need state: remember the session, take the node out afterwards,
   and force it away if an animation never runs.

   Home page only. Everywhere would turn navigation into a series of doors. */
.intro {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: var(--bg-page);
  /* Lifts after the straighten has finished, not before. The first version
     cut short on load, which locally fires around 150ms, so the veil was gone
     before the animation began and the signature was never seen. A moment
     nobody watches is not a moment. */
  animation: intro-out 320ms var(--ease-out) 1960ms forwards;
}
/* The mark leaves just ahead of the veil. Both fading together left it
   ghosting over the headline, which sits in the same part of the screen. */
.intro-mark {
  color: var(--text-primary);
  /* In, hold while the arm straightens and for a beat after it lands, then
     out just ahead of the veil. The straighten runs 450ms to 1350ms. */
  animation:
    intro-mark-in 380ms var(--ease-out) both,
    intro-mark-out 160ms var(--ease-out) 1900ms forwards;
}
@keyframes intro-mark-in {
  from { opacity: 0; transform: scale(.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes intro-mark-out {
  to { opacity: 0; transform: scale(1.02); }
}
@keyframes intro-out {
  to { opacity: 0; visibility: hidden; }
}

/* The safety net, not a shortcut. Only used if the timeline never completes,
   so the veil can never be left sitting over the page. */
.intro.is-done { animation: intro-out 200ms var(--ease-out) forwards; }
.intro.is-done .intro-mark { animation: intro-mark-out 120ms var(--ease-out) forwards; }

/* Seen already this session, so no veil at all. Set by an inline script in
   the head, before the first paint, so there is no flash of it. */
.intro-seen .intro { display: none; }

/* Nothing moves, and there is nothing to lift. */
@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
}

/* The hero had two buttons. "See the work" only scrolled down, which the
   reader was going to do anyway, and a CTA that performs a non-action makes
   the one beside it look decorative too. Both removed: the nav carries
   Contact, and the work starts one section below.

   Button styles live in Design System/design-system.html, so nothing is lost
   by not carrying an unused .btn rule here. */


/* --- Sections ----------------------------------------------- */

.section {
  padding: var(--space-3xl) 0;
  border-top: 1px solid var(--border-default);
  position: relative;
}

/* No section marker. A drawing sat on this hairline for a day and came off
   again, and the reasoning is worth keeping next to the rule it would have
   interrupted.

   Every other drawing on the site does a job. The protea is the thesis in ink
   and the nugget beside it says so, each nuggets icon illustrates one specific
   fact, the About portrait is a likeness. A mark on a divider illustrates
   nothing. It was there because section 07 of the brand guide asked for one.

   The break does not need it either. Each one already carries 104px of
   padding, this hairline, an eyebrow and a display-size question as the
   heading. "What can it do?" is not a landmark anyone misses. The marker was
   a fifth signal at a boundary that was never ambiguous.

   The hairline stays. It is structure rather than illustration, and without
   it the boundary goes soft.

   The drawings are kept in assets/markers/, unplaced. See the brand guide:
   the item is cancelled, not outstanding. */

.sec-head { margin-bottom: var(--space-2xl); }
.sec-head .eyebrow { margin-bottom: var(--space-md); }
.sec-head .sec-intro { margin-top: var(--space-md); }


/* --- Now, in the hero ---------------------------------------- */

/* Current work as a status readout, not a section. It is the only item on the
   site with no artefact behind it, so it earns the smallest footprint that
   still answers "what is she doing right now".

   Condensed Instrument Sans, not mono. The width axis already carries years
   and meta lines, so compression is this site's existing way of saying "this
   is data", and it keeps the one-typeface rule intact. Mono read well but
   would have been a third type treatment for a single widget. */
.now {
  font-stretch: 75%;
  font-size: var(--text-small);
  letter-spacing: .02em;
  line-height: 1.5;
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-lg);
}

.now-head {
  display: flex; align-items: center; gap: var(--space-sm);
  font-size: 11px;
  font-stretch: 87.5%;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent-primary);
  margin-bottom: var(--space-md);
}

/* The live signal. A grain panel does not survive at this size, so the motion
   becomes one slow pulse. Still the only looping thing on the site. */
.now-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent-primary);
  flex-shrink: 0;
  animation: now-pulse 2.4s ease-in-out infinite;
}
@keyframes now-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }

.now-rows > div {
  display: flex; justify-content: space-between; gap: var(--space-md);
  padding: 7px 0;
  border-top: 1px solid var(--border-default);
}
.now-rows > div:first-child { border-top: 0; padding-top: 0; }
.now-rows dt { color: var(--text-secondary); }
.now-rows dd { margin: 0; color: var(--text-primary); text-align: right; }
.now-held { color: var(--accent-primary); }

.now-link {
  display: inline-block;
  margin-top: var(--space-md);
  font-size: 12px;
  color: var(--accent-primary);
  border-bottom: 1px solid color-mix(in srgb, var(--accent-primary) 32%, transparent);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.now-link:hover { border-bottom-color: var(--accent-primary); }


/* --- Work index --------------------------------------------- */

/* The themes, stated rather than filtered.

   The filter was solving a length problem: six cards over 3300px is a list
   nobody can hold in their head. The rows solve that problem instead, and a
   control that cuts five visible items down to three is the gravity of more,
   three rows above a snapshot about resisting it.

   The themes themselves were never navigation, they are the claim, so they
   stay as a line in the section intro. One step up from the secondary text
   around them, which is enough to read as a list without becoming an
   interface. Not teal: teal is the guest and the format labels in the column
   below already spend it. */
.themes-line { color: var(--text-primary); }

/* The format label is the eyebrow now. It used to be a separate teal line
   under the year, back when every entry was a row and the only thing
   distinguishing a snapshot from a case study was a word. The shortlist
   distinguishes them by size, and the eyebrow already says which is which
   beside the date, so the label has nothing left to do. */

.entry-link {
  display: block;
  padding: 0;
}

/* The year leads the meta line rather than sitting in its own column,
   so every title starts flush with the artefact beside or above it. */
.year {
  font-stretch: 75%;                   /* condensed, so the date stays compact */
  letter-spacing: .04em;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  margin-right: 6px;
}

.entry-title {
  font-size: clamp(24px, 2.9vw, 34px);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  display: inline;
  background-image: linear-gradient(var(--accent-primary), var(--accent-primary));
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--dur-base) var(--ease-out);
}
.entry-link:hover .entry-title,
.entry-link:focus-visible .entry-title { background-size: 100% 1px; }

.entry-meta {
  font-size: var(--text-small);
  font-stretch: 87.5%;
  color: var(--text-secondary);
  margin-top: var(--space-md);
}
.entry-meta .year::after {
  content: '\00B7';
  margin-left: 6px;
  color: var(--text-secondary);
}
.entry-hook {
  color: var(--text-secondary);
  max-width: 56ch;
  margin-top: var(--space-sm);
}


/* ── The proof block ──────────────────────────────────────────────────
   One component, four sizes, and the pairing is the whole idea: a figure
   on its own is a claim, a figure sitting under the thing it came from is
   evidence. It appears beside the lead artefact, under each supporting
   card, at the end of the lab band and in the ledger, and the markup is
   identical every time.

   Coral, because the outcome is the one thing in an entry worth stopping
   for, and coral is the constant. */
.metric { margin-top: var(--space-md); }
.metric-k {
  display: block;
  font-size: var(--text-eyebrow);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.metric-v {
  display: block;
  margin-top: 3px;
  font-size: clamp(21px, 1.9vw, 26px);
  line-height: 1.05;
  font-stretch: 75%;                  /* condensed, so a figure stays compact */
  font-variant-numeric: tabular-nums;
  color: var(--accent-primary);
}
.metric-n {
  display: block;
  margin-top: 4px;
  font-size: var(--text-small);
  color: var(--text-secondary);
}

/* ── The artefact plate ───────────────────────────────────────────────
   contain, not cover.

   The first pass cropped: a 1200x648 four-screen strip into a 16/10 box at
   cover cut the left edge off, and "Welcome to your onboarding journey"
   rendered as "elcome to your / boarding journey". Cropping is safe on a
   photograph and never safe on a set of screens, because the thing being
   cropped is one of the screens.

   So the box holds a ratio for the grid's sake and the image letterboxes
   inside it. The mat is --bg-surface either way, so a letterbox reads as
   the panel it already was. */
.plate {
  overflow: hidden;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
}
.plate img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: transform var(--dur-base) var(--ease-out);
}
.entry-link:hover .plate img,
.entry-link:focus-visible .plate img { transform: scale(1.03); }

/* Same three mats as the full-size artefacts, so a small copy never
   misrepresents whether a screen was drawn light or dark. */
.plate-paper { padding: var(--space-sm); }
.plate-paper img { border-radius: 3px; }
.plate-diagram { background: var(--c-neutral-100); padding: var(--space-sm); }
.plate-diagram img { border-radius: 3px; }

/* ── 1. The lead ──────────────────────────────────────────────────────
   Full width for the artefact, because it is the newest piece and the only
   one that is playable. The figure sits top right of the copy, which is the
   closest it can get to the artefact it belongs to. */
.work-lead .shot { margin-bottom: var(--space-xl); }
.work-lead-copy {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 17rem);
  gap: var(--space-2xl);
  align-items: start;
}
.work-lead-copy .entry-title {
  font-size: clamp(30px, 4vw, 46px);
}
.work-lead .entry-meta { margin-top: var(--space-md); }
.work-lead .entry-hook { margin-top: var(--space-md); max-width: 54ch; }
.work-lead .metric { margin-top: 0; }
.work-open {
  display: inline-block;
  margin-top: var(--space-lg);
  font-size: var(--text-small);
  color: var(--accent-primary);
}

/* ── 2. Two supporting reads ──────────────────────────────────────────
   Equal weight, so equal columns and one shared ratio on the plates. */
.work-support {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2xl);
  margin-top: var(--space-2xl);
  padding-top: var(--space-2xl);
  border-top: 1px solid var(--border-default);
}
.work-card .plate { aspect-ratio: 16 / 10; margin-bottom: var(--space-lg); }
.work-card .entry-title { font-size: clamp(23px, 2.5vw, 32px); max-width: 24ch; }
.work-card .entry-meta { margin-top: var(--space-sm); }
.work-card .entry-hook { margin-top: var(--space-md); max-width: 50ch; }

/* ── 3. The lab ───────────────────────────────────────────────────────
   A band rather than a card. It is a collection, not a project, and its
   artefact stays marked up rather than drawn, because three statuses in a
   picture go stale the moment one of them changes. */
.work-lab {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 24rem) auto;
  gap: var(--space-2xl);
  align-items: center;
  margin-top: var(--space-2xl);
  padding: var(--space-2xl) 0;
  border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-default);
}
.work-lab .entry-title {
  display: block;
  margin-top: var(--space-sm);
  max-width: 20ch;
  font-size: clamp(22px, 2.4vw, 30px);
}

/* The status list loses its panel here.

   .lab-thumb was drawn as a hover preview, so it carries a surface, a
   border, a radius and a shadow. The band already has rules top and bottom,
   and a card floating between them is a box inside a box. Stripped back it
   reads as what it is: three names with three statuses, sitting in the
   band rather than on it. The hairlines between the rows stay, because
   those are what pair each name with its own status. */
.work-lab .lab-thumb {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  gap: var(--space-sm);
}
.work-lab .metric { margin-top: 0; text-align: right; }

/* ── 4. Everything else ───────────────────────────────────────────────
   Demoted, not deleted. Each row keeps its artefact and its figure, so the
   rule that nothing in the index is unbacked still holds. What a row loses
   is the hook, which is the right thing to lose: the shortlist above is
   where the reasoning gets read.

   The day a /work page exists this whole block becomes one link. */
.work-rest { margin-top: var(--space-2xl); }
.work-rest-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-lg);
  flex-wrap: wrap;
  padding-bottom: var(--space-md);
}
.work-rest-head p:not(.eyebrow) {
  max-width: 46ch;
  font-size: var(--text-small);
  color: var(--text-secondary);
}
.work-ledger { display: flex; flex-direction: column; }
.work-ledger li { border-top: 1px solid var(--border-default); }
.work-ledger .entry-link {
  display: grid;
  grid-template-columns: 6rem minmax(0, 1fr) 13rem;
  gap: var(--space-lg);
  /* Centred, not top aligned. Both titles here run to one line against a
     200px proof column, and top alignment left a hole down the left of
     every row. */
  align-items: center;
  padding: var(--space-lg) 0;
}
.work-ledger .plate { aspect-ratio: 16 / 10; }
.work-ledger .entry-title { font-size: clamp(18px, 1.7vw, 22px); }
.work-ledger .entry-meta { margin-top: 5px; }
.work-ledger .metric { margin-top: var(--space-sm); }
.work-ledger .metric-v { font-size: 20px; }
.work-year {
  font-size: var(--text-small);
  font-stretch: 75%;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

/* The artefact panel. Overflow hidden so the hover scale is contained. */
.shot {
  overflow: hidden;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--bg-surface);
  box-shadow: var(--shadow-card);
}
.shot img {
  width: 100%;
  transition: transform var(--dur-base) var(--ease-out);
}
.entry-link:hover .shot img,
.entry-link:focus-visible .shot img { transform: scale(1.02); }

/* Artefacts drawn on white stay white, because inverting a screen design
   misrepresents it. But butting a bright rectangle straight against the ink
   is jarring, so they get matted: page, then a raised panel, then the work.
   Two steps down instead of one jump. */
.shot-paper {
  background: var(--bg-surface);
  border-color: var(--border-default);
  padding: var(--space-lg);
}
.shot-paper img { border-radius: var(--radius-sm); }

.shot-diagram { background: var(--c-neutral-100); padding: var(--space-lg); }
.shot-diagram img { border-radius: var(--radius-sm); }

/* A dark-mode screen needs no frame, only an edge. Cropped out of its light
   mat with rounded alpha corners, it sits straight on the page. The rim and
   the shadow are drop-shadows rather than borders so they follow the alpha
   silhouette instead of boxing it. */
.shot-bare {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
  /* The halo uses the primitive rather than a semantic token on purpose. This
     is not text and it is not a border, it is light catching an edge, so the
     value is named for what it is. Same reason .device below uses --c-ink-700. */
  filter:
    drop-shadow(0 0 1px color-mix(in srgb, var(--c-neutral-100) 22%, transparent))
    drop-shadow(0 18px 40px rgba(0, 0, 0, .55));
}

/* ── The live prototype, in a phone ────────────────────────────────────
   A poster frame, not an eager iframe. The still costs 21KB and loads with
   the page; the real app only mounts when somebody asks for it. That keeps
   the page fast and means a bad day at the host costs a still image rather
   than a broken hole in the case study.

   The bezel is drawn, not photographed. A photoreal iPhone would date the
   page and put a hardware brand in the middle of the work. */
.device {
  --bezel: 12px;
  /* width, not just max-width. This sits in a grid column, and the auto
     inline margins below cancel the default stretch, so without an explicit
     width the frame collapses to its own padding. The other artefacts get
     away with it because an <img> carries an intrinsic width. */
  width: 100%;
  max-width: 390px;
  margin-inline: auto;
  padding: var(--bezel);
  border-radius: 44px;
  background: var(--c-ink-700);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--c-neutral-100) 10%, transparent),
    var(--shadow-raised);
}
.device-screen {
  position: relative;
  aspect-ratio: 390 / 844;
  overflow: hidden;
  border-radius: 32px;
  background: var(--bg-page);
}
.device-screen > img,
.device-screen > iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

/* The launch affordance is a real link first. Without JavaScript it opens
   the prototype in a new tab, which is a worse experience but never a dead
   control. The script upgrades it to load in place. */
.device-launch {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  text-align: center;
  text-decoration: none;
  color: var(--text-primary);
  /* Flat scrim plus a blur, not a gradient. A gradient leaves the middle
     band too light, which is exactly where the label sits, so the words
     landed on top of the adviser card and read as a mistake. Blurring the
     still also says "this is a cover" rather than "this is the app". */
  background: color-mix(in srgb, var(--bg-page) 62%, transparent);
  backdrop-filter: blur(3px);
  transition: background var(--dur-base) var(--ease-out);
}
.device-launch:hover,
.device-launch:focus-visible { background: color-mix(in srgb, var(--bg-page) 78%, transparent); }

.device-launch-icon {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-accent);
  background: var(--bg-accent-subtle);
  color: var(--accent-primary);
  font-size: 18px;
  transition: transform var(--dur-base) var(--ease-out);
}
.device-launch:hover .device-launch-icon { transform: scale(1.06); }

.device-launch-text {
  font-size: var(--text-body);
  font-stretch: 88%;
}
.device-launch-note {
  font-size: var(--text-small);
  color: var(--text-secondary);
  max-width: 28ch;
}

/* The desktop variant of the same frame.

   .device draws a phone, and the JPM prototypes are trading workspaces, so
   they need a window rather than a handset. Everything else is shared: the
   poster ships as a real link out, the script swaps in the iframe, and the
   live tag appears the moment it does. Only the bezel changes.

   The chrome is drawn, not photographed, for the same reason the phone is:
   a screenshot of somebody's browser dates the page and puts an OS in the
   middle of the work. Three dots and a bar is enough to say "application". */
.device-wide {
  --bezel: 0px;
  width: 100%;
  max-width: none;
  padding: 0;
  border-radius: var(--radius-card);
  overflow: hidden;
}
.device-wide .device-chrome {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 10px var(--space-md);
  background: var(--c-ink-700);
  border-bottom: 1px solid var(--border-default);
}
.device-wide .device-chrome i {
  width: 9px;
  height: 9px;
  border-radius: var(--radius-pill);
  background: var(--c-ink-600);
}
.device-wide .device-chrome span {
  margin-left: var(--space-sm);
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: .04em;
  color: var(--text-secondary);
  /* The address is fictional, so it is never a link and never selectable. */
  user-select: none;
}
.device-wide .device-screen {
  aspect-ratio: 16 / 10;
  border-radius: 0;
}

@media (max-width: 700px) {
  /* A 16/10 workspace at 342px wide is 214px tall, which is a stamp of a
     trading screen and worth nothing. The frame keeps its width and the
     screen gets taller, so the poster stays readable and the launch label
     still has room to sit in the middle of it. */
  .device-wide .device-screen { aspect-ratio: 4 / 3; }
  .device-wide .device-chrome span { display: none; }
}

/* Once live, the frame says so, because an interactive thing that looks
   like a screenshot gets treated like one. */
.device[data-live] .device-launch { display: none; }
.artefact-portrait .device-live-tag { grid-column: 1; }
.device-live-tag {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  justify-content: center;
  margin-top: var(--space-md);
  font-size: var(--text-small);
  color: var(--text-secondary);
}
.device-live-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--accent-secondary);
}



/* ============================================================
   Case study
   ------------------------------------------------------------
   One rule governs this page: the artefacts are the argument and
   the prose is the caption. So body copy is capped at --max-prose
   and never runs the full width, while artefacts are allowed to.
   ============================================================ */

.backlink {
  display: inline-block;
  font-size: var(--text-small);
  font-stretch: 87.5%;
  letter-spacing: .04em;
  color: var(--text-secondary);
  margin-bottom: var(--space-xl);
  transition: color var(--dur-fast) var(--ease-out);
}
.backlink:hover { color: var(--accent-primary); }

.cs-head { padding: var(--space-2xl) 0 0; }
.cs-title { max-width: 18ch; }

/* The About header, and only that one. .cs-head is shared with all seven case
   studies, so the portrait hangs off .about-head rather than off .cs-head, or
   every case study would go looking for a drawing that is not theirs.

   Bottom aligned, so the drawing stands on the same line as "Let me rewind."
   rather than floating beside the headline. */
.about-head-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 260px);
  gap: 0 var(--space-2xl);
  align-items: end;
}
.about-head-grid > div { grid-column: 1; grid-row: 1; }
.about-head-grid > .rewind { grid-column: 1; grid-row: 2; }
.about-portrait {
  grid-column: 2;
  grid-row: 1 / span 2;      /* so it still stands on the hand-off line */
  align-self: end;
  justify-self: end;
  width: 100%;
  height: auto;
}
.cs-deck {
  font-size: var(--text-body-lg);
  color: var(--text-secondary);
  max-width: var(--max-prose);
  margin-top: var(--space-lg);
}

.cs-meta {
  display: flex; flex-wrap: wrap; gap: var(--space-lg) var(--space-xl);
  margin: var(--space-xl) 0 var(--space-2xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-default);
}
.cs-meta dt {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.cs-meta dd { margin: 0; font-size: var(--text-small); }

/* Section nav on the left, because people read left to right and the nav is
   a map you consult before you read, not after. */
.cs-body {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  gap: var(--space-2xl);
  padding-top: var(--space-3xl);
  padding-bottom: var(--space-3xl);
}
.cs-main { grid-column: 2; min-width: 0; }

/* Prose is capped. Artefacts are not, which is the whole point. */
.cs-main p,
.cs-main .cs-h2,
.cs-main .cs-h3,
.cs-main .tldr,
.cs-main .stats,
.cs-main .disclose,
.cs-main .companion { max-width: var(--max-prose); }

/* The arc is a list of spans, so the paragraph cap above misses it and it
   ran wider than the prose beside it. Slightly wider than the measure,
   because it carries a year column the prose does not. */
.cs-main .arc { max-width: calc(var(--max-prose) + 5.5rem); }

.cs-nav {
  grid-column: 1; grid-row: 1;
  position: sticky; top: 104px;
  align-self: start;
}
.cs-nav-label {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-md);
}
.cs-nav ol { display: flex; flex-direction: column; gap: 10px; }
.cs-nav a {
  font-size: var(--text-small);
  color: var(--text-secondary);
  border-left: 1px solid var(--border-default);
  padding-left: var(--space-md);
  display: block;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.cs-nav a:hover, .cs-nav a.on { color: var(--text-primary); border-left-color: var(--accent-primary); }

/* Prose */
.cs-main p { color: var(--text-secondary); }
.cs-main p + p { margin-top: var(--space-lg); }
.cs-main strong { color: var(--text-primary); font-weight: var(--weight-medium); }
.cs-main em { color: var(--text-primary); font-style: italic; }

.lead-out { color: var(--text-primary) !important; font-size: var(--text-body-lg); }

.cs-section { padding-top: var(--space-3xl); }
.cs-num {
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .1em;
  color: var(--accent-primary) !important;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-sm);
}
.cs-h2 {
  font-size: clamp(26px, 3.2vw, 36px);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  font-weight: var(--weight-regular);
  margin-bottom: var(--space-lg);
}
.cs-h3 {
  font-size: var(--text-h3);
  letter-spacing: var(--tracking-heading);
  margin: var(--space-xl) 0 var(--space-sm);
}

/* TLDR. The one block that has to work in eight seconds. */
.tldr {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-left: 2px solid var(--accent-primary);
  border-radius: var(--radius-sm);
  padding: var(--space-xl);
}
/* The font-size is not redundant. `.tldr p` is 0,1,1 and `.eyebrow` is 0,1,0,
   so every TLDR label on the site had been silently rendering at 19px instead
   of 12, semi-condensed and tracked at 3px, since this component was written.
   Found 17 August while measuring the card at phone width. This selector is
   0,2,0 and wins. */
.tldr .eyebrow {
  margin-bottom: var(--space-md);
  font-size: var(--text-eyebrow);
}
.tldr p { font-size: var(--text-body-lg); }
.tldr-key {
  color: var(--text-primary) !important;
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-default);
}

/* An onward link at the foot of a TLDR, for when the block raises a question
   another page answers. Steps down to body size on purpose: .tldr p is 19px so
   the block reads as one voice, and a 19px link under the key line competes
   with it. This is the footnote to the summary, not a second summary. */
.tldr .tldr-more {
  margin-top: var(--space-md);
  font-size: var(--text-body);
  color: var(--text-secondary);
}

/* No read time here, and that was tested rather than assumed. Built, then
   quietened, then removed. A read time is a blog convention, where the reader
   has already chosen the page and is deciding when to read it. On a case study
   they are deciding whether, and a number that prices the commitment before
   they have read a sentence argues against the summary it sits inside. See the
   log, 17 August. Do not add one back without a reason that answers that. */

/* Numbers, lifted out of the paragraph they used to sit inside in bold. Two
   figures only: the comparison is the finding, and the sample size is the
   caveat, which is already what .tldr-key says. Smaller than .stats on
   purpose. The full row still runs in the outcome section, and a summary that
   shouts louder than the result it summarises is the wrong way round.
   Same auto-fit grid as .stats, with a wider minimum because these two labels
   are sentences rather than the outcome row's short tags. Flex was tried
   first and put the second figure wherever the first label happened to end,
   which reads as an accident rather than a column. */
.tldr-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-lg);
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-default);
}
.tldr-stats .stat-n { font-size: 28px; }

/* The About timeline's TLDR. The section heading already has space-lg beneath
   it, and .tl carries space-xl above it, so the card needs no margin of its own
   in either direction. Declared anyway, and left at zero, because the next
   person to add a card here will otherwise add one. */
.about-tldr { margin: 0; }

/* Matched to .tl-item, not to the prose cap. A TLDR normally sits in a column
   of paragraphs and takes the prose measure with them, but this one sits on top
   of the timeline, and .tl-item is wider by space-xl because it carries the
   spine and its padding. At the prose cap the card stopped about 25px short of
   the timeline text beside it, which does not read as a narrower measure, it
   reads as a misaligned edge. The card's own text measure stays tighter than
   the timeline's, which is correct: it has padding, and it is a summary. */
.cs-main .about-tldr { max-width: calc(var(--max-prose) + var(--space-xl)); }

/* Artefacts run the full column width, wider than any line of text. */
.artefact { margin-top: var(--space-2xl); }
.artefact figcaption {
  font-size: var(--text-small);
  color: var(--text-secondary);
  max-width: var(--max-prose);
  margin-top: var(--space-md);
}
.anno-label {
  display: block;
  color: var(--text-secondary-accent);   /* teal is the guest, and this is its room */
  font-weight: var(--weight-medium);
  margin-bottom: 4px;
}
.artefact figcaption em { color: var(--text-primary); font-style: italic; }

/* A single tall artefact puts its caption alongside rather than beneath.
   Left to itself a phone screenshot fills the column and turns the page into
   a scroll of handsets. */
.artefact-portrait {
  display: grid;
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: start;
}
.artefact-portrait figcaption { margin-top: 0; }

/* Two tall artefacts sit side by side, so the two consolidation calls
   can be compared rather than scrolled past one after the other. */
.artefact-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-xl);
  margin-top: var(--space-2xl);
}
.artefact-pair .artefact { margin-top: 0; }
.artefact-tall .shot { max-width: 460px; }

/* Progressive disclosure, so the main scroll stays short. */
.disclose {
  border-top: 1px solid var(--border-default);
  padding: var(--space-md) 0;
}
.disclose summary {
  cursor: pointer;
  list-style: none;
  display: flex; align-items: baseline; gap: var(--space-sm);
  color: var(--text-primary);
  font-size: var(--text-body);
}
.disclose summary::-webkit-details-marker { display: none; }
.disclose summary::before {
  content: '+';
  color: var(--accent-primary);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.disclose[open] summary::before { content: '\2212'; }
.disclose summary:hover { color: var(--accent-primary); }
.disclose > p { margin-top: var(--space-md); padding-left: var(--space-lg); }

/* Outcome numbers */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: var(--space-lg);
  margin-bottom: var(--space-xl);
  padding: var(--space-lg) 0;
  border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-default);
}
.stat-n {
  display: block;
  font-size: 34px;
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}
.stat-l {
  display: block;
  font-size: var(--text-small);
  font-stretch: 87.5%;
  color: var(--text-secondary);
}

/* Companion link. Never end a candid reflection on a low note. */
.companion {
  margin-top: var(--space-2xl);
  padding: var(--space-xl);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-card);
  background: var(--bg-secondary-subtle);
}
.companion .eyebrow { color: var(--text-secondary-accent); margin-bottom: var(--space-sm); }
.companion-body { margin-bottom: var(--space-lg); }
.companion-link {
  font-size: var(--text-body-lg);
  color: var(--text-secondary-accent);
  border-bottom: 1px solid color-mix(in srgb, var(--accent-secondary) 35%, transparent);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.companion-link:hover { border-bottom-color: var(--accent-secondary); }

/* The two-model diagram.

   Drawn in the page rather than exported, so it takes its colours from the
   tokens and cannot drift from them. Coral marks the thing that differs
   between the two rows, which is the only reason to look at it. Everything
   else is quiet on purpose: a diagram where every box is styled says
   nothing about which box matters. */
.model-diagram { width: 100%; height: auto; display: block; }
.model-diagram .md-k {
  font-size: 11px;
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  fill: var(--text-secondary);
}
.model-diagram .md-box { fill: none; stroke: var(--border-strong); stroke-width: 1; }
.model-diagram .md-focus { stroke: var(--accent-primary); fill: var(--bg-accent-subtle); }
.model-diagram .md-l {
  font-size: 14px;
  fill: var(--text-primary);
  text-anchor: middle;
}
.model-diagram .md-dim { font-size: 11.5px; fill: var(--text-secondary); }
.model-diagram .md-arrow {
  stroke: var(--border-strong);
  stroke-width: 1;
  marker-end: none;
}
.model-diagram .md-n {
  font-size: 11px;
  font-stretch: 87.5%;
  fill: var(--text-secondary-accent);
}

/* Below 620 the two rows stop fitting side by side at a readable type size.
   The SVG scales down as one unit rather than reflowing, so the floor is
   set by the smallest label on it: 11px at 720 wide holds to about 520. */
@media (max-width: 560px) {
  .model-diagram { min-width: 460px; }
  .artefact:has(.model-diagram) { overflow-x: auto; }
}

/* --- Snapshots -----------------------------------------------
   The short form. Problem, the call, what happened. Deliberately
   not a case study, and labelled as such so nobody reads it as a
   thin one. No section nav, so the body runs single column. */

/* No nav column, so the body is one column and the content belongs in it.
   Without resetting grid-column the .cs-main rule above still says "2", which
   creates an implicit second column and indents every solo page away from its
   own header. */
.cs-body.solo { grid-template-columns: minmax(0, 1fr); }
.cs-body.solo .cs-main { grid-column: 1; }

.snap-tag {
  display: inline-flex; align-items: center; gap: var(--space-sm);
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  padding: 5px 14px;
  margin-bottom: var(--space-lg);
}
.snap-tag::before {
  content: '';
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-secondary);
}
.cs-title-sm { font-size: clamp(32px, 4.6vw, 54px); }

/* The one call the piece exists to explain. */
.decision {
  border-left: 2px solid var(--accent-primary);
  padding: var(--space-sm) 0 var(--space-sm) var(--space-lg);
  margin: var(--space-xl) 0;
  max-width: var(--max-prose);
}
.decision-label {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent-primary) !important;
  margin-bottom: var(--space-sm);
}
.decision-body { font-size: var(--text-body-lg); color: var(--text-primary) !important; }

/* Closing line, so a snapshot ends on an offer rather than a stop. */
.snap-end {
  margin-top: var(--space-3xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--border-default);
  display: flex; flex-wrap: wrap; gap: var(--space-lg);
  justify-content: space-between; align-items: baseline;
}
.snap-end p { color: var(--text-secondary); max-width: 46ch; }

@media (max-width: 1100px) {
  .cs-body { grid-template-columns: minmax(0, 1fr); }
  /* Both halves, or neither. Dropping to one column without moving .cs-main
     out of column 2 makes grid invent an implicit second column, and the
     gap between them stays, so every case study under 1100px was reading
     64px narrower than its own container. Same mistake as .cs-body.solo,
     made twice. */
  .cs-main { grid-column: 1; }
  .cs-nav { display: none; }
}

@media (max-width: 700px) {
  .artefact-pair { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xl); }
  .artefact-tall .shot { max-width: none; }
  .artefact-portrait { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
  .cs-title { max-width: none; }
  .stats { grid-template-columns: repeat(2, 1fr); }
  /* Flex wrapping leaves ragged rows here, so pin it to two columns. */
  .cs-meta { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-lg); }
  .cs-meta div:last-child { grid-column: 1 / -1; }
}


/* ============================================================
   The AI lab
   ------------------------------------------------------------
   One landing page and three project pages, built out of the
   patterns that were already here. Nothing below introduces a
   third accent: coral stays the constant and teal stays the
   guest, doing the job it already does inside case studies,
   which is marking evidence and annotation.

   The one new idea is a pair of states for a claim. Solid
   border means settled. Dashed border means open, and it
   borrows the dashed convention .tl-todo and .nug-todo already
   use for "not written yet", because an unresolved rule and an
   unwritten entry are the same signal to a reader: this is a
   hole, and it is meant to be visible.
   ============================================================ */

/* --- Evidence tags -------------------------------------------
   A small label that travels with a claim. Solid and teal when
   the thing is agreed, observed or decided. Dashed and neutral
   when it is not, which is the state the whole lab is about. */

.ev-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary-accent);
  border: 1px solid var(--border-secondary);
  border-radius: var(--radius-pill);
  padding: 3px 10px;
  /* Wraps rather than holding one line. A tag can be a short label or a
     whole unresolved question, and nowrap on the second kind pushed the
     page sideways at 320px. */
  line-height: 1.45;
  max-width: 100%;
}
.ev-tag.is-open {
  color: var(--text-secondary);
  border-color: var(--border-strong);
  border-style: dashed;
}
/* Coral, once per page at most: the moment the agent refuses to overclaim.
   It is the argument of every one of these pages, so it takes the constant
   accent rather than the guest. */
.ev-tag.is-held {
  color: var(--accent-primary);
  border-color: var(--border-accent);
}

/* A run of tags under a heading, wrapping rather than scrolling. */
.ev-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}


/* --- Cards ----------------------------------------------------
   The general-purpose block for a labelled piece of content:
   four documentation types, four story types, the three system
   boundaries, a set of separated meanings. Auto-fit so every
   set collapses to one column without a breakpoint each. */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-md);
  margin-top: var(--space-lg);
}
.cards-2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

.card {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
.card-k {
  display: block;
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-sm);
}
.card-h {
  display: block;
  font-size: var(--text-h3);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--text-primary);
  margin-bottom: var(--space-sm);
}
.card p { font-size: var(--text-small); color: var(--text-secondary); }
.card p + p { margin-top: var(--space-sm); }
.card .ev-tag { margin-top: var(--space-md); }
/* A tag can also open a card, labelling everything under it. */
.card .ev-tag:first-child { margin-top: 0; }
.card .ev-tag + p { margin-top: var(--space-md); }

/* Prose after one of these blocks needs the room a paragraph after a
   paragraph already gets. Without this the copy butts straight up against
   the thing it is explaining. */
.cards + p,
.chain-3 + p,
.notes + p,
.tree + p,
.table-wrap + p,
.rec + p,
.ev-tags + p { margin-top: var(--space-lg); }

/* The open state, for a card describing something not yet settled. */
.card.is-open { background: transparent; border-style: dashed; }


/* --- The chain ------------------------------------------------
   Three things in a line, with the relationship between them
   written on the connector rather than implied by proximity.
   Drawn with a border and a pseudo-element, so it costs nothing
   and it reflows to a vertical stack on a phone. */

.chain-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-lg);
  margin-top: var(--space-lg);
  counter-reset: chain;
}
.chain-3 > li {
  position: relative;
  counter-increment: chain;
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
/* The connector. An arrow between the cards, not inside them. */
.chain-3 > li + li::before {
  content: '\2192';
  position: absolute;
  left: calc(var(--space-lg) / -2 - 6px);
  top: var(--space-lg);
  color: var(--accent-primary);
  line-height: 1;
}
.chain-n {
  display: block;
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .1em;
  color: var(--accent-primary);
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-sm);
}
.chain-h {
  display: block;
  font-size: var(--text-h3);
  letter-spacing: var(--tracking-heading);
  margin-bottom: var(--space-sm);
}
.chain-3 p { font-size: var(--text-small); color: var(--text-secondary); }

@media (max-width: 700px) {
  .chain-3 { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, so the arrow turns to point down and moves to the top edge. */
  .chain-3 > li + li::before {
    content: '\2193';
    left: var(--space-lg);
    top: calc(var(--space-lg) / -2 - 8px);
  }
}


/* --- Tables ---------------------------------------------------
   Only for material that is genuinely tabular: an evaluation
   result, a contribution split. The wrapper scrolls rather than
   the page, so a narrow screen never pushes the whole document
   sideways. */

.table-wrap {
  overflow-x: auto;
  margin-top: var(--space-lg);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
}
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-small);
}
.table th,
.table td {
  text-align: left;
  padding: var(--space-md);
  border-top: 1px solid var(--border-default);
  vertical-align: top;
}
.table thead th {
  border-top: 0;
  font-weight: var(--weight-medium);
  font-stretch: 87.5%;
  letter-spacing: .04em;
  color: var(--text-primary);
  white-space: nowrap;
}
.table td { color: var(--text-secondary); }
.table .num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-primary);
}
.table tbody tr:last-child td { border-top-width: 1px; }
.table .total td { color: var(--text-primary); border-top-color: var(--border-strong); }

/* A limitation that belongs to the table above it, close enough that the
   number is never read without it. */
.table-limit {
  font-size: var(--text-small);
  color: var(--text-secondary);
  margin-top: var(--space-md);
  padding-left: var(--space-md);
  border-left: 2px solid var(--border-strong);
  max-width: var(--max-prose);
}


/* --- Rough notes and records ----------------------------------
   Raw input on one side of the workflow, a structured record on
   the other. Both are set in the site's one typeface. Condensed
   width does the job monospace would do elsewhere, which is the
   same trick .now uses in the hero. */

.notes {
  font-stretch: 87.5%;
  font-size: var(--text-small);
  color: var(--text-secondary);
  background: var(--bg-page);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.notes li { padding-left: var(--space-md); position: relative; }
.notes li::before {
  content: '';
  position: absolute;
  left: 0; top: .7em;
  width: 8px; height: 1px;
  background: var(--border-strong);
}

/* A generated document, rendered as a document rather than described as one. */
.rec {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
.rec-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm) var(--space-md);
  padding-bottom: var(--space-md);
  margin-bottom: var(--space-md);
  border-bottom: 1px solid var(--border-default);
}
/* Scoped to the header, and that is not tidiness. The title is a <p>, so a
   bare .rec-title loses to .cs-main p on specificity and every record title
   rendered in secondary grey at heading size, which reads as disabled. */
.rec-head .rec-title {
  font-size: var(--text-h3);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--text-primary);
}
/* The demonstration label lives in the panel header, which scrolls away
   before a long record does. These blocks are the ones that look like real
   documents, so each one says what it is on its own face. */
.rec-head .rec-flag {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent-primary);
  margin-bottom: var(--space-xs);
}
.rec-field { padding: var(--space-md) 0; border-top: 1px solid var(--border-default); }
/* Matched off the header rather than with :first-of-type. Both are divs, so
   :first-of-type selected the header and left the first field drawing a
   second hairline directly under the one the header had already drawn. */
.rec-head + .rec-field { border-top: 0; padding-top: 0; }
.rec-k {
  display: block;
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-xs);
}
.rec-v { font-size: var(--text-small); color: var(--text-secondary); }
.rec-v strong { color: var(--text-primary); font-weight: var(--weight-medium); }
.rec-list { display: flex; flex-direction: column; gap: var(--space-sm); }
.rec-list li {
  font-size: var(--text-small);
  color: var(--text-secondary);
  padding-left: var(--space-md);
  position: relative;
}
.rec-list li::before {
  content: '';
  position: absolute;
  left: 0; top: .7em;
  width: 8px; height: 1px;
  background: var(--border-strong);
}
.rec-list.is-numbered { counter-reset: rec; }
.rec-list.is-numbered li { padding-left: var(--space-lg); }
.rec-list.is-numbered li::before {
  counter-increment: rec;
  content: counter(rec);
  width: auto; height: auto;
  top: 0;
  background: none;
  color: var(--accent-primary);
  font-variant-numeric: tabular-nums;
}

/* Where a document has to live. A tree, built out of nested lists so a screen
   reader gets the nesting rather than a picture of it. */
.tree {
  font-stretch: 87.5%;
  font-size: var(--text-small);
  color: var(--text-secondary);
  background: var(--bg-page);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: var(--space-lg);
}
.tree ul {
  margin-left: var(--space-sm);
  padding-left: var(--space-md);
  border-left: 1px solid var(--border-default);
}
.tree li { padding: 3px 0; }
.tree li > span { display: inline-block; }
.tree .is-here { color: var(--text-primary); }


/* --- The fictional demonstration ------------------------------
   One component, three pages. The markup ships all six steps as
   an ordered list, so with JavaScript off the whole sequence
   reads top to bottom in order and nothing is lost. Section 8 of
   site.js upgrades it into a step-through.

   The label sits in the header, which stays visible in both
   modes and in every step, because a fictional record that can
   be scrolled past its own caption is a liability. */

.demo {
  margin-top: var(--space-2xl);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  background: var(--bg-surface);
  padding: var(--space-lg);
}
.demo-head { padding-bottom: var(--space-lg); border-bottom: 1px solid var(--border-default); }
/* Scoped, like .rec-flag and .lab-kind below. Each of these is a <p>, so a
   single-class rule loses to .cs-main p and the label renders in body grey.
   The border stayed coral and the words did not, which looked like a bug and
   was one. */
.demo-head .demo-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent-primary);
  background: var(--bg-accent-subtle);
  border: 1px solid var(--border-accent);
  border-radius: var(--radius-pill);
  padding: 4px 12px;
}
.demo-title {
  font-size: var(--text-h2);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  font-weight: var(--weight-regular);
  margin-top: var(--space-md);
}
.demo-lead {
  font-size: var(--text-small);
  color: var(--text-secondary);
  margin-top: var(--space-sm);
  max-width: var(--max-prose);
}

/* The steps. In the unenhanced state this is exactly what it looks like:
   an ordered list of six things, each with its own heading. */
.demo-steps { display: flex; flex-direction: column; }
.demo-step { padding-top: var(--space-xl); }
.demo-step[hidden] { display: none; }
.demo-step .demo-step-n {
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .1em;
  color: var(--accent-primary);
  font-variant-numeric: tabular-nums;
}
.demo-step-h {
  font-size: var(--text-h3);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  margin: var(--space-xs) 0 var(--space-md);
}
.demo-step p { font-size: var(--text-small); color: var(--text-secondary); max-width: var(--max-prose); }
.demo-step p + p { margin-top: var(--space-md); }
.demo-step .cards,
.demo-step .table-wrap,
.demo-step .notes,
.demo-step .rec,
.demo-step .tree { margin-top: var(--space-lg); }

/* The step tabs, built by the script. Wrapping, never scrolling, so 320px
   gets three short rows rather than a sideways drag. */
.demo-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}
.demo-tab {
  font: inherit;
  font-size: var(--text-small);
  font-stretch: 87.5%;
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-sm);
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  padding: 5px 14px;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.demo-tab:hover { color: var(--text-primary); border-color: var(--border-strong); }
.demo-tab-n { font-variant-numeric: tabular-nums; color: var(--accent-primary); }
.demo-tab[aria-selected="true"] {
  color: var(--text-on-accent);
  background: var(--accent-primary);
  border-color: var(--accent-primary);
}
.demo-tab[aria-selected="true"] .demo-tab-n { color: var(--text-on-accent); }

/* Enhanced only. The panel gets its own focus ring because it holds no
   controls of its own, so it has to be reachable to be scrollable. */
.demo.is-stepped .demo-step {
  padding-top: var(--space-lg);
  animation: demo-in var(--dur-base) var(--ease-out);
}
@keyframes demo-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

.demo-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-default);
}
.demo-btn {
  font: inherit;
  font-size: var(--text-small);
  font-stretch: 87.5%;
  color: var(--text-primary);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  padding: 7px 16px;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.demo-btn:hover:not(:disabled) { color: var(--accent-primary); border-color: var(--border-accent); }
.demo-btn:disabled { color: var(--text-secondary); border-color: var(--border-default); cursor: default; }
.demo-count {
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .04em;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}


/* --- The lab index -------------------------------------------- */

/* The three projects on the landing page. Not a grid of equal tiles: they
   are stacked rows, because their evidence differs and a grid of three
   identical cards is an argument that it does not. */
.lab-list { display: flex; flex-direction: column; margin-top: var(--space-lg); }
.lab-item { border-top: 1px solid var(--border-default); }
.lab-item:first-child { border-top: 0; }
.lab-link {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-sm) var(--space-lg);
  align-items: baseline;
  padding: var(--space-xl) 0;
}
.lab-link .lab-kind {
  grid-column: 1 / -1;
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary-accent);
}
.lab-title {
  font-size: clamp(22px, 2.6vw, 30px);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  display: inline;
  background-image: linear-gradient(var(--accent-primary), var(--accent-primary));
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--dur-base) var(--ease-out);
}
.lab-link:hover .lab-title,
.lab-link:focus-visible .lab-title { background-size: 100% 1px; }
.lab-hook { grid-column: 1 / -1; color: var(--text-secondary); max-width: 56ch; }
.lab-status { grid-column: 1 / -1; }
.lab-go { color: var(--accent-primary); font-size: var(--text-small); white-space: nowrap; }

@media (max-width: 700px) {
  .lab-link { grid-template-columns: minmax(0, 1fr); }
  .lab-go { display: none; }
}

/* The thumbnail on the home index. Three statuses, which is the one thing a
   reader needs to know before opening a collection: these are not three of
   the same thing. Built from markup rather than drawn, so it cannot go stale
   against the pages it describes. */
.lab-thumb {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.lab-thumb-k {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.lab-thumb-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-default);
}
.lab-thumb-n { font-size: var(--text-small); color: var(--text-primary); }
.lab-thumb-s {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-secondary-accent);
}


/* --- Small screens for all of the above ------------------------ */

@media (max-width: 560px) {
  .demo { padding: var(--space-md); }
  .card, .rec, .notes, .tree { padding: var(--space-md); }
  .table th, .table td { padding: var(--space-sm) var(--space-md); }
  .demo-nav { justify-content: space-between; }
  .demo-count { order: 3; width: 100%; text-align: center; }
}


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

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: start;
}
.about-copy { max-width: var(--max-prose); }
.about-copy p { color: var(--text-secondary); }
.about-copy p + p { margin-top: var(--space-lg); }

/* The mark and the story that explains it, as one unit. In the hero these
   sat apart and read as a stray logo next to an unrelated anecdote. */
.origin {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin: var(--space-xl) 0;
  padding: var(--space-lg) 0;
  border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-default);
}
.origin figcaption {
  font-size: var(--text-small);
  color: var(--text-secondary);
}
.origin em { color: var(--accent-primary); font-style: italic; }

/* --- About: the rewind --------------------------------------- */

/* The line after the hook. Small, so it reads as a beat rather than a heading. */
.rewind {
  margin-top: var(--space-xl);
  font-size: var(--text-small);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--accent-primary);
}

/* The timeline. A hairline spine with a marker per entry, drawn with borders
   and pseudo-elements rather than an image, so it costs nothing and scales. */
.tl { position: relative; margin-top: var(--space-xl); }
.tl-item {
  position: relative;
  padding: 0 0 var(--space-2xl) var(--space-xl);
  border-left: 1px solid var(--border-default);
  max-width: calc(var(--max-prose) + var(--space-xl));
}
.tl-item:last-child { border-left-color: transparent; padding-bottom: 0; }

/* The marker sits on the spine, not beside it. */
.tl-item::before {
  content: '';
  position: absolute;
  left: -4.5px; top: 6px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border-strong);
}
/* The two ends of the story get the accent. Everything between is neutral,
   so the eye reads start and now without counting the middle. */
.tl-first::before, .tl-now::before { background: var(--accent-primary); }

.tl-when {
  display: flex; align-items: center; gap: var(--space-sm);
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .04em;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.tl-what {
  font-size: var(--text-h3);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  margin: 6px 0 var(--space-sm);
}
.tl-body { color: var(--text-secondary); }

/* The live dot on Now, same signal as the hero widget. */
.tl-dot-live {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent-primary);
  animation: now-pulse 2.4s ease-in-out infinite;
}
.tl-now .tl-when { color: var(--accent-primary); }

/* Entries still waiting on her. Dashed, so they cannot ship by accident. */
.tl-todo { border-left-style: dashed; }
.tl-todo::before { background: transparent; box-shadow: inset 0 0 0 1px var(--border-strong); }
.tl-todo .tl-body em { color: var(--accent-primary); font-style: italic; }


/* The three-word spine, used on the About page. Each word paired with the
   question it stands for, so it reads as a method rather than as adjectives. */
.chain {
  display: flex; flex-direction: column;
  border-top: 1px solid var(--border-default);
  margin-top: var(--space-xl);
  max-width: var(--max-prose);
}
.chain li {
  padding: var(--space-lg) 0;
  border-bottom: 1px solid var(--border-default);
}
.chain-k {
  display: block;
  font-size: var(--text-h3);
  color: var(--accent-primary);
  letter-spacing: var(--tracking-heading);
}
.chain-v {
  display: block;
  font-size: var(--text-small);
  color: var(--text-secondary);
  margin-top: 4px;
}

/* The spine in the right-hand margin, not under the prose. It glosses the
   paragraphs beside it rather than introducing them, so it sits where a
   margin note sits.

   The prose column is the measure itself, not a fraction of the container,
   and the grid is packed to the start. A 1fr prose column stretched to the
   container edge while the text stayed capped at 50ch, which left about
   290px of dead space between the last word and the spine and read as two
   unrelated blocks. The spine belongs next to the text it is about.

   The chain column is fixed at 15rem: the longest key is "Clarifying" at
   20px and the longest question sets on one line at 14px, so the column
   never decides how wide the section is.

   The chain drops its own top margin here. It carries a border-top, and a
   40px gap above it inside a grid row put that rule below the first line of
   prose beside it. */
.chain-split {
  display: grid;
  grid-template-columns: minmax(0, var(--max-prose)) 15rem;
  justify-content: start;
  gap: var(--space-2xl);
  align-items: start;
}
.chain-split .chain { margin-top: 0; max-width: none; }
.chain-split .chain-prose { min-width: 0; }

/* Stacked below 900px, where the wrap padding drops and 15rem plus a 64px
   gap stops leaving the prose a readable measure. The spine lands back under
   the prose, which is where it was before any of this, so the two-column
   layout is a change above 900px only. */
@media (max-width: 900px) {
  .chain-split {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xl);
  }
}

/* The arc. Titles reset when you change country, so this measures growth
   by what she was trusted with instead. */
.arc { display: flex; flex-direction: column; }
.arc li {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--border-default);
  position: relative;
}
.arc li:last-child { border-bottom: 1px solid var(--border-default); }
.arc-when {
  font-size: var(--text-small);
  font-stretch: 75%;
  letter-spacing: .04em;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  padding-top: 3px;
}
.arc-what {
  font-size: var(--text-h3);
  letter-spacing: var(--tracking-heading);
  color: var(--text-primary);
}
.arc-note {
  grid-column: 2;
  font-size: var(--text-small);
  color: var(--text-secondary);
}
/* The most recent rung is the one to notice. */
.arc li:last-child .arc-what { color: var(--accent-primary); }


/* --- Nuggets -------------------------------------------------- */

.nuggets {
  display: grid;
  /* 320px floor lands on three columns at full width, so six nuggets
     sit as two even rows rather than four and an orphan pair.

     Wrapped in min() because a bare 320px floor is wider than the content
     box of a 320px screen, so the home page scrolled sideways by 24px at
     the narrowest supported width. min(320px, 100%) keeps the three-column
     behaviour everywhere it applied before and lets the track shrink below
     its floor only when the column itself is narrower than that. */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: var(--space-md);
}
/* Seven cards on a three column grid leaves the last one alone in a row with
   two thirds of it empty, which is the orphan the column floor above was
   chosen to avoid at six. When the last card is also the first in its row it
   takes the whole row instead, so it reads as a closing note rather than a
   gap. The selector holds at two columns as well, where seven also orphans. */
.nuggets li:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }

.nuggets li {
  background: var(--bg-surface);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-card);
  padding: var(--space-lg);
}
/* The drawings need room. They are illustrations, not icons: at 32px, which
   is what the brand guide asked for, only the Y reads and the other four are
   texture. Legibility starts around 56 and is comfortable at 64. */
.nug-icon {
  /* A fixed square with the drawing fitted inside it, not a fixed height. Each
     drawing has its own framing, so at a common height they came out 43 to 114
     wide and the cards lost their left rhythm. Bottom left so they all stand on
     one line and start on one edge. */
  height: 88px;
  width: 88px;
  object-fit: contain;
  object-position: left bottom;
  margin-bottom: var(--space-md);
}

.nug-k {
  display: block;
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-sm);
}
.nug-v { display: block; font-size: var(--text-small); }
.nug-v em { color: var(--accent-primary); font-style: italic; }

/* Placeholders look unfinished on purpose, so they cannot ship by accident. */
.nug-todo { border-style: dashed; background: transparent; }
.nug-todo .nug-v { color: var(--text-secondary); }


/* --- Contact and footer --------------------------------------- */

.section-contact .eyebrow { margin-bottom: var(--space-md); }
.section-contact .sec-intro { margin-top: var(--space-md); }
.contact-list {
  display: flex; flex-wrap: wrap; gap: var(--space-lg);
  margin-top: var(--space-xl);
  font-size: var(--text-body-lg);
}

.foot {
  border-top: 1px solid var(--border-default);
  padding: var(--space-xl) 0 var(--space-2xl);
  color: var(--text-secondary);
  font-size: var(--text-small);
}
.foot-in { display: flex; flex-wrap: wrap; gap: var(--space-md); justify-content: space-between; }


/* --- Scroll reveal -------------------------------------------- */

.rise {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.rise.in { opacity: 1; transform: none; }

/* Only script can add .in, so with script unavailable this rule was hiding
   every section on every page. The reveal is an enhancement and it has to
   fail open: no JavaScript means no animation, not no content.
   Caught while checking the AI lab demonstrations with JavaScript off, and
   it was never specific to them. */
@media (scripting: none) {
  .rise { opacity: 1; transform: none; transition: none; }
}


/* --- Responsive ------------------------------------------------ */

@media (max-width: 900px) {
  .wrap { padding-inline: var(--space-lg); }
  .nav-in { padding-inline: var(--space-lg); }
  .hero { padding-top: var(--space-2xl); }
  .hero .display { max-width: none; }
  /* Stacked, or the 300px column takes half a phone and the headline sets one
     word to a line. The drawing goes under the headline at a fraction of the
     width, still hard right, so it stays the same gesture as on desktop
     rather than becoming a centred spot illustration. */
  .hero-top { grid-template-columns: 1fr; gap: var(--space-lg); }
  .hero-art { width: 60%; margin-left: auto; }
  .hero-body { grid-template-columns: 1fr; gap: var(--space-xl); }
  /* The mark is a signature, not a hero image. Below the fold it should
     stay a signature. */
  .mark-lg { width: 104px; height: 99px; }
  .about-grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  /* Stacked, and smaller. A 260px drawing above the headline would push the
     h1 most of the way down a phone screen, and the headline is what the page
     is for. Hard right, matching the protea in the home hero. */
  .about-head-grid { grid-template-columns: 1fr; gap: var(--space-lg) 0; }
  .about-head-grid > div,
  .about-head-grid > .rewind,
  .about-portrait { grid-column: 1; grid-row: auto; }
  .about-portrait { width: 45%; }
  /* Stacked, but capped, so it stays a readout rather than becoming a banner. */
  .now { max-width: 340px; }
  .section { padding: var(--space-2xl) 0; }
  .sec-head { margin-bottom: var(--space-xl); }
}

/* The work index.

   1000 is not a device breakpoint, it is where a 17rem figure column stops
   leaving a readable measure beside the lead copy. 760 is where two cards
   side by side stop being cards. */
@media (max-width: 1000px) {
  .work-lead-copy { grid-template-columns: 1fr; gap: var(--space-lg); }
  .work-lead .metric { margin-top: var(--space-md); }

  .work-lab { grid-template-columns: minmax(0, 1fr) minmax(0, 18rem); row-gap: var(--space-lg); }
  .work-lab .metric { grid-column: 1 / -1; text-align: left; }

  .work-ledger .entry-link { grid-template-columns: 5rem minmax(0, 1fr) 12rem; }
}

@media (max-width: 760px) {
  .work-support { grid-template-columns: 1fr; gap: var(--space-2xl); }

  .work-lab { grid-template-columns: 1fr; gap: var(--space-md); }
  /* The thumbnail goes last on a phone. The figure is the faster read and
     it belongs directly under the title it describes. */
  .work-lab .lab-thumb { order: 3; }

  /* The ledger stacks artefact first, because on a phone the picture is the
     fastest thing to read, and the figure last so it stays under its own
     title rather than floating up against the row above.

     The proof block has to come apart to do it. Lifted whole it took the
     figure up with it, and "LAUNCHED Dec '23" landed above its own title,
     close enough to the row above to read as that project's number. So the
     wrapper goes display:contents and its two children take their own
     places. The DOM order is untouched and the row is one link, so nothing
     moves for a screen reader or a tab stop. */
  .work-ledger .entry-link {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }
  .work-ledger .proof { display: contents; }
  .work-ledger .plate { order: -1; aspect-ratio: 16 / 9; }
  .work-ledger .work-year { order: 0; }
  .work-ledger .metric {
    order: 2;
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    flex-wrap: wrap;
    margin-top: 0;
  }
  .work-ledger .metric-k,
  .work-ledger .metric-v,
  .work-ledger .metric-n { margin-top: 0; }

  .work-rest-head { display: block; }
}

@media (max-width: 560px) {
  .nav-links { gap: var(--space-md); }
  .contact-list { flex-direction: column; gap: var(--space-md); }

  /* The TLDR at phone width was 1177px tall against an 844px viewport, and the
     cause was measure, not word count. 40px of padding on both sides left
     259px for text, so 19px type set about 26 characters to the line, well
     under the 45 that makes a readable measure. 85 words then became 19 lines.
     Both steps are on-scale: padding to space-lg, type to body size. The card
     is a summary, and the reason it is set larger than body on a 566px column
     does not survive a 342px one, where the same distinction costs three lines
     a paragraph and buys nothing. */
  .tldr { padding: var(--space-lg); }
  .tldr p { font-size: var(--text-body); }

  /* Two figures side by side rather than stacked. The 180px minimum needs
     376px to fit two columns and had 259px, so the auto-fit grid collapsed to
     one and spent 183px displaying two numbers. Fixed at 1fr 1fr because at
     this width there are only ever the two. */
  .tldr-stats {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-md);
  }

  /* The lead artefact takes the card's mat at phone width.

     shot-paper insets by space-lg, which costs 50px of a 327px column once
     the border is counted. That is affordable on the case-study pages, where
     the artefact is usually one or two screens. It is not affordable here:
     the lead is now a four-up onboarding flow, and 277px put each screen at
     about 65px, which is a thumbnail of a thumbnail.

     space-sm rather than zero. Dropping the padding altogether would butt a
     white artefact straight against the border, which is the exact jump the
     mat exists to avoid. space-sm keeps the two steps and returns 32px, and
     it is the same value plate-paper used when this artefact sat in the card
     slot, so the phone layout matches the mat it had before the swap.

     Scoped to .work-lead on purpose. shot-paper is used on eight pages and
     none of the others have this problem. */
  .work-lead .shot-paper { padding: var(--space-sm); }
}

/* The nav is the only thing on the site that has ever overflowed horizontally,
   and it did so at 320px on every page. The cause is not the breakpoint, it is
   that the row needs a fixed 373.7px: 24 padding, 105.8 of brand, a 24 gap, and
   195.9 of links. Nothing in it shrinks, so the requirement is the same at
   every width and only the viewport changes underneath it.

   Three regions, measured rather than assumed. Below about 350 the row cannot
   fit at all and the page overflows: 320 gave 29px of it, because scrollWidth
   ignores the container's trailing padding, so the real floor is the 349.7 the
   row needs without it. Between about 350 and 374 there is no overflow and the
   shortfall comes out of the padding instead: at 360, a common Android width,
   the right padding measured 10.3px against its designed 24. From 375 up it
   fits properly, padding intact, but 375 clears by only 1.3px.

   The wordmark goes and the mark stays. It is worth 79.8px, the 68.8 it
   occupies plus the 11px gap beside it, which takes the row to 293.9 and
   leaves the padding, the gaps and the type untouched at their designed
   values. The alternative was tightening all three at once, and the arithmetic
   there is not close: gaps alone recover 30px against the 54 needed to honour
   the right padding, and getting the rest meant 8px between touch targets and
   type off the ramp.

   Nothing is lost to a screen reader. Every brand link already carries
   aria-label="Tenielle, home", which is the accessible name whether or not the
   span renders, so this is a purely visual reduction to the mark.

   380 rather than 350, because the scrollbar is the last symptom rather than
   the first, and rather than 375, because 1.3px of clearance is not a margin
   worth trusting across platforms whose font metrics differ. 380 leaves 6.3px
   above the requirement. 390 and up keep the wordmark, so every current iPhone
   does. */
@media (max-width: 380px) {
  .wordmark { display: none; }
}


/* --- Reduced motion --------------------------------------------
   The tokens zero the durations, but SMIL cannot read a CSS
   variable. So the crooked path is swapped for a straight one. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* Belt and braces. JS already refuses to animate, this stops it regardless. */
  .mark-lg.is-animating #crooked { display: none; }
  .mark-lg.is-animating #straight { display: inline; }
  .rise { opacity: 1; transform: none; transition: none; }
  .shot img { transition: none; }
  /* The pulse is the only thing on the site that loops, so it matters most
     here. The dot stays, the breathing stops. */
  .now-dot { animation: none; }
}


/* --- The CV ----------------------------------------------------
   One document, two renderings. On screen it is a page of the site,
   dark, in the design system. On paper it is ONE A4 page, black on
   white, single column. Everything below the divider marked "in
   print" only exists for the second one.

   It is HTML rather than a PDF for one reason: the old CV had no
   source file anywhere, so its dead link to the Framer site could
   not be fixed without rebuilding it from scratch. Here, that is
   one line. */

.cv-wrap { max-width: 860px; padding-block: var(--space-2xl) var(--space-3xl); }

.cv-head { padding-bottom: var(--space-xl); border-bottom: 1px solid var(--border-default); }
.cv-name {
  font-size: clamp(38px, 6vw, 62px);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin-top: var(--space-lg);
}
.cv-role {
  margin-top: var(--space-sm);
  font-size: var(--text-body-lg);
  color: var(--text-secondary);
}
.cv-contact {
  display: flex; flex-wrap: wrap; gap: var(--space-sm) var(--space-lg);
  margin-top: var(--space-lg);
  font-size: var(--text-small);
  color: var(--text-secondary);
}

.cv-section { padding-top: var(--space-2xl); }
/* The section labels borrow the eyebrow, so the CV reads as the same
   voice as the rest of the site rather than as an attachment. */
.cv-h2 {
  font-size: var(--text-eyebrow);
  font-stretch: 87.5%;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-lg);
}

.cv-lede { font-size: var(--text-body-lg); color: var(--text-primary); max-width: 62ch; }

/* Three labelled lines. The label is the width axis doing the work bold
   normally would, same as the years and the eyebrows elsewhere. */
.cv-skills { display: flex; flex-direction: column; gap: var(--space-sm); }
.cv-skills li { color: var(--text-secondary); max-width: 78ch; }
.cv-skill-k {
  display: inline-block;
  min-width: 11ch;
  font-stretch: 87.5%;
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}

.cv-roles { display: flex; flex-direction: column; gap: var(--space-xl); }
.cv-role-item { padding-top: var(--space-xl); border-top: 1px solid var(--border-default); }
.cv-role-item:first-child { padding-top: 0; border-top: 0; }

.cv-role-head {
  display: flex; flex-wrap: wrap; gap: var(--space-sm) var(--space-lg);
  align-items: baseline; justify-content: space-between;
}
.cv-employer { font-size: var(--text-h3); letter-spacing: var(--tracking-heading); }
.cv-place {
  font-size: var(--text-small);
  font-stretch: 87.5%;
  color: var(--text-secondary);
  font-weight: var(--weight-regular);
  margin-left: var(--space-sm);
}
.cv-dates {
  font-size: var(--text-small);
  font-stretch: 75%;                    /* condensed, same as the years elsewhere */
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  white-space: nowrap;
}
.cv-title { margin-top: var(--space-xs); color: var(--text-primary); font-weight: var(--weight-medium); }

.cv-points { margin-top: var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); }
.cv-points li {
  padding-left: var(--space-lg);
  color: var(--text-secondary);
  max-width: 78ch;
}
/* A rule, not a bullet. The same hairline that separates the roles,
   shrunk down, so the page has one mark in it instead of two.

   The marker hangs by negative margin rather than by `position:
   absolute`, and this is load-bearing. Absolute positioning makes
   every bullet a positioned element, and positioned elements paint
   in a later phase than in-flow content, so Chrome wrote all the
   employer headings into the PDF first and all the bullets after,
   in one block. Extracted, the CV read as three job titles followed
   by a wall of achievements belonging to nobody, which is precisely
   what an applicant tracking system parses. Verified by pulling the
   text back out of the generated PDF; do not "tidy" this back to
   absolute without doing the same. */
.cv-points li::before {
  content: "";
  display: inline-block;
  width: 10px; height: 1px;
  margin-left: calc(var(--space-lg) * -1);
  margin-right: calc(var(--space-lg) - 10px);
  vertical-align: 0.34em;
  background: var(--border-strong);
}
.cv-points strong { color: var(--text-primary); font-weight: var(--weight-medium); }

.cv-earlier-line { padding-left: 0; margin-top: var(--space-md); color: var(--text-secondary); max-width: 78ch; }

.cv-plain { color: var(--text-secondary); max-width: 78ch; }
.cv-plain-list { display: flex; flex-direction: column; gap: var(--space-sm); color: var(--text-secondary); }
.cv-plain-list strong { color: var(--text-primary); font-weight: var(--weight-medium); }

.cv-actions {
  margin-top: var(--space-3xl); padding-top: var(--space-xl);
  border-top: 1px solid var(--border-default);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md) var(--space-lg);
}
.cv-print {
  font: inherit; font-size: var(--text-small);
  padding: var(--space-sm) var(--space-lg);
  color: var(--text-on-accent); background: var(--accent-primary);
  border: 0; border-radius: var(--radius-pill); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.cv-print:hover { background: var(--accent-primary-hover); }
/* .cv-actions-note is gone. It described the PDF ("One A4 page, black on
   white") next to the link that produces it, which is a caption for something
   the reader has not opened yet. A stale build of it claimed two pages for
   months. Deleted rather than corrected. */

@media (max-width: 560px) {
  .cv-role-head { flex-direction: column; gap: var(--space-xs); }
  .cv-contact { flex-direction: column; gap: var(--space-sm); }
}


/* --- The CV in print -------------------------------------------
   Black on white, single column, ONE A4 page, which
   tools/build-cv-pdf.mjs asserts on every build. The design system's
   dark surfaces are a screen decision and have no business on paper:
   they cost ink, and a CV printed dark is a CV nobody prints.

   Sizes here are in pt on purpose. px in print is a guess about DPI,
   pt is not. */

@page { size: A4; margin: 13mm 14mm; }

@media print {

  /* Anything that is navigation, chrome or an invitation to click. */
  .nav, .foot, .skip, .cv-screen-only, .intro { display: none !important; }

  html, body {
    background: #fff; color: #16181A;
    font-size: 9.1pt; line-height: 1.28;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }

  .wrap, .cv-wrap { max-width: none; padding: 0; margin: 0; }
  .cv-wrap { padding-block: 0; }

  .cv-head { padding-bottom: 5pt; border-bottom: 0.5pt solid #B8BDC2; }
  .cv-name { font-size: 18.5pt; margin-top: 0; line-height: 1.08; }
  .cv-role { font-size: 9.4pt; margin-top: 1pt; color: #3A4147; }
  .cv-contact {
    margin-top: 4pt; font-size: 8.4pt; color: #3A4147;
    gap: 1pt 11pt;
  }

  .cv-section { padding-top: 5pt; }

  /* The tracking override is not a taste call, it is a parsing one. The eyebrow
     token is 0.16em, which is right on screen at 12px and wrong at 7pt: Chrome
     breaks a widely tracked line into separate glyph runs, and every PDF text
     extractor then reads the gaps as spaces. The headings came out of the PDF
     as "E X P ER I E N C E" and "S U M M ARY". Applicant tracking systems find
     the sections of a CV by matching those words, so tracking that reads as
     considered on screen was quietly making the document unparseable. At 0.06em
     it still reads as an eyebrow and extracts as "EXPERIENCE".

     Screen is untouched. The locked 0.16em eyebrow is a screen decision. */
  .cv-h2 {
    font-size: 7pt; margin-bottom: 3pt; color: #5A6169;
    letter-spacing: 0.06em;
  }

  .cv-lede { font-size: 9.1pt; color: #16181A; max-width: none; }

  .cv-roles { gap: 5pt; }
  .cv-role-item { padding-top: 5pt; border-top: 0.5pt solid #D4D8DB; }
  /* A role split across the page break is the one thing that makes a
     printed CV look broken. Nothing else here needs protecting. */
  .cv-role-item { break-inside: avoid; page-break-inside: avoid; }

  .cv-employer { font-size: 10pt; }
  .cv-place { font-size: 8.2pt; color: #5A6169; }
  .cv-dates { font-size: 8.4pt; color: #3A4147; }
  .cv-title { margin-top: 0; font-size: 9pt; color: #16181A; }

  .cv-points { margin-top: 2pt; gap: 1.5pt; }
  .cv-points li { padding-left: 11pt; color: #16181A; max-width: none; }
  .cv-points li::before {
    width: 6pt;
    margin-left: -11pt; margin-right: 5pt;
    background: #8A9096;
  }
  .cv-points strong, .cv-skills strong { color: #16181A; font-weight: 600; }

  .cv-earlier-line, .cv-plain { color: #16181A; max-width: none; }
  .cv-earlier-line strong, .cv-plain strong { color: #16181A; font-weight: 600; }

  .cv-skills { gap: 1.5pt; }
  .cv-skills li { color: #16181A; max-width: none; }
  /* The aligned label column costs 13ch on every line here, which is a
     whole wrapped line across the block. On paper it goes inline. */
  .cv-skill-k { min-width: 0; margin-right: 3pt; color: #16181A; font-weight: 600; }

  /* Links keep their address rather than their colour: on paper a coral
     word with no URL under it is a dead end. The href is already written
     out as the link text in the markup, so nothing needs appending. */
  a, .inline-link { color: #16181A; border-bottom: 0; text-decoration: none; }

  /* The reveal-on-scroll class is never used on this page, but site.js is
     shared. If it ever lands here, it must not print blank. */
  .rise { opacity: 1 !important; transform: none !important; }
}
