/* Bharat Digital — site stylesheet
   Implements the "New Website 2026 System" (Figma export, July 2026):
   paper & ink — cream page ground, 1px ink rules, radius-0 cards,
   pill buttons (r30), service pills (r21), folder tabs (r12).
   Type: Libertinus Serif Display (display) · Libertinus Serif (section heads,
   italic accents) · Public Sans (UI & body) · DM Mono (data & labels) ·
   Spartan Bold (wordmark). Illustration colours (mint/periwinkle/terracotta/sky
   + pastels) live only inside imagery — never as UI fills or type. */

/* Fonts are requested from the PAGE, not from here. An @import inside a
   stylesheet cannot start until this file has downloaded and parsed, so the
   chain was html -> style.css -> font css -> font files: three serial round
   trips before a single word could be set in the right face. The <link> in
   each <head>, with preconnect, starts the font request in parallel with
   this file instead. See partials/nav.html's sibling markup in each page. */

:root {
  /* ⚠️ THE PAPER GRAIN IS ONE DIAL, AND IT IS THESE TWO NUMBERS (5 Oct 2026,
     the user: "the noise particle that's been used is too big — we want a very
     fine noise particle which creates a very subtle papery effect").
     The tile is a 160x160 feTurbulence plate with a discrete 1/0 alpha, and it
     was drawn at 160px, i.e. 1:1, so one particle was ~1.4 CSS px — coarse
     enough to read as texture ON the colour rather than as the colour's own
     tooth. Drawing the SAME plate into an 80px box halves every particle to
     ~0.7px without touching the turbulence, and the ink comes down with it,
     because a finer grain at the same opacity only reads as a flat darkening.
     ⚠️ SIZE IS THE DIAL, NOT `baseFrequency`. The identical tile is inlined in
     three rules (.lb-tab::after, .lb-sheet::after, .fel-card::after) and in
     the `lbGrain` SVG filter; changing the frequency means changing it in four
     places and they drift. Scaling the tile is one number, here. */
  --grain-size: 80px;
  --grain-ink: .15;
  --serif-display: 'Libertinus Serif Display', 'Libertinus Serif', Georgia, serif;
  --serif: 'Libertinus Serif', Georgia, 'Times New Roman', serif;
  --sans: 'Public Sans', -apple-system, Helvetica, Arial, sans-serif;
  --mono: 'DM Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --wordmark: 'Spartan', 'League Spartan', var(--sans);

  --bg:      #FFFBF2;   /* Paper — page ground */
  --surface: #FCF8EF;   /* Paper soft — panels */
  --ink:     #232323;   /* Graphite — display type & body */
  --black:   #000000;   /* Ink — rules, logo, CTA */
  --muted:   #696969;   /* secondary text */
  --line:    #000000;   /* hairline rules are ink */
  --accent:  #606C47;   /* olive — labels, small accents */
  --blush:   var(--brand-terracotta-soft);   /* service pill fill */
  /* Labs folder tabs — Figma 213:2228/2229. The one UI use of these two hues,
     the way --terracotta-card is for the About panel; the folder body itself
     is --terracotta-card, which is why an open tab simply takes that fill.
     ⚠️ NAMESPACED BECAUSE `--sky` IS ALREADY TAKEN — twice, by the illustration
     palette (#8BD6F8) and again inside a scoped block (#F5EEDF). Declaring a
     third `--sky` up here did not error, it just lost to the one further down
     the same :root, and the tab quietly rendered the illustration blue instead
     of the file's. Grep before naming a token. */
  /* Fieldnotes bands — Figma 213:2394/2397/2400. Three fills that exist for
     one object and carry the section's only large areas of colour. --fn-sage
     IS --drawer by value (#B3C197): the file reuses the drawer's own green
     here, so this is an alias rather than a fourth green, and it must follow
     --drawer if that ever moves. The other two are new and are the design's,
     not the brand's — a warm paper grey and a blush, both chosen to read as
     tinted stock rather than as brand fills. Black type clears AA on all
     three (12.4:1, 15.4:1 and 15.1:1). */
  --fn-sage: var(--brand-green);
  --fn-grey: #E0E0DB;
  /* ⚠️ THE THIRD BAND IS BLUE, NOT THE FILE'S PINK (23 Sep 2026). Chosen to
     sit at sage's tonal weight rather than the pink's — sage is L.500 and the
     pink L.80, so the pink read as a highlight rather than a third sheet.
     #A8C6DA is L.538, and ink on it is 8.80:1 against sage's 8.22:1. */
  --fn-blue: #A8C6DA;
  --folder-sky:        var(--brand-blue);
  --folder-periwinkle: var(--brand-violet-deep);
  --drawer:  var(--brand-green);   /* drawer body — Figma 538:3323. An alias
                           now: this WAS the one true #B3C197 and the brand
                           green was a near-miss #BBD1B3 beside it; the scale
                           above took this value and the two collapsed. */
  /* THE THREE PLANES OF AN OPEN DRAWER, and they must differ or the box
     reads flat. Front faces the reader (--drawer, untouched); the side
     board is a vertical plane turned away from it; the top is the box's
     own opening, looked down into, so it is the darkest of the three.
     The side being left at --drawer is what made the open drawer read as
     one lopsided silhouette with a stray diagonal across its corner. */
  --drawer-side: color-mix(in srgb, var(--brand-green) 84%, #000);
  --drawer-in:   color-mix(in srgb, var(--brand-green) 62%, #000);
  --box:     #FCF8EF;   /* image placeholder fill */
  --terracotta-card: var(--brand-terracotta);  /* About: the origin panel — the one UI use of an illustration colour */

  /* illustration palette — imagery only (kept for inline --fc/--bc compatibility) */
  --terracotta: var(--brand-terracotta);
  /* adopted brand hues. The green is the brand's 6E995D lifted to L76 so ink
     text clears AA on it as a fill — at L48 neither ink nor paper is legible. */
  /* ⚠️⚠️ THE BRAND SCALE — ONE VALUE PER FAMILY, THREE STEPS EACH.
     ⚠️ THE FOUR BASES ARE FIGMA 803:565 AND ARE NOT OURS TO CHOOSE (6 Oct
     2026, the user: "standardized colors across the platform: can we
     standardize the brand color across all the pages?"). That node is the
     file's own swatch set and it names them: --bd-purple #B6C4FD,
     --bd-green #B3C197, --bd-terracotta #EA9B7C, --bd-blue #90CBFF. Two of
     the four moved when it was read: the purple from #B9B0FA and the
     terracotta from #DC8462.
     Both of the file's values were already in this stylesheet before the
     unification pass of 5 Oct — #B6C4FD was the About banner's ground and
     #EA9B7C was the origin spread's leaf — and both were folded INTO the
     wrong survivor on that pass, because it collapsed on frequency rather
     than on the file. They are back, and they are the canon now.
     If these ever look wrong, re-read 803:565; do not re-pick them here.

     The deep and soft steps are OURS, derived to carry the bases:
     --brand-green-deep is the house olive raised to the terracotta's tonal
     weight (see the note below it), --brand-violet-deep is the periwinkle the
     file already uses for the labs tab and the Chandrayaan stamp, and the
     softs are tints for backplates.

     ⚠️ NOTHING ELSE MAY DECLARE A GREEN, TERRACOTTA, PURPLE OR BLUE. The
     legacy names below are aliases so existing rules keep working; point a new
     rule at the --brand-* token, never at a hex.
     The --field-* palette, the illustration colours and the `data-ink` dither
     values are NOT part of this. They are imagery: transcribed from Figma
     frames or tuned against each other for weight in a 1-bit dither, and they
     carry no type. Two of them deliberately differ from the brand value they
     sit next to, and those notes say why. Do not fold them in.

     Contrast of --ink (#232323) on every step, measured not estimated:
       terracotta 7.1:1 · terracotta-soft 10.5:1
       green 8.2:1 · green-deep 5.6:1 · green-soft 10.4:1
       violet 9.2:1 · violet-deep 7.7:1 · violet-soft 12.0:1
       blue 9.1:1
     Every step carries body copy. The new terracotta is LIGHTER than the one
     it replaced (7.1 against 5.4), so nothing that passed before fails now. */
  --brand-terracotta:      #EA9B7C;
  --brand-terracotta-soft: #E7CEC1;
  --brand-green:           #B3C197;
  --brand-green-deep:      #92A06A;
  --brand-green-soft:      #CFD6B8;
  --brand-violet:          #B6C4FD;
  --brand-violet-deep:     #A2B2FD;
  /* ⚠️ RE-DERIVED WITH THE BASE (5 Oct 2026). This was #D9CCFF, a lilac that
     belonged to the old #B9B0FA. Against #B6C4FD it read pink, which is the
     one thing a soft step must not do — it is the same colour with paper
     through it, not a neighbouring one. A 48% tint of the base on --bg. */
  --brand-violet-soft:     #DCE1F7;
  --brand-blue:            #90CBFF;
  /* Olive, lifted to serve as a GROUND. The house olive (--accent #606C47)
     is L14 — 3.76:1 under black, 4.27:1 under paper, so it carries neither
     and fails as a full-bleed screen. #92A06A is the same olive raised to
     terracotta's exact tonal weight (7.42:1 vs 7.52:1 under black), so the
     Labs screen reads with the same force as the Services one instead of
     merely being green. --olive-soft is its blush: matched to --blush's
     luminance (.648 vs .654) so a sheet that has gone behind sits back by
     the same amount on both pages. */
  --brand-olive:      var(--brand-green-deep);
  --olive-soft:       var(--brand-green-soft);
  /* The fellowship mission wash, straight off the Figma frame fill. These are
     NOT the brand fills and should not be substituted for them: --wash-warm is
     hotter than --brand-terracotta (#EA9B7C) and --wash-cool is a purer violet
     than --brand-violet (#B6C4FD), which is blue-leaning. They exist for one full-bleed
     gradient and carry no type, which is the only reason a fifth and sixth hue
     are allowed in at all. --wash-paper is --bg by value, deliberately: the
     gradient's core has to disappear into the page. */
  /* ⚠️ --wash-* ARE NO LONGER THE MISSION'S. The radial wash they were written
     for was replaced by the column field below on 20 Sep 2026. --wash-paper
     and --wash-lilac are still live elsewhere (a panel ground and a sheet
     fill), which is why they stay; --wash-warm and --wash-cool are kept only
     to hold the named set together. Nothing on the fellowship page reads any
     of them now. */
  --wash-paper:  #FFFBF2;
  --wash-warm:   #FFA379;
  --wash-cool:   #AFAFFF;
  --wash-lilac:  var(--brand-violet-soft);

  /* The fellowship mission field, straight off Figma frames 505:1074 / 505:1048
     (20 Sep 2026). These are NOT the brand fills and must not be substituted
     for them — --field-warm is hotter than --brand-terracotta (#EA9B7C), and
     --field-cool/-deep sit either side of --brand-violet (#B6C4FD) rather than
     on it.
     They exist for one full-bleed field that carries no type of its own, which
     is the only reason this many extra hues are allowed in at all.

     --field-paper is --bg by value, deliberately AND at zero alpha: the top of
     every column has to disappear into the page rather than fade to a tint of
     it.

     This replaced a single radial wash (--wash-paper/warm/cool/lilac). Only
     --field-warm carries over from it unchanged. */
  --field-paper: #FFFBF2;
  --field-peach: #FFCFB5;
  --field-warm:  #FFA379;
  --field-dusk:  #DDB5BB;
  --field-cool:  #BAC7FD;
  --field-deep:  #A2B2FD;
  --sage:       #A4E6B4;
  --sky:        #8BD6F8;
  --terracotta-t: #EFC0AE;
  --sage-t:       #D3F2DD;
  --sky-t:        #C4E9FB;
  --line-d:       #696969;

  /* The scatter brush, as tileable masks — the stroke the fellow record is
     drawn in, on the card and on every rule of the index. See "the textured
     rule" under PAST COHORTS for why these are masks and not backgrounds. */
  --tex-h: url(assets/rule-texture-h.svg);
  --tex-v: url(assets/rule-texture-v.svg);
  --tex-len: 128px;   /* tile length — the repeat pitch */
  --tex-t: 3px;       /* tile thickness: ~1.4px core plus its fringe */

  --r:  0px;            /* cards & rules are square */
  --rs: 0px;
  --r-tab:  12px;       /* folder tab */
  --r-pill: 21px;       /* service pill */
  --r-btn:  30px;       /* button */
  --shadow: none;
  /* the system's only elevation — overlays (nav sheet, drawer, modal) ONLY.
     Cards and panels stay flat: --shadow above remains none. */
  --shadow-overlay: 0 18px 40px -12px rgba(20,13,13,.18), 0 2px 6px rgba(20,13,13,.06);
  /* One page measure for the whole site. --maxw is the outer column and
     --gutter its side padding; content width is therefore
     min(100%, --maxw) - 2*--gutter at every viewport. The nav shell and the
     footer are both derived from the same two numbers, so nothing can drift.
     1120/28 made the standard pages too narrow; About's wrap-wide (1728 at
     5.5vw) made that one page too wide. 1240/40 sits between them. */
  --maxw: 1240px;
  --gutter: clamp(20px, 3vw, 40px);
  --bar-h: 68px;      /* the shell's resting height */
}

* { box-sizing: border-box; }
/* Form controls do not inherit `font-family` from the page — left alone, every
   <button> falls back to the UA face. Only one is visible today (the nav
   panel's close ✕, which was rendering in Arial) but the next one would too. */
button, input, select, textarea { font-family: inherit; }
html {
  scroll-behavior: smooth;
  /* Kills the browser's elastic over-scroll, which is what put the beige back.
     Pulling past the end of the document rubber-bands the whole page and
     exposes the CANVAS beneath it — and the canvas carries body's propagated
     background, i.e. --bg. The fixed reveal panel cannot help: it is painted
     inside the page, and the rubber-band gap is outside it.
     granola.ai sets exactly this, which is how they own the over-scroll
     entirely. Wheel events still fire at the boundary, so the footer peek is
     unaffected — it simply replaces the native bounce with our own. */
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  font-family: var(--sans);
  font-size: 16px;
  color: var(--ink);
  background: var(--bg);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }

/* ═══ THE TYPE SCALE ═══════════════════════════════════════════════════════
   Established on fellowship.html (22 Sep 2026), extended to index.html
   (23 Sep 2026) and about.html (25 Sep 2026). Full rationale in
   ../03-design-system.md. Eight tiers — if a new heading does not fit one of
   them, the tier is wrong, not the heading.

     HERO        display · clamp(34, 5.2vw, 104) · lh 1.04 · UPPERCASE
                 ⚠️ NO LIVE USER SINCE 23 Sep 2026. `.bd-hero-head` was the home
                 hero and the collage it belonged to is gone; `.hero-claim`
                 that replaced it is sized in `cqw` off the border artwork, not
                 off the page, because the ornament's opening is what the line
                 has to fit. Kept here because PAGE TITLE quotes its rate.
     PAGE TITLE  display · clamp(40, 5.2vw, 76)  · lh 1.04 · UPPERCASE
                 `.page-head h1`. Same 5.2vw rate as the hero, lower cap.
       …SPLIT    display · clamp(36, 4.6vw, 66)  · lh 1.08 · sentence case
                 ⚠️ THE VARIANT ACTUALLY IN USE. `.page-head.split h1`. In a
                 split header the column already sets the measure, so the 15ch
                 cap comes off and the size steps down; sentence case sets
                 narrower, which is what pays for it. The fellowship page —
                 the scale's own reference — renders THIS, not the row above.
                 ⚠️ NEITHER PAGE-TITLE ROW EVER RUNS THE FULL MEASURE: one
                 caps at 15ch, the other sits in a half column. A statement
                 that runs the whole measure is the STATEMENT line below.
     STATEMENT   serif · clamp(24, 3.125vw, 60) · lh 1.1 — 45px at 1440
                 `.bd-hist-head`, `.svc-chap`, `.ab-head h1`. A long sentence
                 set large across the WHOLE measure. At the page title's own
                 66px this is twice the area of a page title and reads as
                 shouting, which is exactly what About did.
     SECTION     serif · clamp(30, 3.2vw, 44) · lh 1.18 · ls -.2px · mb 14
                 `h2` and the four scene heads. Scene heads swap the size term
                 for `clamp(30, min(3.2vw, 5.4svh), 44)` and change NOTHING
                 else — a pinned frame is short, not a different typeface.
     CLAIM /     serif · lh 1.22 · ls -.2px · 36 IN A PANEL, 27 IN A CARD
     CARD TITLE  `.ex-item h3` and `.ab-card-copy h3` at 36; `.lb-title`,
                 `.rail-card h3`, `.fn-q`, `.vs-row h3`, `.fel-name` and
                 `.crew-card .nm` at 27; `.plc-claim` takes its column's.
                 ⚠️ ONE FACE. These were two tiers and two faces, and the
                 faces crossed at the same sizes: `.lb-title` display at 27
                 beside `.rail-card h3` serif at 27, `.ex-item h3` display at
                 36 beside `.ab-card-copy h3` serif at 36. Same job, same
                 size, different face — which is what reads as the cards
                 being different from one page to the next. Display is now
                 the two page-level tiers and the two `cqw` compositions
                 (`.hero-claim`, `.env-tname`), and nothing else.
     LEAD        sans · clamp(16, 1.25vw, 18) · lh 1.6
     BODY        sans · 16 · lh 1.6
                 TWO body sizes, ONE leading. Not 15, not 1.45.
     MONO LABEL  mono · weight 500 · ls .14em · UPPERCASE
                 Only the SIZE steps by role: 10 · 11 · 12.5. The voice never
                 varies — a mono line at 400 or at .12em is a drift, not a
                 quieter label. Use colour to make a label recede.

   Scene type sized in `cqw` or against `svh` (the drawer cards, the labs
   folder, the tracks envelope, the chapter line) is COMPOSITION, not a tier:
   it scales with its container by design. It still takes the mono voice. */
/* type — display serif titles, serif section heads, mono labels */
h1 { font-family: var(--serif-display); font-weight: 400; letter-spacing: 0; }
/* ⚠️ 14px, NOT 10 — AND THIS IS THE ONE SITE-WIDE CHANGE IN THE FELLOWSHIP
   TYPE PASS. The page has two kinds of section head, an `h2` and an `.eyebrow`,
   and they sat different distances from the line under them: 10px and 14px. One
   of the two numbers had to go, and 10px under a 44px serif was the tight one.
   Everywhere else on the site an h2 is followed by a grid or a block carrying
   its own larger top margin, which collapses over this and is unaffected; the
   only thing that moves is an h2 sitting directly on a paragraph, which is
   exactly the case being aligned. */
h2 {
  font-family: var(--serif); font-weight: 400; font-style: normal;
  font-size: clamp(30px, 3.2vw, 44px); line-height: 1.18; letter-spacing: -0.2px; margin: 0 0 14px;
}
h3, h4 { font-family: var(--sans); font-weight: 600; letter-spacing: 0; }
h3 { font-size: 17px; }
h4 { font-size: 15px; }
.logo { font-family: var(--wordmark); font-weight: 700; }
p { margin: 0 0 14px; }
a { color: inherit; }
p a, .article-body a { color: var(--ink); text-decoration-color: var(--accent); text-underline-offset: 3px; }

.wf-note { display: none; }

/* --- header / nav — ONE expanding surface.
       At rest the shell is exactly the bar. Opening a menu grows the same box
       downward: radius and elevation appear, the page shows either side of it.
       Markup lives once in partials/nav.html — run build-partials.py. --- */
header.site {
  position: sticky; top: 0; z-index: 100;
  height: var(--bar-h);
  /* ⚠️⚠️ THE BAND IS PAPER ACROSS THE WHOLE WIDTH, NOT JUST THE SHELL (5 Oct
     2026, the user: "the Constitution pillar image goes behind the navigation
     at the top in my browser; in the Claude Code browser it doesn't").

     It was `background: none`, and the only opaque thing in the bar was
     `.nav-shell` — which is the CONTENT measure, `min(100%, --maxw) - 2 *
     --gutter`, i.e. 1160px. So on any window wider than about 1240 the header
     had a transparent strip down each side: 380px a side at 1920. The hero is
     full-bleed, so as it scrolled up the gate and the sky ran straight through
     those strips and the nav looked like it was sitting ON the artwork.

     ⚠️ THAT IS WHY IT DID NOT REPRODUCE IN THE PREVIEW PANE. The pane is
     narrower than the measure, so the shell spans the full width and there are
     no strips to show through — the bug only exists above ~1240px. Check this
     one at 1920 or it will look fixed when it is not.

     The ground is the same paper either way, so nothing changes at the top of
     the page; it only stops the bleed once content passes under the bar.
     `body.is-screen header.site` still overrides this during the services
     accent scene — it is (0,2,1) against this rule's (0,1,1). */
  background: var(--bg);
}
/* The paper the bar and its sheets sit on, edge to edge.
   The shell is only as wide as the content measure, so with the rule running
   full width the page's own edges — the banner's top, a section's rule —
   stayed visible in the 58px gutters either side of an open sheet, showing
   THROUGH the header band. The band is the full width of the page; only its
   contents are held to the measure. */
header.site::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0;
  height: calc(var(--bar-h) + var(--panel-h-live, 0px));
  background: var(--bg);
  transition: height .28s cubic-bezier(.4, 0, .2, 1);
}
header.site.open { --panel-h-live: var(--panel-h, 0px); }
body.drawer-open header.site { z-index: 320; }

.nav-shell {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  /* The bar spans the VISIBLE content, not the wrapper box. .wrap is
     --maxw wide but carries 28px of its own padding, so text and cards start
     28px inside it; matching the box still left the bar proud by that much on
     each side. --maxw - 56 is the measure the reader actually sees. The nav
     row and the panel drop their horizontal padding to suit, so the logo and
     the menu contents land on the same left edge as the page. */
  width: calc(min(100%, var(--maxw)) - 2 * var(--gutter));
  background: var(--bg); border-radius: 0;
}
/* --- the bar's rule, and the thing that moves ------------------------
   The shell used to announce an open menu with a radius and a drop shadow.
   A shadow is elevation, and this system elevates nothing but true overlays
   — the bar is part of the page, not floating over it. So the bar simply
   carries the page's own rule along its bottom edge, drawn in the same
   scatter brush as every other rule on the site.

   Opening a menu does not add anything. The shell grows, and because the
   rule is pinned to bottom:0 it TRAVELS DOWN with it on the same .28s — the
   line itself moves and the options arrive underneath it. It firms from a
   half-tone to full ink as it goes, so the movement reads as deliberate.

   It runs EDGE TO EDGE, not at the content measure. This system already
   makes that distinction: structural divisions are full-bleed (.bd-hero and
   .bd-hist-wrap both rule right across the viewport) while rules kept to the
   measure belong to content (.proj-list's bottom border). A header's bottom
   edge is structural. Held to the shell's width it stopped 58px short on
   each side, and since the shell's fill IS the page colour, that line is the
   only thing defining the open sheet — so stopping it short left the sheet
   reading as a box floating in the middle of nothing.

   It therefore lives on header.site, which is full width, rather than on the
   shell, which is centred at the measure. It travels by moving its `top`
   rather than by being pinned to a growing box: --panel-h is the same
   number nav.js measures for the sheet, so the line and the sheet arrive
   together on the same .28s. No vw units, so a scrollbar can never push the
   page sideways. */
/* No z-index: -1 here. At -1 the rule sat behind the shell's own fill, so
   the 1.5px of it above the shell's bottom edge was covered and the 1.5px
   below was not — the line came out thin under the sheet and full weight in
   the gutters either side of it, which read as two different lines. */
header.site::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: var(--bar-h); margin-top: calc(var(--tex-t) / -2); height: var(--tex-t);
  background: var(--dot); pointer-events: none;
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
  -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
  transition: top .28s cubic-bezier(.4, 0, .2, 1), --dot .28s ease;
}
header.site { --dot: rgba(35, 35, 35, .45); }
header.site.open { --dot: var(--black); }
header.site.open::after { top: calc(var(--bar-h) + var(--panel-h, 0px)); }

.nav { display: flex; align-items: center; gap: 24px; height: var(--bar-h); padding: 0; }
/* The standardised lockup — mark, gradient blade and the drawn wordmark, as
   one artwork. It used to be the mark beside live Spartan text, which is a
   reconstruction of the logo rather than the logo, and it made the header
   wait on a webfont to draw the brand. assets/logo-lockup.svg is the dark
   export inked for paper: same geometry, #FDFDFD swapped for currentColor,
   viewBox cropped to the artwork so it packs to its ink and not to Figma's
   frame padding. */
.logo { display: inline-flex; align-items: center; color: var(--black); }
/* The wordmark is set on two lines inside the lockup, so it needs real
   height to be legible — at 30px each line was 12px tall. */
.logo img { display: block; height: 38px; width: auto; }

.nav-main { flex: 1; display: flex; justify-content: center; }
.nav-main ul { list-style: none; display: flex; gap: 2px; margin: 0; padding: 0; }
/* --- the top-level items ---------------------------------------------
   Five items, two of them links (Home, About) and three of them BUTTONS that
   open a sheet. They share .nav-top so they are one thing to look at.

   Hovering draws a line under the label rather than filling a grey pill. The
   pill was borrowed vocabulary — nothing else on this site signals hover with
   a tinted rounded rectangle — where the line is the gesture .proj-row
   already uses on Projects, down to the origin flip: it enters from the left
   and leaves to the right, so it sweeps THROUGH rather than rewinding. The
   current section keeps its line drawn at rest. */
.nav-top {
  position: relative; display: block;
  background: none; border: 0; margin: 0; cursor: pointer;
  text-decoration: none; color: var(--ink);
  font-family: var(--sans); font-size: 15px; font-weight: 500; white-space: nowrap;
  /* Home and About are <a>; the three section parents are <button>. Left to
     themselves the anchors inherit the body's 24px line-height while the UA
     gives the buttons `normal` (~17.5px) — a 6.5px difference in box height,
     which dropped both the labels and their underlines out of line with the
     rest. Every metric that differs between the two elements is pinned here. */
  line-height: 1.2; letter-spacing: inherit; text-align: center;
  padding: 8px 4px 9px;
}
.nav-main ul { gap: 22px; align-items: center; }
/* Drawn in the SAME scatter brush as the rule below it. A 1px hairline here
   against a 3px textured rule forty pixels away read as two unrelated marks
   rather than one system.

   It is revealed by WIDTH, not by scaleX: the mask tiles from the pseudo's
   own left edge, so growing the width uncovers the texture where scaling it
   would stretch it. Anchoring flips with the direction — right-anchored at
   rest so it collapses to the right, left-anchored on hover so it draws from
   the left — which is the same sweep-through .proj-row gets from flipping
   transform-origin. */
.nav-top::after {
  content: ""; position: absolute; right: 0; left: auto; bottom: 3px;
  width: 0; height: 2px; background: var(--black);
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
  -webkit-mask-size: 96px 2px; mask-size: 96px 2px;
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
  transition: width .42s cubic-bezier(.33, 1, .68, 1);
}
.nav-top:hover::after,
.nav-top:focus-visible::after,
li.open > .nav-top::after,
.nav-top.active::after {
  left: 0; right: auto; width: 100%;
}

.nav-end { display: flex; align-items: center; gap: 24px; }
.nav-contact {
  font-family: var(--sans); font-size: 15px; font-weight: 500;
  color: var(--muted); text-decoration: none; white-space: nowrap;
  transition: color .15s ease;
}
.nav-contact:hover, .nav-contact.active { color: var(--ink); }
.nav-cta {
  font-family: var(--sans); font-size: 14px; font-weight: 500;
  background: var(--black); color: #fff; padding: 10px 20px; line-height: 1.4;
  border-radius: var(--r-btn); text-decoration: none; white-space: nowrap;
  transition: background .15s ease;
}
.nav-cta:hover { background: #232323; }

/* --- generic boxes / image placeholders ---
   No keyline. A dashed edge round a picture reads as a crop mark, and round
   a slot that will hold one it reads as scaffolding; the soft fill and the ▢
   say "image to come" without drawing a box nobody asked for. --- */
.box {
  background: var(--box); border: 0; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  text-align: center; color: #A39C8C; font-size: 13px; padding: 20px;
}
.imgph { min-height: 180px; }
.imgph::before { content: "▢"; font-size: 22px; }

.section { padding: 72px 0; }
.eyebrow {
  display: block; font-family: var(--mono);
  font-size: 12.5px; text-transform: uppercase; letter-spacing: .14em;
  color: var(--accent); font-weight: 500; margin: 0 0 14px;
}
.muted { color: var(--muted); }

.grid { display: grid; gap: 24px; }
.g2 { grid-template-columns: 1fr 1fr; }
.g3 { grid-template-columns: 1fr 1fr 1fr; }
.g4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 760px){ .g2,.g3,.g4 { grid-template-columns: 1fr; } }

/* cards — square, 1px ink rule, flat; hover lifts on an ink offset */
.card {
  border: 1px solid var(--black); padding: 28px; background: var(--bg);
  border-radius: 0; box-shadow: none;
  transition: transform .18s ease, box-shadow .18s ease, background .18s ease;
}
.card:hover { transform: translate(-2px,-2px); box-shadow: 3px 3px 0 var(--black); }
.card h3 { margin: 10px 0 6px; }
.card h2 { font-family: var(--serif-display); font-size: 40px; margin: 0 0 4px; }

/* buttons — pill r30 · primary solid ink · secondary ink outline (SM: 15/10·22) */
/* Buttons are UI, not headings. The old base ran 15px/44px tall and three
   separate rules pushed hero and page-head buttons to 18px/55px — a label
   larger than h3 (17px) and than body copy (16px), which is what made them
   read as oversized. Two sizes only, both below the body size:
     .btn     14px · 40px tall   — the default
     .btn.lg  15px · 47px tall   — one per view, for the primary action
   line-height is pinned so height is predictable rather than inherited. */
.btn {
  display: inline-block; border: none; box-shadow: inset 0 0 0 1px var(--black);
  padding: 10px 20px;
  font-family: var(--sans); font-size: 14px; font-weight: 500; line-height: 1.4;
  text-decoration: none;
  color: #140D0D; background: transparent; border-radius: var(--r-btn); margin-right: 10px;
  transition: background .18s ease, color .18s ease;
}
.btn:hover { background: var(--black); color: #fff; }
.btn.solid { background: var(--black); color: #fff; box-shadow: none; }
.btn.solid:hover { background: #232323; }

.lp-hero { position: relative; background: var(--bg); overflow: hidden; }

/* the file has the header 234 of 1080 down a frame whose collage starts at
   471; that is 34.6% of the air above the pictures, which at this measure is
   this clamp rather than a percentage of nothing. */
/* ⚠️ EVERY ONE OF THESE NEEDS `.page-head.split` IN THE SELECTOR. The base
   rules are `.page-head.split .ph-grid` and `.page-head.split h1` at (0,2,1);
   a bare `.lp-head .ph-grid` is (0,2,0) and loses, silently — the grid stayed
   1fr 1fr and the title stayed sentence case with no error anywhere. */
/* ⚠️ THE AIR IS WHAT IS LEFT OVER, AND IT IS COMPUTED, NOT GUESSED. The fold
   has to hold four things: the 68px bar, this header's own content, and a
   collage whose height is 609/1920 = 31.72% OF THE VIEWPORT WIDTH. Only the
   padding is free, so it is exactly the remainder:

       100svh − bar − 31.72vw − the header's content

   split EVENLY. The header sits in the same amount of air above and below it,
   which is what it was asked for twice: first weighted 56/44 (too close to
   the pictures), then 38/62 (which fixed that by shoving the header up under
   the bar). 50/50 is the only split that is not a preference.

   ⚠️ AND THE FLOOR IS 40px, WHICH CAN COST THE BOTTOM OF THE PICTURE. Below
   that the fold stops being breathable and starts being a squeeze, so the
   padding holds and the collage is allowed to run past the fold instead. That
   only bites on a wide, short window — at 1920x900 the picture alone is 609px
   tall and ~18px of it goes under the edge. Everywhere else measured there is
   still clearance: 5px at 1280x720, 6px at 1440x800 and at 1440x900, 90px at
   1440x1000.

   ⚠️ A PURE `svh` CLAMP CANNOT DO THIS, which is what was here first. The
   thing eating the fold scales with WIDTH while the window's budget is its
   HEIGHT, so the two have to appear in the same expression. Tuned only on
   height, 1440x800 fit and 1920x900 still cut 55px off the bottom of the
   illustration; tuned tightly enough to fix 1920x900, 1440x900 threw away
   157px of air it had. Measured after: 6-10px of clearance at 1280x720,
   1440x800, 1920x900 and 45-73px at 1440x900 and 1920x1080.

   ⚠️ 168px IS THE HEADER'S CONTENT — two lines of title against a three-line
   standfirst over the buttons, whichever is taller. It is the one measured
   constant in here; if the copy grows a line the clamps absorb it rather
   than the picture getting cut. */
.page-head.split.lp-head {
  --lp-air: calc(100svh - var(--bar-h) - 31.72vw - 168px);
  --lp-pad: clamp(40px, calc(var(--lp-air) * .5), 112px);
  padding: var(--lp-pad) 0;
}
/* the file's own 741 : 48 : 556, as fractions so it holds at any measure */
.page-head.split.lp-head .ph-grid { grid-template-columns: 741fr 556fr; gap: 48px; }
.page-head.split.lp-head .ph-body { padding-top: 6px; }

/* ── the collage ──────────────────────────────────────────────────────────
   Eleven layers absolutely placed on the file's own 1920x1080 grid, expressed
   as percentages of the VISIBLE BAND — 1920 wide by 609 tall, the strip from
   the collage's top edge (y 471) to the bottom of the frame. Layers taller
   than the band are the file's: it clips them at 1080 and so does this.

   ⚠️ THE FRAME HAS A FLOOR WIDTH AND OVERFLOWS PAST IT. At a phone width a
   1920-proportioned band is 124px tall and the composition is unreadable
   confetti. `max(100%, 1180px)` holds the composition together and lets the
   sides crop instead, which is what a full-bleed collage should do anyway. */
.lp-collage { position: relative; width: 100%; overflow: hidden; }
.lp-frame {
  position: relative; width: max(100%, 1180px);
  margin-left: 50%; transform: translateX(-50%);
  aspect-ratio: 1920 / 609;
}
.lp-l { position: absolute; }
/* a layer that crops its picture — the file scales and offsets the fill
   inside the box, so the box clips and the <img> carries the transform */
.lp-l > .lp-crop { position: absolute; inset: 0; overflow: hidden; }
.lp-l img { position: absolute; display: block; max-width: none; }
/* ⚠️ A PLAIN LAYER STRETCHES ITS PICTURE, IT DOES NOT COVER IT. `object-fit`
   is `fill` on four of the five — the file scales each fill to its box
   independently in x and y, and on the preamble that is the whole effect: a
   420x562 page squashed into a 345x108 strip, which is why both lines of it
   are readable in a box a third their height. Switch it to `cover` and the
   strip becomes a crop of one line. Only the anganwadi is `cover`, and it
   says so below. */
.lp-l > img { inset: 0; width: 100%; height: 100%; object-fit: fill; }
.lp-anganwadi > img { object-fit: cover; }
/* ⚠️ FOUR OF THE NINE ARE DITHERED AND FIVE ARE PLAIN PHOTOGRAPHS. The file
   halftones the water tank, the anganwadi, the metro and the person at the
   desk; the ornament, the map, the mountain road, the seal and the preamble
   are the pictures themselves. Getting this wrong in either direction is
   easy, because the code export cannot serialise a halftone — it hands back
   the CONTINUOUS-TONE source behind every fill, so all nine arrive looking
   plain. `get_screenshot` on the individual node is the only thing that shows
   which is which; the frame-level screenshot is too small to tell.

   The four dithered ones are run through `tools-bake-plate.js` (Bayer 8,
   DOT 2, ink --ink) — the same engine as the mudras and the Constitution
   border — which returns INK ON TRANSPARENT, so their colour is the ground
   underneath and that is why only those four layers carry a `background`.
   Their sources stay in `assets/home/landing/src/`; re-bake from those, never
   from a plate. Exposure is per photograph and is recorded against each.

   ⚠️ `pixelated` IS FOR THE PLATES ONLY. On a photograph it is a bug — it
   turns a smooth downscale into aliased edges. */
.lp-l.is-dither img { image-rendering: pixelated; }
/* ⚠️ `multiply` IS LOAD-BEARING ON THE PLAIN ONES. They are scans and
   photographs on their own white ground; without it each sits on the paper as
   a white card and the collage reads as a contact sheet rather than one
   printed surface. The plates do not need it — they are already transparent.

   ⚠️ AND IT GOES ON THE LAYER, NOT ON THE `img`, BECAUSE THE ENTRANCE ANIMATES
   THE LAYER. `mix-blend-mode` blends with the backdrop OF ITS OWN STACKING
   CONTEXT, and animating `.lp-l`'s opacity and transform makes every layer
   one. With the blend on the `img` the map stopped seeing the periwinkle
   sheet behind it and went back to being a white card on top of it — the
   panel looked like it had simply vanished. On the layer, the layer itself
   blends with what is under it in the frame, which is what the file does. */
.lp-l.is-mul { mix-blend-mode: multiply; }
/* the seal is the exception and keeps it on the picture: its backdrop is its
   OWN paper rectangle (the file lays one under it), so blending the whole
   layer would multiply that paper into the periwinkle behind and tint it. */
.lp-seal { mix-blend-mode: normal; }
.lp-seal img { mix-blend-mode: multiply; }

.lp-ornament { left: 58.6458%; top: 0;        width: 41.8229%; height: 159.4417%; }
.lp-panel-ink{ left: 0;        top: 0;        width: 59.2708%; height: 82.4302%; background: var(--folder-periwinkle); }
.lp-map      { left: -6.6146%; top: 3.7767%;  width: 41.8229%; height: 78.6535%; }
.lp-map img      { top: 0; left: -0.02%; width: 100.04%; height: 199.79%; }
.lp-road     { left: 35.2083%; top: 0;        width: 31.25%;   height: 82.4302%; }
/* DITHERED — plate baked at contrast 1.0, exposure .16, on terracotta */
.lp-anganwadi{ left: -2.3958%; top: 63.5468%; width: 29.2188%; height: 51.7241%; background: var(--brand-terracotta); }
/* DITHERED — the water tank. contrast 1.0, exposure .12, on sage */
.lp-dam      { left: 26.8229%; top: 22.6601%; width: 16.4063%; height: 40.8867%; background: var(--fn-sage); }
.lp-dam img      { top: -38.27%; left: -10.16%; width: 110.16%; height: 213.08%; }
/* DITHERED — contrast 1.0, exposure .03, on sky */
.lp-metro    { left: 43.3854%; top: 63.5468%; width: 15.5729%; height: 45.8128%; background: var(--folder-sky); }
.lp-metro img    { top: -67.81%; left: -70.34%; width: 235.4%;  height: 238.53%; }
/* the seal sits on PAPER, not on the periwinkle behind it — the file lays a
   #FFFBF2 rectangle under it inside the same box */
.lp-seal     { left: -2.2917%; top: 3.9409%;  width: 9.375%;   height: 44.8276%; background: var(--bg); }
.lp-preamble { left: 26.5104%; top: 82.4302%; width: 17.9688%; height: 17.734%; }
/* `object-position` has no effect under `fill` — kept off deliberately. */
.lp-panel-pap{ left: 68.3333%; top: 29.3924%; width: 26.5625%; height: 82.266%; background: var(--bg); }
/* DITHERED — contrast 1.0, exposure .16. Its ground is `.lp-panel-pap`, the
   paper rectangle the file lays directly under it, not a fill of its own. */
.lp-desk     { left: 66.4063%; top: 24.1379%; width: 30.1563%; height: 92.4466%; }
.lp-desk img     { top: -0.54%; left: -0.02%; width: 101.44%; height: 100.54%; }

/* ── the entrance: the collage PRINTS ITSELF ─────────────────────────────
   A landing page gets one moment, and on this site that moment should be the
   thing the whole design system is about — paper, plates and ink. So the band
   does not fade up as one picture. It is made, in front of you, the way the
   artwork it is built from was made:

     1. the two big grounds land     — the periwinkle sheet, then the paper one
     2. the pictures print, left to right, across the band
     3. and each one lands as PLATE FIRST, PICTURE 120ms LATER — every layer's
        colour arrives before the image on it, which is the whole gesture and
        is why the boxes and their `img`s animate separately.

   ⚠️ THE ORDER IS LEFT TO RIGHT BY THE LAYER'S OWN x, NOT DOM ORDER. DOM order
   is z-order, which runs back-to-front and would have the far-right ornament
   arrive before the map on the left. `--d` is authored per layer in the markup
   for that reason — it is a composition decision, not something to derive.

   ⚠️ THE MOTION IS OPT-IN, AND THE DEFAULT STATE IS THE FINISHED ONE. Nothing
   here is hidden until a script says otherwise: `.lp-play` is what arms the
   animations, so no-JS, a thrown script, an old browser and a backgrounded
   tab all land on a collage that is simply THERE. This is the same rule
   stage.js states for the stagger — a CSS-only hidden state disappears the
   content if the script never runs — and a hero is the worst possible place
   to get it wrong.
   ⚠️ IT IS NOT HYPOTHETICAL. Written the other way round first (plain CSS
   `animation` + `fill-mode: both`, no class), and in a tab that was hidden at
   load every layer sat at `opacity: 0` with `playState` reporting "running":
   the animations existed, the clock just never advanced. The fold was blank.
   So the driver also declines to play at all when `document.hidden` is true
   at load — there is no one watching, and the point is to never owe the
   reader an animation frame in order to see the page.

   Total is ~1.15s from the first plate to the last picture. The easing has a
   small overshoot (the house curve is `.22,1,.36,1` and does not); a plate
   that lands dead-flat reads as a page loading rather than a thing arriving,
   and "celebratory" is entirely in that overshoot. */
@keyframes lpPlate {
  from { opacity: 0; transform: translateY(20px) scale(.984); }
  to   { opacity: 1; transform: none; }
}
@keyframes lpInk { from { opacity: 0; } to { opacity: 1; } }
@keyframes lpRise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  .lp-hero.lp-play .lp-l {
    animation: lpPlate .58s cubic-bezier(.2, 1.12, .3, 1) both;
    animation-delay: var(--d, 0ms);
  }
  /* the picture prints onto the plate that just landed */
  .lp-hero.lp-play .lp-l img {
    animation: lpInk .42s ease-out both;
    animation-delay: calc(var(--d, 0ms) + 120ms);
  }
  /* the title leads the whole thing — type and picture arrive together rather
     than the header sitting there while the band builds under it */
  .lp-hero.lp-play .lp-head h1 { animation: lpRise .62s cubic-bezier(.22,1,.36,1) both; }
  .lp-hero.lp-play .lp-head .ph-body { animation: lpRise .62s cubic-bezier(.22,1,.36,1) 90ms both; }
}

@media (max-width: 900px) {
  /* ⚠️ AND SO DOES THE STACK — `.page-head.split` collapses to one column at
     this width, but the desktop rule above it is now (0,3,1) and a bare
     `.lp-head .ph-grid` here is (0,2,0), so it loses to a rule in no media
     query at all. On a phone the title and the standfirst stayed side by side
     in 170px columns with the buttons stacked inside one of them. */
  .page-head.split.lp-head .ph-grid { grid-template-columns: 1fr; gap: 24px; }
  .page-head.split.lp-head .ph-body { padding-top: 0; }
}

.bd-hero {
  position: relative; background: var(--bg);
  height: 100svh; min-height: 540px;
  overflow: hidden;
}
/* ⚠️ HEIGHT-DRIVEN AND ANCHORED TO THE BOTTOM, which is the file: the border
   runs from just under the header to the foot of the frame (136 to 1080 of
   1080) with the 20px of air the file leaves under the bar. The `min()` keeps
   a wide-and-short window from pushing it past the viewport's sides. */
.hero-stage {
  position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  height: min(calc(100svh - var(--bar-h) - 20px), calc(94vw * 944 / 1291));
  aspect-ratio: 1291 / 944;
  container-type: inline-size;
}
.hero-frame { position: absolute; inset: 0; margin: 0; }
.hero-frame > img,
.hero-frame > canvas { display: block; width: 100%; height: 100%; }
/* ⚠️ THE CANVAS REPLACES THE <img>, IT DOES NOT COVER IT. `has-plate` is only
   added once a canvas has actually been built, so the baked plate below is a
   real fallback and not a hope — same contract as the mudras. */
.bd-hero.has-plate .hero-frame > img { display: none; }

/* ── the claim ─────────────────────────────────────────────────────────── */
/* ⚠️ THIS DEPARTS FROM THE HERO TYPE TIER ON PURPOSE. The scale's hero is
   `clamp(34, 5.2vw, 104)`; this is 4.1cqw of a frame that is itself ~80vw, so
   it lands near the file's 60/1920. It is sized by the ORNAMENT, because the
   longest line — "We build technology for" — has to fit the 62.7cqw opening
   with air each side, and a tier that ignored the picture would run the words
   straight into the elephants. */
/* ⚠️ 49.55%, NOT 50%. The opening is not centred in the artwork — it runs
   18.2%..80.9%, so its middle is 49.55% — and a claim centred on the FRAME
   sat 10px right of centre in the HOLE it has to live in, which showed as
   41px of air on the left against 31 on the right. Centre on the opening. */
.hero-claim {
  position: absolute; left: 49.55%; top: 44.7%;
  transform: translate(-50%, -50%) translateY(14px);
  margin: 0; width: 58cqw; text-align: center;
  font-family: var(--serif-display); font-weight: 400;
  /* 4.25cqw puts the longest line — "We build technology for" — at 647 of the
     696 the opening gives, so ~24px of air each side. Going up costs that air
     and the words start to crowd the ornament; the file's 60/1920 was sized
     for the SHORT line and does not survive the long one. */
  font-size: 4.25cqw; line-height: 1.151; letter-spacing: .01em;
  text-transform: uppercase; color: var(--ink);
  opacity: 0;
  transition: opacity .7s ease, transform .9s cubic-bezier(.2,.72,.24,1);
}
.bd-hero.claim-in .hero-claim { opacity: 1; transform: translate(-50%, -50%); }

/* ⚠️⚠️ `nowrap` ON BOTH HALVES IS WHAT BREAKS THE LINE. The claim starts as
   one line — "We the people of India" — and the caret types " build technology
   for" into the first half until it no longer fits beside the second; the
   second then drops WHOLE to line two. No <br>, no character counting, no
   width in px: the break is the measure doing its job, so it lands right at
   every viewport and every font size. Separated by a real space in the source,
   which is the space between "for" and "the". */
.hc-seg { white-space: nowrap; }

/* the caret sits between "We" and "the" and is the only thing on the page that
   promises something is about to happen */
.hc-caret {
  display: inline-block; width: .055em; height: .78em;
  margin-left: .06em; vertical-align: -.06em;
  background: var(--ink);
  animation: hcBlink 1.06s step-end infinite;
}
@keyframes hcBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
/* ⚠️ IT LEAVES WHEN IT IS DONE. A caret that blinks forever under a finished
   headline is a page that never settles; it has made its point once the
   sentence is complete. The delay is what lets you see it land. */
.bd-hero.is-typed .hc-caret {
  animation: none; opacity: 0;
  transition: opacity .5s ease 1.1s;
}

@media (max-width: 760px) {
  /* ⚠️ THE BORDER GOES WIDTH-DRIVEN ON A PHONE. Height-driven at 375 wide would
     hand it a 944-tall box and crop the ornament to nothing; here it takes the
     width it can have and the hero takes whatever height that implies. */
  .bd-hero { height: auto; min-height: 0; padding-bottom: 8px; }
  .hero-stage {
    position: relative; left: auto; bottom: auto; transform: none;
    margin: calc(var(--bar-h) + 12px) auto 0;
    width: 96vw; height: auto;
  }
  /* ⚠️ SMALLER ON A PHONE, NOT LARGER. The instinct is to scale the type UP as
     the frame shrinks; but the opening is a fixed 62.7cqw of the artwork at
     every size, so the long line has to keep fitting it. 4.6cqw put
     "We build technology for" at 253px inside a 253px hole — it was touching
     the left column and 7px INTO the right one. 3.9 gives ~19px each side. */
  .hero-claim { font-size: 3.9cqw; width: 62cqw; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-claim { transition: none; }
  .hc-caret { animation: none; }
}


/* figure entrance — data-anim switches the mode (default: drift) */
@keyframes bdFade { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes bdDrift { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes bdLand  { from { transform: translateY(340px); } to { transform: translateY(0); } }
@keyframes bdGate  { from { transform: translateX(-480px); } to { transform: translateX(0); } }
@keyframes bdMan   { from { transform: translate(360px,40px); } to { transform: translate(0,0); } }
@keyframes bdWoman { from { transform: translateX(420px); } to { transform: translateX(0); } }
@keyframes bdClipUp{ from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes bdRise  { from { transform: translateY(64px); clip-path: inset(100% 0 0 0); } to { transform: translateY(0); clip-path: inset(0 0 0 0); } }
/* As bdRise, but for TEXT. inset(0 0 0 0) clips to the border box, and at
   line-height 1.06 a Libertinus descender hangs ~.04em below it — so the
   resting state of the reveal was shaving the p/g tails. Negative insets let
   the glyphs out of the box once the line has landed. */
@keyframes bdRiseText { from { transform: translateY(64px); clip-path: inset(100% 0 0 0); } to { transform: translateY(0); clip-path: inset(-.2em 0 -.2em 0); } }
/* headline layer */
.bd-hist-head {
  position: absolute; inset: 0; opacity: 0; transition: opacity .6s ease;
  font-family: var(--serif); font-weight: 400;
  /* Same trap the wheel had: this was `3.125vw`, width-driven, while `top`
     below is a percentage of the scene HEIGHT. On a height-capped viewport
     (1920x900) that made the type proportionally huge for the plate and ate
     the gap under it down to 14px. .0556 x the scene height IS 3.125vw at a
     true 16:9, so nothing moves where the box is uncapped. */
  font-size: clamp(24px, calc(var(--hist-h) * .0556), 60px); line-height: 1.1; color: var(--ink);
}
/* beat A — centered full sentence, crossfades out */
.bd-head-a {
  position: absolute; left: 22%; top: 29%; width: 56%; text-align: center;
  transform: translateY(22px); transition: opacity .45s ease, transform .6s cubic-bezier(.2,.7,.2,1);
}
/* beats B & C — persistent "Building" + a tail that swaps under it */
.bd-building, .bd-tail { position: absolute; top: 24.5%; opacity: 0; transition: opacity .45s ease; }
.bd-building { left: 32%; }
.bd-tail { left: 45%; }
.bd-tail-b { width: 50%; }            /* long tail wraps */
.bd-tail em { font-style: italic; }
/* single final card swings in (beat B), then fades for the illustration (beat C) */
/* The final card rides the SAME wheel: same pivot (50% 128%), and now the
   same entry angle as the five before it, so it sweeps the identical arc
   rather than swinging up from -70deg on a path of its own.
   It also starts hidden — at -70deg it hung into the bottom-left corner
   during the arrival beat, visible well before its turn. */
.bd-hist-newwrap { position: absolute; inset: 0;
  transform-origin: var(--hist-pivot-x) var(--hist-pivot-y);
  transform: rotate(-42deg); opacity: 0;
  transition: transform 1.25s cubic-bezier(.3,.35,.1,1), opacity .5s ease; }
/* A link whose destination has not launched yet. build-partials.py moves the
   href to data-href and adds this class; the element and its text stay exactly
   where they were. `.labs-head a` and friends underline by ELEMENT selector, so
   dropping the href is not enough on its own to stop it reading as a link. */
.is-unlinked {
  /* !important on both, deliberately. `.labs-head a` sets the underline with
     the SAME specificity as a bare class and sits thousands of lines further
     down, so it would win on source order. A launch gate that loses a cascade
     fight ships a link to a page that does not exist yet. */
  text-decoration: none !important; cursor: default; pointer-events: none !important;
}
/* Same reasoning. The `hidden` ATTRIBUTE is only a UA `display: none`, which
   any author `display` beats — `.btn` is inline-flex, so a hidden CTA was
   still laying out 59px tall. */
[data-launch][hidden] { display: none !important; }

/* `.bd-through-img` lived here — beat C's illustration, rising and clipping in
   as the final card faded. It is `.svc-mudra img` now, and the art changed with
   it: the terracotta assets/hero-hand-enablement.png (cropped, wrist off the
   left edge) gave way to the file's own green export, assets/home/
   mudra-bhramara.png. The entrance survived the move intact. */
@media (prefers-reduced-motion: reduce){ .bd-tail, .bd-building { transition: none; } }

/* --- hero --- */
.hero { padding: 96px 0 72px; }
.hero h1 {
  font-size: clamp(42px, 5.4vw, 80px); line-height: 1.06; margin: 0 0 24px;
  color: var(--ink); max-width: 18ch;
}
.hero h1 em, h2 em { font-family: var(--serif); font-style: italic; }
.hero p { font-size: 19px; line-height: 1.6; max-width: 620px; color: var(--ink); }
/* hero buttons use MD (18/13·26) */
.hero .btn { font-size: 15px; padding: 13px 24px; }
.hero h1 .h1-line { display: block; white-space: nowrap; }
@media (max-width: 760px){ .hero { padding: 64px 0 48px; } .hero h1 { font-size: 42px; } }
@media (max-width: 520px){ .hero h1 { font-size: 33px; } }
.scroll-cue {
  margin-top: 54px; font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
.scroll-cue span { display: inline-block; animation: cue-bob 1.7s ease-in-out infinite; }
@keyframes cue-bob { 50% { transform: translateY(4px); } }

@media (prefers-reduced-motion: no-preference) {
  .hero h1, .hero > .wrap > p, .hero .hero-actions {
    opacity: 0; transform: translateY(14px);
    animation: hero-in .9s cubic-bezier(.22,1,.36,1) forwards;
  }
  .hero > .wrap > p { animation-delay: .12s; }
  .hero .hero-actions { animation-delay: .22s; }
}
@keyframes hero-in { to { opacity: 1; transform: none; } }

/* rotating hero verb — serif italic, width animates, blur-fade swap */
.rotator {
  display: inline-block; white-space: nowrap; vertical-align: baseline;
  transition: width .5s cubic-bezier(.22,1,.36,1);
}
.rotator-word {
  display: inline-block;
  font-family: var(--serif); font-style: italic; letter-spacing: 0;
  transition: opacity .3s ease, filter .3s ease, transform .3s ease;
}
.rotator-word.swap { opacity: 0; filter: blur(6px); transform: translateY(.16em); }

/* serif italic statement — "eyebrow" voice from the system: italic serif, 40% ink */
.statement {
  font-family: var(--serif); font-style: italic; font-weight: 400;
  font-size: clamp(26px, 2.8vw, 40px); line-height: 1.4; letter-spacing: 0;
  text-align: center; color: color-mix(in srgb, var(--ink) 62%, var(--bg)); max-width: 800px; margin: 0 auto;
}
.section-center { text-align: center; }
.section-center .eyebrow { margin-left: auto; margin-right: auto; }
.section-center > p, .section-center .section-lead { margin-left: auto; margin-right: auto; }
.hl {
  background: linear-gradient(transparent 82%, var(--blush) 82%);
  padding: 0 .04em;
}

/* --- footer --- */
/* ═══════════════════════════════════════════════════════════════════════
   THE FOOTER  (partials/footer.html — never edit it inside a page)
   ═══════════════════════════════════════════════════════════════════════
   Structure after Zaro: the wordmark given real size, four columns each
   under its own hairline, and a legal bar beneath a full-width rule set in
   the mono label voice. The hairline over each heading is the detail that
   makes it read as designed rather than listed, and it is already the house
   idiom — ink hairlines are used this way throughout.

   Replaced a 17px wordmark, four ungoverned columns, no legal line, and a
   `border-top: 1px solid var(--black)` that was black on #140D0D and
   therefore invisible.
   ═══════════════════════════════════════════════════════════════════════ */
footer.site {
  position: relative; z-index: 1;      /* must cover the fixed reveal panel */
  background: #140D0D; color: #FFFBF2;
  padding: clamp(56px, 7vw, 96px) 0 clamp(28px, 3vw, 40px);
  margin-top: 56px;
}
.f-mark { margin: 0 0 clamp(44px, 6vw, 88px); }
.f-mark img { display: block; height: clamp(34px, 4.4vw, 62px); width: auto; }

/* the footer lockup, BIG, and its wheel turns on hover (6 Oct 2026). Sized by
   height; the file is 1282x608. The wheel rotates about the spokes' crossing
   (260, 303.5 in viewBox units) by --turns whole revolutions, which nav.js
   bumps on each pointer-enter — so it always spins forward. */
.f-logo { display: block; height: clamp(72px, 9vw, 130px); width: auto; overflow: visible; }

.f-wheel {
  --turns: 0;
  transform-box: view-box; transform-origin: 260px 303.5px;
  transform: rotate(calc(var(--turns) * 360deg));
  transition: transform 1.4s cubic-bezier(.22, 1, .36, 1);
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) { .f-wheel { transition: none; transform: none; } }
/* the type-only fallback, shown only while the dark export is missing */
.f-mark span { display: none; }
.f-mark.pending span {
  display: block;
  font-family: var(--wordmark); font-weight: 700;
  font-size: clamp(30px, 4.2vw, 58px); line-height: 1; letter-spacing: -.02em;
  color: #FFFBF2;
}
.f-cols {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(20px, 3vw, 40px);
}
/* the hairline over every column, Zaro's one structural move */
.f-col { border-top: 1px solid rgba(255, 251, 242, .26); padding-top: 18px; }
.f-col h4 {
  font-family: var(--sans); font-weight: 500; font-size: 15px;
  letter-spacing: 0; text-transform: none;
  color: #FFFBF2; margin: 0 0 16px;
}
.f-col a {
  display: block; color: rgba(255, 251, 242, .66); font-size: 15px;
  text-decoration: none; margin: 0 0 12px;
  transition: color .15s ease;
}
.f-col a:hover { color: #FFFBF2; }

.f-legal {
  margin-top: clamp(48px, 7vw, 96px); padding-top: 22px;
  border-top: 1px solid rgba(255, 251, 242, .26);
  display: flex; flex-wrap: wrap; gap: 14px 28px;
  align-items: baseline; justify-content: space-between;
}
.f-legal p, .f-legal a {
  font-family: var(--mono); font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .1em; color: rgba(255, 251, 242, .44); margin: 0;
  text-decoration: none;
}
.f-legal nav { display: flex; flex-wrap: wrap; gap: 14px 26px; }
.f-legal a:hover { color: rgba(255, 251, 242, .8); }

@media (max-width: 760px) {
  .f-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 20px; }
  .f-legal { flex-direction: column; align-items: flex-start; }
  /* ⚠ THE FOOTER LINKS ARE THUMB TARGETS HERE, NOT MOUSE TARGETS (6 Oct
     2026). `.f-col a` is a 15px block with a 12px margin, which is a 24px tall
     hit area — fine under a cursor, under the 44px every phone guideline asks
     for. The 12px margin becomes 10px of PADDING top and bottom instead: the
     link box grows to 44 and the gap between two links stays visually the
     same, because the padding of one now sits against the padding of the next.
     The column heading's own margin comes down by the 10px the first link
     gained, so the heading-to-list gap does not open up. */
  .f-col a { padding: 10px 0; margin: 0; }
  .f-col h4 { margin-bottom: 6px; }
  /* the legal row is tertiary, so it takes the same treatment at a smaller
     size — 38px, with the row gap moved into the padding so the two do not
     stack up and double the footer's foot */
  .f-legal nav { gap: 0 26px; }
  .f-legal nav a { padding: 10px 0; }
}

/* --- the footer reveal ------------------------------------------------
   granola.ai's arrangement, matched exactly: the panel is FIXED to the bottom
   of the viewport, permanently, and the footer covers it completely. There is
   no runway and NO RESTING REVEAL — none of it is visible until the reader
   pushes past the end of the page, at which point over-scroll lifts the footer
   off it. On their page the six images sit at opacity 0 at rest for the same
   reason; nothing should be able to flash before the push.

   The earlier build uncovered a 42svh band by ordinary scrolling and the peek
   added to that. Dropping the runway also takes ~42svh off the bottom of all
   13 pages.

   z-index -1 paints above the page canvas, and this site's sections are
   transparent — so the panel is `visibility: hidden` until a peek is actually
   in flight, which nav.js toggles. That also means a dead script leaves no
   artefact: the page simply ends at the footer.
   ---------------------------------------------------------------------- */
.footer-reveal {
  position: fixed; left: 0; right: 0; bottom: 0;
  /* granola's is 260px. Slightly taller than the peek's travel (MAX 260) so a
     full lift can never outrun the image and expose the ground behind it. */
  height: 280px;
  z-index: -1; overflow: hidden; background: #140D0D;
  visibility: hidden;
  /* insurance only — an upward filler in the footer's own colour, so any
     sub-pixel disagreement between the lift and the panel reads as footer
     rather than as a white strip */
  box-shadow: 0 -160px 0 0 #140D0D;
}
.footer-reveal.on { visibility: visible; }
.footer-reveal img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  /* rendering a dither smooth destroys the pattern — keep the pixels hard */
  image-rendering: pixelated;
  /* all of them start at 0: nothing is showing until the peek starts. The
     transition is a CUT, not a crossfade — must match FADE in nav.js. */
  opacity: 0; transition: opacity .04s linear;
}
/* until the three exports land, the band reads as a deliberate dark close
   rather than three broken-image icons */
.footer-reveal.pending::after {
  content: "FOOTER IMAGERY TO COME";
  position: absolute; inset: 0; display: grid; place-content: center;
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  color: rgba(255, 251, 242, .3);
}

/* Peek: the footer lifts against over-scroll at the very bottom and springs
   home on release. No CSS transition on the transform — nav.js animates it per
   frame and a transition would fight the spring. */
footer.site { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  .footer-reveal img { transition: none; }
}

@media (max-width: 760px) {
  .footer-reveal { height: 200px; }
}

/* tags — data & labels voice: DM Mono uppercase pills (r21) */
.tag {
  display: inline-block; background: transparent; border: 1px solid var(--black);
  padding: 7px 16px 6px; border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink); margin: 0 6px 8px 0; font-weight: 500; cursor: pointer;
  transition: background .15s, color .15s;
}
.tag:hover { background: var(--surface); }
/* ⚠ A PILL YOU PRESS NEEDS A THUMB'S WORTH OF HEIGHT (5 Oct 2026). `.tag`
   is 30px tall, which is right for a static label and short for a control; on
   services, labs and projects the whole filter row is `button.tag` and every
   one of them was a 30px target. The floor goes on the BUTTON only, so the
   static pills that use the same class keep their size and the rows that are
   actually operated grow. `inline-flex` + `center` replaces `inline-block`'s
   baseline so the label stays optically centred as the box grows. */
@media (max-width: 760px) {
  button.tag {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px;
  }
}
.tag.active-tag { background: var(--black); color: #fff; border-color: var(--black); }
/* breadcrumbs removed site-wide 14 Sep 2026 — .crumb rules deleted with them */

/* `.marquee`, `.marquee-track`, `.logo-chip` and `@keyframes marquee-scroll`
   lived here — the homepage partner strip, and its only instance. Retired
   22 Sep 2026 when the twelve "Partner logo" chips were replaced with the six
   real marks (`.plc-grid--row` below).

   ⚠️ IT WAS NOT THE ANIMATION THAT FAILED, IT WAS THE ARITHMETIC OF SIX. The
   keyframe translates `-50% - gap/2`, which only loops seamlessly when the
   track is TWO identical halves; six marks at ~190px make a half of 1248px,
   narrower than the viewport, so the strip ran dry at the end of every cycle.
   Covering 1440 meant four copies of six — the same names twice a screen.
   If a real marquee is ever wanted again, the rule is: one half must be wider
   than the widest supported viewport, and the item count must stay even. */


/* --- the expanding part: panels live inside the shell, never below it.
       Height is measured in nav.js and animated on the container, so switching
       menus reads as one box resizing rather than two popovers swapping. --- */
.nav-panels {
  position: relative; height: 0; overflow: hidden;
  transition: height .28s cubic-bezier(.4,0,.2,1);
}
header.site.open .nav-panels { height: var(--panel-h, 0px); }
.nav-panel {
  position: absolute; top: 0; left: 0; width: 100%;
  opacity: 0; pointer-events: none;
  /* visibility, not just opacity — an opacity-0 panel keeps every link inside
     it in the tab order, so the keyboard walked 23 invisible links */
  visibility: hidden;
  transition: opacity .16s ease, visibility 0s linear .16s;
}
.nav-panel.show {
  opacity: 1; pointer-events: auto; visibility: visible;
  transition: opacity .16s ease, visibility 0s;
}
.nav-panel-in {
  display: grid; grid-template-columns: 1.15fr 1fr 300px;
  gap: 48px; padding: 10px 0 34px;
}

/* --- what is inside a sheet ------------------------------------------
   A menu of pages IS an index, and this site has built a good one twice —
   .proj-row on Projects and .fl-row on Past Cohorts. The sheet is the same
   object, so it is made of the same parts: ruled lines in the scatter brush,
   one label size, and the wall receding under the cursor.

   What it replaced was a generic mega-menu — three sans sizes within two
   pixels of each other (13/14/15) doing the work of a hierarchy, titles at
   the h4 weight so every row shouted, sentence-long descriptions that made
   the sheet tall and soft, tinted rounded rectangles on hover at the FOLDER
   TAB radius, and a black promo tile carrying marketing copy. None of that
   vocabulary exists anywhere else here; the descriptions and the tile are
   gone and the labels carry it, which is what a menu label is for. --- */
.nav-panel-in {
  /* Flex, not grid: the three sheets carry different numbers of columns —
     two, one, one — and a track template would have to be written per sheet.
     Columns are sized to their LABELS rather than stretched across the
     measure; at 1fr each the rules ran 549px under a 100px label, which read
     as a table with the data missing rather than as an index. */
  display: flex; align-items: flex-start;
  gap: 22px;
  /* The sheet is inset to the NAV CLUSTER, not to the page measure. Run to
     the measure it started under the logo and ended under the CTA, which is
     a very wide sheet for two short lists — it read as a band rather than as
     a menu belonging to the items above it. --sheet-l / --sheet-r are
     --sheet-l is measured off the <ul> by nav.js, because the cluster is
     centred in the space the logo and the CTA leave, so its left edge is not
     expressible in CSS. The RIGHT edge is the measure's own, which gives the
     card room the 434px cluster could not. */
  padding: 4px 0 30px var(--sheet-l, 0px);
  --fl-rest: 1;
}
/* The column is exactly as wide as the five nav items above it, so the rule
   under each option runs from the first of them to the last. That is what
   ties the sheet to the bar: the dividers are the same span as the thing
   they opened out of. Left to fill the sheet instead they ran ~490px under a
   100px label; held to an arbitrary 300px they stopped short of the cluster
   and lined up with nothing. --cluster-w is measured by nav.js. */
.nav-col { flex: 0 0 auto; width: var(--cluster-w, 300px); min-width: 0; }
/* the sheet recedes and the row under the cursor holds — .proj-list's move */
.nav-panel-in:hover { --fl-rest: .4; }

.nav-col .eyebrow { margin: 0 0 4px; }

.nav-row {
  position: relative; display: block;
  padding: 11px 0 12px;
  font-family: var(--sans); font-size: 15px; line-height: 1.35;
  color: var(--ink); text-decoration: none;
  opacity: var(--fl-rest);
  --dot: rgba(35, 35, 35, .34);
  transition: opacity .3s ease, --dot .3s ease;
}
.nav-row:hover, .nav-row:focus-visible { opacity: 1; --dot: var(--black); }
/* each line is ruled above, in the same brush as the bar's own rule. No
   closing rule: the sheet's bottom edge is the header rule that travelled
   down to meet it. */
.nav-row::before {
  content: ""; position: absolute; left: 0; right: 0;
  top: calc(var(--tex-t) / -2); height: var(--tex-t);
  background: var(--dot); pointer-events: none;
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
  -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
}
.arr { font-size: 12px; color: var(--muted); margin-left: 5px; }

/* --- the card at the end of a sheet ----------------------------------
   One per sheet, and it is not a link to a page — it is the entry point to
   the one deep thing that sheet is for: the application, the conversation,
   the latest essay.

   It is NOT a sharp box. It carries no keyline and its corners are --r-tab,
   the radius this system gives a soft surface — the open nav shell, the
   dialog, the feedback panel. A hard-edged rectangle with a black rule round
   it belongs to .card out on the page, where a card is a record sitting on
   paper; in here it read as a cut-out stuck to the sheet.

   The fill is the section's own colour. --brand-violet is the fellowship
   hue; --blush is defined in :root as the service pill fill, so Offerings
   already had one. Field Notes had neither, and on paper-soft with no
   keyline it stopped reading as a card at all — #FCF8EF on #FFFBF2 is not a
   card, it is a slightly different piece of paper. It takes --sky-t from the
   tint set, which is the pick I am least sure of here. */
/* One size for all three, held whatever the copy does — these will carry a
   brush or an image before long and a card that resizes to its sentence
   cannot. The go-line is pinned to the bottom by margin-top:auto, so the
   slack sits between the headline and the action rather than under it.

   margin-left:auto puts it on the RIGHT EDGE of the measure. Left to sit
   next to its list, the whole sheet bunched into the first half of a 1163px
   shell with five hundred pixels of nothing after it. Sent to the edge it
   brackets the width the way the bar above it already does — logo left and
   CTA right — so the sheet and the bar share one structure. */
.nav-card {
  flex: 0 0 auto; width: 320px; min-height: 190px; margin-left: auto;
  display: flex; flex-direction: column;
  padding: 20px 22px 21px;
  border: 0; border-radius: var(--r-tab);
  text-decoration: none; color: var(--ink);
  background: var(--surface);
  transition: background .2s ease, transform .3s cubic-bezier(.22, 1, .36, 1);
}
.nav-card.is-violet { background: var(--brand-violet); }
/* The fellowship card wears the fellowship banner (4 Oct 2026), not the
   violet: a snapshot of the live scene — arcade, desk, the Earth in its disc —
   composed in the browser from the page's own layers at 960x356
   (assets/nav/fellowship-snap.png) and laid across the card's top as a band,
   with the copy on the card's surface below it. `image-rendering: pixelated`
   because it is a dither like everything else on the site. ⚠️ If the banner's
   artwork changes, re-take the snapshot — it is a picture of the scene, not
   the scene. */
.nav-card.is-scene {
  background: var(--surface) url(assets/nav/fellowship-snap.png) center top / 100% auto no-repeat;
  padding-top: 134px;   /* the band: 320px wide at 960:356 is ~119px, plus the usual 20px gap below it */
  image-rendering: pixelated;
}
.nav-card.is-blush  { background: var(--blush); }
.nav-card.is-paper  { background: var(--sky-t); }
/* it lifts a little rather than tinting — there is no keyline to firm */
.nav-card:hover, .nav-card:focus-visible { transform: translateY(-2px); }

.nav-card-k {
  display: block; font-family: var(--mono); font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--black);
  margin-bottom: 14px;
}
/* Clamped, because the card is a fixed size and the copy is not. At 176px
   the Field Notes headline — a real 76-character essay title — ran to six
   ragged lines and pushed that sheet to 251px while the other two sat at
   224, which is exactly the "all the same" the fixed size was for. */
.nav-card-h {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
  font-family: var(--sans); font-size: 15.5px; line-height: 1.34;
  color: var(--ink); margin-bottom: 20px;
}
.nav-card-go {
  display: block; margin-top: auto;
  font-family: var(--sans); font-size: 14px; font-weight: 500; color: var(--ink);
}
.nav-card-go::after { content: " \2192"; }

/* --- burger + drawer (below 900px) --- */
/* ⚠ 44x44, AND THE BARS STAY WHERE THEY WERE (5 Oct 2026). The button was
   22px of bars plus 8px of padding — a 38x31 hit area for the only control
   that opens the menu on a phone. The box is pinned to 44x44 instead, and the
   bars are held against its RIGHT edge with the old 8px of padding
   (`align-items: flex-end` is the horizontal axis in a column flex), so the
   icon lands on exactly the pixels it did before and only the hit area grew. */
.nav-burger {
  display: none; background: none; border: 0; cursor: pointer;
  flex-direction: column; justify-content: center; align-items: flex-end;
  gap: 5px; margin-left: auto;
  width: 44px; height: 44px; padding: 0 8px 0 0;
}
.nav-burger span {
  display: block; width: 22px; height: 1.5px; background: var(--black);
  transition: transform .2s ease, opacity .2s ease;
}
.nav-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.nav-scrim {
  position: fixed; inset: 0; background: rgba(20,13,13,.35); z-index: 290;
  opacity: 0; pointer-events: none; transition: opacity .28s ease;
}
.nav-scrim.open { opacity: 1; pointer-events: auto; }
/* [hidden] is only display:none from the UA sheet, and the rule below sets
   display:flex — which wins. Without this the drawer stayed in the layout
   and in the tab order at desktop even when marked hidden. */
.nav-drawer[hidden], .nav-scrim[hidden] { display: none; }
.nav-drawer {
  position: fixed; top: 0; right: 0; height: 100dvh; width: min(380px, 88vw);
  background: var(--bg); z-index: 300; padding: 84px 26px 26px;
  transform: translateX(100%); transition: transform .28s ease;
  overflow-y: auto; display: flex; flex-direction: column;
}
.nav-drawer.open { transform: none; }
.drawer-link, .drawer-group > summary {
  display: block; font-family: var(--sans); font-size: 17px; font-weight: 500;
  color: var(--ink); text-decoration: none; padding: 15px 0;
  border-bottom: 1px solid var(--line); cursor: pointer; list-style: none;
}
.drawer-group > summary::-webkit-details-marker { display: none; }
.drawer-group > summary::after { content: "+"; float: right; font-family: var(--mono); color: var(--muted); }
.drawer-group[open] > summary::after { content: "–"; }
.drawer-group a {
  display: block; padding: 11px 0 11px 14px; font-size: 15px;
  color: var(--muted); text-decoration: none;
}
.drawer-group a:hover { color: var(--ink); }
.drawer-foot { margin-top: auto; padding-top: 28px; }
.drawer-foot .btn { display: block; width: 100%; text-align: center; margin: 0; }

@media (max-width: 900px) {
  .nav-main, .nav-end { display: none; }
  .nav-burger { display: flex; }
}
@media (prefers-reduced-motion: reduce) {
  .nav-shell, .nav-panels, .nav-panel, .nav-drawer, .nav-scrim, .nav-burger span { transition: none; }
}

/* --- cohort tabs — mono pills --- */
.tab-btn {
  font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  font-weight: 500; cursor: pointer;
  background: transparent; color: var(--ink); border: 1px solid var(--black);
  padding: 9px 18px 8px; border-radius: var(--r-btn); margin-right: 8px;
  transition: background .15s, color .15s;
}
.tab-btn:hover { background: var(--surface); }
.tab-btn.active { background: var(--black); color: #fff; }

@keyframes faqIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* --- modal (fellows / projects) --- */
.modal-overlay {
  display: none; position: fixed; inset: 0;
  /* the same scrim the nav drawer drops — one screen weight for the whole
     site. No backdrop blur: nothing else on these pages blurs, and on paper
     stock a blurred ground reads as glass, which this system is not. */
  background: rgba(20, 13, 13, .35);
  z-index: 600;
  align-items: center; justify-content: center;
  padding: 24px;
  animation: fade-in .28s ease;
}
.modal-overlay.open { display: flex; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* The dialog is an OVERLAY, and this system already says what an overlay
   looks like — the nav sheet, which is the only other thing on the site that
   lifts off the page. When it opens it takes --r-tab and --shadow-overlay,
   and it carries no keyline at all. --shadow-overlay is even documented at
   the top of this file as "the system's only elevation — overlays (nav
   sheet, drawer, modal) ONLY"; the modal simply never used it.

   What was here instead — a 1px ink box, square corners and a 10px hard
   black offset — is the .card hover lift blown up to 860px. That move is a
   second impression of a card's own edge on paper; at dialog scale it stops
   reading as ink and starts reading as a drop shadow from another system.
   Paper lifted off paper is diffuse and warm (20,13,13 at .18), not a black
   slab behind a sharp rectangle. */
.modal {
  background: var(--bg);
  border: 0;
  border-radius: var(--r-tab);
  box-shadow: var(--shadow-overlay);
  width: 100%; max-width: 860px;
  max-height: 88vh; overflow-y: auto;
  position: relative;
  animation: slide-up .28s cubic-bezier(.22, 1, .36, 1);
}
@keyframes slide-up {
  from { transform: translateY(14px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.modal-close {
  position: sticky; top: 16px; float: right; margin: 16px 16px 0 0;
  background: var(--bg); border: 1px solid var(--black);
  width: 34px; height: 34px; border-radius: 50%;
  cursor: pointer; font-size: 18px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  color: var(--ink); z-index: 10;
}
.modal-close:hover { background: var(--black); color: #fff; }
.modal-body { padding: 0 40px 40px; clear: both; }
.modal-body h2 { font-family: var(--serif-display); }
.modal-hero {
  display: grid; grid-template-columns: 1fr 300px;
  gap: 36px; align-items: start; margin-bottom: 36px;
}
/* the dialog's one rule, drawn in the same scatter brush as the record it
   is describing rather than a hairline of solid ink */
.modal-divider {
  border: 0; height: var(--tex-t); margin: 34px 0 30px;
  background: rgba(35, 35, 35, .34);
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
          mask-image: var(--tex-h);
  -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
}
.modal-meta {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(130px,1fr)); gap: 14px; margin: 24px 0 32px;
}
.modal-meta .card { padding: 16px 18px; }
.modal-meta .card .eyebrow { margin: 0 0 4px; }
.modal-meta .card p + p { margin: 0; font-weight: 600; }
#m-tagline { margin: 0 0 10px; }
/* CLAIM TIER at the panel size — the dialog's title is the same object as a
   card title, one step up because it is the only heading in the sheet. */
#m-title {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(26px, 2.6vw, 36px); line-height: 1.22; letter-spacing: -.2px;
  margin: 0 0 20px;
}
.modal-project-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 36px; align-items: start;
}
/* A project with no screenshot in the source gets no figure column rather
   than an empty grey box beside its copy. */
.modal-project-grid.no-shots { grid-template-columns: 1fr; }
.modal-project-grid h4 {
  font-family: var(--mono); font-weight: 500; font-size: 11px;
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent);
  margin: 0 0 8px;
}
.modal-project-grid > div > * + h4 { margin-top: 26px; }
/* ⚠️ "What the fellows did" IS A LIST and has to read as one. Run together as
   a paragraph, four distinct pieces of work become one run-on sentence. */
.m-list { margin: 0; padding-left: 1.1em; color: var(--muted); }
.m-list li { margin: 0 0 8px; line-height: 1.6; }
.m-list li:last-child { margin-bottom: 0; }
.m-shots { display: grid; gap: 14px; align-content: start; }
.m-shots img {
  width: 100%; height: auto; display: block;
  border-radius: 12px; background: var(--surface);
  box-shadow: inset 0 0 0 1px rgba(35, 35, 35, .12);
}
/* The one project the deck carries no screenshot for. The row keeps its
   columns so the list does not go ragged at that one line. */
.proj-thumb.is-blank { background: var(--surface); box-shadow: inset 0 0 0 1px rgba(35, 35, 35, .10); }
/* The card in the modal hero is the record itself, not a control: no
   pointer, and its type takes the same step down the phone breakpoint uses,
   because the hero column is 300px rather than the 368 the artboard assumes.
   Below that the half-cells break "Product Design" over two lines. */
.fcard.is-static { cursor: default; max-width: 300px; }
.fcard.is-static .fcard-label { font-size: 10px; }
.fcard.is-static .fcard-value { font-size: 13.5px; }

/* On a phone the hero stacks and the card only gets ~245px, where the two
   half-cells break "Product Design" and "RDPR Karnataka" over two lines each.
   The panel becomes four full-width bands instead — the artboard's three-band
   structure doesn't survive at that width, and shrinking the type to 11px to
   force it would be worse. */
@media (max-width: 560px) {
  .fcard.is-static .fcard-info { grid-template-columns: 1fr; }
  .fcard.is-static .fcard-cell { grid-column: 1 / -1; }
  /* every cell is a full band now, so the one vertical rule divides nothing */
  .fcard.is-static .fcard-cell.r-top.r-left::before {
    inset: calc(var(--tex-t) / -2) 0 auto 0; width: auto; height: var(--tex-t);
    -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
    -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
    -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
  }
}

/* --- the folder opening -----------------------------------------------
   The dialog IS the folder, opened — so it should not arrive as a different
   object. document.startViewTransition captures the folder, swaps the names
   onto the dialog, and the browser morphs between the two states. Three
   things are named rather than one, so they travel instead of crossfading:
   the folder box becomes the dialog, the filed sheet becomes the portrait on
   the record card, and the tab becomes the line above the name — which is
   why that line is drawn as a tab here. The tab is the anchor: it barely
   moves, and it is what makes the growth read as one object rather than a
   dissolve.

   No support, or prefers-reduced-motion, and none of this runs — the dialog
   opens the way it always did. cohorts.html decides; this only dresses it. */
.modal-tab {
  display: inline-block; margin: 0 0 16px; padding: 5px 13px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--black);
  border-radius: var(--r-tab) var(--r-tab) 0 0;
}
/* the fade and rise are the view transition's job while one is running */
.modal-overlay.vt, .modal-overlay.vt .modal { animation: none; }

::view-transition-group(fellow-folder),
::view-transition-group(fellow-doc),
::view-transition-group(fellow-tab) {
  animation-duration: .44s;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}
/* the box travels for the full .44s; what is printed on it settles sooner,
   so the morph is read as a folder growing and not as two pictures mixing */
::view-transition-old(fellow-folder),
::view-transition-new(fellow-folder) { animation-duration: .26s; }
::view-transition-old(fellow-doc),
::view-transition-new(fellow-doc) { animation: none; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}

/* who they were embedded with — one line under the project statement, in
   the page's own voice, rather than the boxed panel it replaced. The card
   beside it prints the department's short form; this is the full name. */
.fm-partner {
  margin: 16px 0 0; font-family: var(--sans);
  font-size: 14.5px; line-height: 1.5; color: var(--muted);
}
.fm-partner > span { color: var(--ink); }

/* what each fellow is strong at — the record's last band. Marked with a short
   ink rule rather than a bullet glyph, which is how every other list on this
   site is set. */
.fm-strengths { list-style: none; margin: 10px 0 0; padding: 0; }
.fm-strengths li {
  position: relative; padding-left: 26px; margin: 0 0 12px;
  color: var(--muted); font-size: 15px; line-height: 1.6; max-width: 62ch;
}
.fm-strengths li::before {
  content: ""; position: absolute; left: 0; top: .72em;
  width: 14px; height: 1px; background: var(--black);
}
@media (max-width: 680px) {
  .modal-hero, .modal-project-grid { grid-template-columns: 1fr; }
}

/* --- who worked on it (projects modal) --- */
.people-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr));
  gap: 14px; margin-top: 12px;
}
.person {
  display: flex; align-items: center; gap: 12px;
  border: 1px solid var(--black); border-radius: 0; padding: 12px 14px;
  background: var(--bg);
}
.person strong { font-size: 14px; }
.person-avatar {
  width: 44px; height: 44px; min-height: 44px; border-radius: 50%;
  flex-shrink: 0; margin: 0;
}
.person-avatar::before { content: ""; }

/* --- label plates — service-pill voice --- */
.plate {
  display: inline-block; background: var(--fc, var(--blush));
  border: none; border-radius: var(--r-pill);
  padding: 7px 16px 6px; font-family: var(--mono); font-size: 12px;
  text-transform: uppercase; letter-spacing: .06em; font-weight: 500;
  margin-bottom: 10px; color: var(--black);
}

/* --- scroll reveal --- */
@media (prefers-reduced-motion: no-preference) {
  .reveal { opacity: 0; transform: translateY(18px);
    transition: opacity .7s ease, transform .7s cubic-bezier(.22,1,.36,1); }
  .reveal.in { opacity: 1; transform: none; }
}

/* --- Field Notes expo: stacked bands — paper sheets on ink rules --- */
.expo-wrap { overflow: hidden; padding: 26px 0 70px; }
.expo { display: flex; flex-direction: column; }
.expo-band {
  position: relative;
  width: 100%;
  /* neutralise inline --bc tints: illustration colours stay out of UI fills */
  --terracotta: #F1E7D4;
  --sage: #FCF8EF;
  --sky: #F5EEDF;
  background: var(--bc, var(--surface));
  border-top: 1px solid var(--black);
  box-shadow: 0 -6px 24px rgba(35,35,35,.07);
  margin-top: -12px;
  max-height: 136px; overflow: hidden;
  cursor: pointer;
  border-radius: 0;
  transition: max-height .65s cubic-bezier(.22,1,.36,1), transform .35s ease;
}
.expo-band:first-child { margin-top: 0; }
.expo-band:hover:not(.open) { transform: translateY(-5px); }
.expo-band.open { max-height: 560px; cursor: default; }
.expo-band:last-child { border-bottom: 1px solid var(--black); }
.band-inner {
  max-width: var(--maxw); margin: 0 auto; padding: 26px var(--gutter) 34px;
  display: grid; grid-template-columns: 1.25fr 1fr; gap: 18px 56px;
  align-items: start;
}
@media (max-width: 760px){ .band-inner { grid-template-columns: 1fr; } }
.band-title {
  margin: 0; font-family: var(--serif); font-weight: 400;
  font-size: clamp(24px, 3vw, 38px); line-height: 1.2; letter-spacing: -0.2px;
}
.band-title a { color: inherit; text-decoration: none; }
.band-title a:hover { font-style: italic; }
.band-side { font-size: 14px; line-height: 1.55; padding-top: 8px; }
.band-meta {
  display: flex; gap: 22px; font-family: var(--mono); font-size: 12px;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); margin-bottom: 14px;
}
.band-meta strong { font-weight: 500; letter-spacing: .12em; font-size: 11px; color: var(--accent); }
.band-excerpt { color: var(--ink); margin: 0 0 16px;
  opacity: 0; transition: opacity .45s ease .18s; }
.band-go { font-weight: 600; font-size: 14px; color: var(--ink);
  opacity: 0; transition: opacity .45s ease .28s; }
/* ⚠ THE ONLY WAY INTO THE ARTICLE IS THIS LINE, and it was a 17px-tall
   target (5 Oct 2026). Padding gives it 43; the negative top margin spends the
   excerpt's own 16px gap on it so the band's rhythm does not shift. */
@media (max-width: 760px) {
  .band-go { display: inline-block; padding: 13px 0; margin-top: -13px; }
}
.expo-band.open .band-excerpt, .expo-band.open .band-go { opacity: 1; }
.band-img {
  height: 170px; margin-top: 22px;
  opacity: 0; transition: opacity .45s ease .18s;
}
.expo-band.open .band-img { opacity: 1; }

/* --- article page --- */
.article-body blockquote { font-style: italic; letter-spacing: 0; }

/* --- home: belief pillars --- */
.pillar { text-align: left; }
.pillar .eyebrow { color: var(--accent); margin-bottom: 10px; }
.pillar h3 { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 22px; letter-spacing: 0; margin: 0 0 8px; }
.pillar p { color: var(--ink); font-size: 15px; margin: 0; }

/* --- home: what-we-do cards --- */
.offer-card { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; text-decoration: none; color: inherit; padding: 36px 34px; text-align: left; }
.offer-card h3 { font-family: var(--serif); font-weight: 400; font-size: 30px; letter-spacing: 0; margin: 4px 0 6px; }
.offer-card > p { color: var(--ink); margin-bottom: 16px; }
.offer-card .go { margin-top: auto; font-weight: 600; font-size: 15px; }
.offer-card .go::after { content: " →"; }
.offer-card:hover .go { text-decoration: underline; text-underline-offset: 4px; }
.offer-tags { display: flex; flex-wrap: wrap; gap: 0; margin-bottom: 16px; }
.offer-tags .tag { cursor: default; font-size: 11px; padding: 6px 13px 5px; background: var(--blush); border: none; }

/* ============================================================
   INTERIOR PAGE SHELL — shared by every page except Home.
   Defined with Fellowship (Aug 2026); see 02-page-definitions.md.
   Spacing follows the system scale: 40 · 48 · 64 · 96.
   ============================================================ */

/* --- page header: eyebrow · display h1 · standfirst · CTA · fact strip --- */
.page-head { padding: 56px 0 0; }
.page-head h1 {
  font-family: var(--serif-display); font-weight: 400;
  font-size: clamp(40px, 5.2vw, 76px); line-height: 1.04;
  text-transform: uppercase; color: var(--ink);
  margin: 0 0 24px; max-width: 15ch;
}
.page-head h1 em { font-family: var(--serif); font-style: italic; text-transform: none; }

.page-head .standfirst {
  font-family: var(--serif); font-size: clamp(19px, 1.7vw, 24px);
  line-height: 1.5; color: var(--ink); max-width: 620px; margin: 0;
}
.page-head .head-btns { margin: 40px 0 0; display: flex; flex-wrap: wrap; gap: 12px; }
.page-head .head-btns .btn { font-size: 15px; padding: 13px 24px; margin: 0; }

/* fact strip — the four things a reader needs before scrolling */
.factstrip {
  display: grid; grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--line); margin-top: 64px;
}
.factstrip div { padding: 24px 24px 24px 0; border-right: 1px solid var(--line); }
.factstrip div:last-child { border-right: 0; }
.factstrip dt {
  font-family: var(--mono); font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .12em; color: var(--accent); margin: 0 0 8px; font-weight: 500;
}
.factstrip dd {
  margin: 0; font-family: var(--serif); font-size: 21px; line-height: 1.3; color: var(--ink);
}
.factstrip dd small { display: block; font-family: var(--sans); font-size: 13px; color: var(--muted); margin-top: 4px; }

/* the rule is the row's only structure, and it brightens with its movement */






/* --- criteria checklist: answers "is this me?" without a card grid --- */
.crit { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; margin-top: 40px; }
.crit h3 {
  font-family: var(--serif); font-weight: 400; font-size: 26px; letter-spacing: 0;
  margin: 0 0 20px; padding-bottom: 16px; border-bottom: 1px solid var(--line);
}
.crit ul { list-style: none; margin: 0; padding: 0; }
.crit li { position: relative; padding: 0 0 16px 30px; font-size: 16px; line-height: 1.55; }
.crit li::before {
  content: "→"; position: absolute; left: 0; top: 0;
  font-family: var(--mono); color: var(--accent);
}
.crit .note { font-size: 14px; color: var(--muted); margin: 8px 0 0 30px; font-style: italic; font-family: var(--serif); }

/* --- closing CTA band --- */
.cta-band { padding: 96px 0; text-align: center; }
.cta-band h2 { margin: 0 0 20px; }
.cta-band p { max-width: 560px; margin: 0 auto; color: var(--ink); }
.cta-band .head-btns { justify-content: center; margin-top: 40px; }


@media (max-width: 900px) {
  .factstrip { grid-template-columns: 1fr 1fr; }
  .factstrip div:nth-child(2) { border-right: 0; }
  .factstrip div:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
  
  .crit { grid-template-columns: 1fr; gap: 40px; }
}
@media (max-width: 620px) {
  .factstrip { grid-template-columns: 1fr; }
  .factstrip div { border-right: 0; border-bottom: 1px solid var(--line); }
  .page-head h1 { font-size: 38px; }
}

/* --- filter bar: labelled controls + live result count (cohorts, projects) --- */
.filterbar { border-top: 1px solid var(--line); padding-top: 32px; }
.filterbar .frow { display: flex; align-items: baseline; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
.filterbar .flabel {
  font-family: var(--mono); font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .12em; color: var(--accent); font-weight: 500;
  min-width: 68px; flex-shrink: 0;
}
.filterbar .fset { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; }
.filterbar .fset .tag, .filterbar .fset .tab-btn { margin: 0; }
.result-count {
  font-family: var(--mono); font-size: 12px; text-transform: uppercase;
  letter-spacing: .1em; color: var(--muted); margin: 8px 0 0;
}

@media (max-width: 1100px) {
  .ab-origin { grid-template-columns: 1fr; }
}
@media (max-width: 900px) {
  .ab-banner img { max-height: 38vh; }
}
@media (max-width: 620px) {
  .ab-origin { padding: 20px; border-radius: 16px; }
}
/* --- the apply yard: four shapes on a floor ------------------------------
   The four tracks as four objects you can pick up — flat brand fill, no
   outline, square to the floor line, overlapping. This IS section 11; the
   old .apply-card grid it replaces said the same four words a third time.

   Geometry is authored in percentages of the yard's own width, so the whole
   composition scales as one and the overlaps hold at every size. Each shape
   is square (aspect-ratio 1/1) and bottom-aligned, so the varying widths
   give varying heights for free — which is most of what makes a row of
   shapes read as objects rather than as icons.

   The yard reserves 34% of its width in height: 30% for the tallest shape
   and the rest as headroom for the jostle, which would otherwise clip. --- */

/* nothing sits under the shapes any more, so the band ends where they do */

/* The band the shapes stand in. Its bottom edge is the footer's top edge, so
   the shapes rest ON the footer — which is also why the drawn floor hairline
   was deleted rather than kept: a rule a few pixels above a hard colour
   change is a second line saying the same thing. */


/* footer.site carries a 56px top margin on all thirteen pages, which left the
   shapes hovering above the edge they are supposed to stand on. The general
   sibling combinator scopes the fix to pages that actually have a yard — the
   shared rule is untouched, and nothing else in the site changes. */





/* The hexagon is the only shape with corners to soften, and it gets them from
   a same-colour round-joined stroke rather than a hand-drawn path. The stroke
   INFLATES the shape by half its width, so the polygon is authored inset to
   match — an un-inset one was clipping flat against the viewBox, which is
   also what was flattening the flower's petals and the circle's crown before
   their geometry was brought inside the box. */






/* The word is set in HTML over the shape, not inside the SVG, so it takes
   the house label voice and stays selectable. Ink on all four fills — each
   clears 7:1 against black. */



/* Below 760px there is no cursor to shove anything with and no room for a
   floor of four — a plain 2x2 of the same shapes, physics off. */


/* an answer we do not have yet — say so rather than inventing one.
   ⚠️ THE SELECTOR CARRIES `.faq-a` BECAUSE `.faq-a p` OUTRANKS A BARE CLASS.
   `.faq-a p` is (0,1,1) against this rule's (0,1,0), so the 11px declared here
   never applied — the marker has been rendering at the answer's own body size
   since it was written, which is why it read as a paragraph in caps rather than
   as a label. It is the micro label now, same as `.ex-credit`. */
.faq-a p.answer-pending, .answer-pending {
  font-family: var(--mono); font-weight: 500;
  font-size: 11px; line-height: 1.5; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}

/* --- page banner — the About pattern, reusable on any page --- */
/* --- page banner — one rule for every page. The ratio is the About export's
       own (4096x1289); the vh cap keeps the header underneath it inside the
       first fold on a laptop. --- */
/* BANNER HEIGHT IS TWO DIALS, and all six banner pages (about, cohorts,
   fellowship, services, projects, labs) run off both.

   ⚠️ `--banner-max` IS NOT THE ONE THAT SETS THE HEIGHT. It is a cap, and at
   1440x900 the ratio below yields 453px while 46vh capped at 414 — so raising
   the cap past ~50vh does NOTHING. Measured: 34vh->306px, 46vh->414,
   56vh->453, 70vh->453, 90vh->453. It flatlines the moment the cap stops
   biting, because `aspect-ratio` then owns the box. Raise `--banner-ratio`
   (i.e. make the number smaller) to make banners taller; `--banner-max` only
   protects short and landscape windows from a banner that eats the fold.

   18 Sep 2026: ratio 4096/1289 (3.18:1) -> 2.7:1, which is 453px -> 533px at
   1440 wide, +18%. Cap raised to 62vh so it does not clamp at 900 tall.
   (The dial went briefly to 34vh first, on a misread of "heighten" as "hide" —
   the ask was always for MORE banner.)

   ⚠️ A TALLER BOX CROPS THE SIDES. The source banners are 4096x1289; in a
   2.7:1 box `object-fit: cover` scales to 1694 wide against a 1440 box, so
   ~15% goes off left and right. Every one of these has its subject centred, so
   it survives — but a new banner with something important near an edge will
   lose it. Fellowship's 2.36:1 dither gains here: a taller box crops less of
   its height, not more.

   The pending placeholder reads both variables, so the real banner and the
   "to come" box can never drift apart. */
:root { --banner-ratio: 27 / 10; --banner-max: 62vh; }
.page-banner { width: 100%; background: var(--surface); }
/* :not(.banner-scene) — fellowship's banner is a four-layer composition whose
   images are absolutely placed, and this rule's width/aspect-ratio/object-fit
   would fight every one of them. The other five banner pages are unaffected. */
.page-banner:not(.banner-scene) img {
  display: block; width: 100%; aspect-ratio: var(--banner-ratio);
  max-height: var(--banner-max); object-fit: cover; object-position: center;
  /* EVERY BANNER ON THIS SITE IS A DITHER, and smoothing one destroys the
     pattern — the same reason the footer reveal and the cohort portraits carry
     this. It matters most on fellowship's, which is a 390x165 source stretched
     across a ~1440px slot: without this it resolves to grey mush rather than
     to visible, deliberate pixels. */
  image-rendering: pixelated;
}
/* header split: the title holds the left column, the promise and the actions
   the right, so the two land on one line of sight under the banner */
.page-head.split { padding-top: clamp(36px, 4.5vw, 72px); }
.page-head.split .ph-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start;
}
/* In the split header the column already sets the measure, so the 15ch cap is
   dropped and the size comes down a step — at the full page size this title
   stacked four lines against a 224px right column and pushed the facts strip
   below the fold. */
.page-head.split h1 {
  margin: 0; max-width: none;
  /* sentence case, against the uppercase default for .page-head h1. Sentence
     case sets narrower, so the size can come back up a step without the title
     stacking further. */
  text-transform: none;
  font-size: clamp(36px, 4.6vw, 66px); line-height: 1.08;
}
.page-head.split .ph-body { padding-top: 6px; }
/* this is body copy, so it takes the body face rather than the serif */
.page-head.split .standfirst {
  /* Back to 46ch/22px. It went to 54ch/30px on 20 Sep for a four-clause
     sentence; the copy was cut to two clauses the same day and the wider
     measure just made a short paragraph sit as one long line. */
  margin: 0 0 22px; max-width: 46ch;
  font-family: var(--sans); font-size: clamp(16px, 1.4vw, 18px); line-height: 1.6;
}
/* `.page-head.split .head-promise` lived here — the quieter second line that
   replaced the pillar deck on 18 Sep 2026. The two paragraphs were folded into
   a single standfirst on 20 Sep 2026, so the rule had no markup left; the
   measure went up to 54ch to carry the longer sentence. */
.page-head.split .head-btns { margin: 0; }
/* This was written as `.page-head.split @media (...)` — a selector followed by
   an at-rule, which is a parse error, so the whole block was being dropped and
   the split header never collapsed on narrow screens. */
@media (max-width: 900px) {
  .page-head.split .ph-grid { grid-template-columns: 1fr; gap: 24px; }
  .page-head.split .ph-body { padding-top: 0; }
}

/* touch targets: 40px is comfortable with a mouse but under the 44px guideline
   for fingers, so pad back up where the pointer is coarse */
@media (pointer: coarse) {
  .btn, .nav-cta { padding-top: 12px; padding-bottom: 12px; }
}

/* Services and Labs leave the thumb an empty placeholder block; here it holds
   a photograph. `cover` and no padding — the opposite of what the partner
   marks needed. Those were transparent square logos, which had to be
   `contain`ed with 11% padding or a 4:3 crop ate the space they were drawn
   with; a photograph wants to fill the tile and be cropped to it. The images
   are pre-cropped to 720x540 so `cover` never has much to do. */
.work-thumb img {
  width: 100%; height: 100%; display: block;
  object-fit: cover; border-radius: inherit;
}

/* The heading is an ordinary section head above the fan, not something sitting
   in its well — the reader is told what they are looking at before it arrives.
   This box holds the cards and nothing else, and it runs FULL BLEED: the arc's
   radius is solved from the width it has to fit, so a wider box is the only
   way to make the cards bigger without closing the gaps between them.

   ⚠️ THE HEIGHT IS AN ASPECT-RATIO AND MUST STAY ONE. stage.js used to write
   it off the finished geometry, which is more exact and completely wrong here:
   a full-bleed box takes its width from the viewport, so writing its height
   changed the page height, toggled the scrollbar, changed the viewport width
   and so changed this box's width. The fan then sat centred on a width the
   box no longer had. A ratio gets the same height with no feedback path. 2.45
   is the geometry's own ratio across the range the card width is free in
   (it runs 2.41 at 1024 to 2.58 at 1380), so the slack is never more than a
   few percent. */


/* It is a card, not a cropped photograph: paper ground, an ink hairline, one
   radius, the caption printed under the portrait inside the same object. The
   OUTLINE is what makes five overlapping cards read as five objects — on a
   fan, a tonal ground alone leaves the seams ambiguous where cards lap. It is
   the house card's own rule (`.card`, 1px ink), just on a radius. */

/* the arc has no room for three wrapping tag pills per card; they come back
   in the stacked layout below 900px, where there is */


/* 4:5, not square — a card is taller than it is wide, and a square photo on
   a card with a caption under it made the whole object read as a tile. */




/* The name and the role are ONE pair — 2px apart, not a reserved line each.
   Reserving two lines for both (to keep the cards uniform) opened a gap in
   the middle of every caption that made the type look broken.

   Uniformity now comes from the CAP, which holds one height for all five, so
   the slack falls below the role where it reads as the card's own margin
   instead of a hole in the type. The arc is seated off ONE measured card
   height, so cards that wrap differently would otherwise sit wrong on it. */

/* --- THE DEAL. The stack comes UP FROM THE BOTTOM of the box, squared up
   and barely fanned, arrives at the top of the arc as one object — and only
   then do the cards SWING OUT to their seats. Two phases in one keyframe set,
   because a CSS transition cannot hold a pose and then move.

   The second phase changes NOTHING BUT THE ANGLE, so each card travels along
   the arc about the shared pivot. That is what a hand of cards being spread
   actually does, and it is the difference between this and the first version,
   which interpolated between two translate() positions and therefore fired
   the cards outward in straight lines from a point.

   The stagger is deliberately tiny (see DEAL_STEP in stage.js): the stack has
   to read as a stack — one object arriving — and the spread has to read as
   one gesture, not five cards taking turns.

   Hidden behind .armed, which stage.js adds before it observes, so a script
   that never runs leaves five plainly visible cards. Same contract as
   .scene-ready and .tl-ready. --- */
@keyframes mcard-deal {
  0% {
    opacity: 0;
    animation-timing-function: cubic-bezier(.22, .9, .3, 1);
    transform: translate(calc(var(--ax, 50%) - 50%), calc(var(--ay, 50%) - 50%))
               translateY(var(--rise, 220px)) rotate(var(--drot, 0deg));
  }
  34% {
    opacity: 1;
    animation-timing-function: cubic-bezier(.34, 1.02, .34, 1);
    transform: translate(calc(var(--ax, 50%) - 50%), calc(var(--ay, 50%) - 50%))
               rotate(var(--drot, 0deg));
  }
  100% {
    opacity: 1;
    transform: translate(calc(var(--ax, 50%) - 50%), calc(var(--ay, 50%) - 50%))
               rotate(var(--arot, 0deg));
  }
}



/* --- HOVER PUTS OTHER PEOPLE'S CURSORS ON THE CARD ------------------------
   Each skill arrives as its own Figma-style cursor — arrow plus label, its
   own brand colour, its own spot around the card, its own slight angle. Not a
   column trailing the pointer: stacked in a line they read as a LIST, and the
   point is that several people are pointing at this person at once.

   Placement is authored per slot rather than computed, because "erratic" is a
   composition, not a random number: each slot is chosen to sit off a
   different edge, none of them square to the card, and the set is the same
   every time so it never lands badly. --- */





/* four slots, each off a different edge, none of them square to the card */
/* Four slots, each off a different part of the card, none square to it. They
   START inside the card's own width on purpose: a slot hanging off the left
   was clipped on the outermost card of the fan, and the arc runs to both
   edges of the page. The label grows rightward from its arrow, and the fan is
   inset far enough (see the solve in stage.js) that even the rightmost card's
   longest label stays on the page. */





/* Below 900px the arc is gone — no cursor to answer, and five turned cards on
   a phone-width arc would be thumbnails. An ordinary stacked grid with the
   heading above it, which is the DOM order already. */
@media (max-width: 900px) {
  
  /* the card furniture is for the fan: stacked full-width, an outlined tile
     round a full-bleed photo is a box round a box */
  
  
  
  
  
  
  
  
  
}



/* --- sections no longer divide with an ink rule. Space does the separating;
       a hairline between every section turned the page into a ledger. --- */

/* pillars — complete cards. Each illustration already carries a full-bleed
   brand ground (#FF9A80 / #6969FF / #7FD79C), so the card takes the matching
   tint and the image reads as part of one surface rather than a pasted-in
   panel. Same radius and ink text as the origin deck on About. */
.pillar-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px; margin-top: 40px;
}
.pillar { border-radius: 20px; padding: 20px 20px 26px; }
/* ⚠️ No page renders `.pillar--*` today. Kept consistent anyway: this one
   reached into the illustration tints while its two siblings took brand
   tokens, which is exactly the trap the scale exists to close. */
.pillar--terracotta { background: var(--brand-terracotta-soft); }
.pillar--violet     { background: var(--brand-violet); }
.pillar--green      { background: var(--brand-green); }
.pillar-img {
  aspect-ratio: 620 / 417; overflow: hidden; border-radius: 12px;
  margin-bottom: 20px;
}
.pillar-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pillar .eyebrow { color: #3C2A20; margin: 0 0 8px; }
.pillar h3 { margin: 0 0 10px; color: var(--black); }
.pillar p { color: #1A1209; margin: 0; }

/* who should apply — three blocks against the programme illustration */







@media (max-width: 900px) {
  .pillar-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  
  
}
@media (max-width: 560px) { .pillar-grid { grid-template-columns: 1fr; } }

/* --- typography, aligned to About -------------------------------------
   About sets every card heading in Libertinus Serif 400 and every person's
   name in Public Sans 400/24. Fellowship was inheriting the global
   `h3 { sans, 600, 17px }`, which is why the two pages read as different
   sites. One scale, applied by what the heading IS rather than by tag:

     section head      serif 400   ~40px      (h2, unchanged)
     card head         serif 400   19-26px
     person's name     sans  400   20-24px    (matches .crew-card .nm)
     UI label          sans  500   16px       (.faq-q, buttons)
   --------------------------------------------------------------------- */
.pillar h3,
.pillar h3 { font-size: clamp(22px, 2.1vw, 27px); line-height: 1.25; }

.pillar h3 { color: var(--black); }

/* a banner slot with no image yet — say so rather than leaving a blank band */
/* an empty slot takes the same box as a filled one, so the page does not
   reflow when the real image lands */
.page-banner.pending {
  aspect-ratio: var(--banner-ratio); max-height: var(--banner-max);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px rgba(35, 35, 35, .14);
  display: grid; place-items: center;
}
.page-banner.pending::after {
  content: "Banner image to come";
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}

/* --- the six practice areas as filed sheets ---------------------------
   Each sheet carries its title in a tab at its very top, and locks one tab
   lower than the sheet before it — so a covered sheet still shows its number
   and name, and by the sixth all six titles are legible at once. The sheets
   behind do not scale — each sheet simply takes the screen, full bleed.
   ---------------------------------------------------------------------- */
:root { --tab-h: 62px; }
.sheets { position: relative; }
.sheet {
  position: sticky; top: var(--stack-top);
  /* A sheet's height IS the scroll distance before the next one arrives — at
     238px each service was gone in a flick. Sized off the COMPACT header (a
     stable, measured value) rather than --stack-top, which changes when the
     header pins and would otherwise shift the whole stack mid-scroll. Tall
     enough to reach the fold from its locked position, so no strip of page
     shows beneath the front sheet. */
  min-height: calc(100svh - var(--stack-top, 337px));
  border-radius: 20px 20px 0 0;
}
/* the first service gets an extra beat to land before 02 starts rising */
.sheet:first-of-type { min-height: calc(100svh - var(--stack-top, 337px) + 180px); }
.sheet:nth-of-type(1) { top: calc(var(--stack-top) + 0 * var(--tab-h)); z-index: 1; }
.sheet:nth-of-type(2) { top: calc(var(--stack-top) + 1 * var(--tab-h)); z-index: 2; }
.sheet:nth-of-type(3) { top: calc(var(--stack-top) + 2 * var(--tab-h)); z-index: 3; }
.sheet:nth-of-type(4) { top: calc(var(--stack-top) + 3 * var(--tab-h)); z-index: 4; }
.sheet:nth-of-type(5) { top: calc(var(--stack-top) + 4 * var(--tab-h)); z-index: 5; }
.sheet:nth-of-type(6) { top: calc(var(--stack-top) + 5 * var(--tab-h)); z-index: 6; }

/* the tab — the part that stays readable once the sheet is covered */
.sheet-tab {
  height: var(--tab-h); box-sizing: border-box;
  max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter);
  display: flex; align-items: center; gap: clamp(20px, 3vw, 46px);
}
.sheet-n {
  font-family: var(--sans); font-weight: 400; line-height: 1;
  font-size: clamp(20px, 1.8vw, 26px); color: var(--black);
  flex: 0 0 auto; min-width: 2.4ch;
}
.sheet-tab h3 {
  margin: 0; font-family: var(--serif); font-weight: 400; color: var(--black);
  font-size: clamp(20px, 2.2vw, 30px); line-height: 1.15; letter-spacing: -.3px;
}
/* the rule sits under the number, continuing the rail below */
.sheet-tab::after { content: none; }

.sheet-in {
  max-width: var(--maxw); margin: 0 auto;
  padding: clamp(20px, 3vh, 40px) var(--gutter) var(--gutter);
  display: grid; grid-template-columns: 148px 1fr; gap: clamp(24px, 4vw, 64px);
}
.sheet-rail {
  display: flex; flex-direction: column; gap: 84px;
  padding-right: clamp(24px, 4vw, 64px);
  border-right: 1px solid rgba(20, 13, 13, .32);
}
.sheet-punch { width: 14px; height: 14px; border-radius: 2px; background: rgba(20, 13, 13, .82); }
.sheet-body { max-width: none; }
.sheet-body p {
  margin: 0; max-width: 40ch; color: #1A1209;
  font-size: clamp(16px, 1.4vw, 19px); line-height: 1.6;
}
/* The runway has to outlast the LAST sheet's own screen, or sticky pins the
   final few to the container bottom and they collapse onto one another. One
   full viewport past the deck keeps 06 held at its offset, then releases the
   whole stack together. */
/* Hold only long enough to register the finished stack, then move on. At
   150svh this was 1240px of scrolling after 06 locked; 50svh gives ~380px. */
/* Carries the front sheet's colour, otherwise the page shows through between
   the last sheet and the next section as a cream band. Short, so the process
   section's content arrives as soon as the stack lifts. */
.sheets-runway { height: 0; }

/* One hue in this section and nothing else: the sheet in front is terracotta,
   everything already behind it drops back to the page's own paper. So the
   colour always marks "this is the service you are reading", rather than six
   competing grounds. services.js sets .is-front from scroll position. */
/* front = terracotta; once superseded a sheet fades back to blush, the
   desaturated end of the same hue — not to the page's paper, so the stack of
   finished services still reads as a set rather than dissolving into the
   background. */
.sheet { background: var(--screen-soft); transition: background .34s cubic-bezier(.22,1,.36,1); }
.sheet.is-front { background: var(--screen); }
/* paper-on-paper needs a visible seam once a sheet has gone behind */
/* seam only between sheets, never under the last one — there it drew a line
   across the join into the next section */
.sheet { box-shadow: 0 -1px 0 rgba(20, 13, 13, .22); }
.sheet:last-of-type { box-shadow: none; }
@media (prefers-reduced-motion: reduce) { .sheet { transition: none; } }

@media (max-width: 900px) {
  /* stacking full screens on a phone just buries the reader */
  .sheet { position: static; min-height: 0; border-radius: 0; box-shadow: none; }
  .sheet-tab { height: auto; padding: 28px var(--gutter) 0; }
  .sheet-in { grid-template-columns: 84px 1fr; padding: 16px var(--gutter) 40px; }
  .sheet-rail { gap: 44px; }
  .sheets-runway { display: none; }
  .sheet-body, .sheet-body p { max-width: none; }
}

/* --- services stage — the page header pins under the bar and holds while the
       six sheets stack beneath it. --stack-top is the bar plus the pinned
       header's measured height; services.js keeps it current, because the
       header's height changes between its full and compact states. --- */
.services-stage { position: relative; --stack-top: calc(var(--bar-h) + 200px); }
.stage-sentinel { display: block; height: 1px; margin-bottom: -1px; }

/* The header does not resize. It simply rolls up, sticks under the bar, and
   the services stack beneath it — heading, body copy and buttons all hold
   their size throughout. */
.services-stage .page-head.split {
  position: sticky; top: var(--bar-h); z-index: 20;
  background: var(--bg);
}

@media (max-width: 900px) {
  .services-stage .page-head.split { position: static; }
  .services-stage.is-stuck .page-head.split .head-btns { display: block; }
}

/* --- the engagement, as a flow -----------------------------------------
   Three boxes said "three unrelated things". A rule running through three
   nodes says "one thing, in order" — which is what the section claims.
   No boxes, no fills: a hairline, a node, and the type.
   ---------------------------------------------------------------------- */
.flow {
  list-style: none; margin: 40px 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(24px, 4vw, 56px);
  position: relative;
}
/* the rule the nodes sit on */
.flow::before {
  content: ""; position: absolute; left: 0; right: 0; top: 7px;
  border-top: 1px solid rgba(35, 35, 35, .22);
}
.flow-step { position: relative; padding-top: 34px; }
.flow-step::before {
  content: ""; position: absolute; left: 0; top: 0;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--bg); box-shadow: inset 0 0 0 1.5px var(--ink);
}
.flow-step:first-child::before { background: var(--ink); box-shadow: none; }
.flow-n {
  display: block; font-family: var(--mono); font-size: 11px;
  letter-spacing: .14em; color: var(--accent); margin-bottom: 8px;
}
.flow-step h3 {
  margin: 0 0 8px; font-family: var(--serif); font-weight: 400;
  font-size: clamp(22px, 2.2vw, 28px); line-height: 1.2; color: var(--ink);
}
.flow-step p { margin: 0; color: var(--muted); max-width: 34ch; }

/* Labs' lifecycle is four stages, not three. Same rule, same nodes — only
   the track count changes, and it drops to two-up before it goes vertical so
   four 34ch paragraphs never get squeezed into a tablet width. */
.flow--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1080px) and (min-width: 761px) {
  .flow--4 { grid-template-columns: repeat(2, 1fr); row-gap: 40px; }
  .flow--4::before { display: none; }
  .flow--4 .flow-step { padding-top: 26px; }
}

@media (max-width: 760px) {
  /* vertical: the rule runs down the left instead of across the top */
  .flow, .flow--4 { grid-template-columns: 1fr; gap: 28px; padding-left: 30px; }
  .flow::before { left: 7px; right: auto; top: 6px; bottom: 6px; border-top: 0;
                  border-left: 1px solid rgba(35, 35, 35, .22); }
  .flow-step { padding-top: 0; }
  .flow-step::before { left: -30px; top: 6px; }
}

/* --- the work — a filtered index. The rail holds the heading and the
       filters and stays put; the work scrolls past it on the right. --- */
.work {
  display: grid; grid-template-columns: 320px 1fr;
  gap: clamp(32px, 5vw, 72px); margin-top: 8px; align-items: start;
}
.work-side { position: sticky; top: calc(var(--bar-h) + 28px); }
/* the rail is 320px wide — the section-scale h2 wrapped to three lines in it */
.work-side h2 {
  margin: 0 0 12px;
  font-size: clamp(26px, 2.6vw, 34px); line-height: 1.2;
}
.work-lead { margin: 0 0 28px; color: var(--muted); font-size: 15px; }
/* the filters ARE the house pill (.tag) — mono, uppercase, ink outline. Only
   behaviour and the pressed state live here; the look is defined once. */
.work-filters { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0; }
.work-filter { margin: 0 6px 8px 0; }
.work-filter:hover { background: var(--surface); }
.work-filter[aria-pressed="true"] {
  background: var(--black); color: #fff; border-color: var(--black);
}

.work-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 28px; }
.work-card {
  display: block; text-decoration: none; color: inherit;
  border-radius: 20px; padding: 16px 16px 22px;
  background: var(--surface);
  transition: background .2s ease, transform .35s cubic-bezier(.22,1,.36,1);
}
.work-card:hover { background: var(--screen-soft); transform: translateY(-3px); }
.work-thumb {
  display: block; aspect-ratio: 4 / 3; border-radius: 14px;
  background: rgba(35, 35, 35, .07); margin-bottom: 14px;
}
.work-meta {
  display: block; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: .11em; text-transform: uppercase; color: var(--accent);
  margin: 0 4px 6px;
}
.work-card h3 {
  margin: 0 4px 6px; font-family: var(--serif); font-weight: 400;
  font-size: clamp(19px, 1.7vw, 23px); line-height: 1.25; color: var(--ink);
}
.work-card p { margin: 0 4px; color: var(--muted); font-size: 14.5px; }
.work-card[hidden] { display: none; }
.work-empty { grid-column: 1 / -1; color: var(--muted); margin: 8px 0 0; }

@media (max-width: 980px) {
  .work { grid-template-columns: 1fr; gap: 28px; }
  .work-side { position: static; }
  .work-filters { flex-wrap: wrap; gap: 0; }
}
@media (max-width: 620px) { .work-list { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .work-card { transition: none; } }

/* --- a section that owns the screen ------------------------------------
   Used after the services stack: each of these is a complete scene, so the
   stack scrolls away and the next section arrives whole rather than sliding
   in under the tail of the last one. --- */
.section--screen {
  min-height: 100svh; display: grid; align-content: center;
  padding-top: clamp(48px, 8vh, 110px); padding-bottom: clamp(48px, 8vh, 110px);
}
/* filtering must not resize the page — the list keeps the footprint of its
   fullest state, so nothing below it moves when the set shrinks */
.work-list { min-height: var(--work-list-h, auto); align-content: start; }

@media (max-width: 900px) {
  .section--screen { min-height: 0; }
  .work-list { min-height: 0 !important; }
}

/* during measurement nothing may be mid-transition, or the compact height is
   read while the buttons are still collapsing */

/* --- the process section carries the terracotta on ---------------------
   Sheet 06 leaves in the highlight colour and this section picks it up, so
   scrolling out of the stack reads as the page turning terracotta rather
   than the colour stopping dead. Everything inside is re-toned for the
   darker ground — olive and --muted both fall apart on it. --- */
/* The process section sits in normal flow — the stage lifts away above it. */
/* --- the screen accent -------------------------------------------------
   Services and Labs are one machine with one colour swapped. Everything that
   paints the accent — the front sheet, the sheets behind it, the process
   screen, the nav while it is over either — reads these four variables, so a
   page sets ONE class on <body> and the whole scene follows. Forking the
   rules per page is how the two drift apart.
   --screen      the ground itself
   --screen-soft the same family, desaturated: a sheet that has gone behind
   --screen-ink  body type and nodes ON the ground (~6.5:1 either way)
   --screen-n    the step number: the ground darkened (~4:1, a mono label)
   ---------------------------------------------------------------------- */
:root {
  --screen:      var(--brand-terracotta);
  --screen-soft: var(--blush);
  --screen-ink:  #1A1209;
  --screen-n:    #5A2E1C;
}
body.accent-olive {
  --screen:      var(--brand-olive);
  --screen-soft: var(--olive-soft);
  --screen-ink:  #141A0A;
  --screen-n:    #333F1B;
}

/* --- the process scene -------------------------------------------------
   The stack lifts away and this section takes the screen and PLAYS ITSELF IN
   rather than arriving whole. It pins for just under a screen of scroll while
   the heading slides up, the rule draws left to right, the steps arrive one
   after another and the CTA lands; then the ground hands back from the accent
   to paper, so the index below comes in on beige instead of cutting off a
   solid colour.

   Two earlier shapes were both wrong. A 100svh .section--screen with CENTRED
   content put ~265px of empty ground above the heading and ~365px below —
   "an empty section, and then suddenly another section". Collapsing it to
   content height fixed the void but left the timeline jammed against the
   index with nothing between them. The scroll distance was never the problem;
   having nothing happen across it was.

   stage.js drives --p (0..1 across the pinned range), --rule (the hairline's
   scaleX) and --fade (accent -> paper at the tail). Everything here is a
   function of those three; no timers, so the reader's scroll IS the timeline.
   ---------------------------------------------------------------------- */
.section--accent {
  position: relative;
  min-height: 0; padding: 0;
  background: color-mix(in oklab, var(--bg) calc(var(--fade, 0) * 100%), var(--screen));
}
.section--accent > .wrap,
.mission > .wrap {
  position: sticky; top: var(--bar-h);
  min-height: calc(100svh - var(--bar-h));
  display: grid; align-content: center;
  padding-top: clamp(44px, 6.5vh, 84px);
  padding-bottom: clamp(52px, 8vh, 104px);
}
/* the scroll the choreography is spread over. A real child box, for the same
   reason .sheets-runway is: sticky is bounded by the containing block's
   CONTENT box, so padding here would buy no travel at all. */
/* 62, not 88. At 88 the scene finished its last move at 60% of the runway
   and then held for a further 35vh of nothing but the colour fade — a whole
   screen of empty accent before the index below arrived. */
.scene-runway { height: 62svh; }
.section--accent > .wrap > h2 { margin-top: 0; }

/* Hidden only once stage.js has taken charge — if the script never runs the
   section still reads as a plain, fully visible block. */
.section--accent.scene-ready [data-in] {
  opacity: 0; transform: translateY(22px);
  transition: opacity .6s cubic-bezier(.22, 1, .36, 1),
              transform .6s cubic-bezier(.22, 1, .36, 1);
}
.section--accent.scene-ready [data-in].is-in { opacity: 1; transform: none; }
/* the rule draws rather than appears */
.section--accent.scene-ready .flow::before {
  transform: scaleX(var(--rule, 0)); transform-origin: left center;
}
@media (prefers-reduced-motion: reduce) {
  .section--accent.scene-ready [data-in] { opacity: 1; transform: none; transition: none; }
  .section--accent.scene-ready .flow::before { transform: none; }
}
/* pinning a scene inside a phone viewport buries the reader, same call as the
   sheets: below 900px it is an ordinary stacked section */
@media (max-width: 900px) {
  .section--accent > .wrap {
    position: static; min-height: 0;
    padding-top: clamp(44px, 6.5vh, 84px); padding-bottom: clamp(52px, 8vh, 104px);
  }
  .scene-runway { display: none; }
  .section--accent.scene-ready [data-in] { opacity: 1; transform: none; transition: none; }
  .section--accent.scene-ready .flow::before { transform: none; }
}
/* body.is-screen paints the whole page the accent colour while the scene is
   under the bar — that is how the nav repaints. It was invisible while the
   scene was a full 100svh, because nothing else shared the viewport with it.
   Now that the scene is content-height, the section below it comes into frame
   alongside it and, having no ground of its own, showed the accent through.
   Every screen section that is not the accent one states its ground. */
.section--screen:not(.section--accent) { background: var(--bg); }
/* the timeline is the centrepiece, so it gets the air the voids used to hold */
.section--accent .flow { margin-top: clamp(32px, 5vh, 60px); }
.services-stage + .section--accent { margin-top: 0; }

/* When the stack is done, services.js freezes every sticky element at its
   current visual position and hands it back to the flow, so the header and
   all six tabs travel up together as one block. Without this they unstick at
   different scroll points — the sheets let go ~450px before the header — and
   the stack visibly comes apart on the way out. */
/* released elements are placed absolutely by services.js; the stage keeps a
   frozen height so the flow below it never shifts */
.services-stage { position: relative; }
.services-stage.is-released .page-head.split,
.services-stage.is-released .sheet { z-index: auto; }

/* the whole page, nav included, turns terracotta for this scene */
body.is-screen { background: color-mix(in oklab, var(--bg) calc(var(--fade, 0) * 100%), var(--screen)); }
body.is-screen header.site { background: color-mix(in oklab, var(--bg) calc(var(--fade, 0) * 100%), var(--screen)); }
body.is-screen .nav-shell { background: color-mix(in oklab, var(--bg) calc(var(--fade, 0) * 100%), var(--screen)); }
/* The paper band behind the bar takes the accent too. These screen rules were
   written when header.site itself carried the paper; the band that replaced
   it knew nothing about them, so it went on painting --bg across the full
   width while the shell alone turned terracotta — the accent reached the
   middle of the bar and stopped, and the cream either side of it read as a
   band with a line under it. One surface, one colour. */
body.is-screen header.site::before {
  background: color-mix(in oklab, var(--bg) calc(var(--fade, 0) * 100%), var(--screen));
}
body.is-screen .nav-main > ul > li > a,
body.is-screen .nav-contact,
body.is-screen .logo { color: var(--black); }
body.is-screen .nav-main > ul > li:hover > a { background: rgba(255, 251, 242, .35); }
body, header.site, .nav-shell { transition: background .45s ease; }
@media (prefers-reduced-motion: reduce) {
  body, header.site, .nav-shell { transition: none; }
}
@media (max-width: 900px) {
  .services-stage.is-released .page-head.split,
  .services-stage.is-released .sheet { transform: none !important; }
}
.section--accent h2 { color: var(--black); }
.section--accent .flow::before { border-top-color: rgba(20, 13, 13, .38); }
.section--accent .flow-step::before {
  background: var(--screen); box-shadow: inset 0 0 0 1.5px var(--screen-ink);
}
.section--accent .flow-step:first-child::before { background: var(--screen-ink); box-shadow: none; }
.section--accent .flow-n { color: var(--screen-n); }
.section--accent .flow-step h3 { color: var(--black); }
.section--accent .flow-step p { color: var(--screen-ink); }
@media (max-width: 760px) {
  .section--accent .flow::before { border-left-color: rgba(20, 13, 13, .38); }
}


/* ─── ONE COLUMN, TWO RAMPS ──────────────────────────────────────────────
   Figma 505:1074 / 505:1048 / 515:1289.

   ⚠️ THE TWO FRAMES DIFFER IN MORE THAN POSITION. 505:1074 gives every column
   the SAME gradient. 505:1048 does not — its dusk and cool stops move per
   column, from .6815/.78724 down to as low as .61288/.65, and three columns
   shift their peach and warm stops as well. That is the wave CHANGING SHAPE:
   colour arrives at a different point inside each column, the centre pair
   keeping the stretched ramp while the others compress. Translating the
   columns alone reproduces the tide and loses the wave.

   So each column holds BOTH ramps stacked, and the shift cross-fades between
   them. Written this way for a reason: re-generating a gradient string per
   column per frame means re-rasterising N gradients on every scroll tick,
   where an opacity cross-fade is a compositor job and the two ramps are
   rasterised once. Per frame this costs N transforms and ONE custom property.

   Both gradients are built in stage.js from the radial's own offsets — a stop
   at offset o sits at (354.59 + o * 2234.4) / 2589 of the box — so nothing
   here is eyeballed. Height 310svh: the frames are 1728 x 835, and
   2589 / 835 = 3.101. */
/* left and width are set in stage.js as percentages — eleven columns on
   desktop, five below 900px, fluid at every width in between. */
/* ⚠️ 176.43svh, NOT 310svh — and the two are the same thing. The full column
   is 310svh (2589/835), but its top 43.107% is the gradient's transparent head
   and can never show ink, so the element is the remaining 56.893%:
   310 x .568934 = 176.43. stage.js renormalises the stops into this box and
   adds HEAD back into the transform, so nothing moves on screen; it is 43% of
   the painting, tiling and layer memory that simply stops happening.
   Change one of these numbers and you must change the other. */
.mv-col {
  position: absolute; top: 0;
  height: 176.43svh;
}
/* ⚠️ THE STRIP IS 16px WIDE AND TILED. stage.js paints a dithered 16px strip
   (Bayer 8x8 at Size 2 repeats every 16px, and the gradient is flat across a
   column, so the strip IS the image, not a sample of it) and it repeats
   across. background-size 100% on the vertical is 1:1 because the strip is
   generated at the element's own pixel height — any vertical scaling here
   would resample the dither into mush.

   image-rendering: pixelated is what keeps it crisp on a 2x display: the
   strip is authored in CSS pixels, and the default smooth upscale turns an
   ordered dither into grey. */
.mv-ramp {
  position: absolute; inset: 0;
  background-repeat: repeat-x;
  background-size: 16px 100%;
  image-rendering: pixelated;
}
/* ramp B is 505:1048's per-column ramp, fading in over 505:1074's as the
   sentence completes. One property on the scene drives every column. */
.mv-ramp--b { opacity: var(--mix, 0); }
/* The pillar deck's rules lived here — .deck / .deck-stage / .deck-card /
   .deck-text and their phone fallback. The section was cut 18 Sep 2026 (see
   the note in fellowship.html); the rules are deleted rather than left, because
   three live bugs on this site have now come from a superseded layout's CSS
   surviving its markup. `.deck-stage` also rode on the grouped sticky rule
   shared with .mission > .wrap and .section--accent > .wrap — only its name
   was taken off that selector, not the rule. */

/* ═══════════════════════════════════════════════════════════════════════
   PROJECTS — the index
   ═══════════════════════════════════════════════════════════════════════
   After the Joby news layout the user referenced: filter rail pinned on the
   left, one row per project on the right — date, thumbnail, title, category —
   separated by hairlines. Replaces a 3-up card grid sitting under three
   STACKED filter rows, which between them ate the whole first screen before a
   single project appeared.

   The rail is the same idea as .work-side on Services and Labs, and it suits
   three filter dimensions far better than rows do: they stack down a column
   naturally instead of pushing the content further down the page each time
   another dimension is added.
   ═══════════════════════════════════════════════════════════════════════ */
.proj-split {
  display: grid; grid-template-columns: minmax(0, 210px) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 80px); align-items: start;
}
.proj-rail { position: sticky; top: calc(var(--bar-h) + 28px); }
.proj-group { margin-bottom: 26px; }
.proj-group .eyebrow { margin-bottom: 10px; }

/* Joby marks the live option with a dot rather than a filled pill. It keeps a
   column of options quiet — a stack of filled chips would fight the rows. */
.proj-opt {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; padding: 4px 0 4px 18px;
  position: relative; cursor: pointer;
  font-family: var(--sans); font-size: 15px; line-height: 1.5;
  color: var(--muted); transition: color .15s ease;
}
.proj-opt:hover { color: var(--ink); }
.proj-opt.is-on { color: var(--ink); }
.proj-opt.is-on::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: 7px; height: 7px; margin-top: -3.5px;
  border-radius: 50%; background: var(--black);
}
.proj-count {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin: 30px 0 0;
}

/* --- the row hover, transcribed from jobyaviation.com --------------------
   Measured off their news list rather than guessed. Three things happen at
   once, and the third is the one that is easy to miss by eye:

   1. The divider is TWO stacked 1px lines, not one. A grey base sits at
      .22 ink, and a SOLID BLACK line lies on top of it collapsed to
      scaleX(0), waiting. On hover it wipes to scaleX(1) — so the line does
      not fade from grey to black, a black line is drawn ACROSS the grey one.
      Origin is the right edge, as theirs is, so it draws in and retracts on
      the same side.
   2. The image grows from its top-left corner, not its centre.
   3. Hovering anywhere in the LIST drops every row's text to .4, and the
      hovered row alone returns to 1 — so the row steps forward as much by
      the others receding as by anything it does itself.

   Their timings exactly: .7s for the line, .4s for the image, .3s for the
   text, all on ease-out-cubic.
   ---------------------------------------------------------------------- */
.proj-list {
  display: block;
  --ease-out-cubic: cubic-bezier(.33, 1, .68, 1);
  --row-text-op: 1;
  border-bottom: 1px solid rgba(35, 35, 35, .22);
}
.proj-list:hover { --row-text-op: .4; }

.proj-row {
  position: relative;
  display: grid; align-items: start;
  /* The thumbnail carries the row, so it takes real width. The date column and
     the gaps were tightened to pay for it rather than squeezing the title —
     at 878px of content that leaves ~360px for the headline, which is two
     comfortable lines at this size. */
  grid-template-columns: 76px clamp(240px, 27vw, 300px) minmax(0, 1fr) auto;
  gap: clamp(14px, 2vw, 32px);
  width: 100%; text-align: left; background: none; cursor: pointer;
  border: 0; padding: 28px 0 32px;
}
/* the grey base */
.proj-row::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 100%; height: 1px; background: rgba(35, 35, 35, .22);
}
/* and the black line drawn over it */
.proj-row::after {
  content: ""; position: absolute; top: 0; left: 0;
  width: 100%; height: 1px; background: var(--black);
  transform: scaleX(0);
  /* EXIT origin. Leaving collapses the line toward the right, so its left
     edge keeps travelling right — the same direction it came in. */
  transform-origin: 100% center;
  transition: transform .7s var(--ease-out-cubic);
}
.proj-row:hover::after,
.proj-row:focus-visible::after {
  transform: scaleX(1);
  /* ENTER origin. Drawing from the left edge rightward. Flipping the origin
     between the two states is what makes it sweep THROUGH rather than
     retract the way it came — with a single origin the line rewinds itself,
     which is what it was doing before. */
  transform-origin: 0 center;
}
/* the hovered row is the one that does NOT dim */
.proj-row:hover, .proj-row:focus-visible { --row-text-op: 1; }
.proj-row[hidden] { display: none; }
/* No hover background — the row is a wide band and tinting all of it was
   heavier than the thing it was signalling. The thumbnail zoom is the
   affordance instead. Focus still needs a visible marker of its own, since
   these are buttons stripped of border and background. */
.proj-row:focus-visible { outline: 2px solid var(--black); outline-offset: 3px; }

.proj-date {
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  color: var(--muted); padding-top: 4px;
  opacity: var(--row-text-op, 1); transition: opacity .3s var(--ease-out-cubic);
}
/* The frame clips; the layer inside it moves. Scaling the frame itself would
   push the row's neighbours around, and scaling an <img> with no clipping
   parent would spill over the title beside it. */
.proj-thumb {
  position: relative; display: block; overflow: hidden;
  /* The FRAME grows on hover, not just the picture inside it. Done with a
     transform rather than by widening the grid column: widening would reflow
     the row and shove the headline about mid-hover. A transform grows it
     visually and costs the layout nothing. Origin top-left, matching the
     inner zoom, so it opens out into the gap before the title rather than
     nudging the date column. */
  transform-origin: 0 0;
  transition: transform .4s var(--ease-out-cubic);
  /* 3/2, as the reference — at the same width a 16/10 crop sits noticeably
     shorter and was part of why these read as small */
  aspect-ratio: 3 / 2; border-radius: 12px; background: var(--brand-violet);
}
/* ⚠️ A WINDOW RISING OUT OF THE PLATE (5 Oct 2026) — the fellowship rail's
   `.rail-shot` composition, so a project looks the same wherever it appears:
   a brand-violet plate, and the screenshot as a window the same size in the
   same place on every card (8% in, 12% down, rounded at the top, running off
   the bottom edge), covered from its top-left so the card always shows the
   top of the interface. Containing each shot whole made every card a
   different-sized picture, most of them too small to read. */
.proj-thumb-in {
  position: absolute; inset: 0; border-radius: 12px; overflow: hidden;
  background: var(--brand-violet);
  /* top-left origin and .4s, as theirs — a centre-origin zoom reads as the
     picture breathing, a corner-origin one reads as it pushing in */
  transform-origin: 0 0;
  transition: transform .4s var(--ease-out-cubic);
}
.proj-thumb-in img {
  position: absolute; left: 8%; top: 12%; width: 84%; height: 88%;
  object-fit: cover; object-position: left top;
  border-radius: 6px 6px 0 0;
  box-shadow: 0 0 0 1px rgba(35, 35, 35, .12);
}
/* Two scales, deliberately: the frame opens out by 6% and the picture pushes
   in another 4% within it, so the thumbnail grows AND its contents still move
   against the edge. One alone reads as either a static picture being enlarged
   or a fixed window being panned; together it reads as both. */
.proj-row:hover .proj-thumb,
.proj-row:focus-visible .proj-thumb { transform: scale(1.06); }
.proj-row:hover .proj-thumb-in,
.proj-row:focus-visible .proj-thumb-in { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .proj-thumb, .proj-thumb-in { transition: none; }
  .proj-row:hover .proj-thumb,
  .proj-row:hover .proj-thumb-in { transform: none; }
}
.proj-title {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(19px, 1.9vw, 26px); line-height: 1.22; color: var(--ink);
  padding-top: 2px;
  opacity: var(--row-text-op, 1); transition: opacity .3s var(--ease-out-cubic);
}
.proj-cat {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); padding-top: 6px;
  white-space: nowrap;
  opacity: var(--row-text-op, 1); transition: opacity .3s var(--ease-out-cubic);
}
.proj-empty { color: var(--muted); padding: 30px 0; margin: 0; }

/* rows arrive as they are reached, rather than the whole list at once */
.proj-row[data-rise].armed { opacity: 0; transform: translateY(18px); }
.proj-row[data-rise].armed.is-in {
  opacity: 1; transform: none;
  transition: opacity .55s cubic-bezier(.22,1,.36,1),
              transform .55s cubic-bezier(.22,1,.36,1);
}

@media (max-width: 900px) {
  .proj-split { grid-template-columns: 1fr; gap: 28px; }
  .proj-rail { position: static; }
  .proj-group { margin-bottom: 18px; }
  /* ⚠ 44px, NOT THE DESKTOP RAIL'S 4px (5 Oct 2026). Once the rail becomes an
     inline row of options the only thing separating two of them is 4px of
     padding, so a thumb aimed at "MoSPI" lands on "RDPR". The dot marker is
     `left: 0` and unaffected by the extra padding. */
  .proj-opt { display: inline-flex; align-items: center; width: auto;
    min-height: 44px; padding: 0 18px; }
}
@media (max-width: 620px) {
  .proj-row { grid-template-columns: 1fr; gap: 12px; }
  .proj-thumb { max-width: 320px; }
}
@media (prefers-reduced-motion: reduce) {
  .proj-row[data-rise].armed { opacity: 1; transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   PAST COHORTS — the fellow index
   ═══════════════════════════════════════════════════════════════════════
   Same machine as "the work in practice" on Services and Labs: a sticky
   filter rail on the left, the set on the right. Two filter dimensions here
   rather than one, so the rail carries two labelled groups stacked down the
   column — which is exactly why the rail beats the three stacked filter ROWS
   this page used to have, where the filters ate the first screen before a
   single fellow appeared.

   The card is transcribed from the Figma ID-card: cream stock, ink keyline,
   an open photo window, and a boxed info panel ruled into three bands with
   scattered-ink lines. Every colour in that file already exists in the house
   palette — #FFFBF2 is --wash-paper, #FCF8EF is --surface, #696969 is
   --line-d, #232323 is --ink — so nothing new was introduced.
   ═══════════════════════════════════════════════════════════════════════ */
/* --- the record card --------------------------------------------------
   The Figma ID-card (444:2543): cream stock, ink keyline, a photo window and
   a boxed info panel ruled into three bands. It is no longer the index — the
   dossier is — but it is still the record itself, printed on the dossier's
   cover and standing alone in the modal hero.

   Proportions are read at 0.875 of the 421x564 artboard, which is what a
   368px card gives. .dossier-card puts it back to 1:1, where the artboard's
   own 12/17px type fits. --- */
@property --dot {
  syntax: '<color>'; inherits: true; initial-value: rgba(0, 0, 0, .82);
}
.fcard {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: var(--wash-paper);
  border: 1px solid var(--black); border-radius: 17px;
  padding: 21px;
  font: inherit; color: inherit;
  --dot: rgba(0, 0, 0, .82);
  transition: --dot .25s ease;
}
.fcard:hover, .fcard:focus-visible { --dot: rgba(0, 0, 0, 1); }
.fcard[hidden] { display: none; }

/* the photo window. The artboard draws a dashed keyline round it; it is not
   here, because a dashed edge round a photograph reads as a crop mark. The
   window is the soft fill the picture sits on, nothing more. */
.fcard-photo {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 371 / 347; min-height: 120px; background: var(--surface);
  margin-bottom: 14px;
}
.fcard-photo::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 17px; height: 17px; margin: -8.5px 0 0 -8.5px;
  border: 1px solid #A39C8C; border-radius: 3px;
}
.fcard-photo img {
  display: block; position: relative; z-index: 1;
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(.4) contrast(1.03);
  transition: transform .6s cubic-bezier(.22, 1, .36, 1), filter .45s ease;
}
.fcard:hover .fcard-photo img,
.fcard:focus-visible .fcard-photo img { filter: none; transform: scale(1.04); }
/* the cohort year, as a corner serial rather than a fifth field */
.fcard-year {
  position: absolute; top: 5px; right: 5px; z-index: 2;
  padding: 3px 6px 2px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  color: var(--muted); background: var(--surface);
}

/* --- the textured rule ------------------------------------------------
   Every stroke on the info panel — its outline and the three rules dividing
   name / role | college / department — is the artboard's scatter brush, not
   a CSS border. The tiles are WHITE and used as MASKS over a solid fill: it
   survives both the alpha and the luminance reading of mask-image, and it
   keeps the ink a CSS colour, so --dot still carries the hover.

   Each rule sits on a ::before pushed half a tile past its edge, so the band
   straddles the boundary. The panel outline is the exception: its four bands
   sit just inside the box, the way a border does. --- */
.fcard-info, .fcard-cell.r-top, .fcard-cell.r-left { position: relative; }
.fcard-info::before, .fcard-cell.r-top::before, .fcard-cell.r-left::before {
  content: ""; position: absolute; pointer-events: none;
  background: var(--dot);
}
.fcard-info::before {
  inset: 0;
  -webkit-mask-image: var(--tex-h), var(--tex-h), var(--tex-v), var(--tex-v);
          mask-image: var(--tex-h), var(--tex-h), var(--tex-v), var(--tex-v);
  -webkit-mask-size: var(--tex-len) var(--tex-t), var(--tex-len) var(--tex-t),
                     var(--tex-t) var(--tex-len), var(--tex-t) var(--tex-len);
          mask-size: var(--tex-len) var(--tex-t), var(--tex-len) var(--tex-t),
                     var(--tex-t) var(--tex-len), var(--tex-t) var(--tex-len);
  -webkit-mask-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
          mask-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  -webkit-mask-position: 0 0, 0 100%, 0 0, 100% 0;
          mask-position: 0 0, 0 100%, 0 0, 100% 0;
}
.fcard-cell.r-top::before {
  inset: calc(var(--tex-t) / -2) 0 auto 0; height: var(--tex-t);
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
  -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
}
.fcard-cell.r-left::before {
  inset: 0 auto 0 calc(var(--tex-t) / -2); width: var(--tex-t);
  -webkit-mask-image: var(--tex-v); mask-image: var(--tex-v);
  -webkit-mask-size: var(--tex-t) var(--tex-len); mask-size: var(--tex-t) var(--tex-len);
  -webkit-mask-repeat: repeat-y; mask-repeat: repeat-y;
}
/* college carries both, so they go on one pseudo-element */
.fcard-cell.r-top.r-left::before {
  inset: calc(var(--tex-t) / -2) 0 0 calc(var(--tex-t) / -2);
  width: auto; height: auto;
  -webkit-mask-image: var(--tex-h), var(--tex-v);
          mask-image: var(--tex-h), var(--tex-v);
  -webkit-mask-size: var(--tex-len) var(--tex-t), var(--tex-t) var(--tex-len);
          mask-size: var(--tex-len) var(--tex-t), var(--tex-t) var(--tex-len);
  -webkit-mask-repeat: repeat-x, repeat-y;
          mask-repeat: repeat-x, repeat-y;
  -webkit-mask-position: 0 0, 0 0;
          mask-position: 0 0, 0 0;
}

/* --- the info panel ---------------------------------------------------
   Three bands off the artboard: name across the full width, role | college
   side by side, department across the full width again. The artboard hangs
   each value beside its label and nudges it clear by hand, which only closes
   at 421px; at the width this card actually gets, the label eats enough of a
   half-cell to break "Software Engineer" over two lines. So the value takes
   the full width of its cell and the label sits above it in the corner. --- */
.fcard-info {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0;
}
.fcard-cell {
  display: block; min-width: 0;
  min-height: 44px; padding: 4px 8px 6px;
}
.fcard-cell.wide { grid-column: 1 / -1; }
.fcard-label {
  display: block;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.45;
  text-transform: uppercase; color: var(--black);
}
.fcard-value {
  display: block; text-align: center;
  font-family: var(--sans); font-size: 15px; line-height: 1.5; color: var(--ink);
  overflow-wrap: anywhere;
}

/* --- how each folder is packed ------------------------------------------
   Nineteen folders filed identically read as nineteen copies of one folder.
   These are six arrangements — the print sitting higher or lower, tilted
   left or right, the two sheets peeking from different points along the top
   edge. Dealt f1..f6 round the wall, so no two neighbours match and the row
   above never rhymes with the row below.

   Only the RESTING positions vary. The open state is untouched: dossier.js
   computes each piece's offset as (target - its actual left/top), so
   wherever a variant parks something it still lands on the same mark when
   the dossier opens. Varying where the record is READ from would be worse
   than not varying anything. --- */
/* Every print stands at least 148px proud of the cover, because the cover
   cuts the print at y 44 and a head needs about that much to clear it. The
   earlier set ran -72 to -96, and at -76 the cover sliced Ananya and Anusha
   across the mouth. -104 to -120 now, which shows head and shoulders for
   every one of the nineteen whatever their crop.

   The sheets shift less than they look like they could: the lilac is 403
   wide and the terracotta 311, against a 441 folder, so they only have
   ~38px and ~130px of travel before they hang off its edge. Their peek
   DEPTH carries the rest of the variation instead. */
.dossier.f1 { --doc-x: 34px;  --doc-y: -108px; --doc-rot: -4.6deg; --pro-x: 24px; --str-x: 118px; --pro-y: -18px; --str-y: -24px; }
.dossier.f2 { --doc-x: 168px; --doc-y: -104px; --doc-rot:  3.4deg; --pro-x: 6px;  --str-x: 40px;  --pro-y: -26px; --str-y: -16px; }
.dossier.f3 { --doc-x: 92px;  --doc-y: -118px; --doc-rot: -7.2deg; --pro-x: 34px; --str-x: 128px; --pro-y: -14px; --str-y: -30px; }
.dossier.f4 { --doc-x: 196px; --doc-y: -110px; --doc-rot:  5.4deg; --pro-x: 12px; --str-x: 52px;  --pro-y: -22px; --str-y: -20px; }
.dossier.f5 { --doc-x: 58px;  --doc-y: -114px; --doc-rot: -2.2deg; --pro-x: 30px; --str-x: 100px; --pro-y: -30px; --str-y: -12px; }
.dossier.f6 { --doc-x: 140px; --doc-y: -106px; --doc-rot:  7.6deg; --pro-x: 18px; --str-x: 74px;  --pro-y: -16px; --str-y: -28px; }

/* --- the wall ---------------------------------------------------------
   Three up on the full measure. NOT inside the old filter rail: that took
   320 of the 1160 and left 234px folders, where the record card printed on
   the folder sets its values at 9px. At 365 they read at 14.

   The row gap is generous because each print stands 85px proud of its
   folder and lifts another 24 on hover — the box reserves the 85, not the
   lift. --- */
.dossier-wall {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 54px 30px;
  align-items: end;
}
/* NO content-visibility here. It was an obvious fix for the render cost of
   38 instances of two heavy SVGs — and it implies `contain: layout paint`,
   which clips every dossier to its own box. The moment one opens, its sheets
   fly outside that box and get cut off at the folder's rectangle. Paint
   containment and an element whose contents deliberately escape its bounds
   cannot coexist. The stamp count was halved instead (see
   build-folder-shapes.py) and the shapes want rasterising to PNG if this
   ever needs to be faster. */
/* --- the filters ------------------------------------------------------
   The options are the house .tag, untouched: transparent on a 1px ink
   keyline at --r-pill, mono 11.5 uppercase. I had them overridden down to
   11px at a 4px radius, borrowed off cohort26 — and 4px is not a radius
   this system has. The four it has are 0 for cards and rules, 12 for a
   folder tab, 21 for a service pill and 30 for a button; anything else
   reads as imported, which is exactly how it read.

   No rules between the groups either. This system rules with ink or with
   the scatter brush, and a stack of grey hairlines is neither — it was
   scaffolding standing in for spacing. The groups just sit next to each
   other with air around them. --- */
.fc-bar {
  /* Every trigger is the SAME width. Sized to their own content they ran
     120 / 158 / 127 / 143px, and because the chosen value is printed IN the
     pill, the row re-flowed on every pick: choose a track and the three pills
     after it shuffled sideways. 204px is the widest label+value pair in the
     whole set ("TRACK / PRODUCT DESIGN"), measured. Equal widths are also the
     only way four dropdowns read as one instrument rather than four stray
     pills that happen to be near each other. */
  --fc-w: 204px;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 16px 24px;
  margin: 26px 0 50px;
}
.fc-bar .fc-fgroup { margin: 0; position: relative; }

/* Each dimension is a dropdown: the house pill as the trigger, carrying the
   dimension's name and whatever is chosen, and a panel of options beneath.
   Four pills in a row rather than forty chips — the language rows alone ran
   to ten. The panel is the system's one elevation, --shadow-overlay, the
   same as the nav sheet and the dossier's dialog. */
.fc-drop-t {
  display: inline-flex; align-items: baseline; gap: 9px;
  width: var(--fc-w); margin: 0; text-align: left;
}
.fc-drop-l { color: var(--muted); flex: none; }
.fc-drop-v {
  color: var(--ink); flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* OPEN is not SELECTED. The pill went solid black the moment the panel
   opened — this system's ACTIVE mark (.tag.active-tag) spent on a transient
   state. So an open-but-unset filter looked chosen, and a filter you had
   actually set looked like every other pill the moment the panel closed:
   nothing on the bar told you which dimensions were live. Open is paper-soft
   and a turned chevron; SET is ink. */
.fc-drop-t[aria-expanded="true"] { background: var(--surface); }
.fc-drop.is-set .fc-drop-t { background: var(--black); border-color: var(--black); }
.fc-drop.is-set .fc-drop-l { color: rgba(255, 251, 242, .6); }
.fc-drop.is-set .fc-drop-v { color: #fff; }

.fc-drop-t::after {
  content: ""; width: 5px; height: 5px; flex: none;
  align-self: center; margin-top: -3px;
  border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
  transform: rotate(45deg);
  transition: transform .18s ease;
}
/* the caret never moved — the one signal every reader already knows */
.fc-drop-t[aria-expanded="true"]::after { transform: rotate(225deg); margin-top: 3px; }
.fc-drop.is-set .fc-drop-t::after { color: rgba(255, 251, 242, .7); }

/* The panel is a SHEET, not a menu. It carried --shadow-overlay — the
   system's ONE elevation, which belongs to the nav sheet and the dossier
   dialog — under a 12px radius hung off a 21px pill, floating white over the
   folders. That soft drop shadow is what read as imported: this is a
   paper-and-ink system, and this page in particular is folders and filed
   paper. A sheet here is paper with an ink hairline round it and its rows
   ruled like a form — the same object as the record card on every cover. */
.fc-bar .work-filters {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  width: var(--fc-w); min-width: 100%;
  padding: 0; overflow: hidden;
  background: var(--bg);
  border: 1px solid var(--black); border-radius: var(--r-tab);
  transition: opacity .16s ease, transform .16s ease, visibility .16s;
}
/* `hidden` is the no-JS state and stays in the markup; the script strips it
   at init and drives .is-open instead, which is the only way this can
   transition at all (display:none cannot). Same arming contract as
   .scene-ready and [data-stagger] — a dead script leaves the panels shut,
   not stuck open. */
.fc-bar .work-filters[hidden] { display: none; }
.fc-bar .work-filters:not(.is-open) {
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(-4px);
}
.fc-bar .fc-filter {
  margin: 0; border: 0; border-radius: 0;
  display: flex; justify-content: space-between; align-items: baseline; gap: 18px;
  padding: 8px 12px; text-align: left; background: transparent;
}
/* ruled, not floated: the rows are a form, so they are separated by a rule */
.fc-bar .fc-filter + .fc-filter { border-top: 1px solid rgba(0, 0, 0, .12); }
.fc-bar .fc-filter:hover { background: var(--surface); }
.fc-bar .fc-filter[aria-pressed="true"] { background: var(--black); color: #fff; }
/* the ink row supplies its own edge; a hairline under it doubles up */
.fc-bar .fc-filter[aria-pressed="true"] + .fc-filter { border-top-color: transparent; }

.fc-tail { display: flex; align-items: center; gap: 18px; margin-left: auto; }
.fc-bar .fc-count { margin: 0; }
.fc-reset {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
  background: none; border: 0; padding: 0; cursor: pointer;
  text-decoration: underline; text-underline-offset: 4px;
}
.fc-reset:hover { color: var(--black); }

/* Each option carries how many fellows it would leave, given the other
   filters — so an empty combination reads as 0 before you click it rather
   than as a blank wall after. */
.fc-n {
  margin-left: 8px; font-size: 10px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.fc-filter[aria-pressed="true"] .fc-n { color: rgba(255, 251, 242, .7); }
.fc-filter.is-none { opacity: .38; }

/* On a phone a 204px pill in a 380px bar leaves a ragged right edge four
   rows deep. Full width instead — the fixed measure exists to stop the ROW
   re-flowing, and once they are stacked there is no row to protect. */
@media (max-width: 560px) {
  .fc-bar { --fc-w: 100%; }
  .fc-bar .fc-fgroup { width: 100%; }
  .fc-tail { margin-left: 0; width: 100%; }
  /* ⚠ THE INSTRUMENT IS OPERATED BY THUMB HERE (5 Oct 2026). The four
     triggers came out 30px tall and RESET was 38x15 — a 15px target for the
     one control that undoes a wrong filter. Each trigger takes a 44px floor,
     the option rows take 13px of padding for the same 44, and Reset stops
     being bare text and becomes a box.
     ⚠ `align-items: center` IS SAFE ONLY BECAUSE THE TWO SPANS ARE THE SAME
     SIZE. `.fc-drop-l` and `.fc-drop-v` both inherit `.tag`'s type, so the
     baseline the desktop rule aligns them on and the centre line are the same
     line. Give either one its own font-size and this has to go back to
     `baseline` or the label and its value will sit at different heights. */
  .fc-bar .fc-drop-t { min-height: 44px; align-items: center; }
  .fc-bar .fc-filter { padding: 14px; }
  .fc-reset { display: inline-flex; align-items: center; min-height: 44px; }
}

@media (max-width: 1080px) {
  .dossier-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
  .dossier-wall { grid-template-columns: 1fr; gap: 40px; }
}

/* ══════════════════════════════════════════════════════════════════════
   THE DOSSIER — the fellow record as a folder (Figma 450:2963 / 450:2962)
   ══════════════════════════════════════════════════════════════════════
   A folder with three things filed in it, only their edges showing: the
   fellow's portrait, a lilac sheet carrying the project, a terracotta sheet
   carrying the strengths. Under the cursor they slide out of the folder and
   the record is readable without opening anything.

   Everything is placed on the artboard's own grid — the folder back is
   441x361 and every offset below is the Figma number, measured from its top
   left corner. --s scales the whole assembly; at --s:1 this is the artboard
   at 1:1. Nothing is re-proportioned by hand, so the artboard stays the
   single source of truth for the geometry.

   Two notes on what this design does that the rest of the site does not:
   the folder back is #F9EFD8, a warmer stock than any token here, and the
   two sheets are illustration colours (--wash-lilac, --terracotta) used as
   UI fills, which the 2026 system's header explicitly reserves for imagery.
   Both come straight off the artboard; flagged rather than "corrected".
   ══════════════════════════════════════════════════════════════════════ */
.dossier {
  /* set on the wall by dossier.js from the real column width, so the folder
     fills its column at any viewport; 1 when it stands alone */
  --s: var(--wall-s, 1);
  /* the artboard's own numbers */
  --fw: 441px; --fh: 361px; --peek: 124px;   /* headroom for the deepest variant */
  position: relative; display: block;
  width: calc(var(--fw) * var(--s));
  height: calc((var(--fh) + var(--peek)) * var(--s));
  padding: 0; border: 0; background: none; font: inherit; color: inherit;
  text-align: left; cursor: pointer;
  --dot: rgba(0, 0, 0, .82);
}
/* the folder's own box, with the portrait's overhang reserved above it */
.dossier-in {
  position: absolute; left: 0; bottom: 0;
  width: var(--fw); height: var(--fh);
  transform: scale(var(--s)); transform-origin: 0 100%;
  /* Open at the top and sides, shut along the bottom: the filed papers have
     to stand proud above the folder, but a sheet whose content runs long
     must not hang out of the bottom of it. Released when the dossier opens,
     stepped on the same clock as the sheets' z-index so it never shows. */
  clip-path: inset(-600px -600px 0 -600px);
  transition: clip-path 0s .3s;
}
.dossier.is-open .dossier-in,
.dossier.is-hover .dossier-in {          /* the held-still demo state too */
  clip-path: inset(-600px -600px -600px -600px);
  transition-delay: .38s;
}
/* These are the designer's own shapes, re-exported WITH their scatter stroke
   (454:3122) — 1,900-odd stamps each, the same brush that draws Line 31 on
   the record card. Every stroke on the dossier is therefore the same stroke,
   by construction, rather than by my approximating one to match the other.
   The earlier export dropped the stamps and I restored the outline with a
   speckle pattern; that is what read as a different, thinner brush.

   Each shape carries ~1.14px of stroke bleed beyond its 441px box, so the
   offsets take that back off and the artwork registers on the artboard. */
.dossier-back, .dossier-front { position: absolute; left: -1.15px; display: block; }
.dossier-back  { top: -1.12px; width: 443.287px; height: 363.259px; z-index: 0; }
/* The cover starts at 44 rather than the artboard's 30, so the warmer
   #F9EFD8 back reads as a second sheet of stock rather than a sliver — and
   it is shortened by the same 14px so its bottom still lands on the
   folder's. A 4% vertical squash on a 331px shape, which its tab and
   diagonals absorb without showing. */
.dossier-front { top: 42.86px; width: 443.283px; height: 319.285px; z-index: 3; }

/* --- what is filed inside -------------------------------------------
   Rest: tucked behind the cover, only the top edge of each showing.
   Hover: pulled out, and lifted ABOVE the cover — which is the z-order the
   artboard itself flips between its two frames. --- */
@property --out {
  syntax: '<number>'; inherits: true; initial-value: 0;
}
/* The hover lift is its own number because it is a different MOVE, not a
   fraction of --out: the papers stand UP out of the folder, they do not
   start sliding sideways out of its edge. Named --lift, NOT --peek: --peek
   is already the dossier's 112px of overhang, and registering that name as
   a <number> silently invalidated "112px" and collapsed the box. */
@property --lift {
  syntax: '<number>'; inherits: true; initial-value: 0;
}

/* --- the flight ------------------------------------------------------
   Three up, a dossier is small. HOVER lets the filed papers peek a little
   further out of it, in place — a preview; nothing takes over the page.
   CLICK sends it to the middle, blurs everything behind it, and opens it
   there, at full size, with nothing competing.

   WHERE each piece lands when it opens is not in here — dossier.js measures
   the sheets and sets --dx/--dy per element. It has to be measured: both
   sheets size to their own content, so a fellow with three strengths and one
   with five need different offsets, and every hard-coded set I tried ended
   up overlapping for somebody. The script lays the four pieces out in
   quadrants with a fixed gap and nothing can collide.

   Note what does NOT fix an overlap: a bigger folder. The pieces land where
   the script puts them, not where the folder's edges are, so growing it
   moves nothing apart — it only makes the assembly bigger, and the assembly
   is what the flight shrinks to fit the viewport. A bigger folder means a
   smaller fit scale and SMALLER text on screen.

   --lift raises the filed papers straight UP on hover; --out runs the fan on
   open. Two numbers, not one fraction of the other: moving them sideways on
   hover read as bursting out of the folder's side.

   The open offsets are NO LONGER the artboard's. Its deltas were drawn for a
   210-tall lilac and a 185-wide terracotta; both sheets now size to their
   content, and at 296 and 250 the artboard's numbers piled them on top of
   each other and buried the folder's top. These land the four pieces in
   quadrants — print top left, project top right, folder bottom left,
   strengths bottom right — overlapping only where a fanned stack should:
   16px of the print tucked under the project sheet. Everything else clears.

   The dossier never leaves the grid: --tx/--ty/--tz are measured and set by
   the script, and the whole flight is one transform, so no layout moves and
   nothing below it jumps. --tz is the scale that takes this dossier's
   resting --s up to whatever the viewport can hold. --- */
/* The sequence is strictly one thing at a time, in both directions.
   OUT: the folder travels (.34s), lands, holds shut for a beat, and only
        then do the sheets come out — so it arrives as a closed folder in
        the middle of the page and opens there. .72s end to end: long enough
        to read as two moves, short enough not to be waited on.
   BACK: the sheets go in first, and the folder does not leave until they
        are stowed. Each direction's delay lives on the rule that is active
        in that direction: the base rule is the one in force while closing,
        .is-open the one in force while opening. */
.dossier {
  transition: transform .34s cubic-bezier(.22, 1, .36, 1) .28s, /* closing: wait */
              --lift .18s ease;                                  /* the peek */
  transform: translate3d(0, 0, 0) scale(1);
}
.dossier:hover { --lift: 1; }
.dossier.is-open {
  --out: 1; --lift: 0;                                           /* the lift folds in */
  transition: transform .34s cubic-bezier(.22, 1, .36, 1) 0s,    /* opening: go now */
              --out .34s cubic-bezier(.22, 1, .36, 1) .38s,      /* lands, holds, opens */
              --lift .34s cubic-bezier(.22, 1, .36, 1) .38s;
}
.dossier.is-hover { --out: 1; }
.dossier.is-open {
  position: relative; z-index: 570;
  transform: translate3d(var(--tx, 0), var(--ty, 0), 0) scale(var(--tz, 1));
}
.dossier-scrim {
  position: fixed; inset: 0; z-index: 560;
  background: rgba(20, 13, 13, .16);
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
  opacity: 0; pointer-events: none;
  /* clears only once the folder is on its way home, never out from under it */
  transition: opacity .26s ease .24s;
}
/* it takes the pointer while open, so the wall underneath cannot be hovered
   by accident and a click anywhere off the record dismisses it */
.dossier-scrim.on { opacity: 1; transition-delay: 0s; pointer-events: auto; }

/* A full-page takeover should never be a guessing game about how to leave. */
.dossier-close {
  position: fixed; top: 22px; right: 22px; z-index: 575;
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--black);
  display: flex; align-items: center; justify-content: center;
  font-size: 17px; line-height: 1; color: var(--ink); cursor: pointer;
  font-family: var(--sans);
  opacity: 0; pointer-events: none;
  transition: opacity .22s ease .12s, background .15s ease, color .15s ease;
}
.dossier-close.on { opacity: 1; pointer-events: auto; transition-delay: .12s; }
.dossier-close:hover { background: var(--black); color: #fff; }
@media (prefers-reduced-motion: reduce) {
  /* it still opens — the record is only reachable this way — but it does
     not travel */
  .dossier, .dossier-doc, .dossier-sheet, .dossier-scrim { transition: none; }
}

/* z-index is on the transition too, at 0s duration — so it STEPS rather than
   eases, and the step can be timed. Without it the sheets jump above the
   cover the instant .is-open lands and the folder arrives at the middle of
   the page already spilling its contents, which is the one thing it must
   not do. Going out it steps with them (.56s, below); coming back it waits
   the full .46s so they are stowed before they drop behind the cover. */
.dossier-doc, .dossier-sheet {
  position: absolute; z-index: 1;
  /* the motion comes from --out on the parent; only the z-index step is theirs */
  transition: z-index 0s .3s;
}
/* .48s of flight plus a beat of stillness, then they come out */
.dossier.is-open .dossier-doc,
.dossier.is-open .dossier-sheet { transition-delay: .38s; }
/* pulled out, they ride above the cover AND above the record card — they
   sweep across it on the way out, and the card would otherwise show through */
.dossier.is-open .dossier-sheet,
.dossier.is-hover .dossier-sheet { z-index: 5; }

/* the portrait — a cut-out, filed at a slight angle */
/* The print keeps the artboard's box and the photograph COVERS it from the
   top. Not "fill" — that stretches — and not a height-locked box the picture
   sets the width of, because these cut-outs are trimmed hard at the bottom
   of their own canvas (Anusha's subject runs to row 1023 of 1024). At rest
   that flat cut hides behind the cover; lifted 149px on open it is the first
   thing you see. Anchoring the image to the TOP of a fixed box throws the
   bottom away instead, and works for a square source too.

   The box is 250 rather than the artboard's 209.5 so a portrait is not
   forced into a near-square frame. And the photograph is CONTAINED inside a
   margin rather than covering the box: cover preserves the ratio, but
   filling a frame edge to edge with a crop reads as stretched all the same,
   and it cannot hold a square source and a 3:4 one without cropping one of
   them. Contained, every portrait sits on its print undistorted and
   uncropped, whatever shape it arrives in — the white around it is the
   print's margin, wider at the foot as a mounted photograph's is. And it sits at x 40 / y -85 rather than x -4 / y -112 — further in
   and to the right, so it is filed IN the folder rather than balanced on it,
   and so the back's tab is no longer completely behind it. */
.dossier-doc {
  left: var(--doc-x, 40px); top: var(--doc-y, -85px);
  width: 196.234px; height: 209.534px; overflow: hidden;
  transform: translate(calc(var(--dx, 0px) * var(--out)),
                       calc(var(--dy, 0px) * var(--out) - 13px * var(--lift))) rotate(var(--doc-rot, -4.58deg));
  background: #fff;                 /* the print the cut-out is on */
}
.dossier-doc img {
  display: block; width: 100%; height: 100%;
  /* FILLS the print, as the artboard has it (size-full) — but cover, never
     fill, so the photograph is never distorted. Anchored to the top because
     these cut-outs are trimmed flat at the bottom of their own canvas, so
     the bottom is the only part safe to lose. */
  object-fit: cover; object-position: center top;
}
/* The two sheets. The shapes carry their own colour, punched holes and torn
   edge, so they are background images rather than boxes with a fill — and
   the background is stretched to the box rather than the box fixed to the
   artboard's numbers, because the strengths run to three items for one
   fellow and five for another. A fixed height is wrong for somebody either
   way; min-height holds the artboard's proportions for the common case and
   the sheet grows past it when it has to. */
.dossier-sheet {
  background-repeat: no-repeat; background-size: 100% 100%;
  /* The system's body size — 15px is the most-used size in this stylesheet
     by a distance (21 declarations against 14px's 10). 13px was a number I
     arrived at by fitting, not one this system speaks. */
  font-family: var(--sans); font-size: 15px; line-height: 1.5; color: var(--black);
}
.dossier-sheet.is-project {
  left: var(--pro-x, 25px); top: var(--pro-y, -18px); width: 402.919px; min-height: 210px;
  /* Drawn, not scaled. As one 402x210 export stretched to its box it went
     1.41x tall the moment the sheet grew with its content, and the punched
     holes came out as ovals. Fill plus a px-measured corner cut and four
     px-measured holes: none of it moves when the sheet does. */
  background: var(--wash-lilac);
  clip-path: polygon(0 0, calc(100% - 20.7px) 0, 100% 16.1px, 100% 100%, 0 100%);
  /* The holes are PUNCHED, not painted. They were four --bg circles on a
     pseudo-element, which only looks right over paper — over the blurred
     page behind an open dossier they read as four beige dots.

     Layer order matters and is counter-intuitive: the FIRST image listed is
     the TOP layer, and its mask-composite says how it combines with
     everything below. So the sheet goes first with `subtract` (sheet MINUS
     the holes) and the four circles below it with `add` (their union). List
     them the other way round and it computes circle-minus-sheet, which
     erases the sheet and leaves one stray dot. */
  -webkit-mask-image: linear-gradient(#000, #000),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) 52.5px, #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) 73.4px, #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) calc(100% - 39.7px), #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) calc(100% - 18.8px), #000 98%, transparent 100%);
          mask-image: linear-gradient(#000, #000),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) 52.5px, #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) 73.4px, #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) calc(100% - 39.7px), #000 98%, transparent 100%),
    radial-gradient(circle 6.4px at calc(100% - 12.1px) calc(100% - 18.8px), #000 98%, transparent 100%);
  /* five layers, five values — a short list cycles, which happened to be
     harmless here only because the bottom layer's composite is ignored */
  -webkit-mask-composite: source-out, source-over, source-over, source-over, source-over;
          mask-composite: subtract, add, add, add, add;
  /* Even top and bottom. The top was carrying 46 against the bottom's 16 —
     a hangover from pushing the text below the folder's cover line so it
     would not show in the closed peek. The artboard lets its headers peek,
     and the imbalance cost more than the peek did. 30 clears the clipped
     top-right corner; 40 on the right clears the punched holes. */
  padding: 30px 40px 30px 18px;
  transform: translate(calc(var(--dx, 0px) * var(--out)),
                       calc(var(--dy, 0px) * var(--out) - 24px * var(--lift)));
}
.dossier-sheet.is-strengths {
  /* The width is not arbitrary: the bites sit on an 18px pitch, so a sheet
     that is not a whole number of them slices the last one in half. The
     artboard's own 185 is 18x10 + 5 — ten bites with a matching 5.5px of
     flat at each end. 311 is 18x17 + 5: the same rhythm, seven bites wider.
     Change this width and it must stay 18n + 5, or an edge tears oddly. */
  left: var(--str-x, 107px); top: var(--str-y, -24px); width: 311px; min-height: 361px;
  /* Fill plus masked torn edges rather than one fixed shape scaled to the
     box: the bites keep their own size whatever width the sheet is given and
     however far its strengths run. The sheet is scalloped along BOTH edges —
     three mask layers: the tile along the top, the mirrored tile along the
     bottom, and a solid block between them. */
  background: var(--terracotta);
  -webkit-mask-image: url(assets/folder/scallop.svg),
                      url(assets/folder/scallop-bottom.svg),
                      linear-gradient(#000, #000);
          mask-image: url(assets/folder/scallop.svg),
                      url(assets/folder/scallop-bottom.svg),
                      linear-gradient(#000, #000);
  -webkit-mask-size: 18px 7px, 18px 7px, 100% calc(100% - 12px);
          mask-size: 18px 7px, 18px 7px, 100% calc(100% - 12px);
  -webkit-mask-repeat: repeat-x, repeat-x, no-repeat;
          mask-repeat: repeat-x, repeat-x, no-repeat;
  /* 2.5px puts the first bite's centre at 11.5, where the artboard has it,
     which leaves 5.5px of flat at BOTH ends rather than a sliced bite */
  -webkit-mask-position: 2.5px 0, 2.5px 100%, 0 6px;
          mask-position: 2.5px 0, 2.5px 100%, 0 6px;
  /* even, and clearing the 6px torn edge at both ends */
  padding: 34px 22px 34px;
  /* straight while filed, tipped a little once it is out — a sheet pulled
     from a folder does not come out square */
  /* Tilted AWAY, not toward. A positive angle rotates its bottom-left corner
     into the folder. Pivoted at its bottom left corner, a POSITIVE angle
     swings the top away from the folder, which is the lean wanted; the
     negative one leant it back in. */
  transform: translate(calc(var(--dx, 0px) * var(--out)),
                       calc(var(--dy, 0px) * var(--out) - 24px * var(--lift)))
             rotate(calc(2.4deg * var(--out)));
  transform-origin: 12% 88%;
}
/* A filed sheet shows its colour and its torn edge, never its words. The
   text was legible in the closed peek — the sheets sit above the cover line
   because their padding is balanced, which is how it should look when open.
   Tying opacity to --out solves both: nothing to read while filed, and it
   fades up as the sheet comes out. */
.dossier-sheet h4, .dossier-sheet p { opacity: var(--out); }

.dossier-sheet h4 {
  /* .eyebrow's spec exactly — 12.5px mono, uppercase, .14em — which is the
     label voice everywhere else on the site. Ink rather than .eyebrow's
     olive: these sit on terracotta and lilac grounds, where olive goes
     muddy. Size, case and tracking are the system's; only the colour is
     answering the ground. */
  margin: 0 0 14px; font-family: var(--mono); font-size: 12.5px; line-height: 1;
  letter-spacing: .14em; text-transform: uppercase; color: var(--black); font-weight: 400;
}
.dossier-sheet p { margin: 0 0 12px; }
.dossier-sheet p:last-child { margin-bottom: 0; }

/* --- the record card, printed on the cover --------------------------
   The same panel as the ID card, at the artboard's own 371x151 — so the
   labels and values take the artboard's 12/17px rather than the step-down
   the 368px grid card needed. --- */
.dossier-card {
  position: absolute; left: 35px; top: 173px; width: 371px; z-index: 4;
}
/* The artboard hand-places each value near the middle of its band, which at
   these sizes strands "AI" in the centre of a cell with its ROLE label off in
   the corner. The values sit on the same left margin as their labels instead,
   one indent, so the four bands read as a form. */
.dossier-card .fcard-cell { min-height: 50px; padding: 4px 12px 6px; }
.dossier-card .fcard-label { font-size: 12px; line-height: 1.25; }
.dossier-card .fcard-value { font-size: 17px; line-height: 1.4; text-align: left; }

/* THE RAIL IS GONE. These were its rules and they were still firing on the
   bar that replaced it: `.fc-fgroup + .fc-fgroup { margin-top: 22px }`
   dropped every group after the first by 22px, so the four pills came down
   the page in a staircase — Track on one baseline, the other three on
   another, the count and Reset on a third. In a column that margin was the
   gap between groups; in a flex row it is a fall. Deleted rather than
   overridden: nothing but this bar has used .fc-fgroup since the rail went,
   and an override would have left the trap for the next person. */
.fc-filter { margin: 0 6px 8px 0; }
.fc-filter:hover { background: var(--surface); }
.fc-filter[aria-pressed="true"] {
  background: var(--black); color: #fff; border-color: var(--black);
}
.fc-count {
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin: 26px 0 0;
}
/* the empty state lives INSIDE the grid, not after it. Outside, it added its
   own height to a grid whose height is deliberately frozen, so filtering down
   to nothing still nudged the page. */
.fc-empty { color: var(--muted); margin: 18px 0 0; }

/* One up on a phone the card is NARROWER than it is two up on a desktop —
   335px against 368 — and "Software Engineer" stops clearing its half-cell.
   The panel takes the artboard's type down a step rather than letting the
   middle band run to two lines. */
@media (max-width: 480px) {
  .fcard-label { font-size: 10px; }
  .fcard-value { font-size: 13.5px; }
}
@media (prefers-reduced-motion: reduce) {
  /* the ink and the colour still resolve — only the push on the photograph
     goes, since that is the one part of the hover that is movement */
  .fcard-photo img { transition: filter .45s ease; }
  .fcard:hover .fcard-photo img,
  .fcard:focus-visible .fcard-photo img { transform: none; }
}

/* the phone's portrait envelope — see the note in index.html. Hidden wherever
   the landscape one is shown, which is everything above 860. */
.env-stage {
  position: absolute; left: 50%;
  /* the stage IS the envelope; the card hangs 270/1156 of the width above it,
     so the stage sits that far down plus the air the frame allows */
  top: calc(var(--env-air) + var(--env-w) * .23356);
  width: var(--env-w); aspect-ratio: 1156 / 584;
  transform: translateX(-50%);
  container-type: inline-size;
}
.env-paper { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.env-back  { z-index: 0; }
.env-front { z-index: 2; pointer-events: none; }

/* THE SLOT — clipped at the envelope's bottom edge, open 30cqw above it. That
   asymmetry is the whole trick: the card can hang over the envelope's top when
   it is out and be swallowed whole when it is in, with one `overflow: hidden`
   doing both. */
.env-slot {
  position: absolute; left: 0; right: 0; top: -30cqw; bottom: 0;
  overflow: hidden; z-index: 1;
}

/* ⚠️ THE CARD'S RESTING POSITION IS *OUT*, AND THE BEAT PUSHES IT IN. Written
   the other way round — parked inside, lit by a beat — a reader whose script
   never ran would get an empty envelope and no tracks at all. The layout is
   the finished state; `data-env` only ever takes it away. */
.env-card {
  position: absolute; left: 3.287cqw;       /* 38 / 1156  */
  top: 6.644cqw;                            /* -23.356 + the slot's 30 */
  width: 93.512cqw;                         /* 1081 / 1156 */
  aspect-ratio: 1081 / 624;
  background: var(--drawer);                /* #B3C197 — the file's card green,
                                               the same ink as the drawer wall */
  container-type: inline-size;
  display: flex; flex-direction: column;
  transition: transform 1.1s cubic-bezier(.2,.72,.24,1);
}

/* the four punched holes. ⚠️ THEY ARE PAINTED PAPER, NOT A HOLE — the file
   subtracts them from the card's own shape, which would show whatever is
   behind. Identical here because the only thing behind the card at this point
   IS paper; it stops being identical the moment anything else passes under. */
.env-holes i {
  position: absolute; left: 1.110cqw; width: 2.220cqw; height: 1.524cqw;
  background: var(--bg); border-radius: .370cqw;
}
.env-holes i:nth-child(1) { top: 16.956cqw; }
.env-holes i:nth-child(2) { top: 19.559cqw; }
.env-holes i:nth-child(3) { top: 35.625cqw; }
.env-holes i:nth-child(4) { top: 38.228cqw; }

.env-deadline {
  flex: 0 0 16.096cqw;                      /* 174 / 1081 */
  display: flex; align-items: center;
  margin: 0; padding-left: 3.793cqw;        /* 41 */
  /* ⚠️ THE SECTION-HEAD VOICE, WHICH THE FIGMA FRAME IS NOT. 540:3453 sets this
     in Libertinus Serif DISPLAY at 48 — the only display-face heading below the
     page title, and at 48 the only heading on the page bigger than the 44px h2
     the other three sections use. On the artboard, alone, it reads as a title;
     on the page it read as a different document. It is the `h2` tier now:
     the text face, 44px at the reference width (44/1084.7 = 4.056cqw), 1.18
     leading and the h2's own -0.2px of tracking expressed in em so it survives
     the card scaling. The track names below are the text face too, at the card-title 36
     (see .env-tname). */
  font-family: var(--serif); font-weight: 400;
  font-size: 4.056cqw; line-height: 1.18; letter-spacing: -.0045em;
  color: var(--ink);
  border-bottom: 1px solid var(--black);
}

.env-rows { list-style: none; margin: 0; padding: 0; flex: 1 1 auto; }
/* ⚠️ THE ROW RULES START AT THE VERTICAL LINE, NOT AT THE CARD'S EDGE — the
   file's Line 36 runs the full 1081 under the deadline, Lines 37-39 run 986
   from x 421. So the deadline's rule is a border and the rows' are inset. */
/* ⚠️ THE FIRST ROW IS 121 UNITS AND THE OTHER TWO ARE 124. It looks like a
   nudge and three units is invisible, but matching the file costs one rule and
   removes the question; rules then land on 295 / 419 / 543 exactly. */
.env-row {
  position: relative; height: 11.471cqw;    /* 124 / 1081 */
  display: grid; grid-template-columns: 8.788cqw 1fr;
  /* ⚠️ `align-content`, NOT `align-items`. The label and the name are two
     implicit rows; `align-items: center` centres each INSIDE ITS OWN TRACK and
     leaves the free space distributed between them, which put the pair 14
     units high with a 24-unit gap against the file's 11. `align-content`
     centres the pair as one block and the gap is then the margin below. */
  align-content: center;
}
.env-row:first-child { height: 11.193cqw; }   /* 121 / 1081 */
/* ⚠️ THE CARD'S RULES ARE THE HOUSE BRUSH (5 Oct 2026, the user: "can those
   lines be in the hand-drawn texture as well?"). They were the one set of flat
   1px lines left on the page — every divider, the FAQ and the values list draw
   the textured stroke. Same technique as those: a --tex-t band straddling the
   line's old position, ink as background, masked by the brush tile; the
   vertical takes the vertical tile. Ink stays black, as the flat lines were. */
.env-row::after {
  content: ""; position: absolute; left: 8.788cqw; right: 0;
  bottom: calc(var(--tex-t) / -2); height: var(--tex-t);
  background: var(--black); pointer-events: none;
  -webkit-mask-image: var(--tex-h); mask-image: var(--tex-h);
  -webkit-mask-size: var(--tex-len) var(--tex-t); mask-size: var(--tex-len) var(--tex-t);
  -webkit-mask-repeat: repeat-x; mask-repeat: repeat-x;
}
/* the single vertical rule, drawn once on the list rather than once a row —
   three stacked bands would show their seams */
.env-rows::before {
  content: ""; position: absolute; top: 0; bottom: 0;
  left: calc(8.788cqw - var(--tex-t) / 2); width: var(--tex-t);
  background: var(--black); pointer-events: none;
  -webkit-mask-image: var(--tex-v); mask-image: var(--tex-v);
  -webkit-mask-size: var(--tex-t) var(--tex-len); mask-size: var(--tex-t) var(--tex-len);
  -webkit-mask-repeat: repeat-y; mask-repeat: repeat-y;
}
.env-rows { position: relative; }

.env-tno, .env-tname { grid-column: 2; margin: 0; padding-left: 4.903cqw; }
.env-tno {
  /* SIZE is the file's (24/1081); TRACKING, WEIGHT and CASING are the page's
     mono label voice, which is what stops this reading as a different face from
     every other small-caps line on the page. */
  font-family: var(--mono); font-weight: 500;
  /* ⚠️ THE LABEL TIER, NOT THE FILE'S 24 (4 Oct 2026). 24/1081 rendered 24px at
     1440 — a mono label twice the size of every other one on the site, where
     the tier is 10 · 11 · 12.5. This is the eyebrow's 12.5 expressed against
     the card's reference width (12.5 / 1084.7), so it still scales with the
     composition but reads as the same voice as the section eyebrows. */
  /* ⚠️ FLOORED AT THE TIER (5 Oct 2026, the user: "are we using the right
     typography for tracks? it feels wrong"). The RATIO is right; the ENVELOPE
     is what is wrong. `--env-w` is `min(measure, 1160px, frame-height x
     .6157/.62)` and inside this pinned scene the HEIGHT term wins: 826px at
     1440x900, against the ~1085 these proportions were authored for. So every
     cqw in this block renders at 76%, and 1.152cqw came out 9.5px — under the
     10 / 11 / 12.5 mono tier, i.e. smaller than any other label on the site.
     max() holds the tier while the envelope is squeezed and hands back to the
     ratio on a window tall enough for the full composition. */
  font-size: max(12.5px, 1.152cqw); line-height: 1;
  /* air between label and name: ~11 units of visible gap at this size */
  margin-bottom: 1.2cqw;
  text-transform: uppercase; letter-spacing: .14em; color: var(--ink);
}
.env-tname {
  /* ⚠️ THE TEXT FACE, AT THE CARD-TITLE TIER (4 Oct 2026). This was Libertinus
     Serif DISPLAY — the type scale reserves display for the two page-level
     tiers, and every other card title on the site (`.ex-item h3`,
     `.ab-card-copy h3`, `.rail-card h3`) is the text face. 3.330cqw is still
     36 at the reference width, which is the panel card-title size; leading
     and tracking are the tier's own. */
  font-family: var(--serif); font-weight: 400;
  /* floored at the card-title tier for the same reason as the label above —
     the squeezed envelope was rendering it at 25.7px */
  font-size: max(27px, 3.330cqw); line-height: 1.22; letter-spacing: -.0056em; color: var(--ink);
}
/* ⚠️ THE PILL IS OUT OF THE GRID, AND IT HAS TO BE. Spanning it across the two
   implicit rows centres it against the LABEL-AND-NAME BLOCK, not against the
   row — so it rode 15-18 units high of the file's 217 / 337 / 461, and it moved
   again whenever the type above it changed. Absolute against the row is the
   only position that means "centred in this row". */
.env-apply {
  /* ⚠️ RANGED ON THE TEXT'S OWN INSET, NOT THE FILE'S (5 Oct 2026, the user:
     "the Apply Now buttons feel a little visually misaligned"). It was
     7.678cqw — 1081 - 879 - 119 straight off the frame — which put the pill
     59.3px from the row's right edge while the label and name start 67.9px
     from its left (the rail column, 3.317cqw, plus their own 4.903cqw of
     padding). Two insets that are nearly the same read as a mistake rather
     than as a decision, and the pill is the heaviest object in the row, so
     it is the one that shows it. 8.789cqw is that same 67.9px, so the row's
     content is now inset equally at both ends.
     ⚠️ cqw HERE IS A FRACTION OF THE ROW (772.6px at 1440x900), NOT OF THE
     ENVELOPE — the nearest `container-type` ancestor is the row. Converting
     against the envelope's 826 gives 8.22cqw, which lands 4.4px short; it was
     measured back off the live row rather than divided. */
  position: absolute; right: 8.789cqw;
  top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  /* ⚠️ FLOORED LIKE THE LABEL ABOVE, AND FOR THE SAME REASON (5 Oct 2026, the
     user: "the Apply Now buttons feel too small"). At the squeezed 826px
     envelope these came out 30.6px tall with 11.4px type — against the site's
     own button, which is 15px type in a ~40px pill. The pill is the one thing
     in here a reader has to HIT, so it is the last thing that should scale
     down with the illustration. */
  min-width: max(96px, 11.008cqw); height: max(38px, 3.700cqw);
  padding: 0 max(16px, 1.850cqw);
  border-radius: max(19px, 2.775cqw); background: var(--black); color: #fff;
  /* 15 at the reference width — the site's button size (`.btn.lg`, `.hero .btn`) */
  font-family: var(--sans); font-weight: 500; font-size: max(15px, 1.383cqw);
  line-height: 1; text-decoration: none; white-space: nowrap;
  transition: background .18s ease;
}
.env-apply:hover { background: var(--ink); }
/* ⚠️ NO HOVER STATES WHERE THERE IS NO POINTER (5 Oct 2026, the user: "on
   mobile, at least for the deadline on fellowship, let's not have any hover").
   A touch browser fires `:hover` on tap and then LEAVES IT ON until you tap
   elsewhere, so the pill stayed inverted and the track name stayed italic long
   after the finger had gone. The cursor photograph itself was already gated —
   both in this file and in stage.js — but these two were not. */
@media (hover: none) {
  .env-apply:hover { background: var(--black); }
  .env-row:hover .env-tname, .env-row.is-peek .env-tname { font-style: normal; }
}
/* ── the cursor photograph ───────────────────────────────────────────────
   Over a track row the pointer is replaced by a small photograph from the
   fellowship (`data-cursor-img` on the row). One fixed box for the whole
   page, moved by stage.js with a short lag so it trails the hand rather than
   sticking to it; it comes up from slightly small and a few degrees off
   square, and the row itself hides the system cursor so there are not two.
   The Apply pill keeps its pointer — it is the thing you click. Hover-capable
   pointers only: a phone never sees it. */
/* ⚠️ THE SYSTEM CURSOR STAYS (5 Oct 2026). The row carried `cursor: none`
   because the photograph WAS the pointer; it parks on the right now, so
   hiding the cursor would just leave the reader without one. The carve-out
   that gave the Apply pill its pointer back goes with it — there is nothing
   left to carve out of.
   The track name is the only hover target, so it is the only thing that
   advertises itself. */
@media (hover: hover) and (pointer: fine) {
  .env-row[data-cursor-img] .env-tname { cursor: pointer; }
}
/* the peek: the name goes italic — the site's accent voice, as in the nav's
   active item — for exactly as long as the photograph is on the cursor.
   stage.js toggles `.is-peek`; the pill ends it, so over Apply the name is
   plain and the pointer is the ordinary one. */
.env-row.is-peek .env-tname { font-style: italic; }
.env-cursor {
  --tilt: -3deg;                            /* the row sets it (data-cursor-tilt) */
  position: fixed; left: 0; top: 0; z-index: 70;
  width: clamp(170px, 16vw, 250px); aspect-ratio: 1080 / 1248;
  pointer-events: none; overflow: hidden;
  background: var(--box);
  opacity: 0; transform: translate(-50%, -50%) rotate(calc(var(--tilt) * 1.8)) scale(.84);
  /* ⚠️ `left`/`top` ARE ANIMATED NOW, NOT DRIVEN PER FRAME. The old box was
     lerped along the pointer by rAF, so transitioning them would have fought
     the script; it parks per row now, and a transition is what carries it
     from one row's height to the next instead of teleporting. */
  transition: opacity .22s ease, transform .42s cubic-bezier(.22, 1, .36, 1),
              left .34s cubic-bezier(.22, 1, .36, 1), top .34s cubic-bezier(.22, 1, .36, 1);
}
.env-cursor.on { opacity: 1; transform: translate(-50%, -50%) rotate(var(--tilt)) scale(1); }
.env-cursor img { display: block; width: 100%; height: 100%; object-fit: cover; }
@media (prefers-reduced-motion: reduce) {
  .env-cursor { transition: opacity .2s ease; transform: translate(-50%, -50%); }
  .env-cursor.on { transform: translate(-50%, -50%); }
}
/* the 81-unit band under the last rule is the part that stays tucked in the
   pocket — the card is 624 tall and the pocket mouth crosses it at 565 */
.env-rows > .env-row:last-child { border-bottom: 0; }

.env-stage {
  opacity: 1;
  transition: opacity .55s ease, transform .65s cubic-bezier(.2,.72,.24,1);
}
@media (prefers-reduced-motion: reduce) {
  .env-stage, .env-card { transition-duration: .01ms !important; }
}

/* ─── below 860: no pin, no envelope ───────────────────────────────────── */
/* ⚠️ THE ENVELOPE GOES, AND THAT IS DELIBERATE. Every measurement in it is a
   fraction of its own width, so at 375 the card's track names would set at
   10px — the composition does not shrink, it becomes unreadable. The card
   survives as a plain green sheet at real type sizes and the paper folds are
   dropped rather than rendered at a size where they read as smudges. */
@media (max-width: 860px) {
  .env-paper { display: none; }
  /* `transition: none` matters: there is no scene here, so the stage must be
     simply present. Without it the page loads with `data-env="n"` in the
     markup, the driver flips it to `out` on its first paint, and the whole
     section fades up over .7s — a scene entrance on a section that has none. */
  .env-stage {
    position: static; width: 100%; aspect-ratio: auto;
    transform: none; opacity: 1; container-type: normal; transition: none;
  }
  .env-slot { position: static; overflow: visible; }
  .env-holes { display: none; }
  .env-deadline {
    flex: none; padding: clamp(20px, 5vw, 30px) clamp(18px, 5vw, 26px);
    font-size: clamp(22px, 5.4vw, 32px);
  }
  .env-rows::before { content: none; }
  /* ⚠️ `:first-child` HAS TO BE NAMED AGAIN HERE. A media query adds no
     specificity, so `.env-row:first-child { height: 11.193cqw }` above (0,2,0)
     outranks a bare `.env-row { height: auto }` (0,1,0) inside it — and with
     the card's container-type dropped, that cqw falls through to the viewport
     and squashed track 1 to 42px against the other two at 69. */
  .env-row, .env-row:first-child {
    height: auto; grid-template-columns: 1fr auto; align-items: start;
    gap: 4px 14px; padding: clamp(16px, 4vw, 22px) clamp(18px, 5vw, 26px);
  }
  .env-row::after { left: clamp(18px, 5vw, 26px); }
  .env-tno, .env-tname { grid-column: 1; padding-left: 0; }
  /* the cqw margin above resolves against nothing once the card stops being a
     container — give it a real length */
  .env-tno   { font-size: 12px; margin-bottom: 6px; }
  .env-tname { font-size: clamp(20px, 5vw, 26px); }
  /* ⚠️ BACK INTO FLOW. Absolute is right on desktop, where the row is a fixed
     height and the pill must centre against the ROW rather than against the
     type; here it is wrong, because an out-of-flow pill reserves no column and
     the track name's box ran the full width straight under it. A longer name
     than these three would have collided visibly. */
  .env-apply {
    position: static; transform: none; right: auto; top: auto;
    grid-column: 2; grid-row: 1 / span 2; align-self: center;
    margin-right: 0; min-width: 0; height: auto;
    padding: 9px 16px; border-radius: 30px; font-size: 14px;
  }
}

/* `.tk-band` and every `.tk-*` rule lived here — the olive tracks band that
   closed this page until 22 Sep 2026. It is the envelope scene above now
   (`.env-*`), which moved the section ABOVE the FAQ at the same time.

   ⚠️ THREE OF ITS SELECTORS WERE SHARED AND ARE STILL LIVE, so this was not a
   block delete: `.tk-list::before` / `.tk-foot::before` / `.tk::before` sat in
   the same rules as `.faq::before` and `.rail-track::before` for the textured
   rule, and `.tk:first-child::before` shared the suppression with
   `.faq:first-of-type::before`. Only the `.tk` halves came out; the FAQ below
   draws its row rules through exactly those rules and would have lost them.

   What the band carried that the envelope does not: an eyebrow and h2
   ("Choose your track" / "Three ways in"), a lead paragraph, a one-line
   description under each track, and `.tk-faq` ("Read the FAQs →"). The first
   three are a content decision the frame makes — the card is a picker, not a
   pitch. The last is furniture now that the FAQ is the next section down. */

/* ═══════════════════════════════════════════════════════════════════════
   WHERE THE COHORT IS WORKING — the claim on the left, the marks on the right.

   It was stacked (claim, then a full-measure row of logos) and did not work:
   the statement ran wide and the grid read as a separate band under it rather
   than as its evidence. Side by side, the number and the marks are one
   argument and the eye moves between them.
   ═══════════════════════════════════════════════════════════════════════ */
.plc-split {
  /* 0.85 / 1.15, not 1 / 1.05: the panel holds ten slots across five columns
     now and the claim lost its sub-line, so the weight moved right. At an even
     split the five columns come out ~75px and the widest mark (CEG, whose ink
     is only a third of its canvas) needs ~100px — it would have been forced to
     shrink and broken the one-optical-size normalisation. */
  display: grid; grid-template-columns: minmax(0, 0.7fr) minmax(0, 1.3fr);
  gap: clamp(26px, 3.2vw, 46px); align-items: center;
}
/* `.plc-note` lived here; the sub-line was removed 21 Sep 2026. */

/* ─── THE MARKS ──────────────────────────────────────────────────────────
   NO RULED CELLS. This was a hairline table and it fought the thing it was
   holding: fifteen lines around six logos, so the eye read the grid rather
   than the marks, and reserving ten slots in it left visible gaps that looked
   like missing teeth rather than room to grow.

   One soft panel, air between the marks, nothing drawn. It takes ten without
   changing: drop the files in, give each its `--logo-k`, and the rows extend.
   ─────────────────────────────────────────────────────────────────────── */
.plc-grid {
  /* THREE FIXED — nine slots as three clean rows of three. Five columns put
     the column at 126px and that was the whole cap on how big a mark could be;
     three puts it at ~217px. Fixed, not auto-fit: auto-fit reflows and the
     reserved slots scatter out of the block. */
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(18px, 2.2vw, 26px) clamp(10px, 1vw, 12px);
  align-items: center;
  margin: 0; padding: clamp(20px, 2.2vw, 28px) clamp(16px, 1.8vw, 24px);
  list-style: none; background: var(--surface);
}
/* The cell is only a box to centre in — the normalisation is the same as the
   rail's: `--logo-k` is 600 / this mark's own ink height, so six marks drawn
   into their square canvases at six different scales land at one optical size.
   Without it CEG reads at a third of Tamil Nadu. */
.plc-cell {
  /* ⚠️ FLUID, AND CAPPED BY ONE MARK. The binding constraint is CEG: its ink
     is 83% of its canvas wide but only 33% tall, so its rendered width is
     ~2.52x its ink height — far more than any other mark here. A column is
     ~217px at 1440 at three columns, so the ceiling is far above what the
     section wants — 60px is a composition choice now, not a limit. CEG's box
     at 60px is 182px, still inside the column.

     A FIXED value cannot work across the range: 42px fits at 1440 and
     overflows a 87px column at 1024. So it tracks the viewport and clamps at
     both ends — 2.9vw gives ~30px at 1024 and ~42px at 1440, and CEG measures
     under the column at both. Raising the cap means widening the column
     first, not just this number. */
  --logo-ink: clamp(34px, 4.2vw, 60px);
  position: relative; display: block;
  height: calc(var(--logo-ink) + 6px);
}
/* A RESERVED SLOT, NOT A BORDER. The ruled version of this grid was rejected
   for being all outline; an empty cell here is a quiet tinted block instead —
   obviously a place a mark will go, and it disappears the moment one does.
   Delete the `is-empty` <li> when you add the seventh file. */
.plc-cell.is-empty::after {
  content: ""; position: absolute; inset: 0 19%;
  background: rgba(35, 35, 35, .055);
}
.plc-cell img {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  display: block; width: auto;
  height: calc(var(--logo-ink) * var(--logo-k, 1.8));
  /* ⚠️ NO max-width. THIS WAS THE BUG, and it hid behind a measurement that
     looked right. The sources are SQUARE 600x600 canvases, so at k=3.03 CEG's
     BOX is 3.03x the ink target in BOTH axes — 145px of box for 48px of ink.
     `max-width: 100%` clamped that box to the 126px column and `object-fit:
     contain` then scaled the whole image down to fit, so CEG rendered ~41px of
     ink while every other mark rendered 48. The marks were NOT one size.

     It survived three rounds of checking because getBoundingClientRect()
     returns the BOX, not the painted content after `contain` — so the probe
     read 48 for all six and said equal. Verify by asserting the box is still
     SQUARE (width == height); a clamped box is not.

     The box is allowed to overflow its cell instead. What overflows is the
     source's transparent padding and it paints nothing: CEG's ink is 120px
     wide inside a 145px box, so the INK still sits inside the 126px column. */
}
@media (max-width: 900px) {
  .plc-split { grid-template-columns: 1fr; gap: 36px; align-items: start; }
}
/* ─── HOMEPAGE VARIANT: ONE ROW OF SIX ───────────────────────────────────
   The fellowship panel reserves nine slots in a 3x3 because that programme is
   still filling them. The homepage is not a roster — it shows what there IS,
   six marks on one line, no empty cells.

   ⚠️ THE CEILING IS CEG AGAIN, and it is TIGHTER here than in the 3-up panel.
   CEG's rendered box is ~2.52x its ink height (k 3.03 on a square canvas), and
   six columns inside the 1160px measure — less this grid's own 24px padding
   and 5x14 of gap — gives a 174px column against the 3-up's ~217px. So the cap
   comes down from 60px to 52px: CEG's box is then 158px and still clears it.
   RAISING IT MEANS WIDENING THE COLUMN FIRST — at ink 60 CEG measures 182px
   and overruns its own column at 1440.

   ⚠️ THE TIGHTEST POINT IS 1440, NOT THE NARROWEST VIEWPORT, because 3.8vw
   hits the 52px cap at ~1368 while the measure stops growing at 1240. Measured
   CEG box vs column: 158/174 at 1440, 118/146 at 1024, 104/127 at 901 (the
   last width that is still six across). The floor (28px) is set by the 2-up
   phone layout, not by this one.

   These live outside the `max-width: 560px` block on purpose: `.plc-grid--row
   .plc-cell` outranks the bare `.plc-cell` there, so the row keeps its own
   ramp all the way down rather than inheriting the panel's phone size. */
.plc-grid--row {
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(20px, 2.4vw, 30px) clamp(10px, 1vw, 14px);
}
.plc-grid--row .plc-cell { --logo-ink: clamp(28px, 3.8vw, 52px); }
/* Six across needs ~183px a column; below ~900 there is not room for that, so
   it folds to 3x2 and then 2x3 rather than crushing the marks. */
@media (max-width: 900px) { .plc-grid--row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 520px) { .plc-grid--row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Three columns hold at every width — nine slots stay a 3x3 block rather than
   reflowing into ragged rows. Only the mark size comes down. */
@media (max-width: 560px) {
  .plc-cell { --logo-ink: clamp(24px, 7.5vw, 34px); }
}

/* `.record-sub + .rail` used to set this, and stopped applying the moment the
   rail moved out of the wrap — they are no longer siblings. The margin lives
   on `.rail` itself now. */







/* `.partner-band` and `.pb-*` lived here — a bordered aside that had to look
   like an aside and not like the next section, so a reader here to apply could
   see in one glance it was not for them. The block was removed 20 Sep 2026. */


/* ═══════════════════════════════════════════════════════════════════════
   THE FELLOWSHIP BANNER — a four-layer scene with a parallax on load
   Implements Figma "New ebsite 2026" frame 8211 (node 474:678), 4173x1331.
   ═══════════════════════════════════════════════════════════════════════ */

.banner-scene {
  position: relative; overflow: hidden;
  aspect-ratio: var(--banner-ratio); max-height: var(--banner-max);
  background: var(--bg);
  /* so .bs-stage can size itself off the scene's resolved HEIGHT in cqh below */
  container-type: size;
}

/* ⚠️ THE STAGE IS A COVER FIT, AND IT HAS TO BE. Every layer offset is a
   percentage of the FIGMA FRAME (3.135:1), but the banner box is 2.7:1. Laying
   the layers out against the box would stretch the composition vertically by
   16% and turn the periwinkle disc into an ellipse.

   So the stage keeps the frame's ratio and is grown until it covers the box:
   `max(100%, 100cqh * 3.135)` is "at least as wide as the box, and at least
   wide enough that its own height reaches the box's height". The overflow is
   clipped at the sides, exactly as object-fit: cover would crop a photograph.
   Change --banner-ratio and this keeps working with no numbers touched. */
.bs-stage {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: max(100%, calc(100cqh * 4173 / 1331));
  aspect-ratio: 4173 / 1331;
}

/* ⚠️ `img, canvas` — NOT `img`. Every layer offset below is `left`/`top`, which
   do nothing on a statically-positioned box, so an element this rule misses
   does not shift slightly, it collapses to the stage's top-left corner and
   takes its size from the width/height percentages. A <canvas> globe rendered
   dead-on-arrival at the far left of the arch for exactly this reason, and it
   read as a tone-mapping problem rather than a positioning one for some time. */
.bs-stage img,
.bs-stage canvas { position: absolute; display: block; image-rendering: pixelated; }
/* No `to` and no opacity: each slides from below to whatever its own rules
   already say, so the rest state is declared exactly once and a disabled
   animation leaves every layer correctly placed. */
@keyframes bs-slide-desk   { from { transform: translateY(30cqh); } }
@keyframes bs-slide-arcade { from { transform: translateY(22cqh); } }
@keyframes bs-slide-orb    { from { transform: translateY(34cqh); } }

/* the walkers' slide is on the wrapper, which also carries scaleX(-1) — so
   its `from` keeps the mirror or the party flips on the way in */
@keyframes ab-slide-mountain { from { transform: translateY(26cqh); } }
@keyframes ab-slide-river    { from { transform: translateY(18cqh); } }
@keyframes ab-slide-people   { from { transform: scaleX(-1) translateY(30cqh); } }
@keyframes ab-slide-flags    { from { transform: translateY(-24cqh); } }

/* A phone gets the scene's middle: the composition is centred on the archway,
   so cropping hard to the sides keeps the globe and the desk and loses only
   arcade. */
@media (max-width: 700px) {
  .banner-scene { --banner-ratio: 16 / 10; }
}

/* ⚠️ `margin-bottom: auto` IS WHAT FILLS THE CARD. The row is as tall as the
   folder and the drawer wall are — it takes the frame from 38.43% down to the
   floor — and with everything ranged from the top the cards read as four
   mostly-empty boxes with a caption in the corner. One auto margin on the
   number pushes the name and its line down to the baseline, which is also
   where the drawer's pill and the folder's panel copy sit, so the three
   objects in this scene share a reading line. */
.fel-num {
  /* mono label voice — 12.5 is the `.eyebrow` step; 12 was off the scale */
  font-family: var(--mono); font-weight: 500; font-size: 12.5px;
  text-transform: uppercase; letter-spacing: .14em;
  color: var(--ink); opacity: .55; margin-bottom: auto;
}
.fel-name {
  margin: .5em 0 0; font-family: var(--serif); font-weight: 400;
  /* claim tier, same as `.fn-q` and the fellowship page's `.rail-card h3` */
  font-size: clamp(21px, 1.9vw, 27px); line-height: 1.22; letter-spacing: -.2px;
  color: var(--ink);
}
.fel-copy {
  /* BODY, and body is 16/1.6 everywhere. 15/1.45 was this card's own size. */
  margin: .6em 0 0; font-size: 16px; line-height: 1.6; color: var(--ink);
  opacity: .82;
}

/* ═══════════════════════════════════════════════════════════════════════
   THE DRAWER WALL
   Figma "New ebsite 2026" — 538:3323 (the closed drawer front)
                           — 538:3331 (Vector 3, the side wall)
   ═══════════════════════════════════════════════════════════════════════
   Six drawers, full bleed, three across and two down. Each pulls out of the
   page DOWN AND TO THE LEFT, and a card rides up out of it.

   ⚠️ IT IS AN OBLIQUE PULL, IN ONE DIRECTION — AND THAT IS WHY THE FILE HAS
   ONLY ONE WALL. Vector 3
       M60.5 .22  L0.5 120.723  V409.223  L60.5 287.223 Z
   is a parallelogram whose long edges are BOTH the drawer's own 289 height,
   the near one displaced 120.5px DOWN and 60px LEFT of the far one, and it
   is placed at the drawer's LEFT. So the opening holds the cell, the front
   travels (-60, +120.5) from it, and the only exterior face you can ever
   see is the LEFT one. A symmetrical splay — front widening both ways, a
   wall mirrored onto each side — is a straight-on pull, needs two walls the
   file never drew, and was the earlier cut here.

   Nothing scales. The front is the same 642x289 in both states; it only
   moves. So no transform scaling (which would stretch the mono label and
   oval the knob) and no width animation either — just a translate.

   ⚠️ EVERYTHING IS IN cqw — 1cqw = 1% of one drawer's width — so the Figma
   numbers survive at any viewport. The design's 642-wide drawer carries a
   32px label, and 32/642 = 4.984cqw holds that ratio at any size where a
   clamp() on px would not. `--dr-h` (45.0156cqw = 289/642) is the drawer's
   own height in the same unit and half the geometry below is measured off
   it — change one and you must change the other.
*/
/* ⚠️ ONE ANIMATED NUMBER DRIVES EVERY SURFACE. `--dr-out` is how far the
   drawer is pulled, 0 shut to 1 open, registered with @property so it can
   actually transition. Hover is not a second construction with its own
   rules — it is the SAME geometry at --dr-out: .34. Nothing below reads
   "hover" or "open"; they only read --dx and --dy.

   This replaced a build where each surface scaled its own properties by a
   peek factor, which is only correct at 1: the side board was a stretched
   SVG whose skew is a FRACTION OF ITS OWN BOX, so at partial depth the
   slope landed nowhere near the front's edge, and the top kept its full
   width and clip while only its height shrank. Both looked right open and
   wrong on hover, which is exactly what was reported. */
@property --dr-out { syntax: '<number>'; inherits: true; initial-value: 0; }

/* ═══════════════════════════════════════════════════════════════════════
   FELLOWSHIP ON A PHONE — the same ideas, re-staged in portrait
   ═══════════════════════════════════════════════════════════════════════
   Not a set of responsive fallbacks. Every idea on this page is SPATIAL — a
   pairing, a pull out of a pocket, a reveal — and desktop expresses them with
   width and pinning. A phone has neither, and has two things desktop does not:
   the whole viewport for one thing at a time, and a thumb. So each idea is
   restaged rather than dropped.

   What this block fixes, measured at 390x844 before it existed:
     · `.ex-figs` collapsed to 0x0 — all six cohort photographs unloaded and
       invisible, while all six CREDITS still rendered underneath the copy.
       Six attributions for photographs nobody could see.
     · The envelope was `display: none` — the card was a plain green block,
       the punch holes and the whole slide-out gone.
     · Four designed scenes were static.

   The breakpoint is 900px to match `.ex`'s own, except where a rule already
   had a different one — those are left where they are and noted.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* ─── 1 · THE BANNER HAS TO BE WORTH THE SPACE ─────────────────────────
     16/10 put it at 244px on a 390 screen — 29% of the viewport for a
     four-layer scene, which reads as a strip rather than as an image. 5/4
     gives it 312px (37%) and the layers keep their crop. */
  .banner-scene { --banner-ratio: 5 / 4; }

  /* ─── 5 · TAP TARGETS ──────────────────────────────────────────────────
     Measured before this: the apply pill was 32px. Everything a thumb has to
     hit clears 44. */
  .env-apply { min-height: 44px; padding: 12px 20px; }
  .btn { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE ENVELOPE IN PORTRAIT — max-width: 860px
   ═══════════════════════════════════════════════════════════════════════
   The envelope used to be `display: none` below 860 because the card's type is
   sized in cqw of the card's own width, and at 390px that put the track names
   at 10px. The composition did not shrink, it became unreadable — so the whole
   metaphor was dropped and the card became a plain green block.

   ⚠️ THE ENVELOPE STAYS LANDSCAPE. An envelope IS wider than it is tall; what
   goes portrait is the CARD, which is the right way round anyway — a card that
   is taller than the pocket it comes out of is exactly what a phone screen can
   show and a desktop frame cannot. So the stage keeps its 1156/584 and the
   card becomes a tall sheet rising out of a shallow wide pocket.

   ⚠️ AND THE CARD'S HEIGHT GOES INTRINSIC. On desktop it is `aspect-ratio:
   1081/624` with every child in cqw, because the frame is fixed and the
   composition is the file's. Here the content decides the height and the type
   is in px — which is the ONLY reason the section is readable at 390.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 860px) {
  .env-paper { display: block; }

  /* The stage is the envelope, pinned to the bottom of the frame so the card
     has the whole screen above it to rise into. */
  .env-stage {
    position: absolute; left: var(--gutter); right: var(--gutter);
    /* ⚠️ THIS IS WHAT CENTRES THE COMPOSITION, and it is bigger than it looks
       like it should be. The card and its tuck stand ~403px in a 776px phone
       frame, so a small bottom offset leaves the whole assembly sitting on the
       floor with 339px of empty paper over it — measured, before this. Lifting
       the envelope to ~20svh splits the leftover roughly 55/45 between above
       and below, which is the right way round: the card rises INTO the air
       above it, so that side wants to be the larger one.

       ⚠️ AND THE FLOOR HAS TO BE LOW. At a 90px floor a 360x640 phone put the
       card's top 5px off the frame — the assembly is 438 of a 572 frame there,
       77%, so there is barely any leftover to split and a generous floor eats
       all of it from the wrong end. 14svh tracks the frame instead of fighting
       it. The companion short-screen block below does the other half of the
       job by taking height out of the card itself. */
    bottom: clamp(56px, 14svh, 150px); top: auto;
    width: auto; aspect-ratio: 1156 / 584;
    transform: none; opacity: 1; container-type: inline-size;
    transition: opacity .6s ease, transform .8s cubic-bezier(.2,.72,.24,1);
  }
  .env-slot { position: absolute; left: 0; right: 0; top: -420%; bottom: 0; overflow: hidden; }

  /* the punch holes come back, on the card's own scale */
  .env-holes { display: block; }
  .env-holes i {
    left: 10px; width: 20px; height: 13px; border-radius: 3px;
  }
  .env-holes i:nth-child(1) { top: 21%; }
  .env-holes i:nth-child(2) { top: 26%; }
  .env-holes i:nth-child(3) { top: 62%; }
  .env-holes i:nth-child(4) { top: 67%; }

  /* Type in px, not cqw — see the note above. The deadline keeps the h2 voice
     it was given in the type pass, just at the phone's h2 size. */
  .env-deadline {
    flex: none; padding: 26px 20px 22px 44px;
    font-family: var(--serif); font-weight: 400;
    font-size: clamp(22px, 6.4vw, 30px); line-height: 1.18; letter-spacing: -.2px;
  }
  .env-rows::before { content: none; }
  .env-row, .env-row:first-child {
    height: auto; grid-template-columns: 1fr auto; align-items: center;
    gap: 2px 14px; padding: 20px 20px 20px 44px;
  }
  .env-row::after { left: 44px; }
  .env-tno, .env-tname { grid-column: 1; padding-left: 0; }
  .env-tno   { font-size: 11px; letter-spacing: .14em; margin-bottom: 4px; }
  .env-tname { font-family: var(--serif-display); font-size: clamp(19px, 5.2vw, 24px); line-height: 1.1; }
  .env-apply {
    position: static; transform: none; right: auto; top: auto;
    grid-column: 2; grid-row: 1 / span 2; align-self: center;
    margin: 0; min-width: 0; width: auto; height: auto;
    border-radius: 30px; font-size: 14px;
  }
}

/* ⚠️ ONE-COLUMN PHONES: the pocket is too shallow to read at 4-up of nothing.
   Below 420 the envelope's own height is under 160px, so the card is given a
   little more of the frame and the stage sits lower. */
/* ⚠️ THE NARROW BLOCK MUST NOT RE-STATE `bottom`. It did, at 4svh, which
   silently overrode the centring above on every phone under 420 — the exact
   width the centring was computed for. Only the gutters change here. */
@media (max-width: 420px) {
  .env-stage { left: 12px; right: 12px; }
  /* ⚠️ RIGHT AS WELL AS LEFT (5 Oct 2026, the user: "the deadline envelope
     doesn't have a lot of left space and a lot less right space"). This block
     only ever set `padding-left`, so the card read 38px in on the left against
     the 20px the row's own shorthand left on the right. The Apply pill is
     `position: static`, so the right padding IS its inset — setting both here
     is what squares the card up. */
  .env-deadline { padding-left: 38px; padding-right: 38px; }
  .env-row, .env-row:first-child { padding-left: 38px; padding-right: 38px; }
  .env-row::after { left: 38px; right: 38px; }
}

/* ⚠️ A SHORT PHONE NEEDS THE CARD SHORTER, NOT THE AIR SMALLER. At 360x640 the
   card sets 388px against a 572px frame; trimming the row padding brings it
   back under 350 and gives the composition something to breathe with. This is
   the one place the card's rhythm bends to the device rather than the design. */
@media (max-width: 860px) and (max-height: 720px) {
  .env-deadline { padding-top: 18px; padding-bottom: 16px; }
  .env-row, .env-row:first-child { padding-top: 14px; padding-bottom: 14px; }
}

/* ⚠️ THE `padding-right` ON THE TRACK IS LOAD-BEARING — IT IS WHAT MAKES
   `-50%` EXACT. The retired homepage marquee had to translate
   `-50% - gap/2`, because a flex `gap` falls BETWEEN items and not after the
   last one: the two halves were not identical, and plain -50% jumped half a
   gap every cycle. A trailing pad of exactly one gap restores the symmetry —
   every item is now followed by a gap — so the repeat point lands on half the
   track. MEASURED: track 6336px, copy three starts at 3167.9, half is 3168.
   Carrying the old `- gap/2` correction over would over-shift by 36px a
   cycle. Change one of these two and you must change the other.

   ⚠️ AND ONE HALF MUST BE WIDER THAN THE WIDEST VIEWPORT, which is why there
   are four copies and not two. At `--mark` 58 the nine marks measure 936px of
   box, so one copy is 1584px at 1440 — narrower than a 1920 screen, and the
   strip would run dry before it reset. Two copies to a half is 3168 and
   clears it. Keep the copy count EVEN. */
@keyframes plc-marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* ═══ THE TRACKS ON A PHONE ARE STATIC (4 Oct 2026, the user: "let's not have
   the animation for the tracks on mobile"). This overrides the phone block
   above that re-pinned the envelope with its slide-out beats: no pin, no
   envelope paper, no card travel — the green card simply sits in flow with
   its tracks. stage.js's driver already sets `data-env="out"` whenever the
   scene is not sticky, so nothing stays hidden by a beat. Appended last so it
   wins on order against both earlier phone blocks. ═══ */
@media (max-width: 860px) {
  .env-stage, .env-scene[data-env] .env-stage, .env-scene[data-env=n] .env-stage {
    position: static; width: auto; aspect-ratio: auto;
    transform: none; opacity: 1; transition: none; container-type: normal;
  }
  .env-paper { display: none; }
  .env-slot { position: static; overflow: visible; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE FELLOW RECORD GRID — cohorts.html (5 Oct 2026)
   ═══════════════════════════════════════════════════════════════════════
   One cell per fellow: the photograph, the name, the role, the department
   and the college. Nothing else. It replaces the dossier wall, which is kept
   whole in _cohorts-dossier.html.

   ⚠️ THE GRID IS DRAWN BY THE CELLS, NOT BY THE CONTAINER. The wrapper lays
   down the top and left edge and every cell its own right and bottom, so one
   rule set is correct at five, four, two and one columns with no number to
   keep in step. Drawing it as a repeating background instead breaks the
   moment a row is taller than its neighbours, which these rows are — the
   college names run to two lines and the names to three. */
/* ⚠️ THE OUTER BOX IS THE FRAME'S, THE INNER LINES ARE THE CELLS' — AND THE
   GRID IS PULLED 1px PAST THE FRAME SO THEY NEVER DOUBLE (5 Oct 2026, the
   user: "a box outside, just like the grids inside").
   The container used to draw only the top and left edge and leave the right
   and bottom to whichever cells happened to land there. That closes the box
   only while the grid is exactly full — and it is not, the moment a filter
   hides anyone: five Engineering fellows in a five-column grid still close
   the row, but three leave the right-hand edge ragged and the box open.
   So the frame draws all four sides and the cells draw only their own right
   and bottom. The last column's right border and the last row's bottom border
   would then sit against the frame's and read 2px, so the grid carries
   `margin: 0 -1px -1px 0` and the frame clips it: those two lines fall under
   the frame's own and are hidden. Complete box, even inner weight, any number
   of cells. */
.fg-frame { border: 1px solid var(--line); overflow: hidden; }
.fg-grid {
  display: grid; grid-template-columns: repeat(5, 1fr);
  margin: 0 -1px -1px 0;
}
.fg-cell {
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 22px 20px 26px; margin: 0;
  display: flex; flex-direction: column;
}
/* the blank that closes the grid — see the note in cohorts.html */
.fg-blank { pointer-events: none; }

/* ⚠️ THE PHOTOGRAPH IS A CUTOUT ON PAPER, AND IT IS TILTED. The reference is
   a set of prints dropped into a ruled index, so the picture sits ON the page
   rather than filling the cell: a paper-soft ground, the cutout contained
   inside it, and a degree or two of rotation. The tilt alternates on a
   five-step cycle so no two neighbours lean the same way in any column count.
   ⚠️ `contain`, NOT `cover` — these are transparent PNG cutouts of people
   (826x884 and the like), so cropping them to a box cuts heads off. */
/* ⚠️ NO GROUND BEHIND THE PHOTOGRAPH. It carried `background: var(--surface)`
   and that was wrong twice over: these cutouts are not transparent, they are
   rectangular prints, so the panel showed as a second rectangle around a
   rectangle — and the reference drops its prints straight onto the paper with
   the rule grid as the only structure. The box is now just a reserved area. */
.fg-shot {
  display: block; position: relative; aspect-ratio: 4 / 5; margin-bottom: 18px;
}
.fg-shot img {
  position: absolute; inset: 7%; width: 86%; height: 86%;
  object-fit: contain; object-position: center bottom;
  transform: rotate(var(--tilt, 0deg));
}
.fg-cell:nth-child(5n+1) { --tilt: -2deg; }
.fg-cell:nth-child(5n+2) { --tilt: 1.4deg; }
.fg-cell:nth-child(5n+3) { --tilt: -1deg; }
.fg-cell:nth-child(5n+4) { --tilt: 2deg; }
.fg-cell:nth-child(5n+5) { --tilt: -1.6deg; }
@media (prefers-reduced-motion: reduce) { .fg-shot img { transform: none; } }

/* the name takes the card-title tier; role and department are the mono label
   voice, because the role IS a track word and the department an acronym —
   the same vocabulary the filter bar above uses. The college is body copy. */
.fg-name {
  margin: 0 0 8px; font-family: var(--serif); font-weight: 400;
  font-size: clamp(18px, 1.5vw, 22px); line-height: 1.22; letter-spacing: -.2px;
  color: var(--ink);
}
.fg-role {
  margin: 0 0 4px; font-family: var(--mono); font-weight: 500;
  font-size: 11px; text-transform: uppercase; letter-spacing: .1em;
  color: var(--accent);
}
.fg-college {
  margin: 0; font-family: var(--sans); font-weight: 400;
  font-size: 13px; line-height: 1.35; color: var(--ink-60, var(--graphite));
}

/* ⚠️ NO 3-UP STEP. 20 cells divide by 5, 4, 2 and 1 but not by 3, so a 3-up
   layout leaves two cells short and notches the bottom rule. */
@media (max-width: 1200px) { .fg-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 860px)  { .fg-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px)  {
  .fg-grid { grid-template-columns: 1fr; }
  .fg-cell { padding: 18px 16px 22px; }
  .fg-blank { display: none; }     /* a single column needs no closing cell */
}

/* Get Involved — a real photograph where a placeholder box stood (6 Oct 2026) */
.gi-photo { margin: 0; border-radius: 8px; overflow: hidden; aspect-ratio: 4 / 3; }
.gi-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
