/* ===========================================================================
   EPOCH — the publication.

   One ink, one sheet. There is no palette in this file: every tone is the ink
   at some coverage, and every "shade" a reader perceives is either a mix
   (`--quiet`, `--faint`) or an actual field of dots.

   The halftone is the system. `.screen` paints a dot grid whose coverage sets
   its darkness, and masking that grid with a linear-gradient gives a true
   printed ramp — dots thinning out — rather than a CSS gradient pretending to
   be one. It is the only visual device here, and it does all the work that
   colour would do somewhere else.

   Tokens are generated from core/brand.py. Do not add a colour to this file.
   =========================================================================== */

/* Merriweather ships here as the weight axis only. The masthead and headline
   rules used to carry font-variation-settings for Fraunces' opsz/SOFT/WONK
   axes; none of those exist on this face, so they were removed rather than
   left to read like live tuning. Size and weight are the whole vocabulary now. */
@font-face {
  font-family: 'Merriweather';
  src: url('/news-static/fonts/merriweather.woff2') format('woff2');
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('/news-static/fonts/inter.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --ink: #14130F;
  --paper: #F2F0E9;
  --font-display: 'Merriweather', 'Noto Serif', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', 'Cascadia Mono', Menlo, monospace;
  --cell: 5px;
  --screen-light: 16%;
  --screen-mid: 24%;
  --screen-heavy: 34%;
  --flat: color-mix(in srgb, var(--ink) 100%, var(--paper));
  --read: color-mix(in srgb, var(--ink) 88%, var(--paper));
  --quiet: color-mix(in srgb, var(--ink) 62%, var(--paper));
  --faint: color-mix(in srgb, var(--ink) 42%, var(--paper));
  --ghost: color-mix(in srgb, var(--ink) 22%, var(--paper));
  --trace: color-mix(in srgb, var(--ink) 11%, var(--paper));
}
:root {
  --wrap: 1180px;
  --rule: 1px solid var(--trace);
  /* The page's one margin. It was written out five times across three
     breakpoints — masthead, sections, rail, foot — and they had already drifted
     apart by two pixels in places. One number per breakpoint, and the edge of
     the sheet is the same edge everywhere on it. */
  --gutter: 24px;
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--read);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  font-synthesis-weight: none;
}

/* No screen behind the text. The sheet's tooth looked right on a poster and
   made paragraphs harder to read on a news page, which is the wrong trade.
   The halftone now appears only where nobody is reading a sentence: plates,
   the masthead edge, the foot. */

a { color: inherit; }
img { max-width: 100%; display: block; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

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

/* --- the screen ----------------------------------------------------------
   Three coverages and a ramp. Everything tonal on this site is one of these. */

.screen {
  background-image: radial-gradient(circle at center, var(--ink) var(--screen-mid), transparent calc(var(--screen-mid) + 1%));
  background-size: var(--cell) var(--cell);
}
.screen-light { --screen-mid: var(--screen-light); }
.screen-heavy { --screen-mid: var(--screen-heavy); }

/* A printed gradient: the dots thin out, they do not fade to grey. */
.screen-ramp {
  -webkit-mask-image: linear-gradient(var(--ramp-dir, to right), transparent, #000 70%);
  mask-image: linear-gradient(var(--ramp-dir, to right), transparent, #000 70%);
}

/* --- masthead -------------------------------------------------------------
   One row: the mark, the sections, and the four things a reader needs. It is
   sticky because on a news site the way back to the front page should never be
   a scroll away, and it is short enough that giving up 60px is affordable. */

.masthead {
  border-bottom: 2px solid var(--ink);
  background: var(--paper);
  position: sticky;
  top: 0;
  z-index: 20;
  overflow: hidden;
}
/* The sheet still darkens at the right edge, but at a bar's scale. */
.masthead::after {
  content: "";
  position: absolute;
  inset: 0 0 0 55%;
  pointer-events: none;
  background-image: radial-gradient(circle at center, var(--ink) var(--screen-mid), transparent calc(var(--screen-mid) + 1%));
  background-size: var(--cell) var(--cell);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 130%);
  mask-image: linear-gradient(to right, transparent, #000 130%);
  opacity: .35;
}

.bar {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 12px var(--gutter);
  display: flex;
  /* Centred, not baseline-aligned. Baseline is the right instinct for type of
     similar size, and it was fine when the wordmark was small — but it sets a
     38px serif and a 10px tracked sans on the same line, and matching their
     baselines leaves the wordmark's optical centre ten pixels above the nav's.
     The eye reads that as a mistake rather than as typography. */
  align-items: center;
  gap: 28px;
  position: relative;
  z-index: 1;
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(26px, 3.4vw, 38px);
  line-height: 1;
  letter-spacing: .1em;
  text-decoration: none;
  color: var(--ink);
  flex: none;
}
.wordmark:hover { opacity: .7; }

.beats {
  display: flex;
  gap: 22px;
  overflow-x: auto;
  scrollbar-width: none;
  /* The sections are the long list; they take the slack and scroll on a phone
     rather than wrapping the bar onto a second line. */
  flex: 1 1 auto;
  min-width: 0;
  padding-bottom: 2px;
}
.beats::-webkit-scrollbar { display: none; }

.utility {
  display: flex;
  gap: 18px;
  flex: none;
}

.nav-link {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--faint);
  white-space: nowrap;
  padding-bottom: 3px;
  border-bottom: 2px solid transparent;
}
.nav-link:hover { color: var(--ink); }
.nav-link.is-on { color: var(--ink); border-bottom-color: var(--ink); }
.utility .nav-link { color: var(--quiet); }
.utility .nav-link:hover { color: var(--ink); }

/* --- the grid ------------------------------------------------------------ */

/* The front page above the fold: one lead across two columns, and everything
   else flowing around and under it.

   It used to be two columns — the lead on the left, a stack of cards on the
   right — and the two never agreed on height. The lead is as tall as its
   headline happens to be; the stack is as tall as however many cards were
   configured. Whichever was shorter left a column of empty paper beside the
   other, and no number of cards fixes that, because the mismatch moves every
   time a headline runs to a different number of lines.

   Three columns with the lead spanning two shrank the hole without closing it,
   for the same reason: a grid row is as tall as its tallest cell. The top block
   is a rail now — see the end of this file — and the rules here are what a card
   looks like wherever it appears. */
.card { min-width: 0; }
.card-media {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--paper);
  border: 1px solid var(--ghost);
}
/* height:auto, not 100%. The box above declares a ratio for the generated
   plate, which has no intrinsic size; a real photograph does, and forcing one
   into that box cropped the subject's own wordmark off the edge. With
   height:auto the element takes the picture's ratio and object-fit never
   engages — `cover` is named only so a future box gets filled rather than
   pillarboxed. No max-height: a cap binds before the width does on a wide
   card, and letterboxes the common case to guard a rare one. */
.card-media .shot { width: 100%; height: auto; object-fit: cover; filter: grayscale(1) contrast(1.08); }

/* The plate: the reference banner's composition, per card. Light sheet on the
   left, the screen thickening rightward, the sphere large and cropped by the
   edge. A centred globe on a flat field read as a diagram; this reads as a
   printed page. */
.plate {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background-image: radial-gradient(circle at center, var(--ink) var(--screen-light), transparent calc(var(--screen-light) + 1%));
  background-size: var(--cell) var(--cell);
}
.plate::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle at center, var(--ink) var(--screen-heavy), transparent calc(var(--screen-heavy) + 1%));
  background-size: var(--cell) var(--cell);
  -webkit-mask-image: linear-gradient(to right, transparent 18%, #000 125%);
  mask-image: linear-gradient(to right, transparent 18%, #000 125%);
  opacity: .62;
}
.globe {
  position: absolute;
  top: 50%;
  right: -6%;
  width: 52%;
  transform: translateY(-50%) rotate(var(--turn, 0deg));
  color: var(--ink);
  opacity: .55;
}
.plate-lg .globe { width: 38%; right: 4%; }
.globe svg { width: 100%; height: auto; display: block; }
.card-media:hover .globe { opacity: .74; }

.card-body { padding-top: 14px; }

.eyebrow {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--faint);
}
.eyebrow:hover { color: var(--ink); }
.eyebrow-lg { font-size: 12px; }

.card-head {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
  margin: 8px 0 0;
  font-size: 24px;
  color: var(--ink);
  text-wrap: balance;
}
.card-lead .card-head {
  font-size: clamp(32px, 4.2vw, 48px);
  font-weight: 600;
}
.card-head a { text-decoration: none; }
.card-head a:hover { text-decoration: underline; text-underline-offset: 4px; }

.card-stand { margin: 12px 0 0; color: var(--quiet); font-size: 17px; max-width: 54ch; }

.card-meta {
  margin: 10px 0 0;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--faint);
}
.card-meta span { margin: 0 6px; }

/* --- latest --------------------------------------------------------------- */

.section-head {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .32em;
  text-transform: uppercase;
  margin: 56px 0 18px;
  padding-bottom: 10px;
  border-bottom: 2px solid var(--ink);
  color: var(--ink);
}

.row {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  gap: 24px;
  padding: 22px 0;
  border-bottom: var(--rule);
  align-items: start;
}
.row-media {
  display: block; position: relative; aspect-ratio: 16 / 10;
  overflow: hidden; border: 1px solid var(--ghost);
}
.row-media .shot { width: 100%; height: auto; object-fit: cover; filter: grayscale(1) contrast(1.08); }
.row-head {
  font-family: var(--font-display);
  font-size: 22px; font-weight: 600; line-height: 1.22;
  margin: 8px 0 0; color: var(--ink);
}
.row-head a { text-decoration: none; }
.row-head a:hover { text-decoration: underline; text-underline-offset: 4px; }

/* --- beat page ------------------------------------------------------------ */

.beat-title {
  font-family: var(--font-display);
  font-size: clamp(40px, 7vw, 72px);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  margin: 8px 0 28px;
  padding-bottom: 16px;
  border-bottom: 2px solid var(--ink);
  color: var(--ink);
}

/* --- article -------------------------------------------------------------- */

.article { max-width: 720px; margin: 0 auto; }

.article .headline {
  font-family: var(--font-display);
  font-size: clamp(36px, 5.6vw, 62px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.005em;
  margin: 14px 0 18px;
  color: var(--ink);
  text-wrap: balance;
}
.standfirst {
  font-size: 21px;
  line-height: 1.5;
  color: var(--quiet);
  margin: 0 0 24px;
  max-width: 60ch;
}

.byline {
  display: flex; gap: 18px; align-items: baseline; flex-wrap: wrap;
  padding: 14px 0;
  border-top: 1px solid var(--ink);
  border-bottom: var(--rule);
  font-size: 10.5px; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--faint);
}
.byline .by { color: var(--ink); }

.lead-media {
  margin: 26px 0;
  position: relative;
  aspect-ratio: 2 / 1;
  overflow: hidden;
  border: 1px solid var(--ghost);
}
.lead-media .shot { width: 100%; height: auto; object-fit: cover; filter: grayscale(1) contrast(1.08); }
.lead-media figcaption {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 8px 14px;
  background: var(--ink); color: var(--paper);
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
}

.keys {
  border-top: 2px solid var(--ink);
  border-bottom: 2px solid var(--ink);
  padding: 20px 0;
  margin: 32px 0;
}
.keys h2 {
  font-size: 10.5px; font-weight: 700;
  letter-spacing: .28em; text-transform: uppercase;
  margin: 0 0 12px; color: var(--faint);
}
.keys ul { margin: 0; padding-left: 20px; }
.keys li { margin-bottom: 8px; color: var(--read); }
.keys li::marker { color: var(--ink); }

/* Subheads inside an article: the display face, small and set tight, so a long
   page has landmarks without breaking into a second voice. */
.prose h2 {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  line-height: 1.2;
  margin: 40px 0 14px;
  color: var(--ink);
}
.prose h2 + p { margin-top: 0; }

.prose p { font-size: 18px; line-height: 1.72; margin: 0 0 22px; color: var(--read); }
/* The opening paragraph is set larger — a printed convention, and it tells the
   reader where the article actually starts. */
.prose p:first-child { font-size: 20px; line-height: 1.6; }

.sources { margin-top: 44px; border-top: 2px solid var(--ink); padding-top: 20px; }
.sources h2 {
  font-family: var(--font-display);
  font-size: 13px; letter-spacing: .32em; text-transform: uppercase;
  margin: 0 0 6px; color: var(--ink);
}
.sources-note { margin: 0 0 16px; font-size: 14px; color: var(--faint); }
.sources ol { margin: 0; padding-left: 22px; }
.sources li { margin-bottom: 12px; font-size: 15px; }
.sources a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.src-tier {
  font-size: 9.5px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  padding: 2px 7px; border: 1px solid var(--ghost);
  margin-right: 10px; color: var(--faint);
}
.src-tier.tier-primary { color: var(--ink); border-color: var(--ink); }
.src-host { font-size: 12px; color: var(--faint); margin-left: 10px; }

.disclosure { margin-top: 32px; font-size: 14px; color: var(--faint); font-style: italic; }

.preview-bar {
  background: var(--ink); color: var(--paper);
  font-size: 11px; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  text-align: center; padding: 10px 16px;
}

.empty { padding: 72px 0; }
.empty h1 { font-family: var(--font-display); font-weight: 600; font-size: 42px; margin: 0 0 12px; color: var(--ink); }
.empty p { color: var(--faint); margin: 0; }

/* --- footer --------------------------------------------------------------- */

.foot {
  border-top: 2px solid var(--ink);
  padding: 44px var(--gutter) 60px;
  text-align: center;
  font-size: 14px;
  color: var(--quiet);
  position: relative;
  overflow: hidden;
}
/* The sheet darkens again at the foot, closing the page the way the masthead
   opened it. */
.foot::before {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background-image: radial-gradient(circle at center, var(--ink) var(--screen-mid), transparent calc(var(--screen-mid) + 1%));
  background-size: var(--cell) var(--cell);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 160%);
  mask-image: linear-gradient(to bottom, transparent, #000 160%);
  opacity: .22;
}
.foot > * { position: relative; }
.foot p { margin: 0 auto 10px; max-width: 60ch; }
/* The drawn mark, not the display face setting "E." — the foot is where the
   page signs itself, and a signature is a drawing. It inherits `color`, so the
   one path serves the sheet and the board without a second file. */
.foot-mark {
  color: var(--ink);
  margin: 0 auto 18px;
  line-height: 0;
}
.foot-mark svg { height: 46px; width: auto; display: block; margin: 0 auto; }
.foot-contact {
  list-style: none; margin: 22px auto; padding: 0;
  display: flex; justify-content: center; gap: 40px; flex-wrap: wrap;
}
.foot-contact li { display: flex; flex-direction: column; gap: 4px; }
.foot-contact span {
  font-size: 9.5px; font-weight: 700;
  letter-spacing: .22em; text-transform: uppercase; color: var(--faint);
}
.foot-contact a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; font-size: 14px; }
.foot-thin { color: var(--faint); font-size: 13px; }

/* --- responsive ----------------------------------------------------------- */

/* --- responsive -----------------------------------------------------------
   Three real breakpoints, each fixing something specific rather than just
   shrinking type. */

/* Laptop: the two-column top has no room for a second column of prose. */
@media (max-width: 1000px) {
  .bar { gap: 20px; }
  .beats { gap: 18px; }
  /* ABOUT, TIPS and RSS leave the bar here, not at 620px. Below a thousand
     pixels the five section links, the three utility links and the search
     field stop fitting on one line: the sections scroll under the utility
     block, which is `flex: none`, so FUNDING was rendering half-under ABOUT
     and the search field ran off the right edge. Every one of the three is
     in the footer of every page, and the sections are what a reader came
     for, so the sections keep the bar. */
  .utility .nav-link { display: none; }
}

/* Tablet: the utility links stop competing with the sections for the bar. */
@media (max-width: 800px) {
  :root { --gutter: 20px; }
  .wrap { padding: 22px var(--gutter) 56px; }
  .row { grid-template-columns: 148px minmax(0, 1fr); gap: 16px; }
  .row-head { font-size: 18px; }
  .masthead::after { inset: 0 0 0 40%; }
  .utility { gap: 14px; }
  /* Tablet only: below 620px the utility links leave the bar entirely and
     live in the footer, so this rule governs the band between the two. */
  .utility .nav-link:not([href="/about"]) { display: none; }
  .standfirst { font-size: 19px; }
}

/* Phone: the bar becomes two rows — mark and utility on one, sections
   scrolling beneath — because eight links will not fit beside a wordmark and
   a hamburger would hide the one thing this site is for. */
/* Search: one field, open on a desktop and behind an icon on a phone.
   Declared here rather than further down the file because the phone block
   below overrides them, and a default that comes after its own override
   wins. */
.find { display: flex; align-items: center; gap: 6px; }
/* The icon only exists on a phone; on a desktop the field is always open. */
.find-icon { display: none; }

/* Everything this site does differently on a phone, in one place.

   It was three separate blocks with this identical query, written months
   apart, and two of them declared `.bar` and `.wordmark`. Which one won
   depended on where in the file it happened to sit, so a correct rule
   could be added and simply not take effect — that happened three times
   in one afternoon. One block cannot lose an argument with itself. */
@media (max-width: 620px) {
  /* 18px, not the 16 this used to be. A phone holds the sheet closer to the
     bezel than a laptop does, the curved edge of the screen eats the first
     couple of pixels optically, and a thumb rests over the left margin while
     reading. Type that starts at 16 looks like it is falling off. */
  :root { --gutter: 18px; }

  /* --- layout ------------------------------------------------------ */

  .bar {
    flex-wrap: wrap;
    padding: 10px var(--gutter);
    gap: 10px 16px;
    align-items: center;
  }
  .wordmark { font-size: 26px; }
  .utility { margin-left: auto; }
  .beats {
    order: 3;
    flex-basis: 100%;
    gap: 16px;
    /* Bleed the scroller to the screen edges so it is obviously scrollable. */
    margin: 0 calc(var(--gutter) * -1);
    padding: 0 var(--gutter);
    /* The list overruns a 375px screen by about twenty pixels, which is the
       worst possible amount: too little to look like a scroller, enough to
       look like the last section is broken. Fading the right edge says the
       row continues, and does it without adding an element or a script. */
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 34px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 34px), transparent);
  }

  /* A finger is not a cursor. 23px was roughly half of what either platform
     asks for — 44pt on iOS, 48dp on Android — so the links get a real hit area
     while the type stays exactly where it was. The underline on the current
     section moves with them, drawn inside the taller box rather than under it. */
  .masthead .nav-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-bottom: 0;
    border-bottom: 0;
    position: relative;
  }
  .masthead .nav-link.is-on::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 9px;
    height: 2px;
    background: var(--ink);
  }
  /* The two rows are now sized by their tap targets, so the padding that used
     to space them would double it. */
  .bar { padding: 2px var(--gutter) 0; gap: 0 16px; }
  .wrap { padding: 18px var(--gutter) 48px; }
  .card-lead .card-head, .card-head { font-size: 22px; }
  .card-stand { font-size: 16px; }
  .prose p, .prose p:first-child { font-size: 17px; line-height: 1.65; }
  .standfirst { font-size: 18px; }
  .byline { gap: 10px; font-size: 9.5px; }
  .row { grid-template-columns: 104px minmax(0, 1fr); gap: 13px; padding: 16px 0; }
  .row-head { font-size: 16px; }
  .section-head { margin: 38px 0 12px; }
  .foot-contact { gap: 20px; }
  .globe { width: 62%; right: -10%; }

  /* --- the footer's legal row --------------------------------------- */

  /* Same reason as the masthead: a finger needs a target, not a word. */
  .foot-legal { gap: 4px 20px; }
  .foot-legal a { min-height: 44px; }

  /* --- the masthead: centred wordmark, search behind an icon -------- */

  /* Centred, because with the utility links gone the wordmark is alone on its
     row and a masthead centred on itself reads as a masthead rather than as
     something that failed to fill the space. */
  .bar { justify-content: center; text-align: center; }
  .wordmark { flex-basis: 100%; text-align: center; }

  /* Already in the footer, on every page. */
  .utility .nav-link { display: none; }

  .utility {
    margin-left: 0;
    position: absolute;
    right: 12px;
    top: 4px;
  }

  .find-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -10px;             /* hit area without a 44px hole in the layout */
    color: var(--quiet);
    cursor: pointer;
  }
  .find-icon:active { color: var(--ink); }

  /* Closed: the field is not merely invisible, it is out of the tab order and
     out of the accessibility tree. A search box a screen reader can reach but
     a sighted user cannot see is worse than no search box. */
  .find input[type="search"] {
    width: 0;
    padding: 0;
    border-bottom-color: transparent;
    visibility: hidden;
    transition: width .2s var(--ease), visibility 0s linear .2s;
  }
  .find-toggle:checked ~ input[type="search"] {
    width: min(62vw, 15rem);
    padding: .35rem .1rem;
    border-bottom-color: var(--flat);
    visibility: visible;
    transition: width .2s var(--ease), visibility 0s;
  }
  .find-toggle:checked ~ .find-icon { color: var(--ink); }

  /* Open, the field needs the row, and the row already has a wordmark in the
     middle of it — the first version opened the field straight across "EPOCH".
     The checkbox is nested inside a later sibling, so no combinator reaches
     back to the wordmark; :has() on the bar does, and it keeps the whole open
     state expressed in one place instead of split across two elements. */
  .bar:has(.find-toggle:checked) .wordmark {
    opacity: 0;
    pointer-events: none;
  }
  .bar:has(.find-toggle:checked) .utility {
    left: 12px;
    right: 12px;
  }
  .find-toggle:checked ~ input[type="search"] {
    position: relative;
    z-index: 2;
    width: 100%;
    flex: 1;
  }
  .bar:has(.find-toggle:checked) .find { width: 100%; }

  /* Keyboard: the icon has to show focus, since it is standing in for a button. */
  .find-toggle:focus-visible ~ .find-icon {
    outline: 2px solid var(--accent, #b45309);
    outline-offset: -6px;
    border-radius: var(--pill);
  }
}


/* The gap between a phone and a laptop, where the bar is still one row but the
   search field no longer fits beside five section links. It was rendering
   FUNDING half-cut with COMPUTE scrolled off, which reads as a broken bar
   rather than a scrollable one. The field collapses to the same icon the phone
   uses; the row itself stays as it is, because there is room for a row here —
   only not for a 200px field in it. */
@media (min-width: 621px) and (max-width: 820px) {
  .find-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -10px;
    color: var(--quiet);
    cursor: pointer;
  }
  .find-icon:active,
  .find-toggle:checked ~ .find-icon { color: var(--ink); }

  .find input[type="search"] {
    width: 0;
    padding: 0;
    border-bottom-color: transparent;
    visibility: hidden;
    transition: width .2s var(--ease), visibility 0s linear .2s;
  }
  .find-toggle:checked ~ input[type="search"] {
    width: min(42vw, 15rem);
    padding: .35rem .1rem;
    border-bottom-color: var(--flat);
    visibility: visible;
    transition: width .2s var(--ease), visibility 0s;
  }
  .find-toggle:focus-visible ~ .find-icon {
    outline: 2px solid var(--accent, #b45309);
    outline-offset: -6px;
    border-radius: var(--pill);
  }
}


/* Very narrow: the thumbnail is costing more than it gives. */
@media (max-width: 400px) {
  .row { grid-template-columns: 1fr; }
  .row-media { aspect-ratio: 2 / 1; }
  .row-media:has(.shot) { aspect-ratio: auto; }
}

@media (prefers-reduced-motion: reduce) { * { transition-duration: .001ms !important; } }

/* ===========================================================================
   Figures inside a body.
   Charts are the argument in an analysis piece, so they are set at the width
   of the reading column and allowed to break out of it when the chart is a
   scatter that needs the room. Credit sits under the caption in the quiet
   tone: whose chart it is matters, and it is not commentary.
   =========================================================================== */

.prose .sub {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.6rem);
  font-weight: 600;
  line-height: 1.25;
  margin: 2.4em 0 0.7em;
}
.prose .sub:first-child { margin-top: 0; }

.fig {
  margin: 2em 0;
}
.fig img,
.fig video {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule, rgba(20,19,15,.12));
  border-radius: 2px;
  background: var(--paper-2, #ece8dd);
}
.fig video { background: #000; }

/* A wide chart earns the margin back on a big screen. */
@media (min-width: 1100px) {
  .fig.figure-wide {
    width: min(112%, 1100px);
    margin-left: 50%;
    transform: translateX(-50%);
  }
}

.fig figcaption {
  margin-top: 0.6em;
  font-size: 0.82rem;
  line-height: 1.45;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4em 0.7em;
}
.fig figcaption .cap { color: color-mix(in srgb, var(--ink) 72%, var(--paper)); }
.fig figcaption .credit {
  color: color-mix(in srgb, var(--ink) 45%, var(--paper));
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: 0.72rem;
  align-self: center;
}

/* ===========================================================================
   Softer surfaces.
   Borrowed from Google's blog, which sets almost everything on a 12px radius
   and its controls as pills. The typography here stays what it was — a serif
   masthead over a dot screen is the identity and a rounded corner does not
   change it — but the media and the controls had no reason to be sharp.
   =========================================================================== */

:root {
  --radius: 12px;
  --radius-sm: 8px;
  --pill: 999px;
  --ease: cubic-bezier(.2, .7, .3, 1);
}

.fig img,
.fig video,
.lead-media img,
.lead-media video {
  border-radius: var(--radius);
}

/* The grid units. Most captures carry no picture, so .card-media is often a
   coloured block rather than a photograph — which is exactly the surface
   Google's 12px radius is for. overflow:hidden so the scrim and the
   placeholder are clipped to the same corner as the image. */
.card-media,
.row-media {
  border-radius: var(--radius);
  overflow: hidden;
  display: block;
}
.card-media img,
.row-media img,
.card-media .shot,
.row-media .shot {
  border-radius: 0;
}
.row-media { border-radius: var(--radius-sm); }

/* ---- share ---------------------------------------------------------------
   Pills, quiet until hovered. Rendered under the byline and again at the end,
   because the two moments a reader shares are before reading and after. */

.share {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5em;
  margin: 1.4em 0 0;
}
.article .prose + .sources + .share,
.article .sources + .share { margin: 2.4em 0 1.6em; }

.share-label {
  font-family: var(--font-body);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: color-mix(in srgb, var(--ink) 45%, var(--paper));
  margin-right: 0.15em;
}

.share-btn {
  font: inherit;
  font-family: var(--font-body);
  font-size: 0.8rem;
  line-height: 1;
  padding: 0.55em 1.05em;
  border-radius: var(--pill);
  border: 1px solid color-mix(in srgb, var(--ink) 16%, var(--paper));
  background: transparent;
  color: color-mix(in srgb, var(--ink) 78%, var(--paper));
  text-decoration: none;
  cursor: pointer;
  transition: background-color .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), transform .18s var(--ease);
}
.share-btn:hover {
  background: color-mix(in srgb, var(--ink) 6%, var(--paper));
  border-color: color-mix(in srgb, var(--ink) 30%, var(--paper));
  color: var(--ink);
}
.share-btn:active { transform: translateY(1px); }
.share-btn.is-done {
  background: color-mix(in srgb, var(--confirm, #0f766e) 12%, var(--paper));
  border-color: var(--confirm, #0f766e);
  color: var(--confirm, #0f766e);
}

/* ---- lightbox ------------------------------------------------------------
   Subtle on purpose: 180ms, a few pixels of rise, and nothing at all for a
   reader who has asked the operating system for less motion. */

.is-zoomable { cursor: zoom-in; }
.is-zoomable:focus-visible {
  outline: 2px solid var(--accent, #b45309);
  outline-offset: 3px;
}

body.lightbox-open { overflow: hidden; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in srgb, var(--ink) 88%, transparent);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity .18s var(--ease);
}
.lightbox.is-open { display: grid; opacity: 1; }
.lightbox.is-open.is-animating { opacity: 0; }

.lightbox-figure {
  margin: 0;
  max-width: min(1400px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.8em;
  transform: translateY(6px) scale(.985);
  transition: transform .18s var(--ease);
}
.lightbox.is-open .lightbox-figure { transform: none; }
.lightbox.is-open.is-animating .lightbox-figure { transform: translateY(6px) scale(.985); }

.lightbox-figure img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: calc(100vh - 9rem);
  margin: 0 auto;
  border-radius: var(--radius);
  background: var(--paper);
  cursor: zoom-out;
}
.lightbox-figure figcaption {
  display: flex;
  flex-wrap: wrap;
  gap: .4em .7em;
  max-width: 68ch;
  margin: 0 auto;
  font-family: var(--font-body);
  font-size: .82rem;
  line-height: 1.45;
  color: color-mix(in srgb, var(--paper) 82%, var(--ink));
}
.lightbox-figure figcaption .credit {
  color: color-mix(in srgb, var(--paper) 52%, var(--ink));
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .72rem;
  align-self: center;
}

.lightbox-close {
  position: absolute;
  top: clamp(.75rem, 2vw, 1.5rem);
  right: clamp(.75rem, 2vw, 1.5rem);
  width: 2.4rem;
  height: 2.4rem;
  border-radius: var(--pill);
  border: 1px solid color-mix(in srgb, var(--paper) 30%, transparent);
  background: color-mix(in srgb, var(--paper) 10%, transparent);
  color: var(--paper);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color .18s var(--ease);
}
.lightbox-close:hover { background: color-mix(in srgb, var(--paper) 22%, transparent); }

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox-figure,
  .share-btn { transition: none; }
  .lightbox-figure { transform: none; }
}

/* ---- zoom ----------------------------------------------------------------
   Scroll to zoom about the cursor, drag to pan, buttons for anyone who would
   rather not guess. The percentage doubles as the reset control. */

.zoom-bar {
  position: absolute;
  bottom: clamp(.75rem, 2.5vw, 1.75rem);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: .35em;
  padding: .35em;
  border-radius: var(--pill);
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid color-mix(in srgb, var(--paper) 18%, transparent);
}
.zoom-bar .share-btn {
  border-color: transparent;
  color: color-mix(in srgb, var(--paper) 88%, var(--ink));
  min-width: 2.4em;
  text-align: center;
}
.zoom-bar .share-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--paper) 18%, transparent);
  border-color: transparent;
  color: var(--paper);
}
.zoom-bar .share-btn:disabled { opacity: .35; cursor: default; }
.zoom-bar .zoom-level {
  font-variant-numeric: tabular-nums;
  min-width: 4.2em;
  letter-spacing: .02em;
}

.lightbox-figure img { transform-origin: center center; will-change: transform; }
.lightbox.is-zoomed .lightbox-figure img { cursor: grab; }
.lightbox.is-dragging .lightbox-figure img { cursor: grabbing; }
/* Panning a zoomed picture should not also drag the browser's own gesture. */
.lightbox.is-zoomed .lightbox-figure img { touch-action: none; }
.lightbox.is-zoomed .lightbox-figure figcaption { opacity: .35; }
.lightbox-figure figcaption { transition: opacity .18s var(--ease); }

/* ---- icons ---------------------------------------------------------------
   Line icons at the text's own colour, so a control is one object rather than
   a glyph next to a word. The label stays: an icon alone is a guess. */

.share-btn {
  display: inline-flex;
  align-items: center;
  gap: .45em;
}
.share-btn svg {
  flex: none;
  display: block;
  opacity: .78;
  transition: opacity .18s var(--ease);
}
.share-btn:hover svg { opacity: 1; }
.share-btn.is-done svg { opacity: 1; }

.zoom-bar .share-btn { gap: 0; padding: .5em .7em; }
.zoom-bar .zoom-level { padding: .5em .9em; }

.lightbox-close {
  display: grid;
  place-items: center;
}

/* The picture is a thing to look at, not a thing to drag off the page. The
   browser's native image drag would otherwise hijack every pan. */
.lightbox-figure img,
.is-zoomable {
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

/* A heading that exists for structure rather than for the eye. The masthead is
   an SVG, so the front page would otherwise have no readable <h1> at all — for
   a screen reader or for a crawler. It says what the masthead says. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ===========================================================================
   Pictures that keep their own shape.

   Every media box above declares a fixed aspect ratio, and it had to: for most
   of this publication's life the cards held no photograph at all, only the
   generated `.plate` illustration, which has no intrinsic size and needs a box
   to be drawn into. Cropping was free because there was nothing to crop.

   That stopped being true the moment real announcement images arrived. They are
   16:9 or 1.91:1, they carry the subject's own wordmark near an edge, and a
   3:2 card cropped exactly that off — the Higgsfield card lost the first word
   of its own headline on the left and the logo on the right.

   So the box now depends on what is in it. A plate keeps its designed ratio.
   A real image sets the ratio itself, from the width and height already on the
   tag, and is never cropped. `:has()` is what makes that expressible in CSS
   rather than requiring a second class from the renderer.
   =========================================================================== */

.card-media:has(.shot),
.row-media:has(.shot),
.lead-media:has(.shot) {
  aspect-ratio: auto;
}


/* ===========================================================================
   The scrollbar, and the bar that says how far in you are.

   A browser's default scrollbar is a grey system object sitting against a
   cream page, and on a site this deliberate about its two inks it is the one
   element that belongs to somebody else's palette. It is restyled rather than
   hidden: it is still the only thing you can grab, and taking that away to
   make a page look tidier is a trade against the reader.
   =========================================================================== */

/* Firefox only, and it has to be fenced off. Chrome supports these two
   properties as well, and an element that sets either of them gets the
   standard scrollbar and none of the ::-webkit- rules below — so declaring
   them unconditionally on <html> threw away the entire theme in Chrome, and
   `scrollbar-color` inherits, so it threw it away for every scroll container
   on the page too. Measured: with this rule outside the fence, a
   ::-webkit-scrollbar-thumb painted pure red renders zero red pixels.

   @supports selector() is the fence. Chrome answers yes and skips the block;
   Firefox, which has no ::-webkit-scrollbar, answers no and takes it. */
@supports not selector(::-webkit-scrollbar) {
  html {
    scrollbar-width: thin;
    scrollbar-color: var(--faint) transparent;
  }
}

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--ink) 26%, var(--paper));
  border-radius: var(--pill);
  /* Inset by a transparent border so the thumb reads as a rule in the margin
     rather than a bar bolted to the edge of the glass. */
  border: 3px solid transparent;
  background-clip: content-box;
  transition: background-color .18s var(--ease);
}
::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--ink) 52%, var(--paper));
  background-clip: content-box;
}
::-webkit-scrollbar-corner { background: transparent; }
/* The stepper buttons at the ends. Chrome draws them on a horizontal
   scrollbar as two unstyled grey chevrons, which is a system widget sitting
   in the middle of a page that has its own arrows six pixels away. */
::-webkit-scrollbar-button { display: none; }

/* A long read deserves to say how long. One ink rule across the top, drawn
   only on article pages and only once there is enough page to be worth
   measuring — a progress bar on a screen-and-a-half is decoration. */
.progress {
  position: fixed;
  inset: 0 auto auto 0;
  width: 100%;
  height: 2px;
  z-index: 40;
  transform: scaleX(var(--read-progress, 0));
  transform-origin: 0 50%;
  background: var(--ink);
  pointer-events: none;
}

/* ===========================================================================
   Hover.
   A card is a link the size of a paragraph, so it should answer the pointer —
   quietly. The picture warms a little, the border firms up, the whole card
   rises by a single pixel. Nothing moves far enough to shift what is beside
   it, because a grid that reflows under the cursor is worse than one that
   does nothing at all.
   =========================================================================== */

.card,
.row {
  transition: transform .22s var(--ease);
}
.card-media,
.row-media {
  transition: border-color .22s var(--ease), box-shadow .22s var(--ease);
}
.card-media .shot,
.row-media .shot {
  transition: filter .28s var(--ease), transform .28s var(--ease);
}

.card:hover,
.row:hover { transform: translateY(-1px); }

.card:hover .card-media,
.row:hover .row-media {
  border-color: color-mix(in srgb, var(--ink) 42%, var(--paper));
  box-shadow: 0 2px 14px color-mix(in srgb, var(--ink) 9%, transparent);
}

/* The pictures are set in grey; letting a little of the colour back on hover
   is the cheapest possible signal that the card is live. */
.card:hover .shot,
.row:hover .shot { filter: grayscale(.55) contrast(1.06); }

.card:hover .card-head a,
.row:hover .row-head a { text-decoration: underline; text-underline-offset: 4px; }

@media (hover: none) {
  .card:hover, .row:hover { transform: none; }
  .card:hover .shot, .row:hover .shot { filter: grayscale(1) contrast(1.08); }
}

@media (prefers-reduced-motion: reduce) {
  .card, .row, .card-media, .row-media, .card-media .shot, .row-media .shot { transition: none; }
  .card:hover, .row:hover { transform: none; }
}

/* Reading time sits with the timestamp, in the same quiet register: it answers
   "have I got time for this", which is the same kind of question as "is this
   still current". It is an estimate of an average reader and is not dressed up
   as anything more precise. */
.byline .read-time {
  color: var(--quiet);
  white-space: nowrap;
}
.byline .read-time::before {
  content: "·";
  margin: 0 .55em 0 .1em;
  color: var(--faint);
}

/* --- Search ---------------------------------------------------------------
   The box sits in the masthead's utility row and stays a real form, so it
   works with the script blocked. On a phone the utility row collapses and the
   box goes full width above the results rather than competing with the
   wordmark for a line that has no room for both. */

.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.find input {
  font: inherit;
  font-size: 0.82rem;
  font-family: var(--font-body);
  color: var(--read);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--ghost);
  padding: 0.15rem 0.1rem;
  width: 8.5rem;
  transition: width 160ms ease, border-color 160ms ease;
}
.find input::placeholder { color: var(--faint); }
.find input:focus {
  outline: none;
  width: 12rem;
  border-bottom-color: var(--flat);
}
.find input::-webkit-search-cancel-button { filter: grayscale(1); }

.find-lg { margin: 0 0 1.1rem; }
.find-lg input {
  width: 100%;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 4vw, 2rem);
  padding: 0.3rem 0;
  border-bottom-width: 2px;
}
.find-lg input:focus { width: 100%; }

.search-note {
  font-family: var(--font-body);
  font-size: 0.82rem;
  color: var(--faint);
  margin: 0 0 1.6rem;
}

.search-page .row { border-top: 1px solid var(--trace); }
.search-page .row:last-child { border-bottom: 1px solid var(--trace); }
.row-link { display: block; padding: 1.1rem 0; text-decoration: none; color: inherit; }
.row-link:hover .row-headline { text-decoration: underline; text-underline-offset: 3px; }
.row-headline {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.4vw, 1.4rem);
  font-weight: 600;
  line-height: 1.22;
  margin: 0.3rem 0 0.35rem;
}
.row-excerpt {
  font-family: var(--font-body);
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--quiet);
  margin: 0;
}
/* FTS5 writes these around the matched words. Two inks only, so the hit is
   marked by weight and a rule under it rather than by a highlighter colour. */
.row-excerpt mark {
  background: none;
  color: var(--flat);
  font-weight: 600;
  box-shadow: inset 0 -0.35em 0 var(--ghost);
}

@media (max-width: 720px) {
  .find { order: 99; width: 100%; }
  .find input, .find input:focus { width: 100%; }
}

/* --- The tip form ---------------------------------------------------------- */

.tip-form { display: grid; gap: 0.4rem; margin: 1.8rem 0 1.2rem; max-width: 34rem; }
.tip-form label {
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 0.7rem;
}
.tip-form .opt { text-transform: none; letter-spacing: 0; font-style: italic; }
.tip-form textarea,
.tip-form input {
  font: inherit;
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--read);
  background: transparent;
  border: 1px solid var(--ghost);
  padding: 0.6rem 0.7rem;
  width: 100%;
  resize: vertical;
}
.tip-form textarea:focus,
.tip-form input:focus { outline: none; border-color: var(--flat); }
.tip-form button {
  font-family: var(--font-body);
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paper);
  background: var(--ink);
  border: 0;
  padding: 0.7rem 1.4rem;
  margin-top: 1rem;
  justify-self: start;
  cursor: pointer;
}
.tip-form button:disabled { opacity: 0.45; cursor: default; }
.tip-form .cf-turnstile { margin-top: 1rem; }
.tip-status {
  font-family: var(--font-body);
  font-size: 0.85rem;
  color: var(--quiet);
  margin: 0.6rem 0 0;
  min-height: 1.2em;
}

/* The legal row. An ad network wants a privacy policy reachable from every
   page, and a reader wants to know where a correction goes — the footer is
   where both expectations already live. */
.foot-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  margin: 18px 0 14px;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.foot-legal a {
  color: var(--faint);
  text-decoration: none;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
}
.foot-legal a:hover { color: var(--ink); }


/* ===========================================================================
   The masthead on a phone.

   Three rows for a wordmark, three utility links, a search field and five
   sections is most of what a 375px screen has above the fold. The sections are
   the only part of that a reader came for; About, Tips and RSS are things you
   go looking for once, and they are already in the footer on every page. So on
   a phone the bar keeps the wordmark and the sections, the search collapses to
   its own icon, and the rest moves out of the way.
   =========================================================================== */



@media (prefers-reduced-motion: reduce) {
  .find input[type="search"] { transition: none; }
}

/* ===========================================================================
   The rail.

   The front page used to be a grid: a lead spanning two columns and cards
   filling the rest. Every arrangement of it left a hole, and the reason is
   structural rather than a matter of tuning — a grid row is as tall as its
   tallest cell, the lead is always the tallest thing on the page, and whatever
   sits beside it stops short. Changing the card count moved the hole; it never
   closed it.

   One horizontal row has no second dimension for a hole to live in. What it
   costs is that later stories are off-screen until you scroll, so: it scrolls
   natively with snap points, the scrollbar is visible rather than hidden, the
   first card is wider than the rest so the row obviously continues past the
   edge, and nothing rotates on a timer. Nothing here is behind a mechanism
   that moves without being asked.
   =========================================================================== */

.rail-wrap {
  position: relative;
  /* Declared here rather than on .rail because the arrows are siblings of the
     rail, and they need the card's width to know where the picture ends. */
  --card-w: clamp(272px, 30vw, 392px);
}

.rail {
  display: flex;
  gap: 30px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Must equal the bleed, and was 2px. With `x mandatory` the browser snaps the
     first card to the snapport on load, and the snapport starts at
     scroll-padding-left — so the rail scrolled itself 14px on every phone page
     load and parked the lead card two pixels off the screen edge, eating the
     padding below. The gutter was in the stylesheet; it just never survived to
     paint. */
  scroll-padding-left: var(--rail-bleed, 0px);
  /* Bleed to the page edges so a card can sit half-cut at the boundary, which
     is what tells a reader there is more without needing a caption to say so. */
  margin: 0 calc(var(--rail-bleed, 0px) * -1);
  padding: 2px var(--rail-bleed, 0px) 18px;
  overscroll-behavior-x: contain;
  /* No scrollbar-width / scrollbar-color here on purpose. Chrome drops every
     ::-webkit-scrollbar rule for an element that sets either of them, so
     asking for a thin one traded the site's themed bar for the system's —
     complete with the two grey stepper chevrons the theme exists to avoid. */
}
/* Every card the same width, deliberately. An emphatic lead was tried first —
   wider card, bigger picture — and it put the ragged bottom straight back: a
   16:9 image at 620px is 135px taller than the same ratio at 380px, so the
   short cards trailed empty paper again, only sideways. Equal widths mean the
   only height difference left is the picture's own ratio and a headline's line
   count, which is tens of pixels rather than hundreds. The lead is marked by
   type, not by size. */
.rail > .card {
  flex: 0 0 auto;
  width: var(--card-w);
  scroll-snap-align: start;
}
/* Three lines, so a long standfirst cannot make its card taller than the rest.
   Line-clamp rather than a height in pixels: the box then follows the type
   scale at every breakpoint instead of being re-tuned at each one. */
.rail .card-stand {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}
.rail > .card-lead .card-head { font-size: 27px; }

.rail:focus-visible {
  outline: 2px solid var(--accent, #b45309);
  outline-offset: 4px;
}

/* The arrows are added by news.js. They sit over the bleed, not over a card. */
.rail-nav {
  position: absolute;
  /* Centred on the picture, not on the card — over the headline it sits on the
     words. The fallback assumes .card-media's declared 16:10 box, but a real
     photograph overrides that with its own ratio (see the :has(.shot) rule),
     so the height cannot be known from the stylesheet. news.js measures the
     first picture and writes --media-h; this is what it is worth before that
     happens, and the arrows only exist because news.js ran anyway. */
  top: calc(var(--media-h, calc(var(--card-w) * 0.625)) / 2 - 17px);
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--pill);
  border: 1px solid var(--ghost);
  background: color-mix(in srgb, var(--paper) 88%, var(--ink));
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  transition: opacity .18s var(--ease), background-color .18s var(--ease);
  z-index: 3;
}
.rail-wrap:hover .rail-nav,
.rail-nav:focus-visible { opacity: 1; }
.rail-nav[disabled] { opacity: 0 !important; pointer-events: none; }
.rail-nav:hover { background: var(--paper); }
.rail-prev { left: -6px; }
.rail-next { right: -6px; }

@media (hover: none) {
  /* A finger already knows how to scroll a row. */
  .rail-nav { display: none; }
}

@media (max-width: 620px) {
  /* Bleed to the screen edges so a card sits half-cut at the boundary. On a
     phone that half-card is the only affordance there is: there are no arrows
     and no hover, so the row has to look cut off to look scrollable. */
  .rail { gap: 22px; --rail-bleed: var(--gutter); }
  .rail > .card { width: 82vw; }
  .rail > .card-lead .card-head { font-size: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  .rail { scroll-behavior: auto; }
  .rail-nav { transition: none; }
}

/* ===========================================================================
   Tables and lists in the body.

   Six of these used to be SVGs — typeset text drawn as a picture. That costs
   more than it looks: the words cannot be selected, searched, translated or
   reflowed, a screen reader gets one summary label instead of the cells, and
   correcting a number means redrawing the file. Whatever a picture can say
   that words cannot is still drawn; a table is not one of those things.

   The house style for one: no grid. A page set in this much rule-work already
   has enough lines, so the only ones here are under the head and between rows,
   and they are the same hairline the article uses everywhere else. Figures get
   tabular numerals so a column of them lines up on the decimal.
   =========================================================================== */

.table-scroll {
  overflow-x: auto;
  margin: 2rem 0;
  /* A table wider than the column scrolls inside its own box. Without this the
     page itself scrolls sideways, which moves the article out from under the
     reader to show them a number. */
  overscroll-behavior-x: contain;
}
.table-scroll:focus-visible {
  outline: 2px solid var(--accent, #b45309);
  outline-offset: 3px;
}

.prose table {
  width: 100%;
  min-width: 30rem;
  border-collapse: collapse;
  font: 400 15.5px/1.5 var(--font-body);
  color: var(--ink);
}
.prose thead th {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 1.3px;
  text-transform: uppercase;
  color: var(--quiet);
  text-align: left;
  padding: 0 1rem .55rem 0;
  border-bottom: 1px solid var(--ink);
  /* Auto table layout hands a column width in proportion to its content, so a
     column of short terms got 115px and broke its own two-word heading over
     two lines. The heading is the shortest thing in the column and the least
     able to spare a line; it sets the floor instead. Safe inside the scroller,
     which is what stops a wide table from widening the page. */
  white-space: nowrap;
}
.prose tbody td {
  padding: .72rem 1rem .72rem 0;
  border-bottom: 1px solid var(--ghost);
  vertical-align: top;
}
.prose tbody tr:last-child td { border-bottom: 0; }
.prose th:last-child, .prose td:last-child { padding-right: 0; }

/* Figures line up on the decimal, and sit against the number beside them. */
.prose th.num, .prose td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  /* Deliberately no nowrap and no width. Both were added to stop a table
     overflowing a 375px screen, and both were measured against a stale
     stylesheet — Chrome had cached the previous build, so the reading did not
     reflect the rule being tested. What actually fixed that overflow was
     letting the caption break (see below); a caption counts toward a table's
     width, and one unbreakable endpoint path in it was the whole 46px.

     Re-measured with the cache off, each of these makes things worse rather
     than better. nowrap locks up the prose that shares a figures column —
     "about 6×, over three quarters" became unbreakable and pushed a
     three-column table 77px past the screen. width:1% collapses the column to
     its longest word, which broke "about 7.7×" over two lines on a screen
     with room to spare. Auto layout, left alone, fits all eight tables. */
}
.prose th.mid, .prose td.mid { text-align: center; }

/* width:1% is the old trick for "take only what you need": auto layout reads
   it as a request it cannot honour and falls back to the column's content
   width, leaving the rest to the label column. Without it the two columns
   split the space by their content's proportions, and at 375px the yes/no
   table asked for 385px inside a 339px box — the answer column, the one the
   table exists to show, was the part pushed off the edge. The column still
   ends at the table's right margin, so nothing moves on a wide screen. */


/* The note under a table is the same voice as a figure's caption, because it
   does the same job — where the numbers came from, and what they exclude. */
.prose caption {
  caption-side: bottom;
  padding-top: .9rem;
  font: 400 13.5px/1.55 var(--font-body);
  color: var(--quiet);
  text-align: left;
  /* A caption is part of the table's width calculation, so one unbreakable
     token in it sets the width of the whole table. An endpoint path —
     /v1/compliance/apps/sessions/local/{session_id}/messages — is 300px of
     exactly that, and it alone pushed a two-column table 46px past a 375px
     screen, taking the answer column with it. */
  overflow-wrap: anywhere;
}

/* margin:auto, not text-align. The cell centres its text, but a block-level
   SVG is not text and sat against the left edge under a centred heading. */
.prose td svg { display: block; margin: 0 auto; color: var(--quiet); }

.prose .bullets {
  margin: 1.4rem 0;
  padding: 0;
  list-style: none;
}
.prose .bullets li {
  position: relative;
  padding: 0 0 0 1.15rem;
  margin: 0 0 .6rem;
}
/* An em dash rather than a disc: the page has no other round marks in it. */
.prose .bullets li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--faint);
}

@media (max-width: 620px) {
  /* No floor on a phone. The floor was a guess, and on a 375px screen it cost
     the compliance table its answer column — the reader saw ten labels and had
     to scroll sideways to learn which were yes. Letting the cells wrap gives a
     cramped table; hiding the column that carries the answer gives a wrong
     one. The scroller stays for tables too wide even to wrap. */
  .prose table { min-width: 0; font-size: 14.5px; }
  .prose tbody td { padding: .6rem .7rem .6rem 0; }
  .prose thead th { white-space: normal; padding-right: .7rem; }
}

/* ===========================================================================
   Charts, drawn by the page.

   The reference for these is Artificial Analysis, which publishes the best
   charts on this beat and publishes them as images. An image of a chart is a
   picture of numbers: nothing in it can be selected, searched or read aloud,
   the labels do not reflow, and a leaderboard that moves weekly needs a
   redraw every time it does.

   So the numbers are the markup and the mark is placed from them. Underneath
   every chart here is the same table as anywhere else on the page — same head
   rule, same hairlines, same figures. Turn these rules off and a correct
   table is still standing, which is also what a reader gets if the stylesheet
   never arrives.

   Two shapes, picked by what the numbers do. Bars from zero when the values
   span an order of magnitude; dots on a shared axis when they sit close
   together and their order is the point. Both get an axis, because that is
   the shape people already know how to read.
   =========================================================================== */

.prose table.chart tbody th {
  font-weight: 400;
  text-align: left;
  padding: .5rem 1rem .5rem 0;
  border-bottom: 0;
  vertical-align: middle;
}
.prose table.chart tbody td {
  border-bottom: 0;
  padding: .5rem 1rem .5rem 0;
  vertical-align: middle;
}
.prose table.chart tbody td.num {
  font-variant-numeric: tabular-nums;
  padding-left: 0;
}

/* The plot column takes whatever the labels and figures leave. 52% is a
   request rather than a size — auto layout treats it as a preference and
   serves the label column's own needs first. */
.prose td.plot, .prose th.plot { width: 52%; padding-right: 1rem; }

/* The track is the axis's full width, and carries the gridlines. One
   background image rather than an element per line: the lines are decoration
   over a row that already has its number printed at the end of it. */
.prose .track {
  position: relative;
  display: block;
  height: 20px;
  background-image: linear-gradient(to right, var(--ghost) 1px, transparent 1px);
  background-size: calc(100% / var(--intervals, 4)) 100%;
}
.prose .track::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  border-right: 1px solid var(--ghost);
}

/* --- bars ---------------------------------------------------------------- */

.prose .plot-bar {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: var(--w, 0);
  height: 11px;
  border-radius: 1px;
  background: var(--ink);
  /* A value near zero would otherwise render as nothing at all, which reads
     as a missing row rather than a small number. */
  min-width: 2px;
}

/* --- dots ---------------------------------------------------------------- */

.prose .plot-dot {
  position: absolute;
  left: var(--p, 0);
  top: 50%;
  width: 11px;
  height: 11px;
  margin: -5.5px 0 0 -5.5px;   /* centred on its value, not starting at it */
  border-radius: 50%;
  background: var(--ink);
}
/* The second band is hollow rather than a second colour. Two inks is the
   whole palette, and the distinction then survives being printed and being
   looked at by someone who cannot separate two hues. */
.prose tr.s2 .plot-dot {
  background: var(--paper);
  box-shadow: inset 0 0 0 2px var(--ink);
}
.prose tr.s2 .plot-bar {
  background: repeating-linear-gradient(90deg, var(--ink) 0 2px, transparent 2px 5px);
  box-shadow: inset 0 0 0 1px var(--ghost);
}

/* --- bands, axis --------------------------------------------------------- */

.prose tr.band th {
  padding: 1.5rem 0 .45rem;
  font: 600 11px/1 var(--font-body);
  letter-spacing: 1.3px;
  text-transform: uppercase;
  color: var(--quiet);
}
.prose table.chart tbody tr.band:first-child th { padding-top: .2rem; }

.prose tr.axis .track {
  height: auto;
  background: none;
  border-top: 1px solid var(--ink);
  padding-top: .4rem;
}
.prose tr.axis .track::after { content: none; }
.prose tr.axis td { padding-top: .2rem; }
.prose .tick {
  position: absolute;
  left: var(--p);
  transform: translateX(-50%);
  font: 400 11px/1 var(--font-body);
  font-variant-numeric: tabular-nums;
  color: var(--faint);
  white-space: nowrap;
}
/* The end labels sit inside the axis rather than hanging off it. */
.prose .tick:first-child { transform: none; }
.prose .tick:last-child { transform: translateX(-100%); }

/* --- the light interaction ------------------------------------------------ */

.prose table.chart tbody tr:not(.band):not(.axis):hover { background: color-mix(in srgb, var(--ink) 4%, transparent); }
.prose table.chart tbody tr:hover .plot-dot { transform: scale(1.35); }
.prose table.chart tbody tr:hover .plot-bar { background-color: var(--ink); }
.prose .plot-dot { transition: transform .16s var(--ease); }

/* A guide down to the axis, so a dot can be read against a tick without
   sliding a finger across the screen. Drawn only on hover, and only on the
   row being read. */
.prose table.chart tbody tr:hover .plot-dot::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1px;
  height: 40vh;
  background: linear-gradient(to bottom, var(--faint), transparent);
  transform: translateX(-50%);
  pointer-events: none;
}
.prose table.chart { overflow: hidden; }   /* the guide stops at the chart */

/* Marks grow in when the chart is first scrolled to — news.js adds .is-lit.
   Declared so that without the script the chart is simply already drawn. */
/* A dot is never hidden, only small. Fading it to nothing means a failed
   animation leaves an empty axis, and an empty axis is indistinguishable from
   a chart with no data in it. */
.prose table.chart.will-draw .plot-dot { transform: scale(.4); }
.prose table.chart.will-draw .plot-bar { width: 0; }
.prose table.chart.will-draw.is-lit .plot-dot {
  transform: scale(1);
  transition: transform .35s var(--ease);
}
.prose table.chart.will-draw.is-lit .plot-bar {
  width: var(--w, 0);
  transition: width .5s var(--ease);
}

@media (max-width: 620px) {
  /* The label needs the room on a phone; the plot is what can afford to be
     shorter, since the figure beside it carries the precision. */
  .prose td.plot, .prose th.plot { width: 40%; padding-right: .6rem; }
  .prose .tick { font-size: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .prose table.chart.will-draw .plot-bar { width: var(--w, 0); }
  .prose table.chart.will-draw .plot-dot { transform: scale(1); }
  .prose .plot-dot, .prose .plot-bar { transition: none; }
}
