/* ============================================================
   FONTS - self-hosted, latin subset, variable
   ------------------------------------------------------------
   Both faces are SIL OFL. They were served from fonts.googleapis
   .com, which put two third-party origins on the critical path -
   and cost more than it looks, because the preloader gate waits
   on document.fonts.ready before it will lift the curtain.

   These are the VARIABLE cuts (wght 400-700), not the three
   static weights that were loaded before. That is what makes
   font-weight:440 and :420 in research.html / company.html mean
   what they say; against static 500/600/700 they silently
   snapped to 500.

   Regenerate with span-ai/scripts/optimize-images.sh's sibling
   note in README - these files are committed, not built.
   ============================================================ */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/familjen-grotesk-latin-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/instrument-sans-latin-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   Span AI - shared design system
   Monochrome editorial. Black, white, and a neutral grey ramp -
   no hue anywhere. Reads as a printed medical journal: hairline
   rules, plate-style figures, tabular data, generous margins.
   ============================================================ */

:root {
  /* Surfaces. Warm paper, not stark white: #000 on #FFF is the harshest
     pair available and reads cold and default. The page should feel like
     stock. Hue is deliberate here and is the ONLY place it appears in the
     interface - everything else stays neutral. */
  --bg:        #FAF7F1;   /* warm paper */
  --bg-alt:    #F5F1E9;   /* alternating section wash */
  --bg-tint:   #EFEAE0;   /* quiet block */
  --bg-deep:   #191817;   /* inverted section / footer */

  /* ink - soft black, never #000 */
  --ink:   #1A1918;       /* headings, strong text */
  --ink-2: #3D3B38;       /* body */
  /* Was #6F6B64, which measures 4.96 / 4.70 / 4.42:1 on paper / bg-alt /
     bg-tint. The third of those is under the 4.5 AA floor, and --ink-3 is the
     caption and label colour - so every caption inside a .bg-tint band was
     shipping a knowingly-failing ratio on a site whose whole pitch is rigour.
     #6D6962 is the value --ink-dim already used for exactly this reason
     (5.10 / 4.85 / 4.55:1); the two are now the same colour and --ink-dim is
     kept only so .trail's intent stays readable. Two percent darker: no
     visible change, one fewer contract broken. */
  --ink-3: #6D6962;
  --ink-dim: #6D6962;    /* trailing clause - see .trail */       /* captions, labels (AA on the paper) */
  /* Was #A9A49B, at 2.32 / 2.20 / 2.07:1. "Decorative only" was not true in
     practice - .pill-n sets it at 2.4-3.4rem and .mega-n and the form
     placeholders took it too, all of which are text. Raised to clear the 3:1
     large-text floor on all three light grounds (3.43 / 3.26 / 3.06:1); the
     three small-text users are moved to --ink-3 below rather than being left
     to rely on this. */
  --ink-4: #8A857C;       /* faint - large text and marks only, never body */

  /* lines */
  --line:   #E2DCD1;
  --line-2: #CDC5B8;
  --line-3: #1A1918;      /* emphatic rule */

  /* "accent" is simply ink. Kept as a token so intent stays readable
     and nothing has to be renamed if a single hue ever returns. */
  /* Violet, at the hue sampled from the MYDNA reference (249deg). --accent is
     that hue darkened until it clears AA as text on paper, bg-alt AND bg-tint
     (5.29 / 5.02 / 4.72:1). It measures only 3.13:1 on the inverted band, so
     that ground gets its own lighter value rather than a token that quietly
     fails in one place. --accent-tint is the pale fill sampled straight from
     the reference and is never used as text. */
  --accent:         #6651D6;
  --accent-on-deep: #998BE4;
  --accent-tint:    #C8C0F4;

  /* inverted surfaces */
  --on-deep:    #FAF7F1;
  --on-deep-2:  #B5AFA5;
  /* Was #7A756D: 3.88:1 on --bg-deep, under AA, and it is the colour of
     .bg-deep .muted - body-adjacent text on the inverted band. #8A857C
     measures 4.84:1 there. */
  --on-deep-3:  #8A857C;

  /* Type. Display is a grotesque at 600, not a serif - the serif read
     "literary magazine"; this reads modern bio. */
  --display: 'Familjen Grotesk', 'Instrument Sans', -apple-system, sans-serif;
  --sans:    'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --maxw: 1180px;
  /* Rounded, not square. The plates used to be hard-cornered on the argument
     that print figures are - but every site this one is measured against
     (Formation Bio, Triomics, Noetik) rounds its panels, and against a warm
     paper ground the softer corner is what reads as considered rather than
     unfinished. One scale, used everywhere, so nothing drifts:
     sm for controls, base for cards and panels, lg for photographs. */
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 20px;
  /* Started at clamp(76px, 9.5vw, 148px) - 274px between two full sections at
     1440px. Called out as too much three times, each cut too timid, so this is
     the decisive one: 167px at 1440, 121px at 1024, roughly 39% off the
     original. The other vertical tokens come down with it, because trimming
     section padding alone just moves the slack into the gaps between rows. */
  --pad-section: clamp(52px, 5.8vw, 92px);

  /* Motion scale. Four steps, because the site had thirty-four durations
     picked one at a time - 0.18 through 1.1 in CSS, 0.28 through 1.5 in JS -
     and no two components agreed. The reference has effectively one: every
     motion event in it runs 0.70-0.97s, and its easing lands 87% of the change
     in the first quarter of that. --ease-out already matches at 83%, so the
     curve was right and only the timing was arbitrary.

     tap    direct feedback, wants to feel instant
     ui     a component changing state under the pointer
     move   something travelling a visible distance
     enter  a reveal, and the reference's own 0.8s */
  /* Type scale. The site had 36 distinct font sizes, and in the 10-19px range
     they were crowded to the point of meaninglessness - seven values between
     12.9 and 14.4px, five between 15.0 and 16.1. Nobody can see a 0.4px
     difference; it just made the file impossible to reason about, the same way
     34 durations did.

     Eight steps at roughly a 1.125 ratio off the 17px body. Every fixed size
     snaps to the nearest one and no value moves more than 1px, so this is a
     tidy-up rather than a redesign. The 13 responsive clamps are untouched -
     they scale with the viewport by design and do not belong on a fixed scale. */
  --fs-3xs:  0.62rem;
  --fs-2xs:  0.70rem;
  --fs-xs:   0.79rem;
  --fs-sm:   0.89rem;
  --fs-md:   1.00rem;
  --fs-lg:   1.12rem;
  --fs-xl:   1.35rem;
  --fs-2xl:  1.50rem;

  --t-tap:   0.18s;
  --t-ui:    0.32s;
  --t-move:  0.55s;
  --t-enter: 0.85s;
  --ease: cubic-bezier(.22,.61,.36,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
}

/* ---- reset / base ---- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: var(--fs-md);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
/* Images are the one place colour lives. The interface - type, rules, chrome,
   buttons, inline figures - stays strictly monochrome, so photographs and the
   methods-paper plates read as inset colour against a black-and-white page.
   Do NOT desaturate these: an earlier build did, and it flattened the paper
   figures' colour-coded legends into unreadable grey. */
img, video { max-width: 100%; height: auto; display: block; }
figure { margin: 0; }   /* reset here, NOT on .hero-figure - a later
                           margin shorthand there silently clobbers the
                           .bleed-* / .overlap-* utilities */
a { color: var(--ink); text-decoration: none; }
::selection { background: var(--ink); color: var(--bg); }

/* Print-stock grain. Fixed so it reads as paper the page moves under,
   not as noise attached to the content. Decorative only. */
body::after {
  content: "";
  position: fixed; inset: 0; z-index: 9999;
  pointer-events: none;
  opacity: .028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
}
@media (prefers-reduced-motion: reduce) { body::after { display: none; } }

/* ---- typography ---- */
h1, h2, h3 { color: var(--ink); font-weight: 400; margin: 0; }
/* Display type is deliberately modest. An 80px headline makes the page
   about the sentence; a 44px one at 600 leaves the visual weight to the
   photograph, which is the whole point of the layout. */
.display {
  font-family: var(--display);
  font-size: clamp(2rem, 3.5vw, 2.85rem);
  line-height: 1.1;
  letter-spacing: -0.022em;
  font-weight: 600;
  color: var(--ink);
}
h2, .h2 {
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1.16;
  letter-spacing: -0.018em;
  font-weight: 600;
}
h3, .h3 {
  font-family: var(--sans);
  font-size: var(--fs-lg);
  line-height: 1.32;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
}
.eyebrow {
  font-family: var(--sans);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
}
.eyebrow-rule {
  display: flex; align-items: center; gap: 14px;
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink);
}
.eyebrow-rule::after { content: ""; flex: 1; height: 1px; background: var(--line-2); }
.lead {
  font-size: clamp(1.14rem, 1.55vw, 1.34rem);
  line-height: 1.55;
  color: var(--ink-2);
}
.measure { max-width: 62ch; }
.measure-narrow { max-width: 52ch; }
.muted { color: var(--ink-3); }
.ink { color: var(--ink); }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }

/* editorial furniture */
/* These three replace long inline style attributes repeated across
   company.html and research.html. The 420/440 weights are deliberate and
   only mean anything because the faces are variable cuts. */
.quote-display {
  font-family: var(--display); font-weight: 420; margin: 0;
  font-size: clamp(1.6rem, 3.2vw, 2.5rem); line-height: 1.25;
  letter-spacing: -0.015em; color: var(--ink);
}
.finding {
  font-family: var(--display); font-weight: 440;
  font-size: clamp(1.4rem, 2.4vw, 1.9rem); line-height: 1.2; color: var(--ink);
}
.pub-title {
  font-family: var(--display); font-weight: 440;
  font-size: clamp(1.25rem, 2vw, 1.5rem); line-height: 1.24; color: var(--ink);
}
.caption { font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-3); }
.rule { height: 1px; background: var(--line); border: 0; margin: 0; }
.rule-strong { height: 1px; background: var(--line-3); border: 0; margin: 0; }

/* ---- layout ---- */
.container { max-width: var(--maxw); margin: 0 auto; padding: 0 28px; }
.container-narrow { max-width: 860px; margin: 0 auto; padding: 0 28px; }
.section { padding: var(--pad-section) 0; }
.section-sm { padding: clamp(34px, 4vw, 60px) 0; }

/* A full .section following a .section-sm was opening a lopsided void. The
   small section closes with ~84px and the next opens with ~137px, so the
   divider-top rule between them sat nearer the block above than the one it
   belongs to, with 221px of paper around it at 1440px. A full-bleed image band
   is already an emphatic break; the section after it does not need to restate
   that at full strength.

   Matching the small section's scale makes the air symmetric about the rule
   and takes the gap to 168px. Occurs seven times across five pages, so this is
   a rule rather than a patch on one spot. */
.section-sm + .section { padding-top: clamp(34px, 4vw, 60px); }
.bg-alt  { background: var(--bg-alt); }
.bg-tint { background: var(--bg-tint); }
.bg-deep { background: var(--bg-deep); color: var(--on-deep-2); }
.bg-deep h1, .bg-deep h2, .bg-deep h3, .bg-deep .display, .bg-deep .ink { color: var(--on-deep); }
.bg-deep .eyebrow, .bg-deep .eyebrow-rule { color: var(--on-deep); }
.bg-deep .eyebrow-rule::after { background: var(--on-deep-3); }
.bg-deep .muted { color: var(--on-deep-3); }
.bg-deep a { color: var(--on-deep); }
/* .lead and .caption set their own colour from the light-background ramp, so
   inside an inverted section they kept it: .lead's --ink-2 (#3D3A36) on
   #191817 measures 1.57:1, which is not dim, it is invisible. The lead is
   the most important sentence in the section it opens, so on dark it takes
   the brightest ink (16.6:1) rather than the body tone. */
.bg-deep .lead { color: var(--on-deep); }
.bg-deep .caption { color: var(--on-deep-2); }
.divider-top { border-top: 1px solid var(--line); }

/* The fixed-ratio grid classes that used to live here - .grid, .cols-2,
   .cols-3, .cols-4, .grid-split (0.95fr 1.05fr) and .grid-lede (1fr 1.15fr) -
   are gone. Every section is now placed on .g12 (see ASYMMETRY below), and
   these had zero remaining uses in the markup. They were also the reason
   every section resolved to halves or equal thirds: a component that ships
   its own ratio cannot be varied, only repeated. */

/* ============================================================
   MEGA MENU
   Panels live inside <header> so the sticky context and the scrim stack
   correctly. They ship [hidden]; JS is the only thing that reveals one,
   so with no JS the triggers are inert buttons and every destination is
   still reachable from the page bodies.
   ============================================================ */
/* static, deliberately. The mega panels now live INSIDE their .nav-item so
   that DOM order, visual order and tab order agree - but they still have to
   span the whole header, so the containing block they resolve against must
   remain .site-header. A relative .nav-item would shrink them to the width of
   one trigger. */
.nav-item { position: static; display: flex; }
.nav-trigger {
  appearance: none; background: none; border: 0; cursor: pointer;
  font-family: var(--sans); font-size: var(--fs-sm); font-weight: 500;
  color: var(--ink-2); padding: 4px 0; display: inline-flex;
  align-items: center; gap: 6px; position: relative;
}
.nav-trigger::after {
  content: ""; position: absolute; left: 0; bottom: -2px;
  width: 100%; height: 1px; background: var(--ink);
  transform: scaleX(0); transform-origin: right;
  transition: transform var(--t-ui) var(--ease-out);
}
.nav-trigger:hover { color: var(--ink); }
.nav-trigger:hover::after { transform: scaleX(1); transform-origin: left; }
.nav-trigger[aria-expanded="true"] { color: var(--ink); }
.nav-trigger[aria-expanded="true"]::after { transform: scaleX(1); }
.nav-chev { transition: transform var(--t-ui) var(--ease-out); }
.nav-trigger[aria-expanded="true"] .nav-chev { transform: rotate(180deg); }

.mega {
  position: absolute; left: 0; right: 0; top: 100%;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  box-shadow: 0 24px 48px -32px rgba(26,25,24,0.28);
  z-index: 55;
  padding: clamp(30px, 3.4vw, 52px) 0 clamp(34px, 3.8vw, 56px);
}
.mega[hidden] { display: none; }
.mega-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.62fr) minmax(0, 1.38fr);   /* asymmetric */
  gap: clamp(28px, 4vw, 72px);
  align-items: start;
}
.mega-lede .eyebrow { display: block; margin-bottom: 12px; }
.mega-blurb {
  font-family: var(--display); font-weight: 500;
  font-size: clamp(1.1rem, 1.5vw, 1.35rem); line-height: 1.28;
  letter-spacing: -0.012em; color: var(--ink); margin: 0 0 16px; max-width: 26ch;
}
.mega-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px;
  background: var(--line); border: 1px solid var(--line); }

/* The grid paints --line and each card paints paper on top of it, which is
   what turns the 1px gaps into hairlines. It also means an UNOCCUPIED track
   has nothing painted over it and renders as a solid #E2DCD1 rectangle. The
   science and company panels carry three cards, so both were showing a grey
   block where the fourth would go. An odd last card takes the whole row
   instead. */
.mega-card:last-child:nth-child(odd) { grid-column: 1 / -1; }

.mega-card {
  display: block; background: var(--bg); padding: 22px;
  color: inherit;
  transition: background var(--t-ui) var(--ease);
}

/* Two lines reserved, so both cards in a row are the same height whatever
   their copy does. Row height is set by the taller card, so without this a
   one-line description leaves its card carrying a line of dead space its
   neighbour does not - which is what made the panel look unevenly padded.
   3em is exactly two lines at this element's own 1.5 line-height. Scoped to
   the two-column layout: below 980px the cards stack one per row, so nothing
   needs matching and the reserved line would only cost drawer height. */
@media (min-width: 981px) { .mega-d { min-height: 3em; } }

/* Nudge, not just tint: the menu is the densest set of links on the site and
   colour alone does not tell you which row the pointer is on. The nudge moves
   the CONTENT rather than growing padding-left - padding is a layout
   property, so the old version reflowed and re-wrapped the card's text on
   every frame of every hover, and it made the resting box asymmetric. */
.mega-card > * { transition: translate var(--t-ui) var(--ease-out); }
.mega-card:hover { background: var(--bg-alt); }
.mega-card:hover > * { translate: 6px 0; }
.mega-card:hover .mega-t { color: var(--ink); }
.mega-n {
  display: block; font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.2em;
  /* --ink-4 is a large-text colour; this is 0.70rem. */
  color: var(--ink-3); margin-bottom: 10px; font-variant-numeric: tabular-nums;
}
.mega-t { display: block; font-weight: 600; color: var(--ink); margin-bottom: 5px; }
.mega-d { display: block; font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-3); }

.mega-scrim {
  position: fixed; inset: 0; top: 0; z-index: 40;
  background: rgba(26,25,24,0.24); backdrop-filter: blur(1.5px);
}
.mega-scrim[hidden] { display: none; }

@media (max-width: 980px) { .mega-cards { grid-template-columns: 1fr; } }
@media (max-width: 860px) {
  /* In the mobile drawer the panels become inline accordions. */
  .nav-item { display: block; width: 100%; }
  .nav-trigger { width: 100%; justify-content: space-between; padding: 10px 0; font-size: var(--fs-sm); }
  .mega { position: static; box-shadow: none; border-bottom: 0; padding: 0 0 14px; }
  .mega-grid { grid-template-columns: 1fr; gap: 14px; }
  /* The lede used to be hidden outright here, which took the only direct
     route to science/research/company off the mobile nav - the trigger is a
     button, so without this the section's own page is reachable only through
     a card's #anchor. Keep the link, drop the prose it sat under. */
  .mega-lede .eyebrow, .mega-blurb { display: none; }
  .mega-lede .link-arrow { padding: 2px 0 10px; }
  .mega-scrim { display: none !important; }
}

/* ---- announcement bar ---- */
.announce {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  padding: 9px 28px;
  background: var(--bg-deep); color: var(--on-deep);
  font-size: var(--fs-xs); letter-spacing: 0.005em;
  position: relative; z-index: 60;
}
.announce-tag {
  font-family: var(--sans); font-size: var(--fs-3xs); font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase;
  border: 1px solid rgba(250,247,241,0.34); border-radius: 999px;
  padding: 3px 10px; white-space: nowrap;
}
.announce-text { color: var(--on-deep); }
.announce-go { transition: transform var(--t-ui) var(--ease-out); display: inline-block; }
.announce:hover .announce-go { transform: translateX(5px); }
/* 28px, matching .container. The chrome and the page body have to share one
   left edge or the page has no margin - this dropped to 18 at 720 while the
   nav dropped to 18 at 860 and .container never dropped at all, so between
   720 and 860 there were three different left edges stacked down the screen.
   The gutter is not what is short of room on a phone; the text inside it is,
   and the line-clamp below is what handles that. */
@media (max-width: 720px) {
  .announce { font-size: var(--fs-xs); gap: 10px; padding: 8px 28px; }
  .announce-text { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
}

/* ---- header / nav ---- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(250,247,241,0.88);   /* warm paper, not the old white */
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.nav {
  max-width: var(--maxw); margin: 0 auto;
  padding: 15px 28px;
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
}
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); }
.brand svg { display: block; }
.brand-name { font-size: var(--fs-md); font-weight: 600; letter-spacing: -0.01em; }
.brand-name .sub { color: var(--ink-3); font-weight: 500; }
.nav-links { display: flex; align-items: center; gap: 30px; }
.nav-links a {
  color: var(--ink-2); font-size: var(--fs-sm); font-weight: 500;
  padding: 4px 0; position: relative;
}
.nav-links a::after {
  content: ""; position: absolute; left: 0; bottom: -2px;
  width: 100%; height: 1px; background: var(--ink);
  transform: scaleX(0); transform-origin: right;
  transition: transform var(--t-ui) var(--ease-out);
}
.nav-links a:hover { color: var(--ink); }
.nav-links a:hover::after { transform: scaleX(1); transform-origin: left; }
/* You-are-here. This replaces `.nav-links a.active`, which was styling a
   class that no page and no script ever set - so Team and Contact marked
   themselves with aria-current for screen readers and showed sighted users
   nothing at all.

   Two channels, not one. Colour alone moved --ink-2 to --ink, i.e. 10.44:1 to
   16.42:1 - a shade of near-black against another shade of near-black, and a
   colour-only cue besides. The rule carries it, at 2px so that "this is where
   you are" stays distinguishable from the 1px "this menu is open". */
.nav-links a[aria-current="page"],
.nav-trigger[aria-current="page"] { color: var(--ink); }
.nav-links a[aria-current="page"]::after,
.nav-trigger[aria-current="page"]::after { transform: scaleX(1); height: 2px; }
.nav-cta { display: flex; align-items: center; gap: 16px; }

/* The nav was the only interactive region on the site with no focus style, so
   keyboard focus there fell back to the UA ring and looked foreign next to
   every button, card and chip. Same treatment as the rest. mega-card insets
   its ring because the cards are separated by a 1px gap - an outset ring on
   one would sit on top of its neighbour. */
/* Skip link. There are eleven focusable controls in the chrome before the
   content starts, and a keyboard user was tabbing all of them on every page.
   Moved off-screen with a transform rather than display:none, because a
   display:none control cannot be focused and so can never come back. */
.skiplink {
  position: fixed; top: 10px; left: 10px; z-index: 100;
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--ink); border-radius: var(--radius-sm);
  padding: 10px 16px; font-family: var(--sans); font-size: var(--fs-xs); font-weight: 600;
  transform: translateY(-250%);
  transition: transform var(--t-tap) var(--ease-out);
}
.skiplink:focus { transform: translateY(0); outline: 2px solid var(--ink); outline-offset: 3px; }
#main:focus { outline: none; }

.brand:focus-visible,
.nav-links a:focus-visible,
.nav-trigger:focus-visible,
.nav-toggle:focus-visible,
.link-arrow:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.mega-card:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }

/* The burger becomes the close mark. Each bar translates to the centre line
   first and rotates about it second - `rotate() translateY()`, in that order,
   because the list composes right-to-left and rotating before translating
   leaves both bars off-centre instead of crossing. */
.burger line {
  stroke: currentColor; stroke-width: 1.8; stroke-linecap: round;
  transform-box: view-box; transform-origin: 50% 50%;
  transition: transform var(--t-ui) var(--ease-out), opacity var(--t-tap) var(--ease);
}
.nav-toggle[aria-expanded="true"] .burger-1 { transform: rotate(45deg) translateY(6px); }
.nav-toggle[aria-expanded="true"] .burger-2 { opacity: 0; }
.nav-toggle[aria-expanded="true"] .burger-3 { transform: rotate(-45deg) translateY(-6px); }
.nav-toggle { display: none; background: none; border: 0; cursor: pointer; padding: 6px; color: var(--ink); }

@media (max-width: 860px) {
  .nav-links {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: flex-start; gap: 4px;
    background: var(--bg); border-bottom: 1px solid var(--line);
    padding: 12px 28px 20px; display: none;
  }
  .nav-links.open { display: flex; }
  .nav-toggle { display: inline-flex; }

  /* The drawer is pinned under a sticky header, so anything taller than the
     room below it was simply unreachable - five rows plus one open accordion
     runs about 630px, which overflows any landscape phone. site.js measures
     the room actually left and publishes it here; the fallback covers the
     frame before the first measurement lands. */
  .nav-links {
    /* 16, not 28: every row inside carries a 12px indent for the current-page
       marker, so 16 + 12 puts the drawer's text on the same 28px edge as the
       brand above it and the container below it. */
    padding: 12px 16px 20px;
    max-height: var(--nav-room, 70svh);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* An underline under a full-width stacked row reads as a divider, not as a
     position, so the current row takes a left marker instead. Every row is
     indented by the same amount so that marking one shifts nothing. */
  .nav-links a::after, .nav-trigger::after { display: none; }
  .nav-links a { width: 100%; padding: 10px 0 10px 12px; font-size: var(--fs-sm); }
  .nav-trigger { padding-left: 12px; }
  .nav-links a[aria-current="page"],
  .nav-trigger[aria-current="page"] { box-shadow: inset 2px 0 0 var(--ink); }

  /* The panels sit inside the drawer now, which already has its own gutter. */
  .mega .container { padding: 0; }

  /* The CTA used to be display:none here, which took the one conversion
     action off the persistent chrome on the device where it is hardest to
     find again. It stays, compact.

     28px of gutter, matching .container and .announce - see the note on the
     announcement bar for why all three have to agree. The row in flow here
     is brand + CTA + burger (the drawer is absolutely positioned and does
     not count) and measures about 262px, so 28px either side still clears a
     320px phone. Tight, and the right way round: a chrome gutter that
     disagrees with the body's is visible on every screen, where the last two
     pixels of the narrowest phone still in use are not. */
  .nav { padding: 12px 28px; gap: 12px; }
  .nav-cta { gap: 10px; }
  .nav .nav-cta .btn { padding: 9px 14px; font-size: var(--fs-xs); }
}

/* ---- buttons ---- */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--sans); font-size: var(--fs-xs); font-weight: 600;
  letter-spacing: 0.02em;
  padding: 13px 22px; border-radius: var(--radius-sm);
  border: 1px solid var(--ink); cursor: pointer;
  position: relative; overflow: hidden;
  /* Fast. The fill used to take .45s and the colour .35s, which on a button
     is long enough to feel like lag rather than feedback - you have already
     decided to click by the time it finishes. Around .18s reads as instant
     while still being a movement rather than a jump. */
  transition: color var(--t-tap) var(--ease), border-color var(--t-tap) var(--ease),
              transform var(--t-tap) var(--ease-out), box-shadow var(--t-tap) var(--ease-out);
}
.btn > span { position: relative; z-index: 1; }
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: var(--ink);
  transform: translateY(0);
  transition: transform var(--t-tap) var(--ease-out);
}
/* Every button lifts, whatever its variant - the fill direction differs
   between primary and ghost, this does not. */
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px -12px rgba(26, 25, 24, 0.55); }
.btn:active { transform: translateY(0); box-shadow: none; transition-duration: .07s; }
.btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.btn-primary { background: transparent; color: var(--on-deep); }
.btn-primary:hover { color: var(--ink); }
.btn-primary:hover::before { transform: translateY(-101%); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-ghost::before { transform: translateY(101%); }
.btn-ghost:hover { color: var(--on-deep); border-color: var(--ink); }
.btn-ghost:hover::before { transform: translateY(0); }
/* One place owns the glyph. The arrow answers to the BUTTON's hover rather
   than needing its own, so the whole control moves as a single thing. */
.btn-arrow span::after {
  content: "  \2192"; font-weight: 500;
  display: inline-block; transition: transform var(--t-tap) var(--ease-out);
}
.btn-arrow:hover span::after { transform: translateX(4px); }
.bg-deep .btn-primary { border-color: var(--on-deep); color: var(--ink); }
.bg-deep .btn-primary::before { background: var(--on-deep); }
.bg-deep .btn-primary:hover { color: var(--on-deep); }
.bg-deep .btn-ghost { border-color: var(--on-deep-3); color: var(--on-deep); }
.bg-deep .btn-ghost::before { background: var(--on-deep); }
.bg-deep .btn-ghost:hover { color: var(--ink); }

.link-arrow { font-weight: 600; color: var(--ink); position: relative; display: inline-block; }
.link-arrow::after { content: " \2192"; display: inline-block; transition: transform var(--t-ui) var(--ease-out); }
.link-arrow:hover::after { transform: translateX(5px); }
.btn-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }

/* ---- cards ---- */
.card {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(26px, 3vw, 38px);
}
.card-tint { background: var(--bg-tint); border-color: var(--line); }
.card h3 { margin-bottom: 10px; }

/* Standardised card rows. .g12 aligns items to `start`, so cards end at
   whatever height their text happens to reach and their links land on three
   different lines.

   .g12.row-stretch, not .row-stretch: both are single-class selectors, so
   with a bare class the winner is whichever appears later in the file - and
   .g12 does, several hundred lines down, which is why this silently did
   nothing and the third card kept ending short. Qualifying it settles the
   cascade on specificity rather than on source order, which no longer breaks
   if either rule moves. */
.g12.row-stretch { align-items: stretch; }
/* .card, not a.card: contact's routing cards are <div>s and their links have
   to reach the same bottom line as the anchor-based ones. */
.card { display: flex; flex-direction: column; }
.card .link-arrow { margin-top: auto; padding-top: 20px; }

/* A card that leads with a picture: the image is flush to the card's top
   and side edges, so its own corners have to follow the card's, and the
   padding that would otherwise sit above it is removed. */
.card-media { padding-top: 0; overflow: hidden; }
.card-media > .plate {
  margin: 0 calc(-1 * clamp(26px, 3vw, 38px)) clamp(20px, 2vw, 26px);
  border-radius: var(--radius) var(--radius) 0 0;
}
.js-anim .card-media > .plate.plate-mask { clip-path: inset(0 0 100% 0 round var(--radius) var(--radius) 0 0); }

/* ---- card hover ----
   Four things move, on deliberately different clocks, so it reads as one
   gesture unfolding rather than everything snapping at once: the card lifts
   first, the scrim clears off the photograph, the picture pushes in behind
   its own frame, and a rule draws under the heading last.

   On this page the photographs are the only saturated colour, so the scrim
   is the payoff worth having - at rest every card is held slightly back, and
   the one under the pointer is the one that comes fully on. */
a.card {
  color: inherit;
  transition: border-color var(--t-ui) var(--ease), background var(--t-ui) var(--ease),
              transform var(--t-ui) var(--ease-out), box-shadow var(--t-ui) var(--ease-out);
}
a.card:hover {
  border-color: var(--ink); background: var(--bg-alt);
  transform: translateY(-6px);
  box-shadow: 0 24px 46px -26px rgba(26, 25, 24, 0.55);
}
a.card:active { transform: translateY(-2px); transition-duration: .08s; }
a.card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* The scrim. Sits over the photograph and lifts on hover. */
.card-media > .plate::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  /* Much lighter than it was. This wash existed to hold text off dark
     laboratory photographs; the card imagery is now high-key glass on an
     off-white ground, and the old 30%/22% black would simply grey it. Enough
     is kept to seat the image against the card rather than to darken it. */
  background: linear-gradient(180deg,
    rgba(26, 25, 24, 0.10) 0%, rgba(26, 25, 24, 0.03) 55%, rgba(26, 25, 24, 0.08) 100%);
  opacity: 1;
  transition: opacity var(--t-move) var(--ease);
}
a.card:hover .plate::after { opacity: 0; }

/* `scale`, not `transform`. These plates carry .plate-zoom, and GSAP scrubs
   a yPercent parallax onto the SAME element's transform - a CSS transform
   here would replace it and kill the drift. The individual scale property
   composes with it instead. */
.card-media > .plate img {
  transition: scale var(--t-enter) var(--ease-out), filter var(--t-move) var(--ease);
}
a.card:hover .plate img { scale: 1.07; filter: saturate(1.14) contrast(1.05); }

/* The rule under the heading, drawn last. */
.card-media h3 { position: relative; display: inline-block; }
.card-media h3::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -7px; height: 1px;
  background: var(--ink);
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--t-move) var(--ease-out) var(--t-tap);
}
a.card:hover h3::after { transform: scaleX(1); }

/* The arrow belongs to the card's hover, not only its own. */
a.card .link-arrow { transition: color var(--t-ui) var(--ease); }
a.card:hover .link-arrow::after { transform: translateX(7px); }

/* numbered editorial cards */

/* ---- stats ---- */
.stat-num {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(2.7rem, 5.4vw, 4rem); line-height: 1;
  color: var(--ink); letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.stat-label {
  font-size: var(--fs-sm); color: var(--ink-3); margin-top: 12px;
  letter-spacing: 0.01em;
}
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0;
  border-top: 1px solid var(--line-3); border-bottom: 1px solid var(--line-3); background: var(--bg); }
.stat-row > div { padding: clamp(28px, 3.4vw, 42px) clamp(20px,2.4vw,30px); border-right: 1px solid var(--line); }
.stat-row > div:last-child { border-right: 0; }
@media (max-width: 720px){ .stat-row { grid-template-columns: 1fr; }
  .stat-row > div { border-right: 0; border-bottom: 1px solid var(--line); }
  .stat-row > div:last-child { border-bottom: 0; } }

/* ---- misc ---- */
.kicker { display: inline-flex; align-items: center; gap: 9px; margin-bottom: 22px; }
.kicker .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ink); }
/* align-self matters more than it looks. .card is a flex column, and a flex
   child is stretched to the container's width by default - which blockifies
   this inline-block and turned every number into a full-width capsule with a
   digit stranded at its left edge. */
.pill { align-self: flex-start; display: inline-block; font-size: var(--fs-2xs); font-weight: 600; color: var(--ink);
  border: 1px solid var(--line-2); border-radius: 999px; padding: 5px 13px;
  letter-spacing: 0.08em; text-transform: uppercase; }
.list-clean { list-style: none; padding: 0; margin: 0; }
.list-clean li { padding: 16px 0; border-bottom: 1px solid var(--line); display: flex; gap: 16px; }
.list-clean li:last-child { border-bottom: 0; }
.list-clean .n { font-family: var(--sans); color: var(--ink); font-size: var(--fs-md); min-width: 1.8em;
  font-variant-numeric: tabular-nums; }

/* ============================================================
   IMAGE SYSTEM
   .plate is the figure: rounded corners, hairline rule, caption.
   The photographs are the only saturated colour on the page, and
   that is the point - they are what the greyscale is a ground for.
   ============================================================ */
.plate {
  position: relative; background: var(--bg-tint);
  overflow: hidden; border-radius: var(--radius-lg);
}
.plate img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(1.04) contrast(1.03);   /* print-plate lift, not desaturation */
}
.plate-4x5  { aspect-ratio: 4 / 5; }
.plate-3x2  { aspect-ratio: 3 / 2; }
.plate-21x9 { aspect-ratio: 21 / 9; }

/* The mask a plate reveals itself behind. `round` is load-bearing now the
   plates are rounded: clip-path replaces the border-radius clip while it is
   applied, so an un-rounded inset() would square the corners off for the
   whole wipe and leave them square at rest. site.js reads the plate's own
   computed radius rather than repeating the number, so this stays true if
   the token changes. */
/* .js-anim, not bare. This rule hides the plate so GSAP can wipe it open, and
   site.js removes .js-anim whenever it is not going to animate - reduced motion,
   or GSAP failing to load. Ungated, the hide survived that removal while the
   `clip-path: none` reset below did not, so every masked plate on the site was
   permanently invisible to anyone with prefers-reduced-motion set. Thirteen of
   them. Gating the hide the same way means dropping the class reveals the
   photograph instead of burying it. */
/* No will-change on either of these. clip-path is not a compositable
   property in Blink, so `will-change: clip-path` buys nothing and still
   promotes the element to its own layer; between the thirteen masked plates
   and the eight zoom images that was ~21 layers and ~18MB of texture held
   for the life of the page, allocated at parse time and never released -
   paid in full even under prefers-reduced-motion, where none of it animates
   at all. GSAP adds will-change for the duration of a tween where it helps,
   which is the only window in which it does. */
.js-anim .plate-mask { clip-path: inset(0 0 100% 0 round var(--radius-lg)); }
/* The 1.12 is headroom for the hover zoom and for the scrubbed parallax that
   uses the same slack, and neither exists without a fine pointer - so on a
   touch screen this was cropping 12% off every plate on the site to leave
   room for a movement that can never happen. site.js gates the parallax on
   the same query. */
@media (hover: hover) and (pointer: fine) {
  .plate-zoom img { transform: scale(1.12); }
}

figure.figure { margin: 0; border: 0; border-radius: 0; background: transparent; overflow: visible; }
figure.figure .plate { border: 1px solid var(--line); }
figure.figure figcaption {
  padding: 14px 0 0; font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-3);
  border-top: 0; display: flex; gap: 12px;
}
figure.figure figcaption .fignum {
  font-family: var(--sans); font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink); white-space: nowrap; font-size: var(--fs-2xs);
  padding-top: 1px;
}

/* ---- FAQ ---- */
.faq { margin: 0; }
.faq > div { padding: 20px 0; border-bottom: 1px solid var(--line); }
.faq > div:first-child { border-top: 1px solid var(--line-3); }
.faq > div:last-child { border-bottom: 0; }
.faq dt {
  font-family: var(--display); font-weight: 600;
  font-size: 1.06rem; line-height: 1.32; color: var(--ink);
  letter-spacing: -0.008em; margin-bottom: 8px;
}
.faq dd { margin: 0; color: var(--ink-2); max-width: 64ch; }

/* ---- latest / dated entries ---- */
.latest { list-style: none; padding: 0; margin: 0; }
.latest li {
  display: grid; grid-template-columns: 108px minmax(0, 1fr);
  gap: 20px; padding: 18px 0; border-bottom: 1px solid var(--line);
}
.latest li:first-child { border-top: 1px solid var(--line-3); }
.latest li:last-child { border-bottom: 0; }

/* A dated row is a link target across its whole width, so the whole row
   reacts rather than just the anchor text. The moving part is a rule that
   wipes in under the row from the left - the same slide-behind-a-hard-edge
   idiom the plates and headings use, at the smallest scale on the site, so
   the list feels like it belongs to the same page.

   position:relative + a ::after on the row rather than animating the
   existing border: a border cannot be revealed directionally, only
   recoloured, and a colour flip on a hairline is invisible at this weight. */
.latest li {
  position: relative;
  transition: padding-left var(--t-tap) var(--ease-out), background var(--t-tap) var(--ease);
  border-radius: var(--radius-sm);
}
.latest li::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: var(--ink); transform: scaleX(0); transform-origin: left center;
  transition: transform var(--t-ui) var(--ease-out);
}
.latest li:hover { background: var(--bg-alt); padding-left: 14px; }
.latest li:hover::after { transform: scaleX(1); }
.latest li:hover time { color: var(--ink); }
.latest li:hover .link-arrow::after { transform: translateX(5px); }
/* The whole row is the target, so give it the row's focus ring too. */
.latest li:focus-within { background: var(--bg-alt); }
.latest li:focus-within::after { transform: scaleX(1); }
.latest time {
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3);
  font-variant-numeric: tabular-nums; padding-top: 3px;
}
.latest .caption { margin: 5px 0 0; }
@media (max-width: 620px) {
  .latest li { grid-template-columns: 1fr; gap: 6px; }
}

/* ---- diagrams ----
   Inline SVG schematics. Monochrome by rule: an inline diagram is interface,
   not imagery, so series are encoded by fill and weight rather than hue. That
   keeps them readable in greyscale, in print, and for colour-blind readers -
   the exact failure the methods-paper PNGs have. */
/* .figure.diagram, not .diagram: these are always used together as
   class="figure diagram", and `figure.figure` (0,1,1) sets border:0, which
   outranks a bare `.diagram` (0,1,0). The frames silently never rendered. */
/* Scrolls rather than shrinks, the same treatment .chart-scroll and
   .table-wrap already give wide content. These three schematics are drawn
   against 660-980pt viewBoxes with 10px labels, so a 343px phone rendered
   them at 0.32-0.44x and the labels at 3.2-4.4px - present, and unreadable,
   which is worse than absent. Above ~630px of render width nothing changes,
   because the min-width is never the larger number. */
.figure.diagram {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  overflow-x: auto; overflow-y: hidden;
}
/* On the svg, not on a wrapper: index.html nests its diagram one level deeper
   inside [data-demo-fallback], and overflow from a visible-overflow block
   still reaches the nearest scroll container, so one selector covers both
   shapes without giving the interactive demo beside it a 600px floor. */
.figure.diagram svg { display: block; width: 100%; height: auto; padding: 18px 16px 6px; min-width: 600px; }
/* The caption is prose and belongs to the figure, not to the scrolled
   drawing - sticky keeps it against the left edge of the scrollport instead
   of sliding out of the frame the moment the diagram is panned. */
.figure.diagram figcaption {
  padding: 14px 18px 16px; border-top: 1px solid var(--line);
  position: sticky; left: 0;
}

/* The full-bleed band (.bleed, width:100vw) is gone along with the six
   standalone photographs that used it - one per page, each a picture with no
   text anywhere near it. They are contained, captioned figures now, which
   also lets their rounded corners be seen: at 100vw the corners sit off the
   edge of the screen, so a full-bleed image can never actually look rounded.
   .bleed-right / .bleed-left below survive as grid utilities, but nothing in
   public/*.html uses either of them today - kept because the twelve-column
   grid they belong to is a complete system, not because a page depends on
   them. The earlier version of this note claimed they were "still in use",
   which is the kind of comment that stops the next person deleting dead
   code, so it is corrected rather than left. */

/* `scale`, not `transform`: .plate-zoom images have a GSAP parallax scrubbed
   onto their transform, and setting transform here replaces it, stopping the
   drift dead. The individual scale property composes with it instead. This
   was a latent bug - the hover simply cancelled the parallax wherever a
   plate sat inside a link. */
a .plate img { transition: scale var(--t-enter) var(--ease-out), filter var(--t-move) var(--ease); }
a:hover .plate img { scale: 1.04; filter: saturate(1.12) contrast(1.06); }

/* ---- data table ---- */
.table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.table th, .table td { text-align: left; padding: 13px 14px; border-bottom: 1px solid var(--line); }
.table th { font-weight: 600; color: var(--ink); font-size: var(--fs-2xs); text-transform: uppercase;
  letter-spacing: 0.12em; border-bottom-color: var(--line-3); }
.table td { color: var(--ink-2); }
.table .num { font-variant-numeric: tabular-nums; }
.table-wrap { overflow-x: auto; border-top: 1px solid var(--line-3); background: var(--bg); }

/* ---- footer ---- */
.site-footer { background: var(--bg-deep); color: var(--on-deep-2); }
.site-footer a { color: var(--on-deep-2); transition: color var(--t-ui) var(--ease); }
.site-footer a:hover { color: var(--on-deep); }
.footer-grid {
  max-width: var(--maxw); margin: 0 auto; padding: 76px 28px 34px;
  display: flex; flex-wrap: wrap; gap: 40px; justify-content: space-between;
}
.footer-col h4 { font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--on-deep-3); margin: 0 0 16px; }
.footer-col a { display: block; font-size: var(--fs-md); padding: 5px 0; }
.footer-brand { max-width: 30ch; }
.footer-brand p { color: var(--on-deep-3); font-size: var(--fs-md); margin-top: 12px; }
.footer-bottom { border-top: 1px solid rgba(250,247,241,0.14); }
.footer-bottom div { max-width: var(--maxw); margin: 0 auto; padding: 22px 28px;
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between;
  font-size: var(--fs-xs); color: var(--on-deep-3); }

/* ============================================================
   ANIMATION PRIMITIVES
   Initial states apply only once JS has confirmed it will animate
   (html.js-anim). Without JS everything renders plainly - content
   is never left hidden by a script that failed to load.
   ============================================================ */
/* :not(.is-in) matters. Without it this rule keeps reasserting the hidden
   state for the life of the page and fights whatever GSAP writes, so the
   heading can never come to rest - it just sits masked. JS adds .is-in the
   moment a line has finished (or the safety net gives up waiting). */
.js-anim .anim-line:not(.is-in) > * { transform: translateY(105%); }
.js-anim .anim-up   { opacity: 0; transform: translateY(28px); }
.js-anim .anim-fade { opacity: 0; }
/* .js-anim .plate-mask is NOT repeated here. It was, and the copy came later
   in the file at identical specificity, so it won - dropping the `round`
   that the note above the real rule calls load-bearing and squaring off
   every masked plate's corners for the whole wipe. */

/* The mask each line slides out of. padding/margin pair keeps descenders
   (g, y, p) from being clipped by the overflow while at rest. */
.anim-line { display: block; overflow: hidden; margin-bottom: -0.14em; }
/* The descender allowance lives on the INNER span, not the mask. If the mask
   is taller than the thing it clips, translating the text by 105% of its own
   height leaves the top slice of every glyph visible - which is exactly what
   shipped and what broke the headline. Keeping the padding inside the moving
   box means one translate clears the whole mask.
   nowrap is load-bearing too: the splitter measured this text as exactly one
   line, and without it a sub-pixel difference between the measured and
   rebuilt text can let it wrap again inside its own mask. */
.anim-line > * {
  display: block; white-space: nowrap;
  padding-bottom: 0.14em; will-change: transform;
}

/* Same mask, at word instead of line granularity, for the part of the hero
   headline that loops - inline-block (not block) so words still wrap as
   text, and margin/padding split the same way for the same reason: the
   descender allowance has to live on the moving inner span, not the mask,
   or translating by 105% leaves the top of every glyph's tail visible. */
.word-mask {
  display: inline-block; overflow: hidden; white-space: nowrap;
  vertical-align: bottom; margin-bottom: -0.14em;
}
.word-mask > span { display: inline-block; padding-bottom: 0.14em; will-change: transform; }

/* The legacy .reveal pair was removed along with its IntersectionObserver in
   site.js: every page now uses .anim-up / .anim-fade. Leaving the CSS behind
   would have been worse than dead weight - `.reveal { opacity: 0 }` with
   nothing left to add `.in` meant any future use of the class would render
   permanently invisible. */

@media (prefers-reduced-motion: reduce) {
  .js-anim .anim-line > *, .js-anim .anim-up, .js-anim .anim-fade {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
  .js-anim .plate-mask { clip-path: none !important; }
  .plate-zoom img { transform: none !important; }
  /* The contours hold still rather than disappearing: the drift is the part
     someone asking for reduced motion objects to, not the lines themselves,
     and removing them would take the hero's ground with it. */
  .hero-contours g { animation: none !important; }
}

/* ============================================================
   CROSS-DOCUMENT VIEW TRANSITIONS
   Six static pages that share a header, an announcement bar and
   a footer. Without this, every navigation is a white flash and
   a re-render of furniture that did not change; with it the
   chrome stays put and only the page body crosses over.

   Pure progressive enhancement - no JS, and browsers without
   support simply navigate as before. Deliberately short: this
   should feel like the page settling, not like a transition.
   ============================================================ */
@view-transition { navigation: auto; }

::view-transition-old(root) { animation-duration: 180ms; animation-timing-function: var(--ease); }
::view-transition-new(root) { animation-duration: 260ms; animation-timing-function: var(--ease-out); }

/* Persistent furniture is carried across rather than cross-faded, so the
   nav does not blink on every click. */
.site-header  { view-transition-name: span-header; }
.announce     { view-transition-name: span-announce; }
.site-footer  { view-transition-name: span-footer; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* The PRELOADER block that lived here is gone with the splash itself.
   See the note in site.js: a gate in front of a static six-page site cost
   first-word latency and animated a number, which is the one thing this
   site's argument does not permit. */

/* ============================================================
   ASYMMETRY - ON A GRID, NOT WITH NEGATIVE MARGINS
   ------------------------------------------------------------
   The previous version of this block let blocks "break into the
   section below" with a negative margin. It did exactly that, and
   the result was measurable: at 1440px the hero caption sat 100%
   on top of the WebGL canvas, and the hero photograph covered 29%
   of all three paragraphs in the NEXT section. Asymmetry you
   cannot predict is not asymmetry, it is collision.

   The rule now: SECTIONS ARE SEALED. Nothing crosses a section
   boundary vertically. Every section is a clean band of space.

   Variety comes from somewhere better - one rigid 12-column grid
   underneath everything, and wildly varied PLACEMENT on it. A
   block can start at column 4 and run 7 wide, or start at 1 and
   run 5, or sit two rows down from its neighbour. No two sections
   on a page should resolve to the same shape, but every one of
   those shapes is snapped to the same underlying rhythm, which is
   what stops it reading as noise.

   Horizontal bleed to the viewport edge is still allowed - it
   never causes a collision, because it moves along the axis a
   section has room on.
   ============================================================ */
:root { --gutter: clamp(14px, 1.5vw, 24px); }

/* Reach the viewport edge from inside the centred container. calc(50% - 50vw)
   is the usual trick but resolves 50% against THIS element's width, which is
   wrong for a grid child - it silently did nothing. The gap to close is the
   container's outer margin plus its padding. */
.bleed-right { margin-right: calc(-1 * (max(0px, (100vw - var(--maxw)) / 2) + 28px)); }
.bleed-left  { margin-left:  calc(-1 * (max(0px, (100vw - var(--maxw)) / 2) + 28px)); }

/* ---- the 12-column editorial grid ---- */
.g12 {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(22px, 3vw, 42px);
  align-items: start;
}
/* Start column and span are separate so any placement is expressible:
   class="c4 s7" reads as "start at 4, run 7 wide". */
/* .c3, .c11, .s1 and .s11 are deliberately absent: no page uses them, and an
   unused placement utility is a rule that has to be re-read on every audit
   and can never be verified against anything. Add one back the moment a
   layout wants it. */
.c1  { grid-column-start: 1 }  .c2  { grid-column-start: 2 }
.c4  { grid-column-start: 4 }
.c5  { grid-column-start: 5 }  .c6  { grid-column-start: 6 }
.c7  { grid-column-start: 7 }  .c8  { grid-column-start: 8 }
.c9  { grid-column-start: 9 }  .c10 { grid-column-start: 10 }
.c12 { grid-column-start: 12 }

.s2  { grid-column-end: span 2 }
.s3  { grid-column-end: span 3 }  .s4  { grid-column-end: span 4 }
.s5  { grid-column-end: span 5 }  .s6  { grid-column-end: span 6 }
.s7  { grid-column-end: span 7 }  .s8  { grid-column-end: span 8 }
.s9  { grid-column-end: span 9 }  .s10 { grid-column-end: span 10 }
.s12 { grid-column-end: span 12 }

/* The .y1/.y2/.y3 and .drop/.drop-lg utilities are gone, along with the
   "a row is never level" rule they existed to enforce. Staggering a row of
   peers - three cards, three stats, three steps - does not read as an
   editorial decision, it reads as things failing to line up, which is
   exactly how it was reported. Rows share a top edge now. Asymmetry is
   still carried, but by column WIDTHS, which vary all over this grid.

   Deliberately deleted rather than left in place: an unused offset utility
   is an invitation to reintroduce the same problem later. */

@media (max-width: 860px) {
  .bleed-right, .bleed-left { margin-left: 0; margin-right: 0; }
  /* One column, source order. Asymmetry is a wide-viewport luxury; on a
     phone it is just wasted space and orphaned lines. (The offset-reset
     rules that used to sit here went with the offsets themselves.) */
  .g12 { grid-template-columns: minmax(0, 1fr); }
  .g12 > * { grid-column: 1 / -1 !important; }
}

/* ---- hero: headline-led, full width ----
   The vertical rhythm here is bounded by viewport HEIGHT as well as width.
   Everything in this section competes for the same first screen, so spacing
   picked purely from vw grows on a wide-but-short window (a 1280x720 laptop,
   a resized browser) exactly when there is least room for it. */
.hero {
  padding: clamp(40px, min(6.5vw, 9svh), 92px) 0 clamp(28px, 3vw, 52px);
  position: relative; isolation: isolate; overflow: hidden;
}

/* Two grounds under the hero, both in the existing warm ramp - no new hue
   enters the page to make it feel less bare. The wash gives the section
   depth a flat fill cannot, and the field gives it movement. */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: -2; pointer-events: none;
  background:
    radial-gradient(115% 78% at 82% 6%, var(--bg-alt) 0%, transparent 60%),
    radial-gradient(88% 66% at 4% 98%, var(--bg-tint) 0%, transparent 55%);
}
/* Drifting contour lines. This replaced a canvas running the trajectory
   field: at background weight that read as noise behind the type rather
   than as texture, and it cost a script and a rAF loop to do it.

   Ten lines are drawn from ONE path definition through <use>, in three
   groups that drift at different speeds so the field has depth. The path is
   periodic over 1200 user units and the viewBox is 2400 wide - exactly two
   periods - so translating a group by -1200 lands it exactly where it
   started and the loop has no seam. Only `transform` animates, so this
   stays on the compositor and never touches layout. */
.hero-contours {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  overflow: hidden;
  /* Nothing behind the headline. A background the type has to compete with
     is not a background, and this is the one line on the site that has to
     read instantly. It comes up under the columns and fades at both edges
     so it sits in the page rather than being cropped by it. */
  -webkit-mask-image:
    linear-gradient(180deg, transparent 0%, transparent 24%, #000 56%, #000 90%, transparent 100%),
    linear-gradient(90deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
  mask-image:
    linear-gradient(180deg, transparent 0%, transparent 24%, #000 56%, #000 90%, transparent 100%),
    linear-gradient(90deg, transparent 0%, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.contour-svg {
  position: absolute; top: 50%; left: 0;
  width: 200%; height: 118%; transform: translateY(-50%);
}
.contour-svg use { fill: none; stroke: var(--line-2); vector-effect: non-scaling-stroke; }
.hero-contours g { will-change: transform, translate; }
.c-far  use { stroke-width: 1; opacity: .55; }
.c-mid  use { stroke-width: 1.15; opacity: .8; }
.c-near use { stroke-width: 1.3; opacity: 1; stroke: var(--line-3, var(--line-2)); }
.c-far  { animation: contour-drift 104s linear infinite; }
.c-mid  { animation: contour-drift 74s linear infinite; }
.c-near { animation: contour-drift 52s linear infinite; }
/* -1200 user units is exactly one period of the path, so the reset is
   invisible. Never a percentage: that would resolve against the group's
   own bounding box, which is not the period. */
@keyframes contour-drift { to { transform: translateX(-1200px); } }

/* Pointer parallax. site.js publishes --px/--py (-1..1 from the hero's
   centre) and each layer decides what that is worth to it: the nearest
   contours move most, the farthest barely at all, which is what makes a
   flat field read as having depth.

   `translate`, not `transform`. The drift above already owns transform on
   these same elements, and a second transform would replace it and stop the
   loop dead. The individual translate/rotate/scale properties compose with
   transform instead of overriding it, so a hover response and an infinite
   animation can share one element. That is the whole reason this works.

   The transition lives here rather than in the script: the pointer handler
   only sets a destination, and easing back to rest on pointerleave is the
   same transition running to zero. */
.hero { --px: 0; --py: 0; }
.c-far  { translate: calc(var(--px) * 10px) calc(var(--py) * 5px); }
.c-mid  { translate: calc(var(--px) * 20px) calc(var(--py) * 10px); }
.c-near { translate: calc(var(--px) * 34px) calc(var(--py) * 17px); }
.hero-contours g { transition: translate var(--t-move) var(--ease-out); }
/* And a touch more presence while the pointer is anywhere in the hero. */
.hero-contours { opacity: .88; transition: opacity var(--t-move) var(--ease); }
.hero:hover .hero-contours { opacity: 1; }

/* The revealed contour field. Between the ambient contours (z-index -1) and
   the copy (z-index 1), so the pointer develops detail BEHIND the text and
   never over it - a reveal that paints across a headline is not a background,
   it is an obstruction. */
.hero-inner { position: relative; z-index: 1; }

/* The headline owns the full container. It is now an ordinary [data-split]
   heading, so the splitter measures the browser's own line breaks and the
   old constraint - a fixed lead-in that had to survive on one nowrap line -
   is gone with the rotating tail. The 62px cap is kept because it is the
   size the hero was composed at, and 5.2vw holds the proportion down the
   range. This is still the argument for the full-width headline: sharing the
   row with the photograph capped the same string at 34px.

   Bounded by height as well: the size is chosen from the width available but
   spent out of the height, so on a short window a purely vw-driven headline
   takes the room the photograph needed and squeezes it to a thumbnail. */
.hero-headline { max-width: none; font-size: clamp(30px, min(5.2vw, 7.6svh), 62px); }

/* Supporting copy and photograph, under the headline. The two are sized in
   opposite directions - the picture from the height left on screen, the copy
   from what the picture leaves - so a gap between them is not expressible. */
/* Three columns: supporting copy, the photograph, and the three claims.
   The photograph's track is `auto` and it sets its own width from the height
   left on screen (below), so the two text columns share whatever remains -
   which means a gap between them is not expressible. */
.hero-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 0.9fr);
  column-gap: clamp(28px, 3vw, 48px);
  align-items: start;
  margin-top: clamp(24px, min(3.5vw, 5svh), 52px);
}
.hero-copy .lead { max-width: 42ch; }

/* The right rail. Hairline-separated rather than boxed: three cards here
   would out-shout the photograph they sit beside, and these are a summary,
   not three destinations. */
/* Each row is a link now - to the science, the research and the company, in
   that order - so it gets the full hover the site gives anything pressable:
   a wash, a nudge, an arrow that travels. An earlier version of this rule
   deliberately stopped short of all that on the grounds that hovering prose
   promises a destination that does not exist. There is a destination now.

   The negative top margin is what keeps the rail's first line level with
   the lead paragraph beside it. Padding moved from the <li> to the <a> so
   the hover wash covers the whole row rather than a band inside it, and
   uniform padding on every row would otherwise push the first one down. */
.hero-pillars { list-style: none; margin: -14px 0 0; padding: 0; }
.hero-pillars li { border-top: 1px solid var(--line); }
.hero-pillars li:first-child { border-top: 0; }
/* No `transition` here: the frosted-panel refinement further down declares
   its own on the same selector at the same specificity and supersedes this
   one wholesale, so the pair written here never applied to anything. */
.hero-pillars a {
  display: block; color: inherit; text-decoration: none;
  padding: 14px 10px; margin: 0 -10px;
  border-radius: var(--radius-sm);
}
.hero-pillars h3 { font-size: var(--fs-md); margin-bottom: 5px; }
.hero-pillars p { margin: 0; font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-3); }
.hero-pillars h3, .hero-pillars p { transition: color var(--t-ui) var(--ease); }
.hero-pillars h3::after {
  content: " \2192"; display: inline-block; color: var(--ink-4);
  transition: transform var(--t-ui) var(--ease-out), color var(--t-ui) var(--ease);
}

.hero-pillars a:hover { background: var(--bg-alt); padding-left: 18px; }
.hero-pillars a:hover p { color: var(--ink-2); }
.hero-pillars a:hover h3::after { transform: translateX(4px); color: var(--ink); }
.hero-pillars a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The proof strip under the hero. Same component as before, one size down:
   at the section scale these numbers ran to 64px, which beside a 62px
   headline reads as two things shouting rather than a claim and its
   evidence. */
.stat-strip { padding-bottom: clamp(28px, 4vw, 56px); }
.stat-strip .stat-num { font-size: clamp(2.1rem, 3.4vw, 3rem); }
.stat-strip .stat-row > div { padding: clamp(22px, 2.4vw, 30px) clamp(18px, 2vw, 26px); }

/* .rot-tail is gone with the rotating headline. The h1 is one fixed
   sentence, split into lines by the same splitter every other heading uses. */

.hero-figure figcaption { max-width: 100%; }

/* The long note that used to sit here explained how to crop a framed
   photograph without cutting through the tube - which axis object-fit:cover
   trims, why the box had to stay taller than it was wide, why the width was
   derived from the height. None of it applies to a cut-out: there is no box
   and no crop. What survives is --hero-reserve, and it is still the reason
   this works. site.js measures everything in the hero that is NOT the tube
   (announcement bar, header, section padding, caption) rather than the
   stylesheet guessing at it, because all of it moves with viewport width,
   webfont loading, and the visitor's own font size. svh rather than vh: on
   mobile vh is the viewport at its tallest, with the toolbars retracted, so
   a vh fit is wrong precisely while they are showing. */

/* The cut-out tube. Nothing frames it, so there is no crop to protect and no
   panel to size - only a height, taken from whatever the first screen has
   left. Floored at 260px so a short window gets a small tube rather than a
   sliver; below roughly 720px of viewport height the hero runs a little past
   the fold, which is the deliberate trade.

   The column is wider than the tube on purpose. At this aspect (0.2) the
   image is only about 100px across, and a caption set to that width would
   run to a dozen lines - the column is sized for the sentence, and the tube
   is centred in it. No aspect ratio is written down anywhere: the height is
   set and the width follows from the file, so replacing the asset with a
   differently-proportioned one needs no CSS change. */
.hero-figure {
  --hero-tube-h: clamp(260px, calc(100svh - var(--hero-reserve, 470px)), 620px);
  width: clamp(200px, 21vw, 270px);
}
/* The stack carries every motion, so the ink canvas travels with the image
   it is revealing rather than sliding against it. width:fit-content keeps
   the box exactly the tube's silhouette, which is what the absolutely
   positioned canvas then matches. */
.hero-tube-stack {
  position: relative;
  display: block; width: fit-content; margin: 0 auto;
  animation: tube-float 7s ease-in-out infinite;
  /* Opposite to the contours, and further than any of them. Two things
     moving apart is what reads as depth; two things moving together just
     reads as the page sliding. `translate`, because the float owns
     transform. */
  translate: calc(var(--px) * -16px) calc(var(--py) * -9px);
  transition: translate var(--t-move) var(--ease-out), scale var(--t-ui) var(--ease-out);
}
.hero-tube {
  display: block;
  height: var(--hero-tube-h); width: auto; max-width: 100%;
  /* drop-shadow, not box-shadow: it follows the alpha channel, so the
     shadow is the tube's silhouette rather than a rectangle behind it. */
  filter: drop-shadow(0 22px 26px rgba(26, 25, 24, 0.16));
  transition: filter var(--t-ui) var(--ease-out);
}
/* Exactly over the image it reveals. pointer-events:none so the listener on
   the figure keeps receiving the moves that drive it. */
.hero-tube-ink {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}
/* A slow rise and settle. The droplet is caught mid-fall, so the object is
   already implying motion; this just keeps it from reading as pasted on. */
@keyframes tube-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}
/* Hovering the figure lifts the tube toward the reader: a little larger, a
   longer and softer shadow. `scale` is its own property here for the same
   reason translate is - the float still has transform. It goes on the stack
   so the ink canvas scales with the image and stays registered to it. */
.hero-figure:hover .hero-tube-stack { scale: 1.035; }
.hero-figure:hover .hero-tube { filter: drop-shadow(0 30px 34px rgba(26, 25, 24, 0.22)); }
.hero-figure:hover figcaption { color: var(--ink-2); }
.hero-figure figcaption { transition: color var(--t-ui) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .hero-tube-stack { animation: none; translate: none; transition: none; }
  .hero-figure:hover .hero-tube-stack { scale: 1; }
}

/* The three-column hero collapses at 1000px, not at 860.

   The middle track is the photograph and it sizes itself from the height
   left on screen, so the two text columns divide whatever is left over. At
   861px - one pixel above the old breakpoint - that left the lead measuring
   281px, about 34 characters, and the tube rendering as an 84x420 sliver
   with a caption under it wider than the picture. Three columns stopped
   working long before they stopped being declared; 1000px is where the lead
   still clears 40 characters.

   The figure's own desktop sizing goes with it. Once the hero is one column
   the picture has the full width and has to be capped by height instead, so
   a tall window does not hand it most of the screen - that rule cannot stay
   behind at 860 or the 861-1000 band gets a stacked layout wearing a
   200px-wide column. */
@media (max-width: 1000px) {
  .hero-body { grid-template-columns: 1fr; row-gap: clamp(28px, 6vw, 44px); }
  .hero-figure { width: auto; }
  .hero-tube { height: min(var(--hero-tube-h), 46svh); }
}

@media (max-width: 860px) {
  .hero { padding-bottom: clamp(40px, 8vw, 64px); }
  /* No `order` on the figure. Source order is headline, lead, buttons, then
     the picture, and reordering put the tube at 305-754px of a 375x812
     screen - so the value proposition and both calls to action started below
     the fold on the page's first screen. The photograph is support; it reads
     after the sentence it supports. */
  /* Fewer, slower lines on a phone: at this width the contours crowd, and
     a background animating behind a single column is the first thing to
     cost battery for the least return. */
  .hero-contours { opacity: .6; }
  .c-far { display: none; }
  /* One phone column cannot hold the lead-in on a single line at any size
     worth reading, and .anim-line's nowrap would push it off-screen rather
     than wrap. Let it wrap here: the mask animates a two-line block just as
     happily as a one-line one, since it takes its height from its content. */
  .hero-headline .anim-line > * { white-space: normal; }
}

/* A drifting field is the clearest case of decoration a phone pays for and
   nobody asked for: it animates for as long as the hero is on screen, on the
   battery least able to carry it. The 860px block above already drops the
   far layer; a coarse pointer is the better signal for the middle one, since
   it is the device that matters here rather than the window width. What is
   left is one near layer, which still reads as a ground. */
@media (pointer: coarse) {
  .c-mid { display: none; }
}

/* ---- how-it-works steps ---- */
.steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 3.5vw, 56px);
  align-items: start;
}
.step .plate { margin-bottom: 22px; }
.step-n {
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.2em; color: var(--ink-3); margin: 0 0 8px;
  font-variant-numeric: tabular-nums;
}
.step h3 { margin-bottom: 8px; }
@media (max-width: 860px) {
  .steps { grid-template-columns: 1fr; gap: 44px; }
}

/* A plate holding a drawn figure rather than a photograph. Same tint ground,
   same radius and rounding as the image plates so the row still reads as one
   set - but the artwork is inset and scales with the box instead of being
   cropped to fill it, because a diagram has no safe crop.

   No .plate-mask and no .plate-zoom on these: the clip-wipe and the parallax
   both exist to give a photograph somewhere to travel, and applied to a line
   drawing they only smear the line weights. They enter with the band. */
.plate-fig {
  display: grid; place-items: center;
  padding: clamp(10px, 2.2vw, 20px);
  border: 1px solid var(--line);
}
.plate-fig svg { width: 100%; height: auto; display: block; overflow: visible; }

/* ---- trajectory band ---- */
/* The trajectory band and its canvas layer are gone. The band was a
   full-width section whose only content was one line of text over an
   animated field; the field is now the hero's contour ground and the
   section it lived in was carrying nothing else. */

.eyebrow-row { margin-bottom: 26px; }
/* One scale, both directions. These were two scales wearing the same names:
   mt-s/m/l were 18/30/46 while mb-s/m/l were 22/40/64, so mt-m put 30px above
   an element and mb-m put 40px below it. Sixteen uses of the mb- side were
   quietly getting a different rhythm from the forty-nine uses of mt-.

   Only the down-scale moves, since the up-scale is the one that is used four
   times as often and was already tuned when the section rhythm came down. The
   largest step goes 64px to 46px, which continues in the direction the spacing
   has been going all along.

   Not tokenised further on purpose: the audit that found this also found 87%
   of the site's spacing already on a 2px grid, and the values that sit off it
   are component paddings - 13px 22px on a button, 5px 13px on a pill - where
   the number is set by the type inside, not by a spacing scale. Snapping those
   would be vandalism dressed up as tidying. */
.mt-s { margin-top: 18px; } .mt-m { margin-top: 30px; } .mt-l { margin-top: 46px; }
.mb-s { margin-bottom: 18px; }
.mb-m { margin-bottom: 30px; } .mb-l { margin-bottom: 46px; }

/* ============================================================
   DATA FIGURES  (charts.js, demo.js)
   Charts are drawn as SVG from generated JSON, in the same ink
   ramp as the rest of the page - series are separated by weight,
   dash and fill, never by hue. The .table-details fallback below
   holds the same numbers for no-JS and for screen readers.
   ============================================================ */
h4 { font-family: var(--sans); font-size: var(--fs-md); line-height: 1.34;
  font-weight: 600; color: var(--ink); margin: 0; letter-spacing: -0.004em; }

.chart-figure { border: 1px solid var(--line); background: var(--bg); padding: 20px 20px 4px;
  border-radius: var(--radius); }
.chart-figure svg { overflow: visible; }

/* Wide content scrolls in its own box rather than shrinking to unreadable -
   the charts carry 9px labels drawn against a 640pt viewBox, so below about
   620px of render width the type stops being legible. Same treatment the
   data tables get from .table-wrap. */
.chart-scroll, .demo-plot { overflow-x: auto; overflow-y: hidden; }
.chart-scroll > svg, .demo-plot > svg { min-width: 600px; }
.chart-figure .chart-note {
  margin: 16px 0 16px; padding-top: 13px; border-top: 1px solid var(--line);
  color: var(--ink-3); font-size: var(--fs-xs); line-height: 1.5;
}
.chart-figure[hidden] { display: none; }

/* ---- interactive trajectory demo (demo.js) ---- */
.demo { border: 1px solid var(--line); background: var(--bg); }
.demo[hidden] { display: none; }
.demo-bar {
  display: flex; flex-wrap: wrap; gap: 22px 34px; align-items: center;
  padding: 16px 20px; border-bottom: 1px solid var(--line); background: var(--bg-alt);
}
.demo-group { display: flex; align-items: center; gap: 8px; }
.demo-group-k {
  font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-3); margin-right: 4px;
}
.demo-chip {
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 500;
  color: var(--ink-3); background: transparent;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 6px 13px; cursor: pointer; letter-spacing: 0.01em;
  transition: color var(--t-tap) var(--ease), border-color var(--t-tap) var(--ease),
              background var(--t-tap) var(--ease), transform var(--t-tap) var(--ease-out);
}
.demo-chip:hover { color: var(--ink); border-color: var(--ink); transform: translateY(-2px); }
.demo-chip:active { transform: translateY(0); transition-duration: .07s; }
.demo-chip.is-on { color: var(--on-deep); background: var(--ink); border-color: var(--ink); }
.demo-chip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.demo-plot { padding: 22px 20px 6px; }

/* live readouts - HTML, not SVG, so the numbers stay selectable */
.demo-read {
  display: grid; grid-template-columns: 1.15fr 1.35fr 0.5fr;
  border-top: 1px solid var(--line);
}
.demo-read-col { padding: 16px 20px; border-right: 1px solid var(--line); min-width: 0; }
.demo-read-col:last-child { border-right: 0; }
.demo-read-wide { grid-column: 1 / -1; }
.demo-read-k {
  display: block; font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 9px;
}
.demo-read-v {
  display: block; font-family: var(--display); font-weight: 600;
  font-size: var(--fs-2xl); line-height: 1.05; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.demo-read-s { display: block; font-size: var(--fs-xs); line-height: 1.45; color: var(--ink-3); margin-top: 8px; }
.demo-read-s strong { color: var(--ink); font-weight: 600; }
.demo-read-bar {
  display: block; height: 10px; background: var(--bg-tint);
  border: 1px solid var(--line-2); overflow: hidden;
}
.demo-read-bar > i {
  display: block; height: 100%; background: var(--ink);
  transform-origin: left center; transition: transform var(--t-ui) var(--ease);
}

.demo-controls {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  padding: 15px 20px; border-top: 1px solid var(--line);
}
.demo-scrub { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 220px; }
.demo-scrub[hidden] { display: none; }
.demo-scrub-k {
  font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}
.demo-scrub input[type="range"] {
  flex: 1; -webkit-appearance: none; appearance: none;
  background: transparent; height: 18px; cursor: pointer; min-width: 0;
}
.demo-scrub input[type="range"]::-webkit-slider-runnable-track {
  height: 1px; background: var(--line-2);
}
.demo-scrub input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--ink); margin-top: -6px;
}
.demo-scrub input[type="range"]::-moz-range-track { height: 1px; background: var(--line-2); }
.demo-scrub input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px; border: 0; border-radius: 50%; background: var(--ink);
}
.demo-scrub input[type="range"]:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

@media (max-width: 860px) {
  .demo-read { grid-template-columns: 1fr; }
  .demo-read-col { border-right: 0; border-bottom: 1px solid var(--line); }
  .demo-read-col:last-child { border-bottom: 0; }
  .demo-bar { gap: 14px 20px; }
}

/* full numbers, folded away - the charts are the read, this is the record */
.table-details { border-top: 1px solid var(--line-3); }
.table-details > summary {
  cursor: pointer; list-style: none; padding: 18px 0;
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink);
  display: flex; align-items: center; gap: 12px;
  transition: color var(--t-ui) var(--ease);
}
.table-details > summary::-webkit-details-marker { display: none; }
.table-details > summary::after {
  content: "+"; margin-left: auto; font-size: var(--fs-lg); font-weight: 400;
  color: var(--ink-3); transition: transform var(--t-ui) var(--ease);
}
.table-details[open] > summary::after { content: "\2212"; }
.table-details > summary:hover { color: var(--ink-3); }
.table-details > *:last-child { margin-bottom: 40px; }

/* ---- contact form ---- */
.form-wrap { max-width: 660px; }
.contact-form { display: grid; gap: 20px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 8px; }
.field label { font-size: var(--fs-2xs); font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink); }
.field .req { color: var(--ink); }
.field input, .field select, .field textarea {
  font-family: var(--sans); font-size: var(--fs-md); color: var(--ink); line-height: 1.5;
  background: transparent; border: 0; border-bottom: 1px solid var(--line-2);
  border-radius: 0; padding: 11px 2px; width: 100%;
  transition: border-color var(--t-ui) var(--ease);
  -webkit-appearance: none; appearance: none;
}
.field textarea { resize: vertical; min-height: 130px; }
.field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23767676' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 4px center; padding-right: 34px; cursor: pointer;
}
/* Placeholder text is text: it carries the only example of what a field wants,
   and at --ink-4 it measured 2.32:1. --ink-3 clears AA on every ground a form
   sits on. */
.field input::placeholder, .field textarea::placeholder { color: var(--ink-3); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-bottom-color: var(--ink); box-shadow: none;
}
.form-actions { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-top: 8px; }
.contact-form button[type="submit"] { cursor: pointer; font-family: var(--sans); }
.form-note { font-size: var(--fs-sm); color: var(--ink-3); margin: 0; }
.hp { position: absolute !important; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form-sent { display: none; }
.form-wrap.sent .contact-form { display: none; }
.form-wrap.sent .form-sent { display: block; }
.form-sent h3 { font-family: var(--sans); font-size: var(--fs-xl); color: var(--ink); font-weight: 600; }
.form-sent .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  background: var(--ink); margin-right: 10px; }


/* ==========================================================================
   POLISH PASS
   Gradient, glass, scroll and hover behaviour for the surfaces that were
   still completely inert. The rule I held myself to: an effect has to be
   doing a job. Frosted panels floating on a flat paper background are
   decoration cosplaying as depth - glass is only used below where there is
   genuinely moving content behind it to refract.
   ========================================================================== */

/* ---- reading progress ----------------------------------------------------
   A hairline under the sticky header that fills as the page scrolls. research
   and science are long enough that "how much is left" is a real question.

   Driven by a scroll-driven animation rather than a scroll listener, so it
   runs on the compositor: no rAF loop, no layout read on every scroll event,
   nothing added to site.js. Gated behind @supports with no JS fallback on
   purpose - this is an orientation cue, not content, and a browser without
   animation-timeline simply shows what the site showed yesterday. Shipping a
   scroll listener on all six pages to serve that case is the wrong trade. */
@supports (animation-timeline: scroll()) {
  .site-header::after {
    content: ""; position: absolute; left: 0; bottom: -1px;
    width: 100%; height: 2px;
    background: var(--line-3);
    transform-origin: left center;
    scale: 0 1;
    animation: read-progress linear both;
    animation-timeline: scroll(root block);
  }
  @keyframes read-progress { to { scale: 1 1; } }
}

/* ---- depth in the inverted band -----------------------------------------
   .bg-deep and the footer were a flat fill, which reads as a hole punched in
   the page. Two very low-opacity radials in the existing paper colour let the
   band read as lit from the upper left instead. `background-image` only - the
   `background` shorthand upstream keeps supplying the colour. */
.bg-deep,
.site-footer {
  background-image:
    radial-gradient(125% 95% at 10% -10%, rgba(250, 247, 241, 0.075) 0%, transparent 58%),
    radial-gradient(85% 70% at 104% 108%, rgba(250, 247, 241, 0.05) 0%, transparent 55%);
}

/* ---- stat cells ----------------------------------------------------------
   These are the numbers that count up on entry, and until now they were the
   only lively thing on the page that went dead the moment you reached for it.
   A tint plus a rule drawing in above the hovered cell - the table-row idiom,
   which reads as "highlighted", not as "clickable", which matters because
   these are not links. */
.stat-row > div {
  position: relative;
  transition: background-color var(--t-ui) var(--ease);
}
.stat-row > div::before {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: var(--line-3);
  transform-origin: left center;
  scale: 0 1;
  transition: scale var(--t-move) var(--ease-out);
}
.stat-row > div:hover { background-color: var(--bg-alt); }
.stat-row > div:hover::before { scale: 1 1; }

/* ---- FAQ rows ------------------------------------------------------------
   Same reasoning: a stack of question rows is scanned, and a row tint is what
   tells you which one your eye is on. */
.faq > div {
  padding-left: 14px; padding-right: 14px;
  margin-left: -14px; margin-right: -14px;
  transition: background-color var(--t-ui) var(--ease);
}
.faq dt { transition: translate var(--t-ui) var(--ease-out); }
.faq > div:hover { background-color: var(--bg-alt); }
.faq > div:hover dt { translate: 4px 0; }

/* ---- data table rows -----------------------------------------------------
   research.html carries nineteen tables, several wide enough to scroll
   horizontally. Losing your row halfway across is the actual failure mode
   here, so this one is legibility rather than polish. `tbody` matches even
   where the markup omits it - the parser inserts it. */
.table tbody tr { transition: background-color var(--t-tap) var(--ease); }
.table tbody tr:hover { background-color: var(--bg-alt); }

/* ---- diagram frames ------------------------------------------------------ */
.figure.diagram {
  transition: border-color var(--t-ui) var(--ease), box-shadow var(--t-ui) var(--ease),
              translate var(--t-ui) var(--ease-out);
}
.figure.diagram:hover {
  border-color: var(--ink-4);
  box-shadow: 0 18px 40px -28px rgba(26, 25, 24, 0.45);
  translate: 0 -3px;
}

/* ---- hero pillars: the one honest use of glass ---------------------------
   These sit directly over the drifting contour field, the two hero wash
   radials and the droplet reveal. Frosting them means the movement carries on
   underneath and comes through softened - the panel reads as a pane laid on
   the page rather than a swatch painted onto it. The flat --bg-alt fill this
   replaces covered all of that up.

   background-color, not the shorthand, and placed after the rule it is
   refining so equal specificity resolves in source order. */
.hero-pillars a {
  transition: background-color var(--t-ui) var(--ease), padding-left var(--t-ui) var(--ease-out),
              box-shadow var(--t-ui) var(--ease);
}
.hero-pillars a:hover {
  background-color: rgba(250, 247, 241, 0.5);
  /* Deliberately not transitioned. backdrop-filter cannot interpolate from
     `none`, and declaring a resting blur(0) instead would pin a live backdrop
     layer under all three pillars for the whole page - costly with the contour
     SVG animating behind them. The blur arrives at once under a tint that
     fades in, which reads fine and costs nothing at rest. */
  -webkit-backdrop-filter: blur(7px) saturate(1.2);
  backdrop-filter: blur(7px) saturate(1.2);
  box-shadow: 0 12px 30px -24px rgba(26, 25, 24, 0.55),
              inset 0 0 0 1px rgba(26, 25, 24, 0.06);
}

/* ---- footer links --------------------------------------------------------
   A wiping underline, matching what the nav already does, instead of a colour
   change alone. The gradient is the underline - it animates via background-size,
   which does not touch layout the way a border or text-decoration would. */
.site-footer a:not(.btn):not(.brand) {
  background-image: linear-gradient(var(--on-deep), var(--on-deep));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: color var(--t-ui) var(--ease), background-size var(--t-ui) var(--ease-out);
}
.site-footer a:not(.btn):not(.brand):hover { background-size: 100% 1px; }

/* Movement, as opposed to colour change, comes back out for anyone who has
   asked for less of it. The progress bar stays: it tracks the scroll the user
   is already performing rather than animating on its own. */
@media (prefers-reduced-motion: reduce) {
  .figure.diagram:hover { translate: none; }
  .faq > div:hover dt { translate: none; }
  .stat-row > div::before { transition: none; }
}

/* The nav was not covered by the site's reduced-motion handling - the
   underline wipe, the chevron, the burger morph and the card slide all still
   ran. Colour and state changes stay; only the movement stops. */
@media (prefers-reduced-motion: reduce) {
  .nav-links a::after, .nav-trigger::after,
  .nav-chev, .burger line, .mega-card, .mega-card > *, .skiplink { transition: none; }
  .nav-links a:hover::after, .nav-trigger:hover::after { transform: scaleX(1); }
  .mega-card:hover > * { translate: none; }
}

/* The caught marks blink: they drop to a plain hollow circle and come back.

   The hollow phase is pixel-identical to the marks that were never caught -
   same r, same 1.2 stroke in --line-2 - so a mark genuinely reads as toggling
   between the two states the figure already uses, rather than as a filled dot
   that merely dimmed.

   Two opacities crossfade; no colour is interpolated. Animating `fill` toward
   transparent would ramp through a washed-out grey on the way, which looks
   like a fade rather than a swap. fill-opacity against stroke-opacity trades
   one state for the other cleanly, and the stroke sits on the same path the
   filled dot already occupies, so nothing moves or resizes.

   Every caught mark blinks on the same beat, in both panels at once - no
   per-mark delay, so the whole set drops and returns together.

   Weighted heavily toward filled: about 72% of the cycle inked, with roughly
   260ms fully hollow. That ratio is doing real work now that the marks are in
   unison, because at the bottom of the blink every caught mark is hollow and
   the two panels are momentarily indistinguishable - 10-of-60 and 12-of-60
   both read as sixty empty circles. Keeping the hollow dwell short means the
   figure is legible for most of every cycle and the blink registers as a beat
   rather than as the data leaving. If it ever needs to be less than that,
   shorten the dwell here rather than slowing the cycle. */
@keyframes cohort-blink {
  0%,  60%  { fill-opacity: 1; stroke-opacity: 0; }
  68%, 80%  { fill-opacity: 0; stroke-opacity: 1; }
  88%, 100% { fill-opacity: 1; stroke-opacity: 0; }
}
.cohort-hit {
  stroke: var(--line-2); stroke-width: 1.2;
  animation: cohort-blink 2.2s ease-in-out infinite both;
}
/* animation:none would leave the resting stroke-opacity at 1 and ring every
   filled mark, so the stroke has to be switched off explicitly here. */
@media (prefers-reduced-motion: reduce) {
  .cohort-hit { animation: none; stroke-opacity: 0; }
}

/* "Backed by Entrepreneur First".

   Matched to the reference the author supplied: no pill, no border - the mark
   and the words sit directly on the page, as they do there. The mark is EF's
   own published file downscaled, not a redrawing, and it still carries the
   brand orange - it is an image, and images are not held to a text ratio.

   THE NAME IS NO LONGER ORANGE ON PAPER, and two independent problems are why.

   Contrast: the gradient stops measure 2.19 / 2.50 / 3.37:1 on --bg. All three
   are under the 4.5:1 AA minimum for text and two are under the 3:1 large-text
   floor. The previous build recorded that number in this comment and shipped
   it anyway as a deliberate trade for brand fidelity. It is not a trade this
   site can make: the whole argument of the page beneath it is that Span states
   what it can back, and a self-declared AA rule broken on the investor logo is
   the cheapest possible way to lose that.

   Palette: on the homepage this sits about 100px under the h1, which made it
   the single most prominent colour on a site whose stated system is warm paper,
   ink greys, and one violet accent on data marks. It was the only off-palette
   hue anywhere and it was at the loudest point.

   So: ink on light ground, where it reads at 16.4:1 and belongs to the palette.
   The gradient survives on the footer's dark ground, where it measures 6.62:1
   and passes - the one place it was both legible and quiet. */
.backed {
  display: inline-flex; align-items: center; gap: 9px;
  margin: 0 0 clamp(14px, 1.6vw, 22px);
  font-family: var(--sans); font-size: var(--fs-sm); line-height: 1.4;
  font-weight: 600; color: var(--ink);
}
.backed-mark {
  width: 22px; height: 22px; display: block; border-radius: 4px;
  flex: 0 0 auto;
}
.backed a {
  position: relative;
  font-weight: 600; text-decoration: none;
  color: var(--ink);
}
.backed a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: right center;
  transition: transform var(--t-ui) var(--ease-out);
}
.backed a:hover::after { transform: scaleX(1); transform-origin: left center; }
.backed a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* Footer: dark ground, so the words that are not the brand name invert. */
.site-footer .backed { color: var(--on-deep); margin: 14px 0 0; }
.site-footer .backed a:focus-visible { outline-color: var(--on-deep); }

/* THE BUG THIS SELECTOR EXISTS TO BEAT.

   `.site-footer a:not(.btn):not(.brand)` further up this file weighs (0,3,1)
   and sets background-image to a --on-deep gradient at background-size 0% 1px,
   for the underline wipe every other footer link uses. `.backed a` weighs
   (0,1,1) and loses. With background-clip:text plus color:transparent that was
   fatal: the element painted a zero-width gradient THROUGH transparent text, so
   the computed style of the only piece of social proof on the site was
   rgba(0,0,0,0) over background-size 0% 1px. "Entrepreneur First" rendered
   invisible in the footer of all six pages, leaving it legible exactly once on
   the entire site.

   :not(.btn) is carried here purely for weight - it matches everything this
   selector already matches - so the rule reaches (0,3,1) and wins on source
   order. Anything that reintroduces background-clip:text on a footer link has
   to keep this selector or the link disappears again, silently, with no visual
   diff in any state a hover test would catch. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .site-footer .backed a:not(.btn) {
    background-image: linear-gradient(95deg, #FF8A3D 0%, #FF7448 46%, #F04E23 100%);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size: 100% 100%;
    -webkit-background-clip: text; background-clip: text;
    color: transparent;
  }
}
/* The same footer rule has a :hover half - (0,4,1) - that puts background-size
   back to 100% 1px. On a clipped-to-text background that is the identical
   failure re-armed on pointer-over, so it needs beating at its own weight. */
.site-footer .backed a:not(.btn):hover { background-size: 100% 100%; }
/* And the wipe's transition would now animate the gradient's own box. */
.site-footer .backed a:not(.btn) { transition: none; }
/* currentColor is transparent above, so the underline needs the hue named. */
.site-footer .backed a::after { background: #FF7448; }

@media (prefers-reduced-motion: reduce) { .backed, .backed a { transition: none; } }

/* ==========================================================================
   CONTENT HOVER
   The chrome reacted to the pointer - nav, buttons, cards, footer, tables -
   while the actual content sat still, which is why the site read as inert
   despite all of it. These are the pieces a reader spends most of their time
   over: photographs, the numbered steps, disclosures, form fields.
   ========================================================================== */

/* Every figure's photograph now answers, not only the ones wrapped in a link.
   `scale`, never `transform`: .plate-zoom carries a GSAP yPercent parallax on
   the same element, and a transform here would replace it and kill the drift.
   The independent property composes with it instead. */
figure .plate { transition: box-shadow var(--t-move) var(--ease); }
figure .plate img { transition: scale var(--t-move) var(--ease-out), filter var(--t-move) var(--ease); }
figure:hover .plate { box-shadow: 0 26px 50px -34px rgba(26, 25, 24, 0.5); }
figure:hover .plate img { scale: 1.035; filter: saturate(1.12) contrast(1.06); }

/* The numbered steps. The number is the thing that identifies a step, so it is
   the thing that lights up; the lift is small because eleven of these sit in a
   row on the homepage and a large one would make the section swim. */
.step { transition: translate var(--t-ui) var(--ease-out); }
.step-n { transition: color var(--t-ui) var(--ease); }
.step .plate img { transition: scale var(--t-move) var(--ease-out); }
.step:hover { translate: 0 -4px; }
.step:hover .step-n { color: var(--ink); }
.step:hover .plate img { scale: 1.04; }

/* Disclosure rows: the marker already rotates on open, so hovering grows it
   slightly to say it is the control rather than decoration. */
.table-details > summary { transition: color var(--t-ui) var(--ease); }
/* One rule, not two. The [open] variant said exactly the same thing at
   higher specificity and only made the reduced-motion reset below harder to
   reason about - the marker's rotation is what changes on open, not this. */
.table-details > summary:hover::after { transform: scale(1.3); }

/* Form fields had a focus ring and nothing before it, so there was no signal
   that a field was live until you were already in it. */
.contact-form input,
.contact-form select,
.contact-form textarea { transition: border-color var(--t-ui) var(--ease), background-color var(--t-ui) var(--ease); }
.contact-form input:hover,
.contact-form select:hover,
.contact-form textarea:hover { border-color: var(--ink-3); }

@media (prefers-reduced-motion: reduce) {
  .step:hover { translate: none; }
  figure:hover .plate img, .step:hover .plate img { scale: 1; }
  .table-details > summary:hover::after { transform: none; }
}

/* Cards that are not links had no hover at all - a.card has carried one for a
   while, but the four method cards on science and their kin are plain divs, so
   the densest blocks on the page were the deadest. They lift less than a link
   does and the border does most of the work, because a big lift on something
   that cannot be clicked is a lie. The number filling in is the reward. */
.card:not(a) {
  transition: border-color var(--t-ui) var(--ease), box-shadow var(--t-ui) var(--ease),
              translate var(--t-ui) var(--ease-out);
}
.pill { transition: background-color var(--t-ui) var(--ease), border-color var(--t-ui) var(--ease), color var(--t-ui) var(--ease); }
.card:not(a):hover {
  border-color: var(--line-2);
  box-shadow: 0 18px 40px -30px rgba(26, 25, 24, 0.4);
  translate: 0 -3px;
}
.card:not(a):hover .pill {
  background: var(--ink); border-color: var(--ink); color: var(--on-deep);
}

@media (prefers-reduced-motion: reduce) { .card:not(a):hover { translate: none; } }

/* ==========================================================================
   RUNNING BAND
   A horizontal ticker of the vocabulary the site actually defines. It earns
   its place two ways: it is movement that is always on without asking for
   attention, and it fills width rather than height, which is the opposite of
   what everything else here does.

   Two identical rows, the second aria-hidden. A seamless loop needs a second
   copy to slide in behind the first, and translating by exactly -50% of the
   pair lands the duplicate precisely where the original began, so there is no
   seam and no jump. A screen reader hears the list once.
   ========================================================================== */
.marquee {
  overflow: hidden;
  display: flex; width: 100%;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--bg-alt);
  padding: clamp(16px, 2vw, 26px) 0;
  /* Fades at both edges so terms enter and leave rather than being clipped
     against a hard border. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.marquee-row {
  display: flex; align-items: center; flex: 0 0 auto;
  gap: clamp(24px, 3vw, 44px); padding-right: clamp(24px, 3vw, 44px);
  /* Display scale, not caption scale. The first version of this band was set
     at 0.82rem, which made it a footnote sliding past; in the reference it is
     the largest type on the screen at that moment and that is the whole point
     of it. Duration goes up with the width so the pace stays about 50px/s -
     the same animation over a row three times longer would read as a blur. */
  font-family: var(--display);
  font-size: clamp(1.35rem, 3vw, 2.5rem);
  font-weight: 600; letter-spacing: -0.02em; color: var(--ink);
  white-space: nowrap;
  animation: marquee-slide 60s linear infinite;
}

/* Hovering holds it still, so a term that catches the eye can be read. */
.marquee:hover .marquee-row { animation-play-state: paused; }
@keyframes marquee-slide { to { transform: translateX(-100%); } }

/* The band closes with a hairline and every section opens with one, so an
   adjacent divider-top would stack two rules into a 2px line. */
.marquee + .section.divider-top,
.marquee + .section-sm.divider-top { border-top: 0; }

/* The band holds still. This block used to be written out twice, verbatim,
   about 120 lines apart; the second copy has gone. The scramble on the
   eyebrows is skipped in JS rather than here, because it rewrites text
   rather than transforming it and CSS has nothing to undo. */
@media (prefers-reduced-motion: reduce) {
  .marquee-row { animation: none; }
}

/* Large numerals on the numbered cards, after the MYDNA reference where the
   number is a graphic element rather than a label. Only the NUMERIC pills take
   this: research.html's pills carry words - "Method · Preprint", "Headline
   finding" - and a 3rem capsule-less version of those would be a headline
   competing with the real one, so they stay capsules.

   Declared after .pill and after the card-hover rule, because both are
   single-class selectors of equal weight and source order is what decides. */
.pill-n {
  border: 0; padding: 0; background: none; border-radius: 0;
  font-family: var(--display);
  font-size: clamp(2.4rem, 4vw, 3.4rem);
  font-weight: 600; line-height: 0.82;
  letter-spacing: -0.04em; text-transform: none;
  color: var(--ink-4);
  font-variant-numeric: tabular-nums;
  margin-bottom: 2px;
  transition: color var(--t-ui) var(--ease);
}
/* No `color` here: the sequence-rail block further down sets --accent on this
   exact selector, so the --ink written here only ever described a state the
   page never reached. */
.card:not(a):hover .pill-n { background: none; border-color: transparent; }

/* The `rolling digits` block that lived here is gone with the odometer in
   site.js: mid-roll the columns spelled a different, entirely plausible
   number - 2.567 where 0.656 belongs. No amount of styling fixes that. */

/* ---- two-tone sentences ------------------------------------------------
   The claim stays in the body ink and the elaboration trails off, which is
   the device in the MYDNA reference. The site already had the inverse - a
   .ink span that pushes a phrase DARKER inside an --ink-2 paragraph - so
   these are applied only to leads that carry no .ink highlight. One device
   per sentence; three tones in one paragraph is mud.

   #6D6962 rather than --ink-3, which is two percent darker and the reason is
   bg-tint: --ink-3 measures 4.42:1 there and fails AA. This clears it on all
   three grounds - 5.10:1 on paper, 4.85 on bg-alt, 4.55 on bg-tint - while
   staying far enough from --ink-2's 10.44:1 for the two tones to read as
   different.

   Deliberately NOT applied to two kinds of sentence, whatever their shape:
   the pre-clinical disclaimer on the homepage's dark band, because "no
   cleared assay, no prospective trial" exists to be unmissable and dimming
   any of it would be the opposite of why it is there; and anything whose tail
   is the ask - "reach out", "we'd like to hear from you" - since fading out
   on the call to action is a strange thing to do on purpose. */
.trail { color: var(--ink-dim); }
.bg-deep .trail { color: var(--on-deep-2); }

/* ==========================================================================
   ACCENT
   Applied where it carries meaning rather than everywhere it would fit: the
   marks a figure is actually about, the arrow that says a link goes
   somewhere, the founders' roles, the band's separators. The base stays
   paper-and-ink - which is what the reference does too, a white site with a
   violet accent rather than a violet site.
   ========================================================================== */

/* The marks in the schematics. Attribute-matched on the ink fill they were
   authored with, so the axes, gridlines and labels keep their greys and only
   the data points move - those are the thing each figure exists to show, and
   they are what carries the accent in the reference's process diagram. */
.figure.diagram svg circle[fill="#1A1918"] { fill: var(--accent); }

.link-arrow::after { color: var(--accent); }
.role { color: var(--accent); }
.bg-deep .link-arrow::after, .bg-deep .role { color: var(--accent-on-deep); }

/* Tint, on fills only. A wash under the running band and behind the numbered
   cards on hover - the reference uses this pale lavender the same way, as a
   ground for a panel rather than as an outline or a rule. */
.marquee {
  background-image: linear-gradient(90deg,
    rgba(200, 192, 244, 0.00) 0%, rgba(200, 192, 244, 0.22) 50%, rgba(200, 192, 244, 0.00) 100%);
}
.card:not(a):hover .pill-n { color: var(--accent); }

/* Separators stay quiet; every other TERM takes the accent, which is how the
   reference's band reads - mixed ink and violet rather than one colour. The
   row alternates term, dot, term, dot, so 4n+1 selects every second term. */
.marquee-dot { color: var(--ink-4); font-size: 0.42em; }
.marquee-row > span:nth-child(4n+1) { color: var(--accent); }

/* Screen-reader-only. Needed because the label scramble replaces visible text
   with noise for the best part of a second, and a reader that reaches the page
   mid-flight should still be given the word. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ==========================================================================
   SEQUENCE RAIL  (science, under the 01-04 method cards)
   The reference runs a rail of dots with one caption lit and the rest faded
   almost to nothing. That works there because the captions are secondary; here
   the four cards ARE the explanation, and dimming three of them so one can be
   emphasised would trade the thing a reader came for against an animation. So
   the rail advances and the matching card is marked, and every card stays fully
   legible the whole time.
   ========================================================================== */
.rail {
  position: relative; display: flex; justify-content: space-between;
  margin-top: clamp(26px, 3vw, 40px);
  padding: 0 4px;
}
.rail-line {
  position: absolute; left: 0; right: 0; top: 50%;
  height: 1px; background: var(--line); transform-origin: left center;
}
.rail-dot {
  position: relative; width: 9px; height: 9px; border-radius: 50%;
  background: var(--line-2);
  transition: background-color var(--t-ui) var(--ease), scale var(--t-ui) var(--ease-out),
              box-shadow var(--t-ui) var(--ease);
}
/* The halo is the reference's tell for the active step - a soft ring rather
   than a bigger dot, so the rail's rhythm does not shift as it advances. */
.rail-dot.is-on {
  background: var(--accent); scale: 1.35;
  box-shadow: 0 0 0 6px rgba(102, 81, 214, 0.16);
}

/* The card the rail is pointing at. A mark, not a spotlight: the numeral takes
   the accent and the card lifts a little, and nothing is dimmed. */
.card.is-cur { border-color: var(--line-2); translate: 0 -3px; }
.card.is-cur .pill-n { color: var(--accent); }

/* ==========================================================================
   TILT CARD  (contact hero)
   Flies in on an angle and settles, as the contact card does in the reference.
   It sits beside "Let's talk." rather than across it - that headline is three
   words, so there is room to overlap the whitespace without covering any of it,
   and obscuring type is a liberty a marketing reel can take that an investor
   page cannot.
   ========================================================================== */
.hero .container { position: relative; }
.tilt-card {
  position: absolute; right: 28px; top: 6px; z-index: 2;
  display: flex; flex-direction: column; gap: 4px;
  padding: 16px 20px; border-radius: var(--radius);
  background: linear-gradient(150deg, var(--accent-tint) 0%, #EDEAFB 100%);
  box-shadow: 0 18px 40px -26px rgba(102, 81, 214, 0.45);
  rotate: -6deg;
  animation: tilt-in 1.1s var(--ease-out) both;
  transition: rotate var(--t-ui) var(--ease-out), box-shadow var(--t-ui) var(--ease);
}
.tilt-k {
  font-family: var(--sans); font-size: var(--fs-3xs); font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent);
}
.tilt-card a {
  font-family: var(--sans); font-size: var(--fs-sm); font-weight: 600;
  color: var(--ink); text-decoration: none;
}
.tilt-card a:hover { text-decoration: underline; }
/* Straightens under the pointer, which is the whole interaction. */
.tilt-card:hover { rotate: 0deg; box-shadow: 0 22px 46px -24px rgba(102, 81, 214, 0.5); }
@keyframes tilt-in {
  from { rotate: -20deg; translate: 16px 20px; opacity: 0; }
  to   { rotate: -6deg;  translate: 0 0;       opacity: 1; }
}
/* Below the two-column breakpoint it stops floating and simply sits in flow,
   because an absolute card over a narrow column would land on the lead. */
@media (max-width: 860px) {
  .tilt-card {
    position: static; rotate: 0deg; animation: none;
    align-self: flex-start; width: fit-content; margin-top: 20px;
  }
  .tilt-card:hover { rotate: 0deg; }
}

@media (prefers-reduced-motion: reduce) {
  .tilt-card { animation: none; rotate: -6deg; }
  .rail-dot.is-on { scale: 1; }
}

/* ==========================================================================
   TIMELINE  (company, "why now")
   The reference stacks years down a column and enlarges the active one. Span
   has no dated milestones, so the spine carries the three conditions that had
   to become true rather than invented years - a progression, not a chronology.
   Replacing the old three-column grid also retired its spans, which were
   c1 s3 / c5 s4 / c10 s3: three unequal columns with a whole column of dead
   space between the first and second.
   ========================================================================== */
.tline { list-style: none; margin: 0; padding: 0 0 0 clamp(28px, 4vw, 48px); position: relative; }
.tline-spine {
  position: absolute; left: 0; top: 6px; bottom: 6px; width: 1px;
  background: var(--line-2); transform-origin: top center;
}
.tline-item { position: relative; padding-bottom: clamp(30px, 4vw, 52px); }
/* Two columns above the breakpoint: the figure and its label on the left, the
   explanation on the right. Measured before this, the timeline held the left
   42% of the page and left 857px of the right half empty - the same fault as
   the FAQ sidebar, in code I had written myself a few commits earlier. The
   paragraph spans both rows so it sits beside the figure rather than under it. */
@media (min-width: 861px) {
  .tline-item {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    column-gap: var(--gutter);
    align-items: start;
  }
  .tline-item .stat-num { grid-column: 1; grid-row: 1; }
  .tline-item h3       { grid-column: 1; grid-row: 2; }
  .tline-item p        { grid-column: 2; grid-row: 1 / span 2; max-width: none; margin-top: 6px; }
}
.tline-item:last-child { padding-bottom: 0; }
.tline-node {
  position: absolute; left: calc(clamp(28px, 4vw, 48px) * -1); top: 10px;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--line-2);
  translate: -5px 0;
  transition: background-color var(--t-ui) var(--ease), border-color var(--t-ui) var(--ease),
              scale var(--t-ui) var(--ease-out), box-shadow var(--t-ui) var(--ease);
}
.tline-item .stat-num { transition: color var(--t-ui) var(--ease); }
/* The enlarged active entry, which is the reference's device - here it is the
   node and the figure that change, not the type size, because a heading that
   grows and shrinks as you scroll reflows the paragraph under it. */
.tline-item.is-near .tline-node {
  background: var(--accent); border-color: var(--accent);
  scale: 1.3; box-shadow: 0 0 0 6px rgba(102, 81, 214, 0.16);
}
.tline-item.is-near .stat-num { color: var(--accent); }

/* ==========================================================================
   CAROUSEL  (team)
   Two founders, so pagination cannot be the point - on the desktop grid there
   is nothing hidden to page through. Below the two-column breakpoint the pair
   stops stacking into a tall column and becomes a snap rail you swipe, which
   is the one place the device does real work. Dots appear only there.
   ========================================================================== */
.carousel-dots { display: none; }
@media (max-width: 860px) {
  .g12[data-carousel] {
    display: flex; gap: 16px;
    overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    /* 28px, literally. --gutter is the twelve-column grid's COLUMN gap and
       resolves to 14px on every phone, so this bled the rail out by 14 and
       padded it back by 14 while .container holds 28 - the rail's first card
       sat 14px inside the page edge and its last was cut 14px short of it. */
    margin-inline: -28px;
    padding-inline: 28px;
  }
  .g12[data-carousel]::-webkit-scrollbar { display: none; }
  .g12[data-carousel] > * { flex: 0 0 84%; scroll-snap-align: center; }
  .carousel-dots {
    display: flex; justify-content: center; gap: 8px; margin-top: 18px;
  }
  .carousel-dots > span {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--line-2); transition: background-color var(--t-ui) var(--ease), scale var(--t-ui) var(--ease-out);
  }
  .carousel-dots > span.is-on { background: var(--accent); scale: 1.35; }
}

@media (prefers-reduced-motion: reduce) {
  .tline-item.is-near .tline-node { scale: 1; }
}

/* ==========================================================================
   MOTION TOGGLE
   The reference puts a play/pause beside its stepper heading. The same control
   is more useful here as one switch over everything, because this site had five
   sources of indefinite auto-motion and no way to stop any of them: the band,
   the sequence rail, the blinking cohort marks, the drifting contours and the
   floating tube. Two of those paused on :hover, which is no route at all for
   someone on a keyboard.

   That is WCAG 2.2.2 - motion that starts on its own and runs past five seconds
   next to other content needs a mechanism to pause it. prefers-reduced-motion
   covers the person who set it at the OS; this covers the person who did not,
   and lets them turn it back on.
   ========================================================================== */
.motion-btn {
  display: inline-flex; align-items: center; gap: 9px;
  background: none; border: 0; padding: 6px 2px; cursor: pointer;
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--on-deep-2);
  transition: color var(--t-ui) var(--ease);
}
.motion-btn:hover { color: var(--on-deep); }
.motion-btn:focus-visible { outline: 2px solid var(--on-deep); outline-offset: 3px; }
.motion-ico {
  display: inline-flex; align-items: flex-end; gap: 2px; height: 12px;
}
.motion-ico > span {
  display: block; width: 2px; background: currentColor;
  animation: motion-bar 1.1s ease-in-out infinite alternate;
}
.motion-ico > span:nth-child(1) { height: 6px;  animation-delay: 0s; }
.motion-ico > span:nth-child(2) { height: 12px; animation-delay: .18s; }
.motion-ico > span:nth-child(3) { height: 8px;  animation-delay: .36s; }
@keyframes motion-bar { from { transform: scaleY(0.45); } to { transform: scaleY(1); } }
/* Pressed = motion off, so the icon settles flat and reads as stopped. */
.motion-btn[aria-pressed="false"] .motion-ico > span { animation: none; transform: scaleY(0.4); }

/* One switch, everything ambient. Written as html.motion-off so it outranks the
   component rules without any of them needing to know the toggle exists. */
html.motion-off .marquee-row,
html.motion-off .cohort-hit,
html.motion-off .contour-svg g,
html.motion-off .hero-tube-stack,
html.motion-off .motion-ico > span { animation: none !important; }
html.motion-off .rail-dot { transition: none; }

/* ---- team card hover, from the reference ------------------------------- */
/* Their team cards tilt, fill with a violet gradient and bring the portrait
   forward under the pointer. Span has two founders and their portraits are
   always visible, so there is nothing to reveal - what carries across is the
   tilt and the tint. */
.person .avatar { transition: scale var(--t-ui) var(--ease-out), box-shadow var(--t-ui) var(--ease); }

/* The verifiable facts under each founder: authorship, repos, profile. Set
   at the caption size and hung off a hairline so the card reads name / role
   / sentence / evidence rather than four paragraphs of equal weight. */
.person-facts {
  list-style: none; margin: 14px 0 0; padding: 0 0 0 14px;
  border-left: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 7px;
  font-size: var(--fs-sm); line-height: 1.5; color: var(--ink-2);
}
.person-facts a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px;
  text-decoration-thickness: 1px; text-decoration-color: var(--line-2); }
.person-facts a:hover { text-decoration-color: var(--accent); }
.person-facts a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.card:has(.person) { transition: rotate var(--t-move) var(--ease-out), background var(--t-move) var(--ease),
                                 border-color var(--t-ui) var(--ease), box-shadow var(--t-ui) var(--ease),
                                 translate var(--t-ui) var(--ease-out); }
.card:has(.person):hover {
  rotate: -1.2deg;
  background: linear-gradient(155deg, rgba(200,192,244,0.30) 0%, var(--bg) 62%);
  border-color: var(--accent-tint);
}
.card:has(.person):hover .avatar { scale: 1.06; box-shadow: 0 16px 34px -22px rgba(102,81,214,0.5); }

@media (prefers-reduced-motion: reduce) {
  .card:has(.person):hover { rotate: 0deg; }
  .card:has(.person):hover .avatar { scale: 1; }
  .motion-ico > span { animation: none; }
}

/* ==========================================================================
   METRIC CARD  (research, over the full-bleed plate)
   The reference floats a frosted panel over a media band with a label, a live
   figure and a sparkline. This is genuine glassmorphism rather than the
   decorative kind - there is a photograph behind it to refract, which is the
   test I applied when I declined to use it anywhere else on this site.
   ========================================================================== */
.plate-host { position: relative; }
/* A drawn ground for a .plate-host, in place of the photograph that used to be
   one. It fills the host exactly the way object-fit:cover did, so the metric
   card and the pills that absolutely position over it keep their frame. */
.plate-ground {
  display: block; width: 100%; height: 100%;
  background: var(--bg-tint);
}
@media (max-width: 860px) {
  /* The host drops its own aspect-ratio here, the same as it does for an
     image, so the ground has to carry one or it collapses to zero height. */
  .plate-host > .plate-ground {
    aspect-ratio: 21 / 9; height: auto; border-radius: var(--radius-lg);
  }
}
.metric-card {
  position: absolute; left: clamp(18px, 3vw, 44px); bottom: clamp(18px, 3vw, 44px);
  z-index: 2; margin: 0; width: min(280px, 62vw);
  display: flex; flex-direction: column; gap: 8px;
  padding: 16px 18px 14px;
  border-radius: var(--radius);
  background: rgba(250, 247, 241, 0.72);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
  backdrop-filter: blur(14px) saturate(1.15);
  box-shadow: 0 22px 46px -30px rgba(26, 25, 24, 0.55), inset 0 0 0 1px rgba(250, 247, 241, 0.55);
}
.metric-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.metric-k {
  font-family: var(--sans); font-size: var(--fs-3xs); font-weight: 600;
  letter-spacing: 0.15em; text-transform: uppercase; color: var(--ink-3);
}
.metric-dots { color: var(--ink-4); letter-spacing: 0.1em; font-size: var(--fs-xs); }
/* Overrides .stat-num's own clamp: at 2-4rem the figure would be taller than
   the card that holds it. */
.metric-fig .stat-num { font-size: clamp(1.6rem, 2.4vw, 2.1rem); }
.metric-spark { display: block; width: 100%; height: auto; overflow: visible; }
.metric-note {
  font-family: var(--sans); font-size: var(--fs-3xs); line-height: 1.4; color: var(--ink-3);
}
/* Below the breakpoint it stops floating: a 280px panel over a 21:9 band on a
   phone covers most of the photograph it is supposed to be sitting on.

   Two things were wrong with that, and they compounded. The breakpoint was
   720 while everything else about this layout - .g12, .hero-body, the nav -
   turns at 860, so between the two the card was still floating over a band
   too narrow to hold it. And below 720 it went `position: static` INSIDE a
   .plate, which is a fixed 21:9 box with overflow:hidden - at 375px that box
   is 136.7px tall and entirely filled by the photograph, so the card laid
   itself out below the bottom edge and was clipped away in full, taking the
   "All demo data synthetic" figcaption with it. The one caption on the page
   that says the numbers beside it are not real was invisible on a phone.

   So the plate stops being that box at this width: the 21:9 crop moves onto
   the image, the host takes its height from its content, and the card is
   inside the border box rather than beyond it. border-radius goes to the
   image for the same reason - .plate-mask's clip-path reads the host's
   computed radii, and a rounded host would shave the card's bottom corners
   as the wipe finishes. */
@media (max-width: 860px) {
  .plate-host {
    aspect-ratio: auto; overflow: visible;
    background: none; border-radius: 0;
  }
  .plate-host > picture > img,
  .plate-host > img {
    aspect-ratio: 21 / 9; height: auto;
    border-radius: var(--radius-lg);
  }
  .metric-card {
    position: static; width: auto; margin-top: 14px;
    background: var(--bg-alt); backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow: none; border: 1px solid var(--line);
  }
}

/* ==========================================================================
   AVATAR BADGE  (team)
   Circular thumbnail with a violet arrow badge, as the reference's webinar
   cards use. The badge is a real outbound link to each founder's LinkedIn -
   those URLs were already in the page's JSON-LD, so nothing had to be invented
   to give the arrow somewhere to point.
   ========================================================================== */
/* The avatar frame itself is styled in team.html's inline block, which loads
   after this file and would win on equal specificity anyway. Only the badge
   lives here. */
.av-badge {
  position: absolute; right: -3px; bottom: -3px;
  width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: #fff;
  font-size: var(--fs-2xs); line-height: 1; text-decoration: none;
  box-shadow: 0 0 0 3px var(--bg);
  transition: translate var(--t-ui) var(--ease-out), background-color var(--t-ui) var(--ease);
}
.av-badge:hover { translate: 2px -2px; background: #5341BC; }
.av-badge:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) { .av-badge:hover { translate: none; } }

/* ---- floating pills over imagery --------------------------------------- */
/* The reference floats CTA pills over its media band, opposite the metric
   card. These go to the two artefacts the page is actually about - the paper
   and the benchmark - rather than being generic calls to action, and they are
   frosted to match the card they sit opposite so the plate reads as one
   composition and not as two unrelated overlays. */
.plate-pills {
  position: absolute; top: clamp(14px, 2.4vw, 28px); right: clamp(14px, 2.4vw, 28px);
  z-index: 2; display: flex; flex-wrap: wrap; gap: 8px;
}
.ppill {
  display: inline-flex; align-items: center;
  padding: 8px 14px; border-radius: 999px;
  background: rgba(250, 247, 241, 0.78);
  -webkit-backdrop-filter: blur(12px) saturate(1.15);
  backdrop-filter: blur(12px) saturate(1.15);
  box-shadow: inset 0 0 0 1px rgba(250, 247, 241, 0.6), 0 10px 24px -18px rgba(26,25,24,0.6);
  font-family: var(--sans); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.06em; color: var(--ink); text-decoration: none;
  transition: background-color var(--t-ui) var(--ease), translate var(--t-ui) var(--ease-out);
}
.ppill:hover { background: var(--bg); translate: 0 -2px; }
.ppill:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* 860, with the metric card and the rest of this layout. Between 720 and 860
   the pills were still floating over a band narrow enough that they sat on
   top of the card opposite them. Both destinations - the paper and the
   benchmark - are linked in the page body immediately above and below this
   plate, so nothing is lost by dropping them here. */
@media (max-width: 860px) { .plate-pills { display: none; } }

/* ---- alternating two-tone ------------------------------------------------
   Earlier I applied .trail only to a trailing clause and left the leads that
   carried an .ink highlight alone, on the grounds that three tones in one
   sentence is mud. Looking at the reference again, it is not doing three: its
   base IS the dark tone, and it alternates dark and grey across four clauses.
   .lead-alt does the same - it lifts the paragraph's base from --ink-2 to
   --ink so that .trail is the only other value in play, and the sentence reads
   as two tones rather than a mid-grey with things happening on both sides. */
.lead.lead-alt { color: var(--ink); }
.lead.lead-alt .ink { color: var(--ink); font-weight: 500; }

/* ---- FAQ rows use the width ---------------------------------------------
   Measured off a render: the FAQ used to sit in an 8-column well beside a
   3-column label, and with .faq dd capped at 64ch the ink covered about 40% of
   the column for 900 unbroken pixels. That was the empty middle of the science
   page - not blank rows, which is why a full-width blankness scan found only
   4% of the page empty, but a whole third of the measure with nothing in it.

   The label has moved above the list, as every other section on the site
   already does it. Here each row becomes question-left, answer-right, so both
   halves carry text instead of one. Single column below the breakpoint, where
   a 5fr question column would be about 150px wide. */
@media (min-width: 861px) {
  .faq > div {
    display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    column-gap: var(--gutter); align-items: start;
  }
  .faq dt { margin-bottom: 0; }
  .faq dd { max-width: none; }
}

/* The double-helix band CSS lived here. It is gone with the generator in
   site.js: it was the one visual cliche in an otherwise disciplined system,
   and it was repeated on five of the six pages, which made it the
   most-repeated element on the site. */

/* ---- the three controls the coverage audit found bare -------------------
   Everything else on the site answers both the pointer and the keyboard; these
   three had one side or the other missing, which is exactly the kind of hole
   that survives when hover states are added component by component rather than
   checked as a set. */

/* The burger had the open/close morph and a focus ring, but nothing at all
   before you commit to clicking it. */
.nav-toggle { transition: color var(--t-ui) var(--ease); }
.nav-toggle:hover { color: var(--ink-3); }

/* Disclosure rows were the one control with no focus ring on the site - a
   keyboard user tabbing onto one got whatever the browser chose. Inset,
   because the summary spans the full table width and an outset ring would
   sit on the rule above it. */
.table-details > summary:focus-visible {
  outline: 2px solid var(--ink); outline-offset: -3px;
}

/* The announcement bar is a link across the full width of the page and the
   first thing in the tab order after the skip link, so it needs to show where
   focus is. Inset for the same reason, and it sits on the dark band so the
   ring inverts. */
.announce:focus-visible {
  outline: 2px solid var(--on-deep); outline-offset: -4px;
}
