/* KARA — Direction B design system: deep pine on dark ground, bark for the
   toolkit system, Atkinson Hyperlegible + Literata.

   This IS the production stylesheet: scripts/build-site.py appends
   templates/site-extra.css to it and ships the result as /assets/kara-b.css.
   (The header used to call this the "Mulberry & Blush" preview - a name two
   palettes and one promotion out of date, which is exactly how token drift
   starts.) */

@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-normal-400.woff2") format('woff2');
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-normal-700.woff2") format('woff2');
}
@font-face {
  font-family: 'Atkinson Hyperlegible';
  font-style: italic; font-weight: 400; font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-italic-400.woff2") format('woff2');
}
@font-face {
  font-family: 'Literata';
  font-style: normal; font-weight: 400 700; font-display: swap;
  src: url("/assets/fonts/literata-normal-400-700.woff2") format('woff2');
}
@font-face {
  font-family: 'Literata';
  font-style: italic; font-weight: 400; font-display: swap;
  src: url("/assets/fonts/literata-italic-400.woff2") format('woff2');
}

:root {
  /* ── Colour ──────────────────────────────────────────────────────── */
  --ground: #FBF7F4;
  --surface: #FFFFFF;
  --surface-warm: #EEEAE1;  /* the paper, one shade warmer - callouts */
  --text: #1A1417;
  --secondary: #524950;
  --border: #8A7C80;        /* 3.74:1 on ground: a real edge for non-text UI */
  --rule: #998E86;          /* hairline separators and resting card edges.
                               Was #E7DFDB: 1.23:1 on ground, which is not a
                               faint line, it is no line - no display anyone
                               owns renders it. At 3.00:1 it is a separator
                               you can actually see. It stays a step lighter
                               than --border because the two do different
                               jobs: --rule is passive and 1px, --border is
                               the edge of something you can press. */
  --fill: #EDE6E1;          /* an inert block: a placeholder, a track. The
                               one job --rule used to do that wants a tint
                               rather than a line. */
  --action: #1B5138;        /* deep pine — the only action colour */
  --hover: #123A28;
  --action-pale: #BCE6CC;   /* pine at chrome weight, for fills on ink.
                               13.26:1 on --text, which is where it is used;
                               the teal it replaced measured 13.31:1, so the
                               preview bar reads exactly as it did. */
  --on-action: #FFFFFF;     /* 9.21:1 on action pine */
  --bark: #5F4327;          /* toolkit system ONLY — warm on purpose. It began
                               as moss #40532A; bark is kept because it is the
                               better partner for pine, not just for the teal
                               that briefly replaced it. Simulated for
                               protanopia the pair separates on the blue
                               channel (#48485A vs #474727) where moss gave
                               half that gap — and the blue-yellow axis is the
                               one that survives red-green deficiency. */

  /* ── Type ────────────────────────────────────────────────────────── */
  --display: 'Literata', Georgia, serif;
  --body: 'Atkinson Hyperlegible', -apple-system, BlinkMacSystemFont, sans-serif;

  /* Eight steps, a 1.125 ratio anchored on the 19px body size. The build
     had 25 distinct font sizes, which is not a scale - it is 25 separate
     decisions taken at 25 different moments. Every one of them is within
     a pixel of a step here, so nothing on the page moves more than that,
     and the next person to write a rule has somewhere to look. */
  --t-1: 13px;   /* markers, eyebrows, tags */
  --t-2: 15px;   /* small print, notes, quiet controls */
  --t-3: 17px;   /* UI text, card bodies, nav, buttons */
  --t-4: 19px;   /* body */
  --t-5: 21px;   /* a card's heading */
  --t-6: 24px;   /* a heading in prose */
  --t-7: 27px;   /* a heading above prose, the wordmark, a pull quote */
  --t-8: 30px;   /* the top of the static scale */

  /* Fluid steps. Same apparatus as before - clamp() with the drawn sizes
     as the outer terms, one rule per element rather than a breakpoint per
     element - with the outer terms snapped onto the ladder above and its
     continuation (34, 38, 43, 48, 54). Three near-identical figure clamps
     became one: a number drawn as an object is one decision, not three. */
  --d-lede:   clamp(19px, 4.4vw, 21px);
  --d-guide:  clamp(24px, 4.4vw, 27px);
  --d-h2:     clamp(24px, 5vw, 30px);
  --d-title:  clamp(30px, 6.4vw, 38px);
  --d-hero:   clamp(30px, 7.2vw, 43px);
  --d-figure: clamp(38px, 5vw, 54px);

  /* Four line heights, from 14. Type this size is set by what the line is
     doing, not by which component it landed in. */
  --lh-tight: 1.15;   /* a figure or a headline drawn as an object */
  --lh-head: 1.25;    /* headings */
  --lh-snug: 1.4;     /* dense UI text, card bodies, captions */
  --lh-body: 1.6;     /* running prose. Was 1.7, which is loose enough that
                         a paragraph reads as a stack of separate lines
                         rather than a block - set four full paragraphs at
                         1.7, 1.6, 1.55 and 1.5 on this face at this measure
                         and 1.7 is the only one where the block does not
                         cohere. 1.6 rather than 1.55 because Atkinson
                         Hyperlegible has an unusually large x-height and
                         this audience includes people with low vision:
                         it is the top of the range that works, not the
                         middle of it. */

  /* ── Space ───────────────────────────────────────────────────────── */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-7: 32px; --sp-8: 40px;
  --sp-9: 48px; --sp-10: 64px; --sp-11: 80px; --sp-12: 96px;

  /* ── Shape ───────────────────────────────────────────────────────── */
  /* Seven radii collapsed to two and the two special cases. The rule is
     one line long: controls are --r-sm, surfaces are --r-md. A split
     radius (the search field and its attached button) is a composition
     of --r-sm, never a value of its own. */
  --r-sm: 6px;
  --r-md: 12px;
  --r-pill: 999px;
  --r-circle: 50%;

  /* ── Elevation ───────────────────────────────────────────────────── */
  /* Three levels. --e-1 is a card at rest, --e-2 the same card lifted by
     a pointer, --e-3 something floating over the page. The stage pair
     below is those same levels on ink rather than paper: a shadow that
     reads on #FBF7F4 is invisible on #0F1A14, so the ground gets its own
     values rather than every dark component getting its own rule. */
  --e-1: 0 1px 2px rgba(26, 20, 23, .06);
  --e-2: 0 2px 6px rgba(26, 20, 23, .08), 0 10px 24px rgba(26, 20, 23, .10);
  --e-3: 0 8px 24px rgba(26, 20, 23, .18);
  /* Not a level: the separating ring a floating control draws in the
     page's own paper so its edge reads against whatever is behind it.
     It is a token because it composes - `var(--e-ring) var(--ground),
     var(--e-stage-high)` is a ring plus a shadow, and the colour is the
     caller's to choose. */
  --e-ring: 0 0 0 2px;

  /* ── Targets ─────────────────────────────────────────────────────── */
  /* The floor for anything a reader can press. WCAG 2.5.8 sets 24px;
     platform guidance is 44 to 48. The stated audience for this site is
     older and largely non-technical, which makes the difference between
     those numbers a real failure rather than a checklist item, so the
     floor here is the platform number and the build checks it. */
  --control-h: 48px;
  --target-min: 44px;

  /* ── Measure ─────────────────────────────────────────────────────── */
  /* 34em, not 66ch. `ch` is the advance of the "0" glyph, and this site is
     set in Atkinson Hyperlegible, whose slashed zero is unusually wide - so
     66ch rendered 812px, which measured 93 characters a line against a
     declared intent of 66 and an upper comfortable bound of 75. The number
     was honest and the unit was lying. `em` tracks the type rather than one
     unusually fat figure, and it scales the measure with the size, which is
     what a measure is supposed to do. Verified by counting rendered
     characters, not by trusting the unit. */
  --measure: 34em;
  --col: 720px;

  /* ── Breakpoints ─────────────────────────────────────────────────── */
  /* Two, down from ten. They are recorded here because a custom property
     cannot be used in a media query, so the numbers still have to be
     typed - but they are typed against a written list, and a third one
     needs a reason in this comment before it is added.

       --bp-phone   640px   one column, and the page's padding tightens
       --bp-chrome 1000px   side rails unstack
       --bp-nav    1371px   the nav becomes a drawer

     The third was added 2026-08-21, and this is its reason. --bp-chrome
     did two jobs at one number, and they turned out to want different
     numbers. The header row - wordmark, six destinations, a real search
     field and the Subscribe/Donate pair - was laying out as two stacked
     tiers on an ordinary 1280px desktop, which read as two navigations
     rather than one. Measured on the built page: even with the nav gap
     squeezed to its 10px floor and the field down to 96px, the row still
     wants 112px more than it has at 1001px, 94px more at 1024px and 37px
     more at 1100px. It first fits around 1150px. So the drawer has to
     take over higher than the side rails do, and no amount of clamp()
     closes a 112px gap - which is the test this list asks for before a
     number is added. 1200px rather than 1150px so the fit is not a two
     pixel squeak, and the field still shrinks first if anything moves.

     Everything the other eight breakpoints did is now done by minmax(),
     auto-fit, clamp() or flex-wrap - see .grid, .cols and .result. */
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--ground);
  color: var(--text);
  font-family: var(--body);
  font-size: var(--t-4);
  line-height: var(--lh-body);
}

h1, h2, h3, h4, .display { font-family: var(--display); line-height: var(--lh-head); font-weight: 600; }

a { color: var(--action); text-decoration: underline; text-underline-offset: 3px; }
a:hover { color: var(--hover); }
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
  outline: 3px solid var(--action);
  outline-offset: 2px;
}

.wrap { max-width: 1200px; margin: 0 auto; padding: 0 clamp(20px, 3vw, 32px); }

.skip {
  position: absolute; left: -9999px;
  background: var(--surface); padding: 12px 20px; z-index: 100;
}
.skip:focus { left: 12px; top: 12px; }

/* ── Preview chrome (not part of the design) ───────────────────────── */
.previewbar {
  background: var(--text); color: var(--ground);
  font-size: var(--t-2); line-height: var(--lh-body); padding: 8px 0;
}
.previewbar .wrap { display: flex; gap: 16px; flex-wrap: wrap; align-items: baseline; }
.previewbar a { color: var(--action-pale); }
.previewbar strong { font-weight: 700; letter-spacing: .04em; text-transform: uppercase; font-size: var(--t-1); }

/* ── 1a · Header ───────────────────────────────────────────────────── */
.site-header { background: var(--surface); border-bottom: 1px solid var(--rule); }
.site-header .wrap {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: clamp(12px, 1.6vw, 24px); row-gap: 12px;
  min-height: 84px; padding-top: 12px; padding-bottom: 12px;
}
.wordmark {
  font-family: var(--display); font-size: var(--t-7); font-weight: 700;
  color: var(--action); text-decoration: none; letter-spacing: .01em;
}
.wordmark:hover { color: var(--hover); }
.nav { display: flex; gap: 24px; font-size: var(--t-3); }
/* A destination's name is one object: "Books & Podcast" breaking after the
   ampersand reads as two links and drags a 2px underline through the gap.
   The row wraps between links or not at all. */
/* The six destinations, drawn as pills.
   The underline they used to carry marked the current page with two pixels at
   the bottom of the word. A filled shape says the same thing with the whole
   control, which is what a reader glancing at a header is actually reading -
   and it gives the hover somewhere to happen that is not the text itself. */
.nav a {
  color: var(--text); text-decoration: none; white-space: nowrap;
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-pill);
  border: 1px solid transparent;
  transition: background-color .12s ease, color .12s ease, border-color .12s ease;
}
/* Washes, not the action colour. Subscribe and Donate are an outlined pill and
   a filled pill in the same row; navigation drawn the same way makes every item
   in the header look like a button and costs Donate its emphasis. */
.nav a:hover { background: rgba(27, 81, 56, .08); color: var(--action); }
/* The current page is a tint, not a border like hover: "where you are" and
   "where you would go" should differ in kind, so a reader takes in the row
   at a glance rather than comparing two weights of the same shape. */
.nav a[aria-current] { background: rgba(27, 81, 56, .12); color: var(--action); }
.search { display: flex; }
.header-tools { display: flex; align-items: center; gap: clamp(12px, 1.6vw, 24px); margin-left: auto; }
.search input {
  font: inherit; font-size: var(--t-3); padding: 0 var(--sp-4); min-height: var(--control-h);
  border: 1px solid var(--border); border-right: 0; background: var(--surface);
  color: var(--text); width: 240px;
}
.search button { padding: 0 var(--sp-4); border-color: var(--border); }


/* The two support actions travel as one cluster.
   -------------------------------------------------------------------
   12px apart rather than the header's 24px, so proximity groups them as
   a pair and the eye reads "one place where the asks live" instead of
   two more items in a row of six. */
.header-actions { display: flex; align-items: center; gap: 12px; }

/* Subscribe: the same button metrics as Donate - 48px target, same type,
   same padding rhythm - drawn as an outline. Identical size keeps the
   pair reading as siblings; the fill is the only thing that ranks them,
   which is the cheapest hierarchy signal there is and the one that
   survives greyscale, low vision, and a phone in sunlight. */


/* The search field yields the space, and stops out-ranking the page.
   -------------------------------------------------------------------
   The field is the one element in this row that is useful at more than
   one size, so it is the one that flexes. It used to flex between 150
   and 240px, which with six destinations in the nav made the row wider
   than its own padding at every width - the old sheet hid that by not
   wrapping, so Donate simply hung 25px outside the column on a 1440px
   screen.

   190px is the ceiling now. That is still a real field with a real
   48px target and enough of the placeholder to read, and it is narrower
   than the Donate button beside it - which is the point. On a page
   whose job is donations, the search field is a tool, not the top of
   the page. */
.search input { width: clamp(140px, 12vw, 190px); min-width: 0; }
.search button { padding: 0 var(--sp-4); font-weight: 400; }

.menu-toggle { display: none; }

/* The three header tiers, replaced by the row wrapping when it is full.
   -------------------------------------------------------------------
   There used to be a 1280px query tightening the gaps, a 1080px query
   forcing a two-row layout with a zero-height spacer to control where
   the break landed, and the 1000px drawer below. Three tiers for one
   row, each one a guess about where a particular set of nav labels stops
   fitting - and all three wrong the day a sixth destination was added,
   which is exactly what happened when Contact joined the nav.

   The gaps are fluid now and the row wraps where it actually runs out.
   The zero-height spacer that used to control where the break landed is
   gone too: the row's five children are already the groups it should
   break between - brand, destinations, field, actions - so a natural
   wrap lands in the same places the spacer was forcing, and it lands
   there at whatever width this particular set of nav labels happens to
   stop fitting rather than at a width somebody measured once. */
/* Tighter than it was: the pills carry their own padding now, so the old
   24px gap plus 16px of padding either side read as four separate buttons
   rather than one row. */
.nav { flex-wrap: wrap; gap: clamp(4px, .6vw, 8px); }

@media (max-width: 1371px) {   /* --bp-nav - re-measured 2026-08-23;
                                  the arithmetic is in site-extra.css */
  .site-header .wrap { flex-wrap: wrap; gap: 12px; min-height: 0; }
  .nav { display: none; }
  /* Subscribe hands off to the drawer here. At 390px the first row is
     already wordmark + Donate + Menu with ~65px to spare; a fourth
     control would wrap the row, and a wrapped row of buttons reads as
     clutter exactly where the screen is smallest. The action is not
     lost - it is the last row of the drawer, and the footer form is
     still a form. */
  /* Scoped to the header because the shared control rule below sets
     display on .btn-subscribe, and it is written later in the sheet -
     an unscoped `display: none` here would lose to it and put a third
     button back on the phone's top row. */
  .site-header .btn-subscribe { display: none; }
  .menu-toggle { display: inline-flex; padding: 0 var(--sp-4); }

  /* Section 9's other half: the search field stops out-ranking the hero.
     It used to take a full-width row of its own directly under the
     wordmark - the widest, heaviest object on first paint, above the
     donation ask on a page whose job is donations. It keeps its own row
     (a real field at every breakpoint is not negotiable here) but shares
     it with Menu and takes only the space Menu leaves, so it reads as a
     tool in the bar rather than as the top of the page. */
  /* The group dissolves here so the field can take a row with Menu
     rather than travelling with the two buttons. */
  .header-tools { display: contents; }
  .search { order: 5; margin-left: 0; flex: 1 1 12rem; min-width: 0; }
  .search input { width: auto; flex: 1 1 auto; min-width: 0; }
  .nav-drawer { order: 6; }
  .menu-toggle { margin-left: 0; }
}

/* ── 3 · Breadcrumb ────────────────────────────────────────────────── */
.crumb { background: var(--ground); border-bottom: 1px solid var(--rule); font-size: var(--t-3); }
.crumb .wrap { padding-top: 12px; padding-bottom: 12px; color: var(--secondary); }
.crumb a { color: var(--action); }

/* ── Layout ────────────────────────────────────────────────────────── */
main { display: block; padding-bottom: 64px; }
.section { padding: 56px 0; border-bottom: 1px solid var(--rule); }
.section:last-child { border-bottom: 0; }
.section > .wrap > h2 { font-size: var(--t-8); margin-bottom: 8px; }
.lede { color: var(--secondary); max-width: var(--measure); }

.marker {
  display: inline-block; font-size: var(--t-1); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary); margin-bottom: 10px;
}

/* The hero eyebrow. .marker's shape, in the action colour rather than the
   quiet grey, because this one line is doing a different job: .marker
   labels the block under it, this asserts what KARA is before the H1 says
   what it does. Slightly looser tracking than .marker - it is a sentence
   rather than a two-word label, and at 12px uppercase a sentence needs the
   air to stay readable. */
.eyebrow {
  font-size: var(--t-1); font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--action); margin-bottom: 12px;
}

/* Prose beside a rail. Weighted on purpose - the two sides are different
   kinds of object, which is the one case the grid rule allows - and it
   wraps rather than waiting for a breakpoint: the rail states the width
   it needs and the column takes the rest until there is not enough for
   both, at which point they stack. Two media queries fewer, and it is
   right inside a narrow embed as well as on a phone. */
.cols, .cols-left { display: flex; flex-wrap: wrap; gap: clamp(40px, 4vw, 64px); align-items: flex-start; }
.cols > *, .cols-left > * { min-width: 0; }
.cols > :first-child { flex: 999 1 min(var(--col), 100%); }
.cols > .rail { flex: 1 1 300px; }
.cols-left > .rail { flex: 1 1 260px; order: -1; }
.cols-left > :not(.rail) { flex: 999 1 min(var(--col), 100%); }

.prose { max-width: var(--measure); }
.prose p { margin: 0 0 1.2em; }
.prose h2 { font-size: var(--t-7); margin: 1.8em 0 .5em; }
.prose h3 { font-size: var(--t-6); margin: 1.6em 0 .4em; }
.prose ul, .prose ol { margin: 0 0 1.2em 1.3em; }
.prose li { margin-bottom: .4em; }

blockquote.pull {
  font-family: var(--display); font-style: italic; font-size: var(--t-7); line-height: var(--lh-snug);
  border-left: 4px solid var(--action); padding-left: 24px; margin: 1.6em 0;
}

/* ── Blocks ────────────────────────────────────────────────────────── */
.card { background: var(--surface); border: 1px solid var(--rule); padding: 20px; }
/* Peer grids. Equal tracks, as the grid rule requires, but the number of
   them comes from the width rather than from a breakpoint.

   Two numbers per variant: --cols, the column count the layout was drawn
   at, and --track, the narrowest a card may be before a column has to
   go. The track floor the browser is given is the larger of the two -
   an even share of the row, or --track - so the grid never exceeds the
   drawn count on a wide screen and steps down one column at a time as
   the space runs out. min(..., 100%) is what stops a 220px floor
   overflowing a 320px phone.

   This is what replaced the 1000px and 700px queries, and it is better
   than they were rather than merely equivalent: the old rules knew two
   widths and nothing in between, so a four-up grid sat at 240px a card
   at 1010px and jumped to two at 999px. The floors below are set so
   every grid on the site draws the same number of columns at 375, 768,
   1024 and 1440 as the breakpoints did, and sensible counts everywhere
   the breakpoints had no opinion. */
.grid {
  display: grid; gap: var(--gap, 24px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(var(--track, 240px),
    (100% - (var(--cols, 3) - 1) * var(--gap, 24px)) / var(--cols, 3))), 1fr));
}
.grid-4 { --cols: 4; --track: 220px; }
.grid-3 { --cols: 3; --track: 190px; }
.grid-2 { --cols: 2; --track: 200px; }

/* The one place the formula needs telling rather than working it out.
   A four-column grid stepping 4 - 3 - 2 - 1 is right for a wall of
   logos, where the last row simply has fewer in it. It is wrong for the
   footer and for any four-up grid of unequal blocks, where three across
   and one below reads as a mistake rather than as a row that ran out.
   Below the chrome breakpoint those pair up instead. No new breakpoint:
   this is the same 1000px line the nav already turns at. */
@media (max-width: 1000px) {
  .grid-4 { --cols: 2; }
}

.ph {
  background: var(--rule); color: var(--secondary);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--t-1); letter-spacing: .08em; text-transform: uppercase;
}
.ph-43 { aspect-ratio: 4 / 3; }
.ph-32 { aspect-ratio: 3 / 2; }
.ph-169 { aspect-ratio: 16 / 9; }

.rail > * + * { margin-top: 32px; }
.rail h2, .rail h3 {
  font-family: var(--body); font-size: var(--t-1); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--secondary);
  padding-bottom: 8px; border-bottom: 1px solid var(--rule); margin-bottom: 12px;
}
.rail ul { list-style: none; }
.rail li { margin-bottom: 8px; font-size: var(--t-3); line-height: var(--lh-snug); }

.donate-rail { background: var(--surface); border: 1px solid var(--action); padding: 20px; }
.donate-rail p { font-size: var(--t-3); margin-bottom: 16px; }
/* ── The control ───────────────────────────────────────────────────
   One button, three variants, and every button on the site is one of
   them. There were six font sizes and four radii across the homepage's
   buttons alone, and the header stacked three different control heights
   before the reader reached the hero.

   The variants rank by fill and nothing else: primary is solid, secondary
   is the same shape unfilled, quiet is a link with a real target around
   it. That is the cheapest hierarchy signal there is and the only one
   that survives greyscale, low vision, and a phone in sunlight.

   --control-h is the floor for every interactive target on this site,
   not just for buttons: see the target rules further down. */
.btn, .btn-donate, .btn-subscribe, .menu-toggle, .search button,
.prose .link-row > a {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sp-2); min-height: var(--control-h); padding: 0 var(--sp-6);
  font: inherit; font-size: var(--t-3); font-weight: 700;
  border-radius: var(--r-sm); border: 1px solid transparent;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  background: var(--action); color: var(--on-action);
  transition: background-color .16s ease, color .16s ease, border-color .16s ease;
}
.btn:hover, .btn-donate:hover, .prose .link-row > a:first-child:hover {
  background: var(--hover); color: var(--on-action);
}
/* Secondary: the same object, unfilled. */
.btn-quiet, .btn-subscribe, .menu-toggle, .search button,
.prose .link-row > a:not(:first-child) {
  background: transparent; color: var(--action); border-color: var(--action);
}
.btn-quiet:hover, .btn-subscribe:hover,
.prose .link-row > a:not(:first-child):hover {
  background: var(--action); color: var(--on-action); border-color: var(--action);
}
/* Quiet: a link, with a target the size of a button around it. Used
   where a control would out-rank what it sits beside - "or give
   monthly" under the donation amounts, "see all 45" under a grid. */
.btn-plain {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--control-h); padding: 0 var(--sp-2);
  font-size: var(--t-3); font-weight: 400;
  background: transparent; color: var(--action); border: 0;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.btn-plain:hover { color: var(--hover); }

/* ── Donation ask (1b · 2) ─────────────────────────────────────────── */
.ask { background: var(--surface); border: 1px solid var(--rule); padding: 32px; max-width: 620px; }
.amounts { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }
.amounts label {
  border: 1px solid var(--border); padding: 12px 18px; min-height: 48px;
  display: inline-flex; align-items: center; cursor: pointer; background: var(--ground);
}
.amounts input { position: absolute; opacity: 0; }
.amounts input:checked + span { font-weight: 700; }
.amounts label:has(input:checked) { border: 2px solid var(--action); background: var(--on-action); color: var(--action); }
.trust { font-size: var(--t-2); color: var(--secondary); margin-top: 14px; }

/* ── Bark: the toolkit system, and nowhere else ────────────────────── */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  border: 2px solid var(--bark); color: var(--bark); background: var(--surface);
  padding: 8px 16px; min-height: 44px; display: inline-flex; align-items: center;
  text-decoration: none; font-size: var(--t-3);
}
.chip:hover { background: var(--bark); color: var(--on-action); }

/* ── Hubs / blog ────────────────────────────────────────────────── */
.hub-grid a.hub { display: block; text-decoration: none; color: var(--text); }
.hub-grid a.hub:hover { border-color: var(--action); }
.hub-grid h3 { font-size: var(--t-5); margin-bottom: 4px; }
.hub-meta { font-size: var(--t-3); color: var(--secondary); }
.hub-rail { display: flex; flex-wrap: wrap; gap: 8px 24px; font-size: var(--t-3); }

.results { list-style: none; }
.result { display: flex; flex-wrap: wrap; gap: 24px; padding: 24px 0; border-bottom: 1px solid var(--rule); }
.result > * { min-width: 0; }
.result > :first-child { flex: 1 1 180px; }
.result > :first-child:not(:last-child) { flex: 0 1 180px; }
.result > :last-child:not(:first-child) { flex: 999 1 min(20rem, 100%); }
.result h3 { font-size: var(--t-6); margin: 8px 0; }
.result h3 a { color: var(--text); }
.result h3 a:hover { color: var(--action); }
.tags { display: flex; gap: 8px; flex-wrap: wrap; font-size: var(--t-1); letter-spacing: .06em; text-transform: uppercase; color: var(--secondary); }
.byline { font-size: var(--t-3); color: var(--secondary); }

.filters { display: flex; flex-wrap: wrap; gap: 24px; padding: 20px; background: var(--surface); border: 1px solid var(--rule); font-size: var(--t-3); }
.filters .group { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.filters .group b { font-size: var(--t-1); letter-spacing: .08em; text-transform: uppercase; color: var(--secondary); }

.pager { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; padding: 24px 0; font-size: var(--t-3); }
.pager a[aria-current] { font-weight: 700; text-decoration: none; color: var(--text); }

.years { display: flex; flex-wrap: wrap; gap: 8px 16px; font-size: var(--t-3); }

/* ── Newsletter ────────────────────────────────────────────────────── */
.news { background: var(--surface); border: 1px solid var(--rule); padding: 28px; max-width: 620px; }
.news form { display: flex; gap: 0; margin-top: 16px; flex-wrap: wrap; }
.news input {
  font: inherit; font-size: var(--t-3); padding: 12px 16px; min-height: 48px; flex: 1 1 240px;
  border: 1px solid var(--border); border-right: 0; background: var(--ground); color: var(--text);
}
/* The pair wraps onto two rows on a narrow screen, and when it does the
   input's right edge is exposed. It keeps its border and the button
   overlaps it by the one pixel they share, so the seam is closed on one
   row and the edge is drawn on two - without a 500px query having to
   know which is happening. */
.news input { border-right: 1px solid var(--border); }
.news form > .btn { margin-left: -1px; }  /* off-scale: the one pixel the pair shares, not a gap */

/* ── Sticky mobile read bar (1c · 7) ───────────────────────────────── */
.readbar { display: none; }
@media (max-width: 1000px) {
  .readbar {
    display: flex; position: sticky; top: 0; z-index: 20; align-items: center;
    gap: 12px; min-height: 44px; padding: 0 20px; font-size: var(--t-2);
    background: var(--surface); border-bottom: 4px solid var(--rule); color: var(--secondary);
  }
  .readbar::after { content: ''; position: absolute; left: 0; bottom: -4px; height: 4px; width: 38%; background: var(--action); }
  .readbar a { margin-left: auto; }
}

/* ── 5 · Footer ────────────────────────────────────────────────────── */
.site-footer { background: var(--surface); border-top: 1px solid var(--rule); padding: 48px 0 64px; font-size: var(--t-3); }
.site-footer .grid { --track: 220px; }  /* --cols comes from .grid-4 */
.site-footer h2 { font-family: var(--body); font-size: var(--t-1); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--secondary); margin-bottom: 12px; }
.site-footer ul { list-style: none; }
.site-footer li { margin-bottom: 8px; }
.footer-fineprint { font-size: var(--t-2); color: var(--secondary); margin-top: var(--sp-3); }
.site-footer .legal { margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--rule); font-size: var(--t-2); color: var(--secondary); max-width: 70ch; }

/* ── Targets ───────────────────────────────────────────────────────
   The floor for anything a reader can press, applied once here rather
   than component by component.

   The count before this rule was 37 to 61 elements under 44px on the
   five pages that were measured, several at 18 to 21px: the whole
   footer's link columns, every nav destination (42.9px, three pixels
   short), the hub rails, the role-group summaries, and every link
   inside a sentence.

   WCAG 2.5.8 sets the floor at 24px. Platform guidance is 44 to 48. The
   stated audience for this site is older and largely non-technical, so
   the difference between those two numbers is the difference between a
   page they can use and a page that technically passes, and the floor
   here is the platform number.

   Two techniques, because a link in a list and a link in a sentence are
   different problems:

   - A link that owns its line becomes a box: inline-flex, centred, with
     --target-min as its floor. Where that would double a row's height
     the row gives up its margin instead, so the block grows by the
     difference rather than by the whole amount.

   - A link inside running text cannot take a height without pushing its
     neighbours apart, so it takes padding instead. Padding on an inline
     box expands the box the pointer hits and the background that paints,
     and does not enter the line-box calculation at all - the leading is
     identical with and without it, which is checked at the bottom of
     scripts/check-design.py. The cost is that hit areas on adjacent
     lines overlap; the browser gives the press to the topmost, which is
     the line the reader is pointing at. */

/* Links that own their line. */
.wordmark,
.nav a,
.hub-rail a,
.years a,
.pager a,
.site-footer li > a,
.site-footer .site-index a,
.rail li > a,
.filters .group a {
  display: inline-flex; align-items: center; min-height: var(--target-min);
}
/* Width too, and horizontally it has to come from padding rather than from
   min-width: a nav row of boxes all 44px wide whatever their label reads as a
   table. The pill rule above sets that padding at 16px each side, which is
   past the floor on its own - so there is nothing to add here. */
/* Those rows carried a margin that was there to separate 21px lines. The
   target height does that job now, so the margin comes off rather than
   being added to it. */
.site-footer li, .rail li { margin-bottom: 0; }

/* Links inside running text. --target-min less the line box, halved:
   the padding is symmetrical so the expanded box stays centred on the
   words. Anything that is already a control, a card or a chip is
   excluded - those have their own box and this would add to it. */
:where(p, li, dd, figcaption, blockquote, h1, h2, h3, h4, .crumb, .tags)
  a:not(.btn, .btn-quiet, .btn-plain, .btn-donate, .btn-subscribe,
        .card, .chip, .hub, .link-card, .project-card, .group-card,
        .rec-cta, .menu-toggle, .to-top, .quick-exit) {
  padding-block: calc((var(--target-min) - 1em) / 2);
}

/* Width, not just height: a two-character amount chip was 40px across,
   and the resources tab was a 34px-wide button. */
.amounts a, .amounts label, .chip, .pager a, .years a,
.res-tab, [class*="resource-tab"], [class*="drawer-tab"] {
  min-width: var(--target-min);
}

/* ── Annotation callouts (preview only) ────────────────────────────── */
.note {
  border-left: 4px solid var(--border); background: var(--surface);
  padding: 12px 16px; font-size: var(--t-2); line-height: var(--lh-body); color: var(--secondary);
  max-width: var(--measure); margin: 16px 0;
}
.note b { color: var(--text); }

/* Appended to preview/kara-b.css when the site build runs. Everything here is
   site chrome the preview pages did not need. */

/* Quick Exit. Carried over from the current site: a safety control for a site
   about child abuse. Direction B's wireframes never mention it, which is
   exactly why it needs writing down - it would otherwise be lost in the
   rebuild. */
.quick-exit {
  position: fixed; right: 16px; bottom: 16px; z-index: 60;
  background: var(--text); color: var(--on-action); text-decoration: none;
  font-size: var(--t-2); font-weight: 700; padding: 12px 16px; min-height: 48px;
  display: inline-flex; align-items: center;
}
.quick-exit:hover { background: var(--action); color: var(--on-action); }

/* Clearance for Quick Exit, derived from the control's own box rather
   than from a number typed once and left behind: its height, the gap it
   keeps above and below itself, its width, and whatever the phone's home
   indicator claims. Change the control's padding and every clearance
   below follows.

   This is not a cosmetic reservation. On a site serving people in abusive
   situations Quick Exit is the one control that must be unambiguous and
   must never sit over something else that is pressable - a misfire here
   is not a cosmetic bug. At a 894px viewport it was sitting on top of
   card body text in the hub grid, over the card links themselves. */
:root {
  --exit-h: 48px;
  --exit-gap: 16px;
  /* The keyboard hint is the only thing that changes the control's width, and
     it is shown on pointer devices only - so its contribution is a token that
     is zero everywhere else, and every width derived from the control picks
     the change up instead of being retyped. */
  --exit-hint-w: 0px;
  --exit-w: calc(152px + var(--exit-hint-w));
  --exit-clearance: calc(var(--exit-h) + var(--exit-gap) * 2);
}
body {
  /* Plain value first: a browser that does not understand env() drops the
     whole declaration rather than part of it, and would keep no clearance
     at all. */
  padding-bottom: 80px;
  padding-bottom: calc(var(--exit-clearance) + env(safe-area-inset-bottom, 0px));
}

/* An anchor jump lands the target at the top of the viewport, which is
   why the sticky header already has a scroll-padding-top. The bottom
   needs the same: a jump to the last heading on a page, or a "back to
   the contents" link near the foot of a toolkit, would otherwise put the
   thing the reader asked for underneath the exit control. */
html {
  scroll-padding-bottom: calc(var(--exit-clearance) + var(--exit-gap));
}

/* The corner the control occupies, reserved in the page rather than left
   to chance. The last card on a page gets the control's whole box out of
   its way at the bottom, and any grid of pressable cards keeps the
   control's width clear on its right-hand side at the widths where the
   grid otherwise runs to the screen edge - so the lowest card in the
   last row cannot put a link under it. */
.section:last-child > .wrap {
  padding-bottom: calc(32px + var(--exit-clearance));
}
@media (max-width: 1000px) {
  .grid:last-child,
  .hub-grid:last-child,
  .link-cards:last-child,
  .rec-grid:last-child,
  .results:last-child {
    padding-bottom: var(--exit-clearance);
  }
}

/* Listings as content cards.
   ---------------------------------------------------------------------
   Every result on a blog listing, a topic hub or a term page becomes a
   card, so the whole site reads as one system: the home page's project cards,
   the toolkit cards and these are the same object.

   Two columns, because a card that spans the full measure is a row wearing a
   border and gains nothing. They collapse to one below 900px.

   The dense year index on /blog/all/ is deliberately NOT carded - it lists
   the whole blog, and 1,874 cards is a scroll, not a page. It stays a
   compact list, which is the right shape for something you scan by date. */
.results {
  list-style: none; display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 20px; margin-top: 20px;
}
.results .result {
  display: block; background: var(--surface); border: 1px solid var(--rule);
  padding: 20px; border-bottom: 1px solid var(--rule);
}
.results .result h3 { font-size: var(--t-5); margin: 8px 0 0; }
.results .result .byline { margin-top: 8px; }


/* Feature lists — the book's four ways in, and the international reporting.
   A link plus a line saying what is behind it, so nobody has to click to find
   out. The note is secondary colour and not underlined; only the link is. */
.feature-list { list-style: none; margin-top: 16px; }
.feature-list li { padding: 14px 0; border-bottom: 1px solid var(--rule); }
.feature-list li a { font-weight: 700; }
.feature-list li span {
  display: block; margin-top: 4px; font-size: var(--t-3); color: var(--secondary);
}

/* The book: cover beside the heading, stacking on a phone. */
.book-head { display: flex; gap: 24px; align-items: flex-start; }
.book-head img { flex: 0 0 auto; height: auto; }
@media (max-width: 640px) {
  .book-head { flex-direction: column; gap: 16px; }
}

/* Third-party embeds (the podcast). Full width of their column, and never
   taller than they need to be. */
.embed { width: 100%; border: 0; display: block; }

/* The book section's podcast column is the narrower of the two on desktop and
   sits below on a phone, which .cols already handles. */
@media (max-width: 1000px) { .embed { margin-top: 12px; } }

/* Nothing on this site animates today. This is here so that if something is
   added later - a sticky rail, a card that eases in - it cannot ship without
   respecting a vestibular-sensitivity setting the reader has already made. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Click-to-play video.
   The poster is a link until JavaScript upgrades it, so it has to look like
   something you press either way. 16:9 box so nothing reflows when the player
   replaces the image. */
.video { position: relative; aspect-ratio: 16 / 9; background: var(--text); }
.video-poster { display: block; position: relative; height: 100%; text-decoration: none; }
.video-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-frame { width: 100%; height: 100%; border: 0; display: block; }
.video-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 76px; height: 76px; border-radius: var(--r-circle);
  background: var(--action); color: var(--on-action);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--t-7); padding-left: 8px;
}
.video-note {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: rgba(26, 20, 23, .82); color: var(--on-action);
  font-size: var(--t-2); padding: 8px 16px;
}
.video-poster:hover .video-play { background: var(--hover); }
.video-poster:focus-visible { outline: 2px solid var(--action); outline-offset: 3px; }

/* The podcast facade has no poster image, so it needs its own ground and a
   shape that does not pretend to be 16:9 video. */
.embed-facade { aspect-ratio: auto; min-height: 232px; background: var(--surface);
  border: 1px solid var(--rule); }
.embed-facade .video-poster { display: flex; align-items: center;
  justify-content: center; min-height: 232px;
  /* Wraps rather than clips. The label is one flex line, the parent is
     overflow:hidden, and at 1440 it ran 10px past its box with no scroll to
     recover it - the end of "loads when you press play" was simply gone. */
  flex-wrap: wrap; gap: 8px; padding: 0 16px; text-align: center; }
.embed-facade .video-play { position: static; transform: none; }

/* Two columns that both carry weight.
   .cols exists for a body-plus-narrow-rail layout and its 300px right column
   is too tight for the donate card's four amounts. This is the pairing case:
   the argument on the left, the thing to do about it on the right. */
.split {
  display: flex; flex-wrap: wrap; gap: clamp(32px, 4vw, 48px); align-items: flex-start;
}
.split > * { flex: 1 1 22rem; min-width: 0; }
/* In the hero the ask sheds only kara-b.css's width cap: it sits on the
   same .home-card frame as the mission and the figure (Sam's direction,
   2026-08-18), so the panel kara-b.css gives .ask elsewhere is replaced
   by the card rather than removed outright. */
.split .ask { max-width: none; background: transparent; border: 0; }
.split .ask.home-card {
  background: var(--ground); border: 1px solid var(--rule);
  border-left: 4px solid var(--action);   /* it is the ask - see "What the
                                             accent rule is for" below */
}

/* The figure is the loudest thing on the page after the mission line, and it
   scales down before it wraps - "$428 billion" breaking across two lines reads
   as two numbers. */
.stat-figure { font-size: var(--d-figure); line-height: var(--lh-tight); }

/* The intro sets a shade tighter than body prose - it is display
   support, not a column of reading, and at 1.7 it read as bulk. */
.page-home main h1 + .lede { line-height: var(--lh-body); }

/* The hero's three objects - mission, figure, ask - on the Key projects
   card (Sam's direction, 2026-08-18): the same accent-bordered frame the
   project and role-group cards carry, so the whole page runs one card
   system. Ground panel on the tinted section, because a surface card on
   the surface section would be invisible. */
.home-card {
  background: var(--ground); border: 1px solid var(--rule);
  border-radius: var(--r-md); padding: 32px;
}
/* The mission panel and the cost figure are the page's argument, not its
   ask, so they rank by paper rather than by the bar: they sit on the
   ground where the ask beside them sits on the same ground with the bar
   on it. One bar on the page, and it is on the thing to press. */
.split > .home-card { height: 100%; }
@media (max-width: 640px) { .home-card { padding: 24px; } }

/* Section rhythm. The page was one continuous ground with hairline rules, so
   nothing told the eye where one idea ended and the next began. Alternating
   the two surfaces already in the palette gives that break without adding a
   colour: no new token, no new contrast pair to re-check. */
.section-tint { background: var(--surface); }
.section-tint + .section { border-top: 0; }

/* Amounts as links, not dead radios. Same shape as before so nothing about the
   design moves; the suggested one is marked, and 48px keeps the tap target. */
.amounts { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 0;
  list-style: none; padding: 0; }
.amounts li { flex: 1 1 68px; }

.amounts a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; flex: 1 1 auto; min-width: 0; padding: 0 8px;
  border: 1px solid var(--rule); background: var(--ground);
  color: var(--text); text-decoration: none; font-weight: 700;
}
.amounts a:hover { border-color: var(--action); color: var(--action); }
.amounts a.is-suggested { border: 2px solid var(--action); color: var(--action); }

/* The flex row wrapped ragged - $25 and Other landed misaligned at unequal
   widths on the second row. A grid makes every chip the same width wherever
   it lands, and auto-fit means the number of columns follows the width
   instead of a 700px query deciding it is three. */
.amounts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(6rem, 100%), 1fr)); }
.amounts li { flex: none; }
.amounts a { width: 100%; }

/* "or give once" wrapped mid-phrase alongside the button. It gets its own
   line, which also puts more room between the two different asks - and it
   gets it at every width rather than below a 900px line, because a
   secondary ask sitting on the primary one's row is the same mistake on a
   desktop. The !important went with the query: the two rules that were
   fighting are one rule now. */
.ask .btn { display: block; text-align: center; }
.ask .btn + span { display: block; margin: 16px 0 0; }

/* Grid tracks must be allowed to be narrower than their content.
   `1fr` is `minmax(auto, 1fr)`, and `auto` floors the track at the content's
   min-content width - so one long title in one card pushed the whole listing
   track to 528px inside a 335px column and the page scrolled sideways at
   375px. minmax(0, 1fr) removes that floor; the wrap rule then breaks the
   string that caused it rather than letting it hang out of the card. */
.results {
  --cols: 2; --gap: 24px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%,
    max(20rem, (100% - var(--gap)) / var(--cols))), 1fr));
}
.results .result h3, .results .result .byline, .results .result a {
  overflow-wrap: anywhere;
}

/* The state map is an SVG with an intrinsic width, so on a phone it was wider
   than the screen and took the page with it. */
#kara-state-map svg, .kmap svg { max-width: 100%; height: auto; display: block; }

/* The resources drawer is off-canvas: fixed, and pushed out with
   translateX(100%). A transformed element still counts toward the document's
   scrollable width, so on a phone every page could be dragged sideways into
   375px of empty space beside the content - on all 2,215 of them, because the
   panel is site-wide.

   `clip` rather than `hidden`: hidden would make the root a scroll container
   and break `position: sticky` anywhere inside it. This clips the overflow
   without changing what anything else can do.

   Note for whoever runs the layout audit next: this makes
   documentElement.scrollWidth stop reporting sideways overflow, so measure
   per-element right edges instead - that is what found this one. */
html, body { overflow-x: clip; }
/* The states page needed the body rule too: with only the root clipping, its
   map still dragged the viewport 210px sideways. */

/* Partners. Equal slots, whatever shape the artwork is.
   ---------------------------------------------------------------------
   As a wrapping flex row, every tile took the width of its own logo and
   the wall rendered 180×27 beside 56×56 beside 166×56 - five sizes, no
   rhythm, and the widest wordmark read as the most important partner.

   It is a grid of identical cells now. Every tile is the same width
   (equal auto-fit tracks), and inside each one the logo is drawn into
   the same fixed 48px-tall box: `width:100%` with `object-fit: contain`
   means the box is the layout object and the artwork centres itself
   inside it, both axes, whatever its aspect ratio. A 1:1 seal fills the
   box's height; a 3:1 wordmark fills its width; neither can dominate a
   row because neither can leave its box. That is what "equal in weight,
   not equal in pixels" cashes out to - the eye compares boxes, and the
   boxes are identical. */
.partners {
  list-style: none; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px; margin-top: 8px; padding: 0;
}
.partner {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 8px;
  min-height: 96px; padding: 16px 20px;
  background: var(--ground); border: 1px solid var(--rule);
}
.partner a { text-decoration: none; display: block; width: 100%; }
.partner img {
  display: block; width: 100%; height: 48px; object-fit: contain;
}
.partner-name { font-weight: 700; font-size: var(--t-4); text-align: center; }
.partner-note { font-size: var(--t-2); color: var(--secondary); text-align: center; }
.partner a:hover .partner-name { color: var(--action); text-decoration: underline; }
@media (max-width: 640px) {
  .partners { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .partner { min-height: 80px; padding: 12px 16px; }
  .partner img { height: 40px; }
}

/* =====================================================================
   The dark stage.
   =====================================================================

   Every content section becomes a lit paper card floating on a dark
   ground. Two rules made this shape and not a "dark mode":

   1. The words never move onto the dark. Long-form reading is this
      site's whole job, its audience skews older, and dark-ground body
      text measurably raises reading effort for aging eyes (halation).
      So prose stays ink-on-paper at 17:1, exactly as it was - the
      darkness lives BETWEEN the cards, where it does the framing.
   2. Depth comes from light, not motion. The site promises
      reduced-motion respect and no animation; a shadow system and a
      fixed gradient read as dimensional at zero motion cost. The only
      transitions are on hover, and the global reduced-motion guard
      above already zeroes them.

   The Direction B daylight tokens are untouched - --ground and
   --surface still mean the paper the words sit on. The stage gets its
   own tokens. Every pair below was checked, not eyeballed:
   chrome text 14.6:1, secondary 7.9:1, bright links 10.3:1, card edge
   16.6:1 against the shell (scripts/check-design.py asserts them). */

:root {
  --shell: #0F1A14;         /* the stage: ink with the action pine's undertone */
  --shell-raised: #1A2A20;  /* header, footer, breadcrumb - one step lifted */
  --shell-text: #EEEAE1;    /* chrome text, warm to match the paper */
  --shell-secondary: #B3ACA6;
  --shell-rule: rgba(239, 233, 227, .14);
  --action-bright: #8FD3AC; /* the pine, brought up to carry on dark ground */
  --action-lit: #A9E3C2;    /* --action-bright under the pointer. The filled
                               header button used to hover to --shell-text, a
                               beige, so the one primary action on the bar lost
                               its colour when chosen and read as disabled.
                               12.27:1 against --shell. */
  --placeholder: #696568;   /* prompt text in a field. The browser default is
                               #757575: 4.61:1 on white but 4.33:1 on --ground,
                               which is the ground the footer form sits on, so
                               it failed AA on every page. This is 5.38:1 on
                               --ground, 5.73:1 on white, 4.78:1 on the warm
                               tint - and still clearly lighter than
                               --secondary, so it still reads as a prompt. */
  --e-stage: 0 1px 2px rgba(0, 0, 0, .28), 0 12px 32px rgba(0, 0, 0, .38);
  --e-stage-high: 0 2px 4px rgba(0, 0, 0, .30), 0 18px 44px rgba(0, 0, 0, .46);
}

/* The ground is not flat black: a fixed radial wash puts a key light
   high on the page, so scrolling moves the cards through the light.
   That is the whole "cinematic" apparatus - one gradient, no pixels
   shipped, nothing animated. */
body {
  background:
    radial-gradient(1200px 640px at 50% -160px, rgba(143, 208, 223, .10), transparent 68%),
    radial-gradient(900px 520px at 85% 108%, rgba(143, 208, 223, .05), transparent 60%),
    var(--shell);
  background-attachment: fixed;
}
@media (max-width: 640px) {
  /* Fixed backgrounds repaint badly on mobile compositors. */
  body { background-attachment: scroll; }
}

/* Every section's content column becomes the card. This single rule is
   what boxes "literally all text" site-wide - articles, listings,
   toolkits, everything renders inside a .section > .wrap. */
.section {
  padding: 8px 16px;
  border-bottom: 0;
}
.section:first-child { padding-top: 24px; }
.section:last-child { padding-bottom: 32px; }
/* 34px of paper above and below the content, not 48px. At 48px a card
   carrying one heading and a two-line paragraph was more margin than
   words, and twelve of those stacked put a third of the home page's
   height into padding. The horizontal inset stays generous - that one
   is holding the measure, not the rhythm. */
.section > .wrap {
  background: var(--ground);
  border-radius: var(--r-md);
  padding: 32px 40px;
  box-shadow: var(--e-stage);
}
/* The alternation .section-tint used to do with a ground swap it now
   does with the two papers: tinted sections get the brighter surface. */
.section-tint { background: transparent; }
.section-tint > .wrap { background: var(--surface); }

@media (max-width: 640px) {
  .section { padding: 8px 8px; }
  .section:first-child { padding-top: 16px; }
  .section > .wrap { padding: 32px 20px; border-radius: var(--r-md); }
}

/* Inner cards sit ON the paper now, so they carry a hairline and a
   whisper of depth rather than competing with the big shadow. */
.card, .results .result, .partner {
  border-radius: var(--r-md);
  box-shadow: var(--e-1);
}
.amounts a, .video, .embed-facade { border-radius: var(--r-md); }
.video, .video-frame, .video-poster img { border-radius: var(--r-md); overflow: hidden; }

/* Cards that are links lift toward the reader on hover - 2px and a
   longer shadow, 160ms. The reduced-motion guard zeroes the transition;
   the hover state itself (border colour) survives it. */
.results .result, .card {
  transition: transform .16s ease, box-shadow .16s ease;
}
.results .result:hover, .card:hover {
  transform: translateY(-2px);
  box-shadow: var(--e-2);
  border-color: var(--border);
}

/* --- Chrome: header, breadcrumb, footer go to the stage. ------------ */

.site-header {
  background: rgba(17, 26, 30, .86);
  border-bottom: 1px solid var(--shell-rule);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  backdrop-filter: saturate(1.4) blur(14px);
}
/* Sticky on desktop only. The mobile header is three rows tall, and a
   sticky element that eats half a phone screen is worse than none; the
   article readbar already does the on-scroll job there at 44px. */
@media (min-width: 1001px) {
  .site-header { position: sticky; top: 0; z-index: 50; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header { background: var(--shell-raised); }
}
.site-header .wordmark { color: var(--action-bright); }
.site-header .wordmark:hover { color: var(--shell-text); }
/* The pills, on the stage - and deliberately quieter than the two controls
   beside them.
   
   Drawn the obvious way, an outlined pill on hover and a bright filled one for
   the current page, they came out identical to Subscribe and Donate, which sit
   six pixels to the right and are an outlined pill and a filled pill. The
   header then offered eight things that all looked like buttons, and the one
   the page is actually asking for stopped being the loudest thing in the row.
   
   So navigation gets washes rather than the action colour: a warm tint on
   hover, a pine tint for the current page with bright text on it. Both read
   as "part of the chrome", and the only filled bright shape in the header is
   still Donate. */
.nav a { color: var(--shell-text); }
.nav a:hover {
  background: rgba(239, 233, 227, .10); color: var(--shell-text);
  border-color: transparent;
}
.nav a[aria-current] {
  background: rgba(143, 211, 172, .16); color: var(--action-bright);
  border-color: transparent;
}
/* Scoped to the header, because the header is the only search field on the
   stage. These rules were unscoped, so the two search fields that sit on
   paper - the blog's "By search" and the field on /search/ - were drawn in
   shell colours on a white card: a form the reader could barely see, on the
   two pages whose whole job is finding something. */
.site-header .search input {
  background: rgba(239, 233, 227, .08); color: var(--shell-text);
  border: 1px solid var(--shell-rule);
}
.site-header .search input::placeholder { color: var(--shell-secondary); opacity: 1; }
.site-header .search button {
  background: rgba(239, 233, 227, .08); color: var(--shell-text);
  border: 1px solid var(--shell-rule);
}
.site-header .search button:hover {
  border-color: var(--action-bright); color: var(--action-bright);
}

/* The one contrast failure the arithmetic found: the deep pine button
   body is 1.7:1 against the raised header - invisible. On dark chrome
   the primary action flips to the bright pine with ink text (10.3:1),
   which also makes Donate the brightest object in the header. That is
   the hierarchy working, not a compromise. */
.site-header .btn-donate {
  background: var(--action-bright); color: var(--shell);
}
/* Brighter on hover, not paler. This went to --shell-text, a warm beige: the
   one filled button on the bar lost its colour the moment a pointer touched
   it and read as disabled at exactly the moment it was being chosen. A
   primary action should gain energy under the pointer. 12.27:1 on the label. */
.site-header .btn-donate:hover { background: var(--action-lit); color: var(--shell); }

/* Subscribe on the same dark chrome: bright pine drawn as an outline.
   The deep pine that works on paper is 1.7:1 here - the same failure the
   filled button had - so the border and the label both take the bright
   pine, which reads at about 10:1 on this ground. Filled Donate stays the
   one solid shape in the bar; Subscribe is the same size and half the
   weight, which is the whole hierarchy in one property. */
.site-header .btn-subscribe {
  background: transparent; color: var(--action-bright);
  border-color: var(--action-bright);
}
.site-header .btn-subscribe:hover {
  background: var(--action-bright); color: var(--shell);
}
/* On /newsletter/ itself the button is a link to where you already are.
   It keeps its place - moving controls between pages costs more than it
   saves - and says so instead, the same way the nav marks its own row. */
.site-header .btn-subscribe[aria-current] {
  background: rgba(169, 220, 192, .16);
  color: var(--action-bright);
}

.menu-toggle { color: var(--shell-text); border-color: var(--shell-rule);
  background: transparent; }

/* Focus rings on dark chrome need the bright pine to be seen at all. The
   footer is light ground now, so it takes the ordinary ring instead. */
.site-header :focus-visible,
.crumb :focus-visible, .quick-exit:focus-visible {
  outline: 3px solid var(--action-bright); outline-offset: 2px;
}

.crumb { background: transparent; border-bottom: 0; }
.crumb .wrap { color: var(--shell-secondary); }
.crumb a { color: var(--action-bright); }

/* The footer sits on paper, not on the shell.
   -------------------------------------------------------------------
   It was four stacked panels of near-identical dark green - crisis bar,
   university line, partner chips, footer - separated by hairlines too faint
   to divide anything, so the whole bottom of every page read as one muddy
   block with no order in it.

   Light ground fixes it by taking the colour away from everything that was
   not earning it. Exactly one band stays green: the crisis bar, which is now
   the only saturated thing down here and therefore the first thing seen. The
   university line is a warm tint, the footer proper is the page's own paper,
   and the pine returns as link colour rather than as background.

   Every pairing below is AAA on its own ground: text 18.16:1, secondary
   8.65:1, action 9.21:1 on white; 7.21:1 at worst on the warm tint. */
.site-footer { background: var(--ground); border-top: 1px solid var(--rule);
  color: var(--text); }
/* The 48px of top padding kara-b.css puts on the footer was written when the
   footer began with its own grid. The bands are inside it now, so that
   padding was landing above the university strip and rendering as a bare
   stripe of ground between the crisis bar and everything else. It belongs to
   the grid, so it moves onto the grid - and still applies when the bands are
   switched off, because .wrap is there either way. */
.site-footer { padding-top: 0; }
.site-footer > .wrap { padding-top: 40px; }
.site-footer a { color: var(--action); }
.site-footer a:hover { color: var(--hover); }
.site-footer h2 { color: var(--secondary); }
.site-footer .wordmark { color: var(--action); }
.site-footer .legal { border-top: 1px solid var(--rule); color: var(--secondary); }
.site-footer p, .site-footer li { color: var(--text); }
.site-footer .footer-fineprint { color: var(--secondary); }

/* Quick Exit: readable on both surfaces, and never behind anything.
   ---------------------------------------------------------------------
   This was paper-coloured, chosen when the only thing behind it was the
   dark stage - where a white block is the loudest thing available. But
   the stage is not what it floats over. It floats over the cards, and
   the cards are the same paper, so at most scroll positions it was
   cream on cream: a button that dissolved into the card underneath it,
   directly on top of that card's links.

   Ink fill with a paper ring reads on both grounds - the fill separates
   it from a cream card, the ring separates it from the dark shell - and
   it is fully opaque against either.

   The ring is a box-shadow rather than an outline on purpose: `outline`
   belongs to the focus indicator above, and a decorative ring written
   there would have quietly repainted the keyboard focus ring in the
   wrong colour on the one control that most needs to be findable.

   z-index above the resources drawer (70) and its backdrop (65), not
   below them. Everything else on this site may be covered by a modal.
   This may not: it is how a reader leaves the page in a hurry, and a
   reader who needs it while a drawer happens to be open needs it more
   than most, not less. */
.quick-exit {
  z-index: 80;
  background: var(--text); color: var(--ground);
  border-radius: var(--r-md);
  box-shadow: var(--e-ring) var(--ground), var(--e-stage-high);
  position: fixed;   /* stated again: the buffer below is positioned to it */
}

/* The near-miss buffer.
   ---------------------------------------------------------------------
   A fixed control floats over whatever is scrolled under it - that is
   what fixed means, and no amount of page padding changes it above the
   last screen. The clearance rules above stop content ending underneath
   it; this stops the thing that actually hurts.

   A reader who needs Quick Exit is in a hurry and is aiming at a corner.
   Miss by a few pixels and, before this, the tap went to whatever card
   link happened to be under the control - navigating them deeper into a
   site about child abuse instead of away from it. The buffer is part of
   the anchor, so a miss by up to 12px in any direction still fires Quick
   Exit. It is transparent and it is not a separate control: nothing to
   see, nothing extra in the accessibility tree, and the visible box is
   unchanged.

   12px is the buffer the platforms use for the same problem, and it
   takes the effective target from 152x48 to 176x72. */
.quick-exit::before {
  content: ''; position: absolute; inset: -12px; border-radius: inherit;
}

/* The keyboard route, printed on the control instead of hidden in a tooltip.
   ---------------------------------------------------------------------
   Shown only where the hint is true. A coarse pointer is a touchscreen, and
   a touchscreen has no Escape key to press twice, so telling that reader
   about one is worse than telling them nothing - it is a control that
   appears not to work. The description they get instead is the anchor's
   aria-label, which a screen reader announces on any device.

   Sized and dimmed so it reads as a footnote to "Quick Exit ✕" rather than
   a second thing to press: the whole anchor is still one target, and the
   near-miss buffer above still covers it. */
.quick-exit-key { display: none; }

@media (hover: hover) and (pointer: fine) {
  :root { --exit-hint-w: 52px; }

  .quick-exit-key {
    display: inline;
    margin-left: 8px;
    font-size: var(--t-1);
    opacity: 0.72;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
}
/* Inverts; it does not turn green. This hovered to --action-bright, which is
   the exact colour of the Donate button in the header - so on a site about
   child abuse the escape control and the donation ask shared a state. Quick
   Exit is not a brand moment and must never be mistaken for one. Inverting
   ink and ground is an unmistakable change that belongs to no other control
   on the page. 17.05:1 either way round. */
.quick-exit:hover {
  background: var(--ground); color: var(--text);
  box-shadow: var(--e-ring) var(--text), var(--e-stage-high);
}

/* The article's sticky mobile read bar is chrome, so it joins the
   stage - a white strip floating over dark cards read as a glitch. The
   progress underline flips to the bright pine for the same reason the
   links did. */
@media (max-width: 1000px) {
  .readbar {
    background: var(--shell-raised); color: var(--shell-secondary);
    border-bottom-color: var(--shell-rule);
  }
  .readbar::after { background: var(--action-bright); }
  .readbar a { color: var(--action-bright); }
}

/* The previewbar and skip link sit on the stage too. */
.previewbar { background: var(--shell-raised); color: var(--shell-text);
  border-bottom: 1px solid var(--shell-rule); }
.previewbar a { color: var(--action-bright); }
.skip { background: var(--ground); color: var(--text); }

/* =====================================================================
   The stage learns to move.
   =====================================================================

   Motion, added on one thesis: the material is light and paper. The
   dark stage already says depth comes from light; these rules let the
   paper move through that light, and nothing else moves. What that
   buys, concretely:

   - one authored arrival: the first paper card rises into the key
     light when a page opens;
   - continuity between pages: the stage holds still while the paper
     changes (cross-document view transitions);
   - a reading-progress bar on the article readbar that is real - it
     was a static 38% strip pretending;
   - cards in lists rising into the light as the reader scrolls to
     them (scroll-driven, no observer, no script);
   - paper that presses down under a finger, because on a phone there
     is no hover to lift it.

   Every one of these is CSS. Nothing here runs a script, nothing
   blocks a browser that lacks the feature (they get the still page
   they had yesterday), and everything respects reduced motion - the
   two scroll-driven pieces need their own guards, written below,
   because the global duration-zeroing guard cannot reach a
   timeline-driven animation or a navigation snapshot. */

/* --- Arrival. -------------------------------------------------------
   The one entrance that is authored rather than reactive: the first
   card rises 14px into the key light while the page fades in. 500ms
   at a hard ease-out means it is essentially settled by 250ms - a
   reader chaining blog pages is never waiting on theater. Guarded
   by no-preference (belt) on top of the global guard (suspenders). */
@media (prefers-reduced-motion: no-preference) {
  main .section:first-child > .wrap {
    animation: paper-arrives .5s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  }
}
@keyframes paper-arrives {
  from { opacity: 0; transform: translateY(14px); }
}

/* --- Continuity between pages. --------------------------------------
   Cross-document view transitions: pure CSS opt-in, and the reason it
   fits this site is that the stage is identical on every page - so a
   navigation reads as the paper changing on a stage that holds still.
   Chromium and Safari 18.2+ animate; Firefox ignores the at-rule and
   navigates the way it always did. The old page exits faster than the
   new one enters, per the house timing rule.

   The global reduced-motion guard cannot see the transition's
   pseudo-elements (the universal selector matches no pseudos), so the
   opt-out is written at the same level as the opt-in. */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}
::view-transition-old(root) { animation-duration: .16s; }
::view-transition-new(root) { animation-duration: .22s; }

/* Two pieces of chrome persist through the swap instead of fading with
   the page. Quick Exit first: a safety control must never appear to
   blink out, even for 160ms. The desktop header is sticky and
   identical everywhere, so it holds still too; the mobile header
   scrolls away with the page and is deliberately unnamed. */
.quick-exit { view-transition-name: quick-exit; }
@media (min-width: 1001px) {
  .site-header { view-transition-name: site-header; }
}

/* --- Reading progress, made honest. ----------------------------------
   The readbar's pine underline was a fixed 38% strip - decoration
   shaped like an instrument. Where scroll-driven animations exist, it
   becomes the instrument: scaleX maps 1:1 onto how far down the page
   the reader is. This stays on under reduced motion on purpose - it
   only moves as much and as fast as the reader's own scroll, the same
   contract as the scrollbar - and the duration-zeroing guard is
   irrelevant to it, because a timeline-driven animation has no
   duration. Browsers without the feature keep the static strip. */
@supports (animation-timeline: scroll()) {
  @media (max-width: 1000px) {
    .readbar::after {
      width: 100%;
      transform: scaleX(0);
      transform-origin: 0 50%;
      animation: reading-progress linear both;
      animation-timeline: scroll(root block);
    }
  }
}
@keyframes reading-progress {
  to { transform: scaleX(1); }
}

/* --- Cards rise into the light. --------------------------------------
   List cards - latest reporting, hub cards, listing results, the
   partner wall - arrive 14px low and slightly dim, and settle as they
   enter the viewport. view() gives each card its own timeline, so a
   column staggers itself by position with no delay arithmetic, and a
   card already on screen at load renders settled - there is no
   page-load choreography to sit through.

   The explicit reduced-motion guard matters here: the global guard
   zeroes durations, but a timeline-driven animation ignores duration,
   so it must be switched off by name. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* .reveal opts out: those cards are driven by assets/js/reveal.js
       instead, and a view()-timeline animation here would fight it for
       the same opacity and transform. Everything without a script of
       its own still arrives this way. */
    .grid .card:not(.reveal), .results .result, .partner, .rec-card,
    .timeline > li {
      animation: rise-into-light linear both;
      animation-timeline: view();
      animation-range: entry 5% entry 60%;
    }
  }
}
/* .82, not .5. A scroll-timeline animation holds its start state for as long
   as the element is in the entry range, so "fading in" meant text sat at half
   opacity the whole time it was in the lower half of the viewport - which is
   exactly where a reader reads. Measured: 404 text nodes below AA while
   scrolling, worst 2.22:1, on text that passes at 8.12:1 once it settles.
   .80 is where the darkest body pairing (#524950 on --ground) clears 4.5:1;
   .82 keeps a margin. The rise is carried by the transform anyway. */
@keyframes rise-into-light {
  from { opacity: .82; transform: translateY(14px); }
}

/* --- Paper under a finger. -------------------------------------------
   On a phone there is no hover, so the lift the pointer gets becomes a
   press the finger gets: the card goes down 1px while touched, the way
   paper does. Buttons and chips press too. The browser's grey tap
   flash is turned off only where a control now answers for itself -
   prose links keep the default, because there it is the feedback. */
@media (hover: none) {
  .results .result:hover, .card:hover {
    transform: none;
    box-shadow: var(--e-1);
    border-color: var(--rule);
  }
}
.card:active, .results .result:active, .partner:active {
  transform: translateY(1px);
  box-shadow: var(--e-1);
}
.btn:active, .btn-donate:active, .search button:active, .chip:active,
.amounts a:active, .menu-toggle:active, .hub-rail a:active {
  transform: translateY(1px);
}
.btn, .btn-donate, .search button, .chip, .amounts a, .menu-toggle,
.card a, .results .result a, .hub-rail a, .quick-exit {
  -webkit-tap-highlight-color: transparent;
}

/* =====================================================================
   The phone, rethought rather than shrunk.
   ===================================================================== */

/* --- The menu is a menu. ---------------------------------------------
   "Menu" used to be a link that jumped to the footer: it answered the
   question but lost the reader's place doing it. It is now a <details>
   drawer - open and close in every browser, JavaScript off, back
   button unharmed. The summary reuses the .menu-toggle dress so the
   control looks exactly as it always has. */
.nav-drawer { display: none; }
.menu-toggle { list-style: none; cursor: pointer; }
.menu-toggle::-webkit-details-marker { display: none; }
.menu-toggle::after {
  content: '▾'; margin-left: 8px; font-size: var(--t-1);
  transition: transform .16s ease;
}
.nav-drawer[open] .menu-toggle::after { transform: rotate(180deg); }
.nav-drawer[open] .menu-toggle {
  color: var(--action-bright); border-color: var(--action-bright);
}

/* --bp-nav, the matching half of the header collapse in kara-b.css. The
   two move together: .nav-drawer is display:none by default and only
   turns block in here, so a breakpoint changed in one file and not the
   other hides the nav and the control that replaces it at once. */
@media (max-width: 1371px) {
  .nav-drawer { display: block; }
  /* Two rows, not three: wordmark, Donate and Menu share the first
     row; search keeps a full-width row of its own below (a real field
     at every breakpoint - that is not negotiable). order moves the
     search after the controls without touching the markup. */
  /* On the flex item, not on the button inside it: the header row is
     what has free space to give, so this is what puts Donate on the
     right of the brand row and keeps the field and Menu together on the
     row below - the same two-row shape at 320px and at 1000px. */
  .header-actions { margin-left: auto; }

  /* The drawer panel is chrome, so it is stage material: raised shell,
     shell text, one hairline per row, 52px targets. Anchored to the
     header's bottom edge so it reads as a sheet the header opened. */
  .site-header { position: relative; }
  .nav-drawer nav {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 55;
    background: var(--shell-raised);
    border-top: 1px solid var(--shell-rule);
    border-radius: 0 0 var(--r-md) var(--r-md);
    box-shadow: var(--e-stage-high);
    padding: 8px 20px 16px;
    display: grid;
  }
  .nav-drawer nav a {
    display: flex; align-items: center; min-height: 52px; padding: 0 4px;
    color: var(--shell-text); text-decoration: none; font-size: var(--t-4);
    border-bottom: 1px solid var(--shell-rule);
  }
  .nav-drawer nav a:last-child { border-bottom: 0; }
  /* The drawer's one action, told apart from the five destinations above
     it by weight and by the bright pine the header gives every action.
     Above the rule, not below it: the destinations are a list and this
     is not a sixth item in that list. */
  .nav-drawer nav .drawer-action {
    font-weight: 700; color: var(--action-bright);
    border-top: 1px solid var(--shell-rule); border-bottom: 0;
    margin-top: 4px;
  }
  .nav-drawer nav .drawer-action::after {
    content: '\2192'; margin-left: auto; font-size: var(--t-3);
  }
  .nav-drawer nav a:hover, .nav-drawer nav a[aria-current] {
    color: var(--action-bright);
  }
  .nav-drawer[open] nav {
    animation: drawer-open .22s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  }
}
@keyframes drawer-open {
  from { opacity: 0; transform: translateY(-6px); }
}

/* --- Card rails. -----------------------------------------------------
   On a phone, the home page's three "latest reporting" cards become
   one swipeable row instead of another 60vh of stack: 82%-wide cards,
   so the next one always peeks in from the edge and announces itself.
   scroll-snap settles a swipe on a card boundary; proximity, not
   mandatory, so a slow drag is never fought. The negative margin lets
   cards run to the paper's edge while staying aligned with the text
   column at rest. Plain overflow scrolling underneath - no script,
   and the thin scrollbar stays visible as a second affordance for
   anyone who does not think to swipe. */
@media (max-width: 640px) {
  .rail-x {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 82%;
    gap: 12px;
    overflow-x: auto;
    margin-inline: -20px;
    padding-inline: 20px;
    padding-bottom: 12px;
    scroll-snap-type: x proximity;
    scroll-padding-inline: 20px;
    scrollbar-width: thin;
  }
  .rail-x > * { scroll-snap-align: start; }
  /* Inside a horizontal scroller, view() would track the wrong axis -
     the rail, not the page - and leave cards half-dim. They opt out
     and stay settled. */
  .rail-x > .card { animation: none; }

  /* The topic-hub rail gets the same treatment, as chips: boxed like
     the donation amounts, one row, swipe to the rest. On desktop it
     stays the quiet wrapped line of links it already was. */
  .hub-rail {
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 8px;
    margin-inline: -20px;
    padding: 4px 20px 12px;
    scroll-snap-type: x proximity;
    scroll-padding-inline: 20px;
    scrollbar-width: thin;
  }
  .hub-rail a {
    flex: 0 0 auto;
    scroll-snap-align: start;
    display: inline-flex; align-items: center;
    min-height: 48px; padding: 0 16px;
    background: var(--ground); border: 1px solid var(--rule);
    border-radius: var(--r-md);
    text-decoration: none; font-weight: 700;
    white-space: nowrap;
  }
}

/* --- The hardware around the screen. ---------------------------------
   viewport-fit=cover (set in base.html) makes the safe-area insets
   real; Quick Exit then clears the home indicator and rounded corners
   instead of sitting under them. max() keeps the old 16px wherever
   the inset is zero. */
.quick-exit {
  right: max(16px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
}

/* --- Type, set by the browser's newer hands. --------------------------
   balance stops a heading from stranding its last word; pretty does
   the same for paragraph rag. Both are rendering hints: a browser
   without them shows the text it always showed. */
h1, h2, h3 { text-wrap: balance; }
.prose p, .lede { text-wrap: pretty; }

/* =====================================================================
   Recovered shapes.
   =====================================================================

   The programme, history and book pages were lifted once from the old
   site's compiled export, and the extraction kept the semantic HTML but
   lost the class names that gave it layout. For a while the shapes were
   recovered here by pointing at whatever the compiler had left behind -
   the aria-label a section happened to keep, and the number of <div>
   levels between it and the thing being styled:

       .page-book .prose section[aria-label="Get the book"]
           > div > div > div > h3

   Thirty-nine rules were written that way, and all thirty-nine were
   time bombs. An aria-label is editorial text: renaming "Get the book"
   to "Buy the book" would have unstyled that page with no error
   anywhere, and nobody would have connected the two changes. A depth
   chain breaks the moment anything wraps a div around part of the
   subtree. And the WordPress port cannot reproduce those depths at all,
   so every one of them was a page that would land unstyled after
   ingestion - the same work, paid for twice.

   The shapes are named at build time now, by structure, once, for every
   page: build_lib.name_components(). This section styles the names. The
   names describe the component rather than the page it first appeared
   on, so two pages showing the same pattern share one rule instead of
   holding two page-scoped copies that drift apart. The build prints
   what it found and complains when a shape stops being found, which is
   the error the old selectors could not raise.

   Nine .page- rules went with them. What is left of .page- here is
   page-level context - a background, a header variant - which is what
   the class is for. */

/* A prose page whose sections carry components rather than paragraphs
   needs the width the components were drawn at; running text inside it
   gets the measure back per element, so a card grid can breathe without
   a paragraph running to 120 characters. Keyed off the components
   themselves, so a page that grows its first grid tomorrow is right
   without anyone remembering to add it to a list. */
.prose:has(.rec-grid, .media-split, .column-list) { max-width: none; }
.prose .sec-body > p,
.prose .sec-body > ul,
.prose .sec-body > ol,
.prose .sec-body > blockquote { max-width: var(--measure); }

/* Sections breathe like sections, not paragraphs: space where one idea
   ends and the next begins, and a rule only where the sections are
   objects rather than continuous argument. */
.prose > .sec + .sec { margin-top: var(--sp-9); }
.prose:has(.rec-grid, .media-split) > .sec + .sec {
  padding-top: var(--sp-8); border-top: 1px solid var(--rule);
}

/* --- The card, everywhere the export left a run of peers. -------------
   /book/, /what-we-do/ and its programme pages, /who-we-are/board/ and
   /who-we-are/partners/ all showed the same object - a heading, a line
   of text, sometimes a marker above and a link below - and each had its
   own copy of this rule keyed to its own aria-label. One shape, one
   rule.

   The grid is intrinsic. It was three fixed tracks with a breakpoint at
   1000px and another at 640px on one page, two fixed tracks with a
   breakpoint at 800px on another; now the track floor is a property the
   variant sets and the browser decides how many fit. That is three
   media queries deleted and a grid that is right at widths nobody
   thought to test. */
.rec-grid {
  display: grid; gap: var(--sp-4); margin-top: var(--sp-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--rec-track, 260px)), 1fr));
}
/* A grid whose cards carry a marker above the name, or an interview
   below it, is a grid of people or of tiers - those cards hold
   paragraphs rather than labels, so the tracks are wider. Asked of the
   cards rather than of the page, so /who-we-are/board/ is not the only
   place that can have one. */
.rec-grid:has(.rec-eyebrow),
.rec-grid:has(.rec-media) { --rec-track: 340px; gap: var(--sp-6); }

/* The section's own heading and standfirst are not cards, so they run
   across the top of the grid rather than taking a track. */
.rec-grid > h2,
.rec-grid > h3,
.rec-grid > p { grid-column: 1 / -1; max-width: var(--measure); }

.rec-card {
  display: flex; flex-direction: column; gap: var(--sp-2); height: 100%;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-md); padding: var(--sp-6);
  box-shadow: var(--e-1);
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.rec-card > h3 { margin: 0; font-size: var(--t-5); }
.rec-card > p { margin: 0; font-size: var(--t-3); color: var(--secondary); }

/* The marker above the name it belongs to: "Technical Director" over
   Allen Huang, "Paperback" over Print copy. Same quiet uppercase eyebrow
   the rest of the site uses, and it does not grow - the description
   does. */
.rec-eyebrow {
  font-size: var(--t-1); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary); flex-grow: 0;
}
/* The description is what stretches, so a short bio and a long one still
   line their links and their interviews up across a row. This is the
   pattern .offer-cta used to reach with an !important; it is a rule
   about the card's own children now, so it does not need one. */
.rec-lede { flex-grow: 1; }
.rec-cta { margin-top: var(--sp-2); font-weight: 700; }
.rec-cta:not(a) { color: var(--secondary); font-size: var(--t-2); font-weight: 400; }
.rec-media { margin-top: var(--sp-2); }

/* Lifted by a pointer, pressed by a finger - the same 160ms and the same
   2px as every other card here, and the global reduced-motion guard
   zeroes it in the same place. */
.rec-card:hover { transform: translateY(-2px); box-shadow: var(--e-2); border-color: var(--border); }
.rec-card:active { transform: translateY(1px); }
@media (hover: none) {
  .rec-card:hover { transform: none; box-shadow: var(--e-1); border-color: var(--rule); }
}

/* --- Prose beside a picture or a film. --------------------------------
   /book/ sets a cover beside the book's introduction, /who-we-are/
   founder/ does the same with the jacket and a caption, /who-we-are/
   runs the Don Shelby film beside what it is, and /what-we-do/podcast/
   runs the Spotify player beside where else to listen. Four pages, one
   pattern, one rule - it used to be three, each keyed to its own page
   and its own aria-label, and the podcast had none so it stacked.

   Wrap rather than a breakpoint: the media half declares the width it
   needs and the prose half takes the rest until there is not enough,
   at which point they stack. No query, and it is right on a 480px
   phone and a 1600px monitor alike. */
.media-split {
  display: flex; flex-wrap: wrap; gap: var(--sp-6);
  align-items: flex-start; margin-top: var(--sp-2);
}
.media-split-media { flex: 0 1 var(--media-split-w, 240px); min-width: 0; }
.media-split-body { flex: 1 1 22rem; min-width: 0; }
.media-split-body > :first-child { margin-top: 0; }
.media-split-media > img,
.media-split-media img {
  max-width: 100%; height: auto;
  border-radius: var(--r-md); box-shadow: var(--e-2);
}

/* A cover with a caption under it: the media column is the width of the
   artwork and the caption is centred on it. The variant is the caption -
   a media half carrying text as well as a picture - so it is asked for
   by shape rather than scoped to the one page that has one today. The
   image comes from build_lib.PLACEHOLDER_ART; /who-we-are/founder/ spent
   its life so far saying "Cover image pending" instead. */
.media-split-media:has(img):has(p) {
  --media-split-w: 139px;
  display: flex; flex-direction: column; gap: var(--sp-2); text-align: center;
}
.media-split:has(> .media-split-media:has(img):has(p)) { gap: var(--sp-7); }
.media-split-media:has(img):has(p) > img { order: -1; }
.media-split-media:has(img):has(p) > p {
  margin: 0; font-size: var(--t-2); line-height: var(--lh-snug); color: var(--secondary);
}
.media-split-media:has(img):has(p) > p:first-of-type { font-weight: 700; color: var(--text); }

/* A film is 16:9 and wants room, so its half of the split is the larger
   one and it keeps its aspect rather than being a thumbnail. */
.media-split-media:has(.video, .embed-facade, iframe) { flex: 1 1 26rem; }
.media-split .video,
.media-split .embed-facade { width: 100%; }

/* --- A long list of short peers. ---------------------------------------
   Fifty institutions on /who-we-are/partners/, each one two or three
   words, set as a single column that ran the better part of a phone
   screen and a third of a desktop one. They are peers and they are
   short, so they read as columns: the reader scans a block instead of
   scrolling a rope. Column count comes from the width, so this is one
   rule and no breakpoints, and break-inside keeps a wrapped name
   whole. Named by shape - ten or more short plain items - so the next
   list like it is right without a rule of its own. */
.column-list { columns: 260px 3; column-gap: var(--sp-7); margin-left: 1.3em; }
.column-list li { break-inside: avoid; }

/* --- The timeline. ----------------------------------------------------
   An <ol> of year-plus-event entries that rendered as "1. 1994" - the
   list numbering fighting the years it numbered. It becomes a rail:
   one line, a dot per year, the year set in display type. The rail
   fades out at the bottom because the record is still being written.

   Named by its entries, not by the page it is on: any ordered list whose
   items each open with a year is one of these. /who-we-are/history/ was
   the only page that could have a timeline while the selector said so,
   and renaming that page would have flattened it back into a numbered
   list. */
.timeline {
  list-style: none; margin: var(--sp-7) 0 0; padding: 0; position: relative;
}
.timeline::before {
  content: ''; position: absolute; left: 8px; top: 8px; bottom: 8px;
  width: 2px;
  background: linear-gradient(180deg, var(--action) 82%, transparent);
}
.timeline > li {
  position: relative; margin: 0; padding: 0 0 var(--sp-7) var(--sp-8);
}
.timeline > li:last-child { padding-bottom: var(--sp-2); }
.timeline > li::before {
  content: ''; position: absolute; left: 1px; top: 5px;
  width: 10px; height: 10px; border-radius: var(--r-circle);
  background: var(--ground); border: 3px solid var(--action);
}
.timeline > li > p:first-child {
  font-family: var(--display); font-weight: 700; font-size: var(--t-6);
  line-height: var(--lh-tight); color: var(--action); margin: 0 0 6px;
}
.timeline > li > p { margin: 0; max-width: 56ch; }

/* --- The kicker (any page whose export carried one). -------------------
   The eyebrow above the title: "The Record · MN" on a state page, the
   byline and date on a blog post. Same quiet uppercase marker the rest
   of the site uses for eyebrows, so 97 pages stop opening with a stray
   sentence fragment between the standfirst and the first paragraph.
   Lifted out of the prose by build_lib.lift_kicker(). */
.kicker {
  font-size: var(--t-1); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary);
  margin: 0 0 var(--sp-3);
}

/* The closing link rows rendered as one run-on word -
   "Listen freeDonateAbout the founder" on /book/, "DonateVolunteer" on
   /who-we-are/partners/, "DonateAbout the book" on /book/listen/ -
   because the anchors are inline and the whitespace between them never
   survived compilation. Two links reading as one word is two
   destinations a reader can neither tell apart nor aim at.

   They become the buttons they were: the first is the filled primary,
   the rest are quiet. The hook is build_lib.mark_link_rows(), which
   names the shape (a div of nothing but plain-text anchors) rather
   than the page - this was scoped to .page-book by the aria-label its
   section happened to keep, and the pages whose sections kept no label
   stayed broken. */
.prose .link-row {
  display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px;
}
.prose .link-row > a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 0 24px; border-radius: var(--r-md);
  font-weight: 700; text-decoration: none;
  border: 2px solid var(--action); color: var(--action);
}
.prose .link-row > a:hover {
  background: var(--action); color: var(--on-action);
}
.prose .link-row > a:first-child {
  background: var(--action); color: var(--on-action); border-color: var(--action);
}
.prose .link-row > a:first-child:hover {
  background: var(--hover); border-color: var(--hover);
}

/* The same damage in its other shape: anchors that carry structure, so
   they were cards. On /states/ this ran all fifty-two together -
   "Alabama42 articlesAlaska29 articles..." - as one wall of prose where
   the reader is meant to pick their own state, under a map that already
   works. Equal tracks, auto-fit, like every other grid of peers here. */
.prose .link-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px; margin-top: 24px;
}
.prose .link-grid > a {
  display: flex; flex-direction: column; gap: 4px;
  min-height: 48px; padding: 16px;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-md); text-decoration: none; color: var(--text);
  box-shadow: var(--e-1);
}
/* Whatever leads the card is the thing that has to look like a link -
   a state's name where the card is a name and a count, the heading
   where it is a heading and a description. Colour is not a link
   affordance for this audience, so it is underlined, as everywhere. */
.prose .link-grid > a > :first-child {
  margin: 0; font-weight: 700; color: var(--action);
  text-decoration: underline; text-underline-offset: 3px;
}
.prose .link-grid > a > h3:first-child { font-size: var(--t-5); }
.prose .link-grid > a > p {
  margin: 0; font-size: var(--t-3); color: var(--secondary); flex-grow: 1;
}
.prose .link-grid > a > span + span,
.prose .link-grid > a > p + span {
  font-size: var(--t-3); color: var(--secondary);
}
.prose .link-grid > a:hover { border-color: var(--border); }
.prose .link-grid > a:hover > :first-child { color: var(--hover); }

/* =====================================================================
   The screening room (/who-we-are/ and its family).
   =====================================================================

   The one part of the site that tells KARA's own story, and it is
   built around a film - Don Shelby's "Who will speak for them?". So
   this family gets the camera's language, three moves and no more:

   1. Titles enter like film titles: tracked wide, settling as they
      fade in. Family-wide, one element, once per page.
   2. Scenes come into focus: the chapter cards and the film block
      arrive soft and sharpen as they enter - a rack focus, driven by
      the reader's own scroll, not a timer.
   3. The film gets a slow push-in: its poster scales from 1 to 1.06
      across its pass through the viewport. A dolly, again tied 1:1
      to scroll.

   Every move is guarded twice - @supports for the timeline features,
   no-preference for the reader's motion setting - and the words stay
   ink on paper throughout. The camera moves; the text never does. */

/* 1 · Title card. The h1 settles from tracked-out letters while the
   standfirst follows a beat behind - inside the paper card that is
   itself arriving, so the whole opening reads as one staged cut. */
@media (prefers-reduced-motion: no-preference) {
  body[class^="page-who-we-are"] main h1 {
    animation: title-card .8s cubic-bezier(0.16, 1, 0.3, 1) backwards;
  }
  body[class^="page-who-we-are"] main h1 + .lede {
    animation: title-fade .6s cubic-bezier(0.16, 1, 0.3, 1) .15s backwards;
  }
}
@keyframes title-card {
  from { opacity: 0; letter-spacing: .06em; }
}
@keyframes title-fade {
  from { opacity: 0; transform: translateY(8px); }
}

/* 2 · Chapters - retired. The corpus's four chapter cards (people,
   partners, history, founder) were the section's map, and the Ways in
   card at the top of the page is that map now, in the same shape every
   L1 page uses. The corpus section is dropped at render (see the
   /who-we-are/ correction in build-site.py); these rules went with it
   so the nth-of-type key could not land on the film section that
   follows. */

/* 3 · The film. Its introduction sits beside it on a desktop, above
   it on a phone, and the facade recovers the presentation the corpus
   lost: 16:9, the film's own poster behind the play control, a quiet
   vignette grading the edges, and the deep shadow of the site's
   highest paper. Nothing loads from YouTube until play is pressed -
   the poster is served from this site like every other image. */
/* The film runs beside what it is, and it is the larger half: a 16:9
   frame set against a column of prose. .media-split does the wrapping, so the
   900px query this used to carry is gone - the two halves stack when
   there is no longer room for both, whatever width that turns out to
   be. */
.media-split:has(.embed-facade) { gap: var(--sp-8); align-items: center; }
.media-split:has(.embed-facade) > .media-split-media { flex: 1.2 1 26rem; }
.media-split:has(.embed-facade) > .media-split-body { flex: .8 1 20rem; }
.media-split-body > h2:first-child { margin-top: 0; }

/* The frame itself: the brightest object on the page, lit from behind
   and sitting on the highest paper. Nothing loads from YouTube until
   play is pressed - the poster is served from this site like every
   other image. The poster is this one film's artwork, so it is the one
   thing here that is still keyed to the page it belongs to. */
.media-split .embed-facade {
  aspect-ratio: 16 / 9; min-height: 0; border: 0;
  position: relative; isolation: isolate;
  background: var(--text);
  box-shadow: var(--e-stage-high);
}
.media-split .embed-facade::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: radial-gradient(120% 90% at 50% 42%, transparent 52%, rgba(10, 10, 14, .5));
}
.page-who-we-are .media-split .embed-facade::before {
  background:
    radial-gradient(120% 90% at 50% 42%, transparent 52%, rgba(10, 10, 14, .5)),
    url('/branding/who-will-speak-poster.jpg') center / cover no-repeat;
}
.media-split .embed-facade .video-poster { display: block; height: 100%; min-height: 0; }
.media-split .embed-facade .video-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
}

/* The camera moves, scroll-driven and guarded like everything else:
   scenes rack into focus on entry, and the film's poster takes a slow
   dolly push across its whole pass through the viewport. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .media-split:has(.embed-facade) {
      animation: rack-focus linear both;
      animation-timeline: view();
      animation-range: entry 5% entry 65%;
    }
    .page-who-we-are .media-split .embed-facade::before {
      animation: dolly-push linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes rack-focus {
  from { opacity: .35; transform: translateY(10px) scale(.98); filter: blur(8px); }
  60% { filter: blur(1px); }
  to { filter: blur(0); }
}
@keyframes dolly-push {
  from { transform: scale(1); }
  to { transform: scale(1.06); }
}

/* --- The reel. --------------------------------------------------------
   The story itself, run across the screen instead of clicked into: a
   full-bleed strip of the dark stage cuts through the paper card, and
   every entry of the history timeline rides it as a lit frame. The
   frames ARE the site's world made literal - paper in the dark,
   moving through the light - and the sprocket rows along the edges
   are the one piece of decoration, because they are what says "film"
   at a glance.

   The band breaks out of the paper column with the 50%-50vw margin
   trick; html/body overflow-x:clip (set far above for the resources
   drawer) absorbs the scrollbar-width overhang that trick can leave.
   The strip underneath is plain overflow scrolling: drag, wheel,
   trackpad, keyboard - the scroller is focusable - and a thin visible
   scrollbar. Nothing here runs a script. */
.reel { margin: 48px 0 4px; }
.reel-hint {
  font-size: var(--t-1); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary); margin-bottom: 14px;
}
.reel-scroller {
  width: 100vw; margin-left: calc(50% - 50vw);
  background: var(--shell);
  position: relative;
}
/* Sprocket holes, top and bottom edges of the band. */
.reel-scroller::before, .reel-scroller::after {
  content: ''; position: absolute; left: 0; right: 0; height: 6px;
  background: repeating-linear-gradient(90deg,
    rgba(239, 233, 227, .16) 0 10px, transparent 10px 28px);
  pointer-events: none;
}
.reel-scroller::before { top: 9px; }
.reel-scroller::after { bottom: 9px; }
.reel-scroller:focus-visible { outline: 3px solid var(--action-bright); outline-offset: -3px; }

.reel-strip {
  list-style: none; margin: 0;
  display: flex; gap: 16px; align-items: stretch;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 28px max(20px, calc(50vw - 580px)) 30px;
  scroll-padding-inline: max(20px, calc(50vw - 580px));
  scrollbar-width: thin;
  scrollbar-color: var(--shell-secondary) transparent;
}
.reel-frame {
  flex: 0 0 auto; width: min(78vw, 380px);
  scroll-snap-align: center;
  display: flex; flex-direction: column; gap: 8px;
  background: var(--ground); border-radius: var(--r-md);
  padding: 24px 24px 24px;
  box-shadow: var(--e-stage);
}
.reel-year {
  font-family: var(--display); font-weight: 700; line-height: var(--lh-tight);
  font-size: var(--d-figure); color: var(--action);
  margin: 0;
}
.reel-event { margin: 0; font-size: var(--t-3); line-height: var(--lh-body); color: var(--text); }
.reel-end { justify-content: center; background: var(--surface); }
.reel-end .reel-event { font-weight: 700; font-size: var(--t-4); }

/* The gate. Where the browser can drive animation from the strip's own
   inline scroll, each frame brightens and straightens as it crosses
   the viewport and dims again as it leaves - the frame in the gate is
   the lit one. Reduced motion or no support: every frame simply full. */
@supports (animation-timeline: view(inline)) {
  @media (prefers-reduced-motion: no-preference) {
    .reel-frame {
      animation: through-the-gate linear both;
      animation-timeline: view(inline);
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes through-the-gate {
  from { opacity: .45; transform: scale(.94); }
  35% { opacity: 1; transform: scale(1); }
  65% { opacity: 1; transform: scale(1); }
  to { opacity: .45; transform: scale(.94); }
}

/* =====================================================================
   Cards, everywhere a list of choices used to be.
   =====================================================================

   Three shapes join the system here, and they are deliberately one
   object with three dresses rather than three designs:

     .link-card    a destination plus the line that says what is behind
                   it - the book's four ways in, the international
                   articles. Was a stacked <ul> with a hairline between
                   each row, which ran the height of a 66ch measure and
                   left the right two-thirds of the paper empty.
     .role-card    a collapsed <details> per toolkit group. Eight of
                   them carry all 45 roles.
     .project-card one of the six programmes.

   They share the accent bar, the radius, the border and the motion, so
   a reader who has learned one has learned all three.

   On the dark stage depth cannot come from drop shadows - a shadow on
   a near-black ground is invisible, which is why the stage already
   builds its depth from light. So these get their weight from a
   lighter surface fill and a hairline, and the shadow is reserved for
   the hover lift, where it has the paper of the section card behind it
   to fall on. */

.link-cards {
  display: grid; gap: var(--gap, 16px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, max(var(--track, 240px),
    (100% - (var(--cols, 3) - 1) * var(--gap, 16px)) / var(--cols, 3))), 1fr));
}
.link-cards-2 { --cols: 2; --track: 260px; }
.link-cards-3 { --cols: 3; --track: 210px; }

/* The accent bar is a border, not a pseudo-element: it cannot be
   knocked out of alignment by padding, and it survives a forced-colours
   mode where backgrounds are discarded. 4px radius here rather than the
   10px the inner cards use - these are the tight, bordered treatment,
   and the difference is what separates "a destination" from "a panel". */
.link-card {
  display: block; text-decoration: none; color: var(--text);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: 16px 16px;
}
.link-card-title {
  display: block; font-family: var(--display); font-weight: 700;
  font-size: var(--t-4); line-height: var(--lh-head); color: var(--action);
  text-decoration: underline; text-underline-offset: 3px;
}
.link-card-note {
  display: block; margin-top: 8px;
  font-size: var(--t-3); line-height: var(--lh-body); color: var(--secondary);
}
.link-card:hover { border-color: var(--border); }
.link-card:hover .link-card-title { color: var(--hover); }

/* On the tinted sections the card ground and the paper are the same
   white, so the fill stops doing the separating and the border has to.
   The warmer paper is the one that reads as "lifted" there. */
.section-tint .link-card, .section-tint .role-card, .section-tint .project-card {
  background: var(--ground);
}

/* --- The role matrix. ------------------------------------------------
   Bark, not pine: the toolkit system owns that colour everywhere else
   on the site and this is the toolkit system arriving on the home page.

   A <details> in a grid grows its own row when it opens, which is the
   behaviour worth having - the seven other groups stay exactly where
   the reader left them. */
.role-card {
  border: 1px solid color-mix(in srgb, var(--bark) 42%, var(--rule));
  border-radius: var(--r-md);
  padding: 0;
}
/* Two rows inside the summary: the group name and its chevron, then the
   status line under both. It has to be allowed to wrap - without that the
   status is a 100%-basis item on a single-line flex row, which squeezes the
   name into a fifth of the card and pushes "7 roles · in development" out
   through the right border. */
.role-card > summary {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  padding: 16px 16px; cursor: pointer;
  list-style: none;
}
.role-card > summary::-webkit-details-marker { display: none; }
.role-group-name {
  font-family: var(--display); font-weight: 700; font-size: var(--t-3);
  line-height: var(--lh-head); color: var(--bark); flex: 1 1 0; min-width: 0;
}
.role-status {
  display: block; font-size: var(--t-2); color: var(--secondary);
  flex: 0 0 100%; order: 3;
}
/* The chevron. Drawn as a rotated border corner so it needs no font,
   no icon file and no character that a screen reader would read out. */
.role-card > summary::after {
  content: ''; flex: 0 0 auto; margin-top: 4px;
  width: 9px; height: 9px;
  border-right: 2px solid var(--bark); border-bottom: 2px solid var(--bark);
  transform: rotate(45deg);
  transition: transform .2s ease-out;
}
.role-card[open] > summary::after { transform: rotate(-135deg); }
.role-card[open] > summary { padding-bottom: 8px; }
.role-card:hover { border-color: var(--bark); }

/* A closed card should not be stretched to the height of an open one
   beside it - seven roles of white space under "Health and mental
   health" reads as a card that failed to load. */
.role-grid { align-items: start; }

.role-list { list-style: none; margin: 0; padding: 0 16px; }
.role-list li {
  padding: 7px 0; border-top: 1px solid var(--rule);
  font-size: var(--t-3); line-height: var(--lh-snug);
}
.role-list a { font-weight: 700; }
/* Inline with the role it belongs to, not pushed to the far edge: in a
   270px column `margin-left: auto` threw the flag onto a line of its
   own under every wrapped name. */
.role-flag {
  font-size: var(--t-1); font-weight: 700; color: var(--bark);
  letter-spacing: .04em; text-transform: uppercase; white-space: nowrap;
}
.role-more { padding: 8px 16px 16px; font-size: var(--t-2); }

/* "Ready to read now" - the three toolkits that are actually written,
   on one line with the heading so it reads as a caption to the matrix
   rather than a section of its own. */
.ready-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px;
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--rule);
}

/* --- The six projects. ----------------------------------------------
   A whole card is the link, so the target is the card and not six words
   inside it. The arrow is the affordance; the heading carries the
   underline, because a card-sized link with nothing underlined in it
   does not read as a link to this audience. */
.project-card {
  display: flex; flex-direction: column; gap: 8px;
  text-decoration: none; color: var(--text);
  border-radius: var(--r-md);
  padding: 16px 20px;
}
.project-card h3 {
  font-size: var(--t-5); line-height: var(--lh-head); color: var(--action);
  text-decoration: underline; text-underline-offset: 3px;
}
.project-card p { font-size: var(--t-3); line-height: var(--lh-body); color: var(--secondary); margin: 0; }
.project-card .project-more {
  margin-top: auto; padding-top: 8px;
  font-size: var(--t-2); font-weight: 700; color: var(--action);
}
.project-card:hover { border-color: var(--action); }
.project-card:hover h3, .project-card:hover .project-more { color: var(--hover); }

/* --- The eight role-group cards. -------------------------------------
   The same frame as .project-card, but a <div> rather than an anchor,
   because each one now holds two links (the toolkits, and the writing)
   and an anchor cannot contain an anchor.

   What the anchor gave for free and this has to rebuild: pressing
   anywhere on the card. .group-open stretches over the whole card with a
   pseudo-element, so the card body is still one big target for the
   toolkit link; the reading link sits above it on the z-axis so it stays
   separately clickable. Both are real links either way - with the
   stretch removed, the card is still two working destinations. */
.group-card {
  display: flex; flex-direction: column; gap: 8px;
  color: var(--text);
  border-radius: var(--r-md);
  padding: 16px 20px;
  position: relative;
}
.group-card h3 { font-size: var(--t-5); line-height: var(--lh-head); }
.group-card h3 a {
  color: var(--action); text-decoration: underline; text-underline-offset: 3px;
}
.group-card .hub-meta { margin: 0; }
.group-card:hover { border-color: var(--action); }
.group-card:hover h3 a { color: var(--hover); }

.group-open::after {
  content: ""; position: absolute; inset: 0; border-radius: var(--r-md);
}

/* The role names. Smaller and quieter than the card's own prose: this is
   the line somebody scans for their own job title, not something they
   read through. It wraps to as many lines as it needs - the groups run
   from three roles to seven, and clamping the long ones would hide
   exactly the role a reader is looking for. */
.group-roles {
  font-size: var(--t-2); line-height: var(--lh-snug); color: var(--secondary); margin: 0;
}

/* The two destinations, stacked so neither reads as the other's caption.
   position:relative lifts them above .group-open's stretched hit area,
   which would otherwise swallow the reading link.

   Each one is a 48px target with 8px between them, which is what the rest
   of this stylesheet gives every control (.btn, .chip, the search field)
   and what a page built for a non-technical, older audience owes a reader
   on a phone. Stacked 15px links at their natural 26px height, 4px apart,
   were two thumb-sized mistakes waiting to happen - and Lighthouse scored
   the home page down for exactly that. */
.group-links {
  display: flex; flex-direction: column; gap: 8px;
  margin: 0; margin-top: auto; padding-top: 12px;
  font-size: var(--t-2); font-weight: 700;
  position: relative;
}
.group-links a {
  display: flex; align-items: center; min-height: 48px;
}

/* --- Since 1994: the record, then the two films it is told in. -------
   The record used to sit in a column beside a single film. With a second
   film there is no side to put it on that keeps the two equal, so the
   paragraph runs above at reading measure and the films run under it as
   two columns of the same width - neither one framing the other. One
   column on a phone, in source order. */
.story-lede { max-width: 66ch; }
.story-films {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px;
}
.story-films figure { margin: 0; }
.story-films figcaption { margin-top: 8px; }
@media (max-width: 1000px) {
  .story-films { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* Get involved: two cards and the newsletter as one row of three. The
   newsletter form has to stop behaving like the wide standalone block
   it used to be - inside a third of the row, a flex-1 240px input and
   a button will not share a line, so they stack and the field keeps
   its full width rather than collapsing to nothing. */
.involve-grid { align-items: stretch; }
.involve-grid > .news, .involve-grid > .card {
  max-width: none; padding: 16px 20px; margin: 0;
  display: flex; flex-direction: column; gap: 8px;
}
.involve-grid > .news form { margin-top: auto; padding-top: 8px; display: grid; gap: 8px; }
.involve-grid > .news input {
  flex: none; width: 100%; border-right: 1px solid var(--border);
}
.involve-grid > .card p:last-child { margin-top: auto; padding-top: 8px; }

/* Home page grid rhythm. 24px between cards is right when a grid is the
   only thing in a section; this page now runs six of them, and at that
   density the gaps add a card's worth of height on their own. 18px still
   separates cleanly against the paper. */
.page-home .grid, .page-home .link-cards { gap: 16px; }

/* =====================================================================
   Motion on the card grids.
   =====================================================================

   Two behaviours, both on the same easing and both switched off by the
   reader's own setting:

   1. hover lifts the card 3px and scales it 1.5%, with the shadow
      growing underneath. Scale is small on purpose - a card that grows
      more than that shoves its neighbours' apparent position around
      and the grid reads as unstable.
   2. entry reveals each card from 16px low and transparent over 400ms
      on a hard ease-out, staggered along the grid.

   Both are written as explicit transform/box-shadow transitions, and
   scripts/check-design.py fails the build on a `transition` shorthand
   that says `all`. That shorthand would also animate the border-colour
   changes above - and, worse, anything a later edit adds - which is how
   a hover meant to lift a card ends up animating its layout. */

.link-card, .role-card, .project-card {
  transition: transform .2s ease-out, box-shadow .2s ease-out,
              border-color .2s ease-out;
}
.link-card:hover, .project-card:hover, .role-card:not([open]):hover {
  transform: translateY(-3px) scale(1.015);
  box-shadow: var(--e-2);
}
/* The existing inner-card lift gains the same scale, so the latest
   reporting row and the Get involved trio move like everything else. */
.results .result:hover, .card:not(.role-card):hover {
  transform: translateY(-3px) scale(1.015);
}
/* An open disclosure is a panel being read, not a target being chosen.
   Lifting it under the pointer would move the text the reader is in. */
.role-card[open]:hover { transform: none; }

/* Focus has to do everything hover does, or a keyboard reaches the same
   cards and is told nothing. */
.link-card:focus-visible, .project-card:focus-visible {
  transform: translateY(-3px) scale(1.015);
}

/* --- Scroll reveal. --------------------------------------------------
   Read the top of assets/js/reveal.js before editing any of this.

   THE HIDDEN STATE IS NOT IN THIS FILE, AND MUST NOT BE PUT IN IT.
   `.reveal` is an ordinary, fully visible card. It is hidden only while
   it carries `is-armed`, and only assets/js/reveal.js ever adds that
   class - which it does one card at a time, after it has already
   established that it can reveal them again.

   That inversion is the whole design. Hiding cards in the stylesheet
   and handing responsibility for showing them to a script makes every
   line of that script load-bearing: a parse error, a blocked file, an
   observer that never fires, and a section of the home page is blank
   with no way back. Hiding them from the script instead means the
   worst case is a page that does not animate.

   There is deliberately no prefers-reduced-motion wrapper here either.
   The script returns before arming anything when the reader has asked
   for reduced motion, so under that setting `is-armed` is never on the
   page and there is nothing for a media query to switch off. One
   mechanism, in one place.

   The stagger is a per-card custom property the script sets from the
   card's position in its own grid, so a row of three arrives left to
   right and the next row follows - no per-selector delay arithmetic,
   and a grid that changes length does not need this file edited.

   The arrival is an animation and not a transition, and that is
   load-bearing for a different reason. A transition to the settled
   state has to declare `transform: none` on the settled card, at a
   specificity no plain `.card:hover` can beat - so every card revealed
   correctly and then refused to lift under the pointer for the rest of
   the session. An animation with `backwards` fill owns the transform
   only while it runs: it holds the from-state through the stagger
   delay, plays, and hands the property back with nothing left behind. */
.reveal.is-armed { opacity: 0; }
.reveal.is-revealed {
  animation: rise-into-place .4s cubic-bezier(0.16, 1, 0.3, 1)
             var(--reveal-delay, 0ms) backwards;
}
@keyframes rise-into-place {
  from { opacity: 0; transform: translateY(16px); }
}

/* Touch: no hover, so the lift becomes the press the finger gets, the
   same way the inner cards already behave. */
@media (hover: none) {
  .link-card:hover, .project-card:hover, .role-card:hover {
    transform: none;
    box-shadow: var(--e-1);
  }
}
.link-card:active, .project-card:active, .role-card > summary:active {
  transform: translateY(1px);
}

/* =====================================================================
   The donate page.
   =====================================================================

   One page, one action, and it used to be laid out as though the action
   were incidental: two text links near the top, one link 1,900px later,
   then 1,565px of trust copy with no way to give at all. What follows is
   four placements of one module, plus a rail that follows the reader
   through the statistics.

   The module reuses .amounts and .btn, which the home page's ask already
   owns, because a reader arriving from that ask should meet the same
   object here rather than a second design for the same decision. */

.give {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: 20px 24px;
}
.section-tint .give { background: var(--ground); }
.give-head {
  font-size: var(--t-5); line-height: var(--lh-head); margin: 0 0 12px;
}
.give .amounts { margin-top: 0; }
.give-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px;
  margin-top: 16px;
}
.give-actions .btn { flex: 1 1 auto; }
.give-once { font-size: var(--t-3); white-space: nowrap; }
.give .trust { margin-top: 12px; }

/* The chip the reader has chosen. .amounts already draws the suggested
   one; this is the same treatment moved by the script when they pick
   another, so the page always shows one selected amount and never two. */
.amounts a[aria-current] {
  border: 2px solid var(--action); color: var(--action); background: var(--surface);
}

/* --- The top of the page. --------------------------------------------
   Headline and module side by side rather than stacked, so the first
   screen carries the ask instead of only introducing it. */
.donate-top { align-items: start; gap: 40px; }
@media (max-width: 1000px) { .donate-top { gap: 24px; } }

/* --- The body: argument on the left, ask on the right. ---------------- */
/* No `align-items: start` here, and that is the whole trick.
   A sticky element travels inside its nearest scrolling ancestor's box,
   which for a grid item is the grid area it was given. `start` shrinks
   the rail column to the height of the module inside it, leaving it
   nothing to travel through - the rail scrolls away with the page and
   looks exactly like a sticky rule that was never applied. The default
   `stretch` gives the column the full height of the row, which is the
   run the module then sticks along. */
.donate-body {
  display: grid; grid-template-columns: minmax(0, 1fr) 300px;
  gap: 48px;
}
.donate-hook {
  font-family: var(--display); font-size: var(--t-6); line-height: var(--lh-snug);
  max-width: 34ch;
}

/* The rail follows the reader down the statistics. `align-items: start`
   on the grid is what gives the column a height to stick inside; without
   it the column stretches and there is nothing to travel through. The
   offset clears the sticky desktop header. */
.donate-sticky { position: sticky; top: 104px; }

@media (max-width: 1000px) {
  /* No rail below the point where the columns stop being comfortable -
     the bottom bar takes over there, and two asks fixed to the same
     screen is one too many. */
  .donate-body { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .donate-rail-col { display: none; }
}

.trust-card h3 { font-size: var(--t-4); margin-bottom: 8px; }
.trust-card p { font-size: var(--t-3); line-height: var(--lh-body); color: var(--secondary); }

/* Give modules inside the body column get air above them without a rule,
   which would read as a section break in the middle of an argument. */
.donate-body .give { margin-top: 24px; }

/* --- The phone's rail. ------------------------------------------------
   Fixed to the bottom, one row. It sits above the page but below Quick
   Exit, which outranks everything (z-index 80) - a reader who needs to
   leave must never have to get past a donation prompt to do it. The
   right inset keeps the button clear of Quick Exit's corner. */
.give-bar { display: none; }
@media (max-width: 1000px) {
  .give-bar[hidden] { display: none; }
  .give-bar {
    display: block; position: fixed; left: 0; right: 0; bottom: 0; z-index: 55;
    background: var(--shell-raised);
    border-top: 1px solid var(--shell-rule);
    box-shadow: var(--e-stage-high);
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
  }
  .give-bar-inner {
    display: flex; align-items: center; gap: 12px;
    max-width: 1200px; margin: 0 auto;
    /* Quick Exit steps above this bar on the donate page (see the
       .page-donate rules near .to-top), so the bar's controls get the
       full width back. */
  }
  .give-bar-label {
    font-size: var(--t-2); line-height: var(--lh-head); color: var(--shell-text);
    flex: 1 1 auto; min-width: 0;
  }
  .give-bar .btn { flex: 0 0 auto; }
  .give-bar .give-once { color: var(--action-bright); font-size: var(--t-2); }
  /* The bar is a second floor to the page, so the page needs to clear it
     as well as Quick Exit. */
  body.page-donate { padding-bottom: calc(var(--exit-clearance) + 76px); }
}
/* Below about 620px the label and both controls will not share a line
   without the button shrinking to nothing. The label goes; the bar is one
   button and one link, which is all it ever needed to be. Asked of the
   bar rather than of the viewport, so it is still right if the bar ever
   stops being full-width. */
.give-bar-inner { container-type: inline-size; }
@container (max-width: 620px) {
  .give-bar-label { display: none; }
}
.give-bar-inner {
  /* off-scale: Quick Exit clearance, plus the keyboard hint when it is
     showing - a narrow window on a laptop draws both this bar and the
     hint, and 148px alone would let the button slide under the control. */
  padding-right: calc(148px + var(--exit-hint-w));
}

/* Trust cards keep their columns even. Long unbroken strings in this
   column - an email address, a URL - would otherwise set the track's
   min-content width and leave one card a third the width of the other. */
.trust-card { min-width: 0; }
.trust-card p { overflow-wrap: anywhere; }

/* =====================================================================
   What We Do: two flagships, and a catalogue behind them.
   ===================================================================== */

/* The feature block. Full width, and deliberately not a card in a grid -
   the whole problem it fixes is that Deep Dive was a card in a grid and
   read as one offering of seven, and KARA Conversations had no block at
   all. The argument on the left, what you actually receive on the right. */
.feature {
  display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr);
  gap: 48px; align-items: start;
  margin-top: 24px; padding-top: 24px;
  border-top: 1px solid var(--rule);
}
/* The rule separates the feature from an intro above it. KARA Conversations
   has its own section with nothing above it, so there is nothing to separate
   from - the rule read as the underside of a heading that was not there, over
   26px of blank paper. */
.feature:only-child { margin-top: 0; padding-top: 0; border-top: 0; }
.feature h2 { font-size: var(--t-8); margin: 0; }
.feature-aside {
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--bark) 42%, var(--rule));
  border-radius: var(--r-md);
  padding: 16px 20px;
}
.section-tint .feature-aside { background: var(--ground); }
.feature-list-plain { list-style: none; margin: 0; padding: 0; }
.feature-list-plain li {
  position: relative; padding: 8px 0 8px 24px;
  font-size: var(--t-3); line-height: var(--lh-body);
  border-top: 1px solid var(--rule);
}
.feature-list-plain li:first-child { border-top: 0; }
/* A check drawn in CSS rather than a character: no icon file, and nothing
   for a screen reader to read out as punctuation before every line. */
.feature-list-plain li::before {
  content: ''; position: absolute; left: 2px; top: 15px;
  width: 5px; height: 9px;
  border-right: 2px solid var(--bark); border-bottom: 2px solid var(--bark);
  transform: rotate(45deg);
}
@media (max-width: 1000px) {
  .feature { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* Offer cards. The call to action sits on the bottom edge of every card in
   a row, so a row scans as a row of choices rather than a ragged column of
   sentences that happen to end in links.

   It used to get there with `margin-top: auto !important` - the auto
   margin lost to `.offer-card p { margin: 0 }` on specificity, and the
   !important was the patch. The description growing is the same result
   from the other end, it needs no !important, and it is the pattern
   .rec-lede already uses on every other card with a bottom CTA. One way
   to align a row of cards, not two. */
.offer-card { display: flex; flex-direction: column; gap: 8px; padding: 16px 20px; }
.offer-card h3 { font-size: var(--t-4); line-height: var(--lh-head); }
.offer-card p { font-size: var(--t-3); line-height: var(--lh-body); color: var(--secondary); margin: 0; }
.offer-card > p:not(.offer-cta) { flex-grow: 1; }
.offer-cta { padding-top: 8px; }
.offer-cta a { font-weight: 700; }
.offer-soon {
  font-size: var(--t-2); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--secondary);
}

/* =====================================================================
   The phone's top of page.
   =====================================================================

   Every page template set its h1 and its standfirst in fixed pixels, and
   set them inline - 44px on the home page, 40px everywhere else, 21px
   for the lede. Inline styles beat every rule in this file, so no media
   query has ever been able to touch them: a 390px phone was rendering
   the same 44px display type as a 1440px desktop, three lines of it
   before the first sentence, and a 21px lede under that.

   Those declarations are gone from the templates now, and the sizes
   live here where a viewport can reach them. clamp() means one rule per
   element rather than a breakpoint each: the middle term is the fluid
   one, and the outer two are the sizes this design was drawn at. */
main h1 {
  font-size: var(--d-title);
  line-height: var(--lh-tight);
}
.page-home main h1 {
  /* The mission line is the one piece of display type on the site that
     is allowed to be louder than the rest. */
  font-size: var(--d-hero);
}
main h1 + .lede,
.media-split .lede:first-of-type {
  font-size: var(--d-lede);
}
/* Section headings were 30px flat and set the same trap one level down. */
.section > .wrap > h2 { font-size: var(--d-h2); }

/* The measure cap on a headline. It was five inline max-width values and
   one rule that reached through the style attribute to cancel them:

       main h1[style*="max-width"] { max-width: none !important; }

   which is a selector matching on how a value was written rather than on
   what the element is - it would have stopped working the day someone
   wrote max-width with a space after the colon. min() does the same job
   with no query, no !important and nothing to match on: 24ch until 24ch
   is wider than the column, then the column. Pages that want a different
   cap set --cap and nothing else. */
.title-cap { max-width: min(var(--cap, 24ch), 100%); }

/* --- The resource tab, off the paper. --------------------------------
   The tab is fixed to the right edge at mid-height. On a desktop it sits
   in the margin outside the 1200px column and overlaps nothing; on a
   390px phone the content card runs from 10px to 380px and the tab sits
   at 362-390, directly on top of the words - measured, not guessed.

   Below the breakpoint where the header already collapses to a drawer,
   the tab is hidden and the resource library is opened from that drawer
   instead, which is where a phone's secondary navigation belongs.
   assets/js/resources-panel.js adds the row; base.html renders the slot
   it goes in, so with JavaScript off there is simply no row rather than
   a control that does nothing. */
@media (max-width: 1000px) {
  #kres-tab { display: none; }
}
.nav-drawer nav .drawer-open {
  display: flex; align-items: center; width: 100%;
  min-height: 52px; padding: 0 4px;
  background: none; border: 0; border-bottom: 1px solid var(--shell-rule);
  font: inherit; font-size: var(--t-4); color: var(--shell-text);
  text-align: left; cursor: pointer;
}
.nav-drawer nav .drawer-open:hover { color: var(--action-bright); }

/* =====================================================================
   A migrated toolkit: the guide, on the page.
   =====================================================================

   Mike's "What You Can Do" posts now render inside the toolkit for their
   role rather than as a card linking to the blog. Three parts: who
   wrote it and when, the action headings as a contents list, then the
   prose. See templates/toolkit.html and data/toolkit-migrated.json. */

.guide-head { border-bottom: 1px solid var(--rule); padding-bottom: 18px; }
.guide-title { font-size: var(--d-guide); line-height: var(--lh-head); margin: 8px 0 0; }

/* The contents list is the useful object on this page: eight to ten action
   headings are the whole shape of the job, and a reader who only has two
   minutes should be able to read just this. Numbered, because the guides
   are written as a sequence. Bark, because this is the toolkit system. */
.guide-toc {
  margin-top: 24px; padding: 16px 20px;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--bark) 42%, var(--rule));
  border-radius: var(--r-md);
}
.section-tint .guide-toc { background: var(--ground); }
.guide-toc ol { margin: 0; padding-left: 1.3em; }
.guide-toc li { margin-bottom: 8px; font-size: var(--t-3); line-height: var(--lh-snug); }
.guide-toc li:last-child { margin-bottom: 0; }
.guide-toc li::marker { color: var(--bark); font-weight: 700; }

/* The guide's own prose. .prose already carries the reading measure and the
   heading rhythm; this adds the scroll offset, so that following a contents
   link does not put the heading under the sticky desktop header. */
.guide-body { margin-top: 32px; }
.guide-body h2 { scroll-margin-top: 104px; }
@media (max-width: 1000px) { .guide-body h2 { scroll-margin-top: 20px; } }

.guide-also {
  margin-top: 32px; padding: 16px 16px;
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--r-md);
}
.section-tint .guide-also { background: var(--ground); }
.guide-also ul { list-style: none; margin: 0; padding: 0; }
.guide-also li { padding: 8px 0; border-top: 1px solid var(--rule); font-size: var(--t-3); }
.guide-also li:first-child { border-top: 0; }
.guide-also .byline { display: block; font-size: var(--t-2); }

.guide-source { margin-top: 20px; }

/* =====================================================================
   The spacing system. A standing rule, not a cleanup.
   =====================================================================

   Every gap, margin and padding on this site comes from one scale:

       4  8  12  16  20  24  32  40  48  64  80  96

   Before this there were nineteen different spacing values in the two
   stylesheets - 5, 6, 7, 10, 14, 18, 22, 26, 28, 34, 44, 56, 72 among
   them - and no way to tell a deliberate rhythm from a number somebody
   liked that afternoon. Now a value off the scale is a build failure:
   scripts/check-design.py reads every gap/margin/padding declaration in
   the built stylesheet and every inline style in templates/, and fails
   on anything that is not on the list. The one escape hatch is an
   `off-scale:` comment on the same line naming the reason - the two
   Quick Exit clearances use it, nothing else should need to.

   The same section carries the grid rule: A GRID WHOSE ITEMS ARE PEERS
   USES EQUAL TRACKS - repeat(n, minmax(0, 1fr)) or auto-fit - so
   content can never widen its own column. The footer's four columns
   rendered at 274.875 / 229.062 / 229.062 / 349px because someone gave
   them fractional weights; they are peers, and now they are equal.
   Weighted tracks are allowed only where the two sides are different
   kinds of object (prose beside a rail, argument beside a video), and
   each such grid should say so in a comment.

   The tokens exist so new CSS can name its step instead of retyping a
   number. Values typed as raw px are fine too - the checker holds them
   to the same scale either way.

   The --sp-* declarations used to live here, a thousand lines below the
   colour tokens and out of sight of anyone reading them. They are in the
   one :root block in assets/kara-b.css now, beside the type, radius and
   elevation scales, because a token file split across two files at
   opposite ends of a build is how a second copy gets written. */

/* =====================================================================
   Back to top.
   =====================================================================

   The migrated toolkits run to twenty thousand pixels, and the only way
   back to the contents list was the scrollbar. A plain anchor to the
   top of the page - works with JavaScript off, at every breakpoint,
   with nothing to initialise.

   Bottom-left, mirroring Quick Exit's bottom-right, and z-indexed well
   under it (40 vs 80): a convenience must never outrank the safety
   control. Where scroll-driven animations exist the button keeps out of
   the way until the reader has actually scrolled - the same
   scroll(root) instrument the reading-progress bar uses, which maps to
   the reader's own movement and nothing else. Browsers without the
   feature simply always show it, which is the honest fallback. */
.to-top {
  position: fixed;
  left: max(16px, env(safe-area-inset-left));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: 40;
  display: inline-flex; align-items: center;
  min-height: 48px; padding: 12px 16px;
  background: var(--action); color: var(--on-action);
  font-size: var(--t-2); font-weight: 700; text-decoration: none;
  border-radius: var(--r-md);
  box-shadow: var(--e-ring) var(--ground), var(--e-stage-high);
}
.to-top:hover { background: var(--hover); color: var(--on-action); }
@supports (animation-timeline: scroll()) {
  .to-top {
    opacity: 0; pointer-events: none;
    animation: to-top-arrives linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 120vh;
  }
}
@keyframes to-top-arrives {
  0%, 60% { opacity: 0; pointer-events: none; }
  100% { opacity: 1; pointer-events: auto; }
}
/* The donate page's phone bar owns the bottom edge there; both floating
   corner controls step above it. Quick Exit especially: the one control
   that must never be covered by, or sit on top of, anything else - at
   bottom:16px it landed on the give bar's own buttons. */
@media (max-width: 1000px) {
  .page-donate .to-top { bottom: 96px; }
  .page-donate .quick-exit {
    bottom: calc(96px + env(safe-area-inset-bottom, 0px)); /* off-scale: give-bar clearance */
  }
}

/* No scroll-behavior: smooth, and that is a decision rather than an
   omission. Global smooth animates every programmatic scroll on the page -
   hash arrival, back-button restoration, find-in-page - not only the anchor
   clicks it was meant for. It made the reveal observers measure mid-flight,
   and a reader arriving at /#key-projects watched the page slide down to it.
   The site's jumps are instant everywhere else; the back-to-top jump is too. */

/* =====================================================================
   Rounded controls (Sam's direction, 2026-08-14).
   =====================================================================

   Every button and chip on the site was a hard rectangle while every
   card around them sits at 8-12px - the controls read as belonging to a
   different, sterner site than the surfaces they sit on. 8px matches
   the radius the widgets (audiobook player, resources drawer) already
   use for their controls; cards keep 10px, so a control still reads as
   a control.

   The two joined input+button pairs round only their outer corners -
   a seam with a radius in it reads as a gap. Where the newsletter pair
   stacks (≤500px, the input takes its own row), each piece gets its
   own full rounding back. */
.btn, .btn-donate, .btn-subscribe, .btn-quiet,
.amounts a, .amounts label {
  border-radius: var(--r-md);
}
.search input { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.search button { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
.news input { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.news form .btn { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
/* When the pair wraps, each piece is a whole control again. The card is
   asked how wide it is rather than the viewport: the newsletter block is
   620px wide in the page and full width in the footer, so the width the
   wrap happens at was never one number, which is how this came to be a
   500px viewport query that was wrong in one of the two places. */
@container (max-width: 26rem) {
  .news input, .news form .btn { border-radius: var(--r-sm); }
  .news input { border-right: 1px solid var(--border); }
}
.news { container-type: inline-size; }

/* =====================================================================
   Site-wide search — /search/, and the suggestion list under the header
   field on every page.

   The site's search field submitted to /blog/?q= for its whole life, which
   a static host cannot answer: the query arrived at the blog front door and
   was ignored, so every search returned the newest fifty articles whatever
   was typed. /search/ is where it goes now, over an index of every route
   the site serves.

   Nothing here is a new design system. Results are the .results cards the
   blog listings already use, one column because a result carries a lede;
   the filters are pine, not bark, because bark belongs to the toolkits and
   these filter the whole site.
   ===================================================================== */

/* The two search fields that sit on paper rather than on the stage. The
   header's field is a fixed 240px pushed to the right of a nav row; a field
   that is the point of the page it is on wants the width it is given. */
.section .search { margin-left: 0; max-width: 100%; }
.section .search input { flex: 1 1 auto; width: auto; min-width: 0; }
.ks-form input { font-size: var(--t-4); }

.ks-filters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.ks-chip {
  font: inherit; font-size: var(--t-3); min-height: 44px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 16px; border-radius: var(--r-sm);
  border: 2px solid var(--border); background: var(--surface); color: var(--text);
}
.ks-chip:hover { border-color: var(--action); color: var(--action); }
.ks-chip.is-on { border-color: var(--action); background: var(--action); color: var(--on-action); }
.ks-chip .ks-n { font-variant-numeric: tabular-nums; font-weight: 700; }
.ks-chip.is-on .ks-n { color: var(--on-action); }

.ks-results { list-style: none; display: grid; gap: 16px; margin: 0; }
.ks-hit { padding: 20px; }
.ks-hit h3 { font-size: var(--t-5); margin-top: 4px; }
.ks-hit h3 a { color: var(--action); }
.ks-kind {
  font-size: var(--t-2); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary);
}
.ks-snip { margin-top: 8px; max-width: var(--measure); }
.ks-meta { margin-top: 8px; }
.ks-empty { padding: 24px 0; max-width: var(--measure); }
.ks-empty p + p { margin-top: 12px; }

/* The matched words. Pale pine on paper: the same family as the action
   colour, and dark text is kept on top of it so a highlight never costs
   contrast. text-decoration comes off because inside a result heading the
   mark would otherwise sit under the link's underline. */
mark {
  background: var(--action-bright); color: var(--text);
  padding: 0 4px; border-radius: var(--r-md); text-decoration: none;
}

/* The header field's suggestion list. Absolutely positioned under the
   field, which means .search has to become a positioning context - but
   only where the script has actually built a list, so the header of a
   page whose scripts never ran is untouched. */
.search.ks-has-suggest { position: relative; }
.ks-suggest {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
  list-style: none; margin: 4px 0 0; padding: 0;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  box-shadow: var(--e-3);
  max-height: 60vh; overflow-y: auto;
}
.ks-sug + .ks-sug { border-top: 1px solid var(--rule); }
.ks-sug a {
  display: block; padding: 12px 16px; font-size: var(--t-3);
  text-decoration: none; color: var(--text);
}
.ks-sug a:hover, .ks-sug[aria-selected="true"] a { background: var(--ground); }
.ks-sug-kind {
  display: block; font-size: var(--t-2); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--secondary);
}
.ks-sug-title { display: block; line-height: var(--lh-snug); }
.ks-sug-all a { font-weight: 700; color: var(--action); text-decoration: underline; }

/* On a phone the header field is full width and the drawer sits under it,
   so the list can be full width too - and it must not push the page. */
@media (max-width: 1000px) {
  .ks-suggest { max-height: 50vh; }
}

/* =====================================================================
   One card, everywhere.

   Four card families had grown up on this site with three different
   shapes. The home page's book and get-involved tiles (.link-card) and
   its project tiles carried a 4px pine bar down their left edge, a 4px
   radius and an underlined pine title. Everything else - the blog's
   result cards, the Ways in tiles on every L1 page, the panels on the
   donate and programme pages, the search results - was a plain hairline
   box with a 10px radius and, on the link tiles, a heading the same
   colour as body copy.

   That was two answers to one question on pages a reader moves between
   in a single visit. The bar wins, because it is the one that says
   "this is a thing you can go into" at a glance and holds up for a
   reader who is not looking closely - which on this site is most of
   them. (Sam's direction, 2026-08-18.)

   The bar is a border rather than a pseudo-element, following
   .link-card: padding cannot knock it out of alignment, and it survives
   a forced-colours mode where backgrounds are discarded.
   ===================================================================== */

/* --- What the accent rule is for. -------------------------------------
   The 4px pine left rule used to be on the hero panel, the cost-of-abuse
   figure, the donation ask, every hub card, every listing row, every
   search result, every project tile and every link card - the same
   weight and the same colour on the page's primary claim and on a
   tertiary card three screens down. An accent that marks everything
   marks nothing: a reader who sees the same bar on both stops reading it
   as emphasis and starts reading it as decoration, which is the state it
   had reached.

   Cover the page and ask what the bar is for. The answer now is one
   thing: **this is an ask for money.** The donation ask, the give module
   on /donate/, the rail, the bequest card. Nowhere else, ever.

   Everything else ranks by surface and edge weight, which is the same
   hierarchy expressed in properties that do not compete with the ask:
   a card that is a destination sits on the brighter paper, a panel sits
   on the ground, and the edge goes from --rule at rest to --border under
   a pointer. Bark still belongs to the toolkits, and says so in the
   border colour rather than in a second bar. */
.accent-rule,
.ask, .give, .donate-rail, .bequest-card {
  border-left: 4px solid var(--action);
}

.card,
.results .result,
.ks-hit {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

/* Bark for the toolkit system. Where a card *leads* decides, not where
   it sits: the eight group tiles under "Find your role" are the toolkit
   system arriving on the home page, and they are bark there for the same
   reason they are bark on /toolkits/. The page scope then covers the
   cards on the toolkit pages that are not links into it. */
a.card[href^="/toolkits"],
body[class^="page-toolkits"] .card,
body[class^="page-toolkits"] .results .result {
  border-color: color-mix(in srgb, var(--bark) 42%, var(--rule));
}
a.card[href^="/toolkits"]:hover,
body[class^="page-toolkits"] .card:hover {
  border-color: var(--bark);
}

/* Under a pointer the edge takes weight. That is the whole hover signal
   on a card now - the bar it used to deepen is not there to deepen. */
.card:hover,
.results .result:hover,
.ks-hit:hover {
  border-color: var(--border);
}

/* On a tinted section the card ground and the paper are the same white,
   so the fill stops doing the separating - the same correction
   .link-card, .role-card and .project-card already carry. */
.section-tint .card,
.section-tint .results .result { background: var(--ground); }

/* A phone has no pointer, so the lift is cancelled there (see "Paper
   under a finger" above) - but the bar is not a hover state and must
   not be cancelled with it. */
@media (hover: none) {
  .card:hover,
  .results .result:hover,
  .ks-hit:hover { border-color: var(--rule); }
  a.card[href^="/toolkits"]:hover,
  body[class^="page-toolkits"] .card:hover {
    border-color: color-mix(in srgb, var(--bark) 42%, var(--rule));
  }
}

/* A card that is a link underlines exactly one thing: its title.

   .link-card, .project-card and the Ways in tiles each said this for
   themselves; the "Start anywhere" cards on /toolkits/ and the offer
   cards on /what-we-do/ did not, so the anchor's default underline ran
   under their body copy and their dates as well - three underlined
   lines in a card with one destination. The Ways in tiles had the
   opposite problem: their heading was drawn in body ink with no
   underline, the one card title on the site that did not look like the
   link it is.

   Display is deliberately not set here: these cards are variously
   block and flex, and their own rules own that. */
/* When the whole card is the target, the heading is not the link
   affordance and does not need to advertise itself. Eleven headings on
   the home page carried a resting underline, and on a two-line Literata
   heading that underline runs under both lines and collides with the
   descenders of g, y and p - the single most reliable visual signal
   that a layout was assembled rather than designed.

   The affordance is the card: the whole thing lifts and its edge takes
   weight under a pointer, the heading takes the action colour, and the
   underline arrives on hover and on focus where it confirms what is
   about to happen. Colour alone is still not the affordance - the card's
   own lift and edge are, and the focus ring is untouched.

   Inline prose links keep their resting underline. That rule is not
   negotiable on this site and nothing here reaches them. */
a.card { text-decoration: none; color: var(--text); }
/* The same argument where the link is inside the heading rather than
   around the card: .group-open stretches a pseudo-element over the
   whole group card, and a listing row lifts and takes an edge under a
   pointer, so in both the card is the affordance and the title does not
   need to advertise itself. On a two-line Literata heading the resting
   underline ran under both lines and through the descenders of g, y and
   p, which is the tell.

   It arrives on hover and on focus, on the card, where it confirms what
   is about to happen. The focus ring is untouched, and an inline link in
   a sentence still carries its underline at rest - that rule does not
   move. */
.card h3 > a,
.results .result h3 > a,
.group-card h3 > a,
.hub-grid h3 > a {
  text-decoration: none; text-underline-offset: 3px;
}
.card:hover h3 > a,
.card:focus-within h3 > a,
.results .result:hover h3 > a,
.results .result:focus-within h3 > a,
.group-card:hover h3 > a,
.group-card:focus-within h3 > a,
.hub-grid .hub:hover h3 > a,
.hub-grid .hub:focus-within h3 > a {
  text-decoration: underline;
}
a.card h3 {
  color: var(--action);
  text-decoration: none; text-underline-offset: 3px;
}
a.card:hover h3, a.card:focus-visible h3 {
  color: var(--hover); text-decoration: underline;
}
a.card p { text-decoration: none; }
a.card[href^="/toolkits"] h3,
a.card[href^="/toolkits"]:hover h3,
body[class^="page-toolkits"] a.card h3,
body[class^="page-toolkits"] a.card:hover h3 { color: var(--bark); }

/* The search results are the newest card family and inherit all of it;
   the snippet keeps the measure so a long lede does not run the width
   of a desktop card. */
.ks-hit .ks-snip { max-width: var(--measure); }

/* The subscribe form's bot trap.
   -------------------------------------------------------------------
   assets/js/subscribe.js has read a "company" field since it shipped and
   silently accepts any submission that fills it, because a person never
   sees the field and a bot fills everything it finds. The field was in the
   markup and the check was in the script, but nothing ever hid it - so the
   trap was showing every reader a "Company" box on /newsletter/ and asking
   them not to fill it in. It caught bots and confused people at the same
   time.

   Positioned off-screen rather than display:none or hidden: a bot that
   skips fields it can tell are unrendered walks straight past the trap,
   which is the one thing it must not do. aria-hidden on the container
   already keeps it out of the accessibility tree, and tabindex="-1" on the
   input keeps it off the keyboard path, so nobody reaches it by accident.

   Keyed on the container the corpus already emits, because content/ is
   never edited by hand. */
form[data-kara-subscribe] [aria-hidden="true"] {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden;
}

/* --- The arrow that cannot orphan. ------------------------------------
   An arrow written into the markup is a text character, so the browser
   is free to break the line in front of it - and on the home page it
   did: "Mental health · 227 posts" kept the arrow and dropped it alone
   onto the next line at ordinary desktop widths, which reads as a bullet
   rather than as a direction.

   build_lib.name_arrows() strips every trailing arrow out of the emitted
   HTML and puts this class on the element it came from, so there is no
   arrow glyph in the document at all. The no-break space in front of the
   glyph is what glues it to the last word of the label: there is no
   break opportunity before a U+00A0, at any width.

   Where the element is already a flex container - a button, a card CTA -
   the pseudo-element becomes a flex item and the nbsp reads as the gap. */
.go::after { content: '\00a0\2192'; }

/* The footer's social row.
   -------------------------------------------------------------------
   Five destinations that used to be five stacked list rows, taking as
   much column height as the address and registration details above them
   for what is, in a footer, a footnote. One wrapped row instead: same
   links, same order, a quarter of the height.

   The 44px min-height is the site's touch-target floor, and these sit
   close together horizontally, so the row-gap keeps them apart on a
   phone where they wrap to two or three lines. */
.site-footer .social {
  list-style: none; margin: 16px 0 0; padding: 0;
  display: flex; flex-wrap: wrap; column-gap: 20px; row-gap: 4px;
  font-size: var(--t-3);
}
.site-footer .social a { display: flex; align-items: center; min-height: 44px; }

/* The footer's newsletter form.
   -------------------------------------------------------------------
   .news is a boxed, 620px-wide module everywhere else on the site, and
   the footer borrowed it for a 272px column: no box wanted, and the
   input's flex:1 1 240px basis plus the button overflowed the column
   with no wrap to catch it, clipping "Subscribe" on every page.

   Stacked instead, so both take the full column width. The joined
   radii the side-by-side pair uses are undone here, because two
   stacked controls that each look like half a pill read as broken. */
.site-footer .footer-news {
  background: none; border: 0; padding: 0; max-width: none;
  display: grid; gap: 8px; margin-top: 8px;
}
.site-footer .footer-news input {
  flex: none; width: 100%; min-width: 0; border-right: 1px solid var(--border);
  border-radius: var(--r-md);
}
.site-footer .footer-news .btn { width: 100%; border-radius: var(--r-sm); }

/* The footer's site index, in two columns.
   -------------------------------------------------------------------
   Twelve links stacked ran the index column to roughly twice the height
   of the three columns beside it, so the footer was as tall as its
   longest list and two-thirds of it was empty paper. Two columns puts
   the same twelve links in six rows and the four columns finish within
   sight of each other.

   columns rather than a grid: the list is one flow and the browser
   balances it, so adding or removing a link needs no change here.
   break-inside stops a row splitting across the column boundary. */
.site-footer .site-index { columns: 2; column-gap: 24px; }
.site-footer .site-index li { break-inside: avoid; }
@media (max-width: 640px) { .site-footer .site-index { columns: 1; } }

/* Wills, stock and beneficiary gifts, on /donate/.
   -------------------------------------------------------------------
   The three cards borrow .trust-card, which is the right frame: these
   are the same kind of object, a short claim with an explanation under
   it, and inventing a second card for them would put two card systems
   on one page.

   The block underneath is not a card of that kind. It is the thing a
   donor prints or forwards to their attorney, so it is wider, quieter,
   and its content is set to be read slowly rather than scanned. */
.bequest-card {
  border-radius: var(--r-md);
  padding: 24px;
}
.bequest-card h3 { font-size: var(--t-5); color: var(--action); }

/* Not italic. <address> is italic by default in every browser, and this
   one is a postal address somebody copies, not an aside. */
.bequest-address {
  font-style: normal; margin-top: 8px; line-height: var(--lh-body);
}

/* The sample wording. Indented and set on the surface so it reads as
   the quoted clause it is, and user-select is untouched because copying
   it is the entire point of printing it. */
.bequest-wording {
  margin: 8px 0 0; padding: 16px 20px;
  background: var(--surface); border-left: 3px solid var(--rule);
  font-size: var(--t-3); line-height: var(--lh-body);
}

/* ── The header is one bar, not two ───────────────────────────────────
   The header row was laying out as two stacked tiers on an ordinary
   desktop: the wordmark and the six destinations on one line, the search
   field and the Subscribe/Donate pair dropped onto a second underneath.
   Not a wrap that only showed up at an awkward width - at 1280px, which
   is what a 1920 laptop reports at the 150% scaling Windows ships with,
   the bar was 129px tall and read as two navigations rather than one.

   The cause was arithmetic. .wrap caps every container on this site at
   1200px with 32px of padding either side, so the header had a 1136px
   content box however wide the screen was, and its children wanted 1225:

       wordmark  83     nav  527     search + the two buttons  567
       plus two 24px gaps                                       48
                                                              1225

   Re-measured 2026-08-23, when the six destinations were renamed to the
   conventional nonprofit set:

       wordmark  83     nav  602     tools with a 150px field  492
       plus two 24px gaps                                       48
                                                              1225

   Shortening two labels bought 50px of that back, and I then declared the
   breakpoint safe on the strength of two measurements - the field was 150px
   and the row did not overflow. Both were true. Donate was 150px off the
   right-hand edge of the screen at 1204px the whole time, because nothing
   had measured where Donate was. That is the same error as the collision
   this rule already documents, made twice, one element along.

   So the breakpoint moves. One row needs about 1371px to hold six
   destinations, a usable field and two buttons; below that the drawer layout
   takes over, which is designed for exactly this and still shows Donate.
   Swept 1180-1920 in 4px steps: no overlap, nothing off-screen, no
   overflow.

   Above --bp-nav the bar is one line and cannot become two. The field
   absorbs the range instead of a breakpoint doing it, and it shrinks
   before anything else on the row, so if a font ever loads at different
   metrics the bar loses field width rather than clipping a destination.

   Below --bp-nav the row genuinely cannot hold six destinations, a real
   field and two buttons - measured, and written up in the Breakpoints
   note in assets/kara-b.css, which is where that number is justified.
   Those widths get the bar they already got below 1000: one row, with
   the wordmark, the field and Menu. Still one layer, which is the point. */

@media (min-width: 1372px) {   /* --bp-nav, re-measured above */
  /* Wider than the 1200px reading column, still bounded - a bar running
     the full width of a 2560px monitor puts the wordmark and Donate so
     far apart they stop reading as one object. Scoped to .site-header;
     the content column is untouched. */
  .site-header .wrap { max-width: 1560px; flex-wrap: nowrap; }

  /* The six destinations stay on one line with each other too. Without
     this the nav becomes the second tier - the same fault one box in. */
  .nav { flex-wrap: nowrap; }

  /* min-width:0 on the group as well as the field. A flex item's automatic
     minimum size is its min-content width, so without this .header-tools
     refuses to go below the field's own width and the row spills past the
     padding box at the breakpoint instead of the field giving way. The
     field is what should give way - it is the only thing on the row that
     is still usable narrower. */
  .header-tools { min-width: 0; }

  /* min-content, not 0. The previous pair of rules said "the group may
     collapse" and "the input may not go below 150px" at the same time, and
     the only other thing in the form - the 104px Search button - is not
     shrinkable. So between 1200px and 1292px the form computed to 150px with
     254px of content inside it, and the button painted 104px outside its own
     border box.

     Flexbox lays the next sibling out from the form's BOX edge, not from
     where its overflowing child lands, so Subscribe was positioned 86px
     inside the search button's real extent. And .search carries
     position:relative for the suggestions dropdown, which put it above
     Subscribe in paint order: the two labels overprinted, and
     elementFromPoint across the left two thirds of Subscribe returned the
     search button. It was not ugly, it was a dead control, at the most
     common laptop width there is.

     min-content on the form means it can never be smaller than input plus
     button. The input gives way instead, which is what the old comment
     already said should happen - it just put the floor on the wrong element.

     The old comment claimed this was "measured at 1201, 1240, 1280, 1366 and
     1440". It was: the row's own scrollWidth was measured, and the row never
     did overflow. Whether the row's children overlapped each other was never
     checked, which is the only thing a reader could actually see. */
  .search { flex: 0 1 auto; min-width: min-content; }
  /* The ceiling stays. Dropping the 150px floor also dropped the 240px cap,
     and an unbounded field ate the space Donate was standing in - at 1240 the
     primary action was pushed off the right edge of the bar entirely. The
     field may grow into slack and give way under pressure, between 0 and the
     same 240px it always had. */
  .search input { flex: 1 1 auto; width: auto; min-width: 0; max-width: 240px; }

  /* One rhythm across the row. The gap between the field and Subscribe came
     from .header-tools (19-24px) and the gap between Subscribe and Donate
     from .header-actions (12px), so the same bar had two spacings in it. */
  .header-actions { gap: 16px; }
  .header-tools { gap: 16px; }
}

/* The embedded donation widget on /donate/.
   Givebutter styles the form itself; this is only the frame around it, so the
   block reads as part of the page rather than as something pasted into it. */
.give-embed {
  margin-top: 32px; padding: 24px;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-md);
}
.give-embed .give-head { margin-top: 0; }
.give-embed givebutter-widget { display: block; margin-top: 16px; min-height: 120px; }
.give-embed-actions { display: flex; gap: 16px; align-items: center;
  flex-wrap: wrap; margin-top: 16px; }
.give-embed-note { margin-top: 12px; }

/* --- The bands above the footer. --------------------------------------
   One green band, then two quiet ones. The crisis bar carries the hotline and
   is the only saturated colour at the bottom of the page, which is the whole
   point of it: a reader in trouble should find that number without reading
   anything else.

   All three come from data/footer-bands.json and all three disappear if it is
   emptied, so this is styling only - nothing here decides what is said. */
.subfooter {
  background: var(--action); color: var(--on-action);
  font-size: var(--t-2);
}
.subfooter .wrap {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 16px;
  padding-top: 16px; padding-bottom: 16px;
}
.subfooter-links {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 12px; list-style: none; margin: 0; padding: 0;
}
/* A printed rule rather than a "|" character: a screen reader announces the
   pipe as "vertical line" between every link, and it is decoration. */
.subfooter-links li + li {
  border-left: 1px solid rgba(255, 255, 255, .35); padding-left: 12px;
}
.subfooter-links a { color: var(--on-action); font-weight: 600; }
.subfooter-links a:hover { text-decoration-thickness: 2px; }
.subfooter-crisis { margin: 0; color: var(--on-action); }
/* White, not the pale pine: pale on pine measures 6.72:1, which is AA and
   this site holds AAA. White on pine is 9.21:1. */
.subfooter-crisis strong { color: var(--on-action); }
.subfooter :focus-visible { outline: 3px solid var(--on-action); outline-offset: 2px; }

.uni-strip {
  margin: 0; padding: 12px 24px; text-align: center;
  font-size: var(--t-2); color: var(--secondary);
  background: var(--surface-warm);
  border-bottom: 1px solid var(--rule);
}
.uni-strip strong { color: var(--text); }

.partner-row {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 12px; list-style: none; margin: 0; padding: 24px 24px 0;
}
/* The chip carries the tap target, not the link inside it.
   kara-b.css gives every `.site-footer li > a` a 44px minimum height, and
   says in its own comment that chips are excluded because they have a box of
   their own - but this chip did not exist when that was written, so the one
   chip whose name is a link stood 60px tall beside three 36px ones. Putting
   the minimum on the chip keeps the target honest and makes all four the same
   height whether or not anything inside them is clickable. */
.partner-chip {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  color: var(--secondary);
  font-size: var(--t-1); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  padding: 8px 16px;
}
/* kara-b.css gives links inside an `li` 15.5px of vertical padding so a link
   in running text is still a 44px target. Its exclusion list is written
   against classes on the anchor, and this anchor has none - so the chip got
   the prose treatment and stood 70px tall next to three 44px ones. The chip
   is the target now, so the anchor gives its padding back. */
/* The anchor is laid over the chip's whole box, so the target IS the chip.
   Levelling the chip heights earlier moved the 44px minimum onto the chip and
   left the only clickable thing inside it 169x21: the box passed the check
   and the target did not. */
/* In flow, stretched, and pulled out over the chip's own padding.
   Two earlier attempts got this wrong in opposite directions: `align-self:
   stretch` alone left the link 169x21 inside a 44px chip (box passed, target
   did not), and `position: absolute; inset: 0` filled the box but took the
   link out of flow, so the chip had nothing to size from and collapsed from
   195px wide to 34px. Staying in flow keeps the chip the width of its own
   name; the negative margin lets the link cover the padding that name sits
   in, so the whole 44px chip is the target. */
.site-footer .partner-chip > a {
  display: flex; align-items: center; justify-content: center;
  align-self: stretch; min-height: 0;
  /* The chip's own padding, plus its 1px border, written as that sum rather
     than as 9 and 17 - which are not on the spacing scale and would be two
     numbers nobody could trace. The border sits outside the padding box, so
     covering only the padding left the link 42x42 in a 44x44 chip: two pixels
     short of the target this rule exists to restore. */
  margin: calc(-8px - 1px) calc(-16px - 1px);
  padding: calc(8px + 1px) calc(16px + 1px);
}
.partner-chip a { color: var(--action); }
.partner-chip a:hover { color: var(--hover); }

/* --- The page hero. ----------------------------------------------------
   A deep pine panel at the top of a hub or landing page, inside the paper
   card rather than full-bleed across it. Full-bleed was the obvious reading
   of the design this came from, and it is wrong here: the header and the
   breadcrumb above it are already dark, so a third dark strip would rebuild
   at the top of the page exactly the muddy stack that had to be taken out of
   the bottom of it. Inset and rounded, it reads as one object on paper.

   White on pine is 9.21:1 and the pale pine standfirst is 6.72:1, which is
   AA; the standfirst is set at --on-action too, and the pale pine is kept for
   the eyebrow, where it is a label rather than reading matter. */
.page-hero {
  background: var(--action); color: var(--on-action);
  border-radius: var(--r-md);
  padding: 40px;
  margin-bottom: 40px;
}
.page-hero .kicker { color: var(--action-pale); margin: 0; }
.page-hero h1 { color: var(--on-action); margin-top: 12px; }
.page-hero .lede { color: var(--on-action); max-width: 60ch; }
.page-hero .hero-actions {
  display: flex; flex-wrap: wrap; gap: 12px;
  margin: 24px 0 0;
}
/* The filled button is white on pine rather than pine on white: on this
   ground the site's own filled button would disappear into the panel. */
.page-hero .btn {
  background: var(--on-action); color: var(--action); border-color: var(--on-action);
}
.page-hero .btn:hover { background: var(--action-pale); color: var(--hover); }
.page-hero .btn-hero-quiet {
  background: transparent; color: var(--on-action);
  border: 1px solid rgba(255, 255, 255, .55);
}
.page-hero .btn-hero-quiet:hover {
  background: rgba(255, 255, 255, .12); color: var(--on-action);
}
.page-hero :focus-visible { outline: 3px solid var(--on-action); outline-offset: 2px; }
@media (max-width: 640px) { .page-hero { padding: 24px; } }


/* --- Link targets that stack. ------------------------------------------
   kara-b.css pads links inside running text to a 44px target. On a single
   line that is right. On consecutive lines it is not: at 19px/30.4px the
   padded fragment is 49px tall on a 30.4px advance, so each link's hit box
   overlaps its neighbour above by ~19px and a click near the boundary lands
   on the wrong link. Measured across the site: 124 overlapping pairs, worst
   in .prose paragraphs and in the toolkit contents lists.

   The fix is not to remove the target - it is to stop the target growing
   past the line it sits on. The padding is capped at half the leading, so a
   link can fill its own line and never reach into the one above. Links that
   sit alone on a line are unaffected; they were already inside their box. */
/* An inline link in a sentence gets no vertical padding at all.
   WCAG 2.5.8 exempts links whose box is determined by the line they sit in,
   and for good reason: at 19px on a 30.4px line there is no padding that
   reaches 44px without reaching into the line above. The old rule tried
   anyway, giving a 49px box on a 30.4px advance - 19px of every inline
   link's target belonged to its neighbour, and a click near the boundary
   went to the wrong page. 124 pairs across the site.

   A link that occupies its own row is a different object and keeps its
   target; the rule below gives those rows the room to hold it. */
:where(p, dd, figcaption, blockquote, h1, h2, h3, h4) a:not(.btn, .btn-quiet,
  .btn-plain, .btn-donate, .btn-subscribe, .card, .chip, .hub, .link-card,
  .project-card, .group-card, .rec-cta, .menu-toggle, .to-top, .quick-exit) {
  padding-block: 0;
}

/* Rows that are a link and nothing else.
   The padding comes off here too. A 44px box cannot sit on a 24px line
   without reaching into the row above it, and reaching into the row above is
   the harm - it sends a click to the wrong guide. WCAG 2.5.8's floor is 24px
   for a target whose size is set by the text it sits in, and the row spacing
   below clears that: 17px at 1.7 is 29px, plus 12px between rows. */
.guide-toc li, .sec-body ul li { margin-bottom: 12px; }
/* The row's height comes from the 12px above and the list's own leading; the
   anchor adds nothing. An earlier version of this rule set line-height 1.7
   here to make room for padding that no longer exists. */
.guide-toc li a, .sec-body ul li a { padding-block: 0; }

/* --- The newsletter field's placeholder. -------------------------------
   The browser default is #757575, which is 4.61:1 on white and 4.33:1 on the
   warm ground the footer form actually sits on - under AA, on every page of
   the site. This passes on all three grounds it meets: 5.38:1 on --ground,
   5.73:1 on white, 4.78:1 on the warm tint. Still clearly lighter than
   --secondary, so it still reads as a prompt rather than as typed text. */
.news input::placeholder { color: var(--placeholder); opacity: 1; }


/* --- The years row. -----------------------------------------------------
   The separators are gone, not restyled. .years is a flex row with its own
   16px column gap, so the interpuncts between the links were anonymous flex
   items - and when the row wrapped, one of them could land last on a line.
   Every wrapped row on /blog/ ended in a floating dot. The gap was always
   doing the separating; the dots were doing nothing but that. */
.years { row-gap: 8px; column-gap: 16px; }
