/* ═══════════════════════════════════════════════════════════════════
   PROTOTYPE — shared stylesheet for the "editorial paper" exploration
   Standalone. Not linked to, or built from, the live site's CSS.
   Extends the palette/typography proven on the homepage exploration to
   every page type the real site has.
═══════════════════════════════════════════════════════════════════ */

:root, [data-theme="paper"] {
  /* Horizontal gutter for .wrap and for every full-bleed element that
     cancels it with a negative margin. Declared once so the two can
     never drift — see .masthead for what happened when they did. */
  --wrap-pad: 22px;
  --font-serif: 'Source Serif 4', Georgia, serif;
  --font-sans:  'Inter', system-ui, sans-serif;
  --font-mono:  'IBM Plex Mono', monospace;
  --bg:        #fdfcf9;
  --surface:   #ffffff;
  --surface-2: #f6f2e9;
  --rule:      #e2dccc;
  --ink:       #191712;
  --ink-soft:  #4a4438;
  --ink-mute:  #6f6857;
  --accent:    #17457e;
  --accent-tint: #dfe7f2;
  --status-live:     #3f6b46;
  --status-trial:    #a67c1f;
  --status-paused:   #8a5a2e;
  --status-ended:    #8c8570;
  --status-proposed: #6b7280;
  --shadow: 0 1px 2px rgba(28,26,21,0.04), 0 8px 24px rgba(28,26,21,0.06);
}
[data-theme="ink"] {
  --bg:        #14130f;
  --surface:   #1b1913;
  --surface-2: #221f18;
  --rule:      #332f24;
  --ink:       #f3ede1;
  --ink-soft:  #d2cab6;
  --ink-mute:  #9d957f;
  --accent:    #7fa8db;
  --accent-tint: #1a2537;
  --status-live:     #7fb787;
  --status-trial:    #d4a94a;
  --status-paused:   #c98a52;
  --status-ended:    #8c8570;
  --status-proposed: #9aa3b0;
  --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 28px rgba(0,0,0,0.45);
}

* { box-sizing: border-box; }
:root, [data-theme="ink"] { color-scheme: light; }
[data-theme="ink"] { color-scheme: dark; }
html, body { background-color: var(--bg) !important; }
body {
  margin: 0; color: var(--ink);
  font-family: var(--font-sans); font-weight: 400;
  font-size: 16px; line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
.wrap { max-width: 900px; margin: 0 auto; padding: 0 var(--wrap-pad); background-color: var(--bg); }
/* Now the same width as .wrap. It was 700px, which is what held the
   long-form guide, about, story and technology pages inside a narrower
   column than the rest of the site. Kept as a separate class rather than
   swapped out across fourteen files: the distinction is still meaningful
   if a genuinely narrower measure is ever wanted again, and collapsing
   the two would mean editing every page that references it. */
.wrap-narrow { max-width: 900px; margin: 0 auto; padding: 0 var(--wrap-pad); background-color: var(--bg); }
a { color: inherit; text-decoration: none; }
.serif { font-family: var(--font-serif); }
.eyebrow {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--accent);
}
.meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute); }

/* ── MASTHEAD ── */
header { border-bottom: 1px solid var(--rule); padding: 20px 0 0; }
/* A photograph from the site's own existing image set (the same
   halftone-newsprint treatment already used for story thumbnails, so this
   reads as consistent with the rest of the site rather than a bolted-on
   stock photo) rather than a flat colour, deliberately kept full-bleed to
   the true viewport edge via the same padding/negative-margin technique
   used on .masthead-bar below.

   The scrim is the part that actually matters for readability. A JPEG's
   tones cannot be trusted to contrast correctly with text on their own —
   this blends the page's own background colour over the photo at 87%
   opacity using color-mix(), so the overlay is always the right colour
   for whichever theme is active without maintaining two separate values,
   and the text underneath is reading against something very close to its
   normal background, with just a hint of the image showing through. */
/* Fixed navy, not a theme variable, and deliberately so: the masthead
   reads as a consistent brand banner in both the paper and ink themes,
   rather than flipping to a pale band that would look like a mismatched
   patch sitting in the middle of an otherwise dark page. #133a6b is a
   slightly darkened version of the site's own accent colour (#17457e,
   the paper theme's --accent), reused here rather than introducing an
   unrelated new colour. Text colours below are fixed to match, for the
   same reason. */
.masthead {
  /* Full-bleed: the negative margin must always equal the wrap's padding.
     It was hardcoded to -22px while the wrap dropped to 15px on mobile,
     so the masthead bled 7px past the viewport on every phone width and
     put a horizontal scrollbar on every page of the site. Both now derive
     from --wrap-pad and cannot drift apart again. */
  text-align: center; padding: 20px var(--wrap-pad) 18px;
  margin: 0 calc(var(--wrap-pad) * -1);
  background: #133a6b;
}
.masthead .eyebrow { display: block; margin-bottom: 12px; color: #fdfcf9; }
.wordmark {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(36px, 8.5vw, 58px); letter-spacing: -0.02em;
  color: #fdfcf9; margin: 0;
}
/* A lighter blue than the site's normal navy accent, specifically because
   a close variant of that navy is what the masthead background now is —
   navy-on-navy would vanish. This is the ink theme's own --accent value,
   which already exists precisely to read clearly against a dark
   background, reused here rather than inventing a third blue. */
.wordmark em { font-style: italic; color: #7fa8db; }
/* Base colour stays --ink-mute: this class is reused on the home page for
   the lead story's summary text (render-home.js), a completely different
   context on the normal light page background, not the masthead. Fixed
   light colour for masthead use only, scoped below. */
.dek {
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-mute);
  margin-top: 8px;
}
.masthead .dek { color: #fdfcf9; opacity: 0.75; }
.masthead-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  border-top: 1px solid var(--rule); background: var(--surface-2);
  padding: 6px var(--wrap-pad); margin: 0 calc(var(--wrap-pad) * -1); position: relative;
}
.primary-nav { display: flex; gap: 4px; flex-wrap: wrap; margin: -8px 0; min-width: 0; }
.primary-nav a {
  font-size: 14px; font-weight: 600; color: var(--ink-soft);
  padding: 10px 10px 8px; border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
  white-space: nowrap;
}
.primary-nav a:hover, .primary-nav a.current { color: var(--accent); border-bottom-color: var(--accent); }

/* Hamburger toggle: a plain CSS-drawn icon rather than a Unicode glyph,
   which renders with inconsistent weight across platforms. Now the
   permanent mechanism for reaching primary nav at every width, not a
   narrow-screen-only fallback — see the removed @media block below this
   rule for what that used to mean and why it changed. */
.nav-toggle {
  display: flex; width: 40px; height: 40px; flex-shrink: 0;
  background: transparent; border: 1px solid var(--rule); border-radius: 4px;
  cursor: pointer; padding: 0; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px;
}
.nav-toggle span {
  display: block; width: 18px; height: 2px; background: var(--ink);
  border-radius: 1px; transition: transform 0.2s ease, opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) {
  .nav-toggle span { transition: none; }
}

/* The dropdown panel: hidden until opened, at every width, rather than a
   flat row on wide screens that becomes a dropdown only below 640px.
   Anchored with left:0 alone, matching the hamburger button's own
   position as the first item in .masthead-bar (which is what left:0
   actually aligns against, since that is the nearest positioned
   ancestor) — a real, previously-shipped bug here was pairing left:0
   with right:0, which forces the panel to stretch all the way to the
   bar's opposite edge. That produced the correct look purely by
   coincidence on narrow screens, where the bar itself is narrow enough
   that "full bar width" and "a sensible dropdown width" are close to
   the same thing. At desktop widths the same rule would have stretched
   an open menu across the entire header, which is what "anchored" means
   here: sized to its own content, pinned to the button, not stretched
   to fill whatever container happens to contain it. */
.primary-nav {
  display: none; position: absolute; top: 100%; left: 0; z-index: 50;
  flex-direction: column; flex-wrap: nowrap; gap: 0; margin: 8px 0 0;
  min-width: 240px; width: max-content; max-width: min(320px, calc(100vw - 32px));
  background: var(--bg); border: 1px solid var(--rule);
  box-shadow: 0 10px 24px rgba(0,0,0,0.14);
  max-height: calc(100vh - 90px); overflow-y: auto;
}
.primary-nav.open {
  display: flex; animation: navReveal 0.18s ease both;
}
.primary-nav > a {
  padding: 14px 18px; width: 100%; border-bottom: 1px solid var(--rule);
  box-sizing: border-box; white-space: normal;
}
.primary-nav > a:last-child { border-bottom: none; }

/* ── SUBMENU (Tracker's own children) ──
   A second, nested disclosure inside the first: the outer hamburger
   panel holds top-level destinations, and Tracker is a button rather
   than a link within it, revealing its own children in place rather
   than navigating away. Indented and lightly shaded so it reads as
   "inside Tracker" rather than as more top-level siblings. */
.nav-submenu { border-bottom: 1px solid var(--rule); }
.nav-submenu:last-child { border-bottom: none; }
.nav-submenu-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: 14px 18px; box-sizing: border-box;
  background: none; border: none; cursor: pointer;
  font: inherit; font-weight: 600; color: var(--ink-soft); text-align: left;
}
.nav-submenu-toggle:hover, .nav-submenu-toggle.current { color: var(--accent); }
.nav-submenu-chevron { flex-shrink: 0; transition: transform 0.15s ease; }
.nav-submenu-toggle[aria-expanded="true"] .nav-submenu-chevron { transform: rotate(180deg); }
.nav-submenu-panel { display: none; background: var(--surface-2); }
.nav-submenu-panel.open { display: block; }
.nav-submenu-panel > a {
  display: block; padding: 12px 18px 12px 32px; box-sizing: border-box;
  border-top: 1px solid var(--rule); font-size: 13.5px;
}
.nav-submenu-panel > a:first-child { border-top: none; }
@media (prefers-reduced-motion: reduce) {
  .nav-submenu-chevron { transition: none; }
}

@keyframes navReveal {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .primary-nav.open { animation: none; }
}
.theme-btn {
  width: 40px; height: 40px; background: transparent; border: 1px solid var(--rule);
  color: var(--ink); cursor: pointer; font-size: 14px; border-radius: 50%; flex-shrink: 0;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.theme-btn:hover { border-color: var(--accent); }
.theme-btn:active { transform: scale(0.92); }

/* ── FEED STRUCTURAL / JS-MARKER CLASSES ──
   .feed-sentinel is the element the infinite-scroll IntersectionObserver
   watches, and .video-remember is the consent checkbox the video handler
   reads — both hooks with no visual properties of their own.

   .feed-timeline is now a tile grid rather than a single-column timeline
   with a connecting spine. This is, again, pure CSS layered onto markup
   that already exists unchanged: render-feed.js still groups stories by
   day and still emits the same .feed-item and .feed-date-divider
   elements it always has. CSS Grid lets a divider still span the full
   row above each day's group of tiles (grid-column: 1 / -1, set on
   .feed-date-divider further down) without either file's markup
   generation changing at all — the invariant that the server-rendered
   HTML and the client-side re-render on filter change must stay
   byte-identical holds exactly as it did for the timeline design. */
.feed-timeline {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  align-items: start;
}
.feed-sentinel { height: 1px; grid-column: 1 / -1; }
.video-remember { accent-color: var(--accent); }
.feed-item { position: relative; }

/* Date dividers get a larger, hollow marker — a waypoint distinct from an
   individual story, since a divider represents a boundary between days
   rather than a single piece of coverage. */
/* ── FALLBACK STORY IMAGES ──
   Marks an image as an abstract stand-in rather than a real photograph.
   Deliberately sized shorter than a real image: a placeholder should not
   claim the same visual weight as an actual picture of the event, and the
   difference reads as intentional rather than as a missing photo.
   object-fit: contain (not cover) keeps the geometry uncropped, and
   padding keeps it off the edges of its tinted panel. */
.feed-item-image.is-fallback,
.story-image.is-fallback {
  /* These are photographs now, not flat geometry, so they carry the same
     visual weight as a real publisher image and no longer need to be cut
     down to a thin band. Kept slightly shorter than a real photo, and
     slightly desaturated, so a reader can still tell the difference on a
     second look without it shouting. */
  object-fit: cover;
  max-height: 210px;
  filter: saturate(0.82) contrast(1.02);
}
.story-image.is-fallback { aspect-ratio: 16/7; max-height: 260px; }
.lead img.is-fallback { object-fit: cover; opacity: 0.72; filter: saturate(0.82); }

/* ── FALLBACK IMAGES ON THE INK THEME ──
   These are macro photographs of newsprint, and newsprint is inherently
   a light, warm-grey material — there is no physically plausible way to
   make it read as "dark paper" the way the rest of the ink theme is
   dark, so a straight brightness cut just makes them look muddy rather
   than intentional.

   Instead this leans on something newspapers actually produce as part of
   their own process: a negative or photostat proof. filter: invert(1)
   turns the pale paper dark and the dark ink pale, which is optically
   exactly what a negative of the same photograph looks like — not an
   effect bolted on, but a genuine alternate artefact of the same print
   process the light-theme version depicts. CSS filter values were
   checked against a real rendered screenshot before settling here, not
   assumed from the numbers alone.

   Brightness and contrast are nudged rather than left at a flat
   invert(1), because the un-adjusted inversion of a warm mid-grey image
   comes out slightly too flat and light for a near-black page. It is
   still meaningfully brighter than the surrounding chrome even after
   this — matched to it exactly would mean crushing all the texture that
   makes it worth including, and mainstream dark-mode news sites do not
   dim their real photography to match UI chrome either, for the same
   reason. */
[data-theme="ink"] .feed-item-image.is-fallback,
[data-theme="ink"] .story-image.is-fallback {
  filter: invert(1) brightness(0.88) contrast(1.08) saturate(0.82);
  border: 1px solid var(--rule);
}
[data-theme="ink"] .lead img.is-fallback {
  filter: invert(1) brightness(0.85) saturate(0.82);
  opacity: 0.85;
}

/* ── DEPLOYMENT CARDS (tracker) ──
   Replaces the table the tracker used to be. Each card has a scannable
   header (who / what / status) laid out on a grid, then prose that runs
   the full column width. The header collapses to a single column on
   mobile, which removes the column-squeezing problem entirely rather
   than managing it with breakpoint tweaks. */
.deploy-list { display: block; }
.deploy {
  margin: 0 -16px;
  border-bottom: 1px solid var(--rule);
  transition: background-color 0.15s ease;
}
.deploy:hover { background-color: var(--surface-2); }
.deploy:first-of-type { margin-top: -6px; }
.deploy:last-of-type { border-bottom: none; }

/* The summary is the whole point of this rework: force, country,
   technology and status at a glance, one row, no scrolling past prose to
   see what exists. Everything else moves into .deploy-detail, revealed
   only on open. Force and technology text is allowed to wrap rather than
   truncate — several force names in the real data run to a full sentence
   ("1,000+ US agencies across 49 states, incl. Fort Worth TX...") and
   an ellipsis would quietly hide the rest of that with nowhere else it
   ever gets shown in full. */
.deploy-summary {
  display: flex; align-items: center; gap: 14px;
  padding: 15px 16px; cursor: pointer; list-style: none;
}
.deploy-summary::-webkit-details-marker { display: none; }
.deploy-summary-text {
  display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0;
}
.deploy-force {
  font-family: var(--font-serif); font-weight: 700;
  font-size: 16px; line-height: 1.3; color: var(--ink);
}
.deploy-tech-label { font-size: 13px; color: var(--ink-soft); }
.deploy-status {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--ink-soft); white-space: nowrap; flex-shrink: 0;
}
.deploy-chevron { flex-shrink: 0; color: var(--ink-mute); transition: transform 0.15s ease; }
.deploy[open] .deploy-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .deploy-chevron, .deploy-detail { transition: none; animation: none !important; }
}

.deploy-detail {
  padding: 2px 16px 26px;
  animation: fadeUp 0.2s ease both;
}
.deploy-detail-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px;
  margin-bottom: 14px; padding-top: 2px;
}
.deploy-country-full { font-size: 12.5px; color: var(--ink-mute); }
.deploy-tech {
  font-size: 14.5px; font-weight: 500; color: var(--ink-soft);
  border-bottom: 1px solid var(--rule);
}
a.deploy-tech:hover { color: var(--accent); border-bottom-color: var(--accent); }
.deploy-vendor { font-size: 12px; color: var(--ink-mute); }
.deploy-since {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute);
}

/* Prose. Runs the full column width, which is the whole point of the
   rework — the old detail row was capped at 62ch inside a 900px column. */
.deploy-facts {
  display: grid; grid-template-columns: max-content minmax(0, 1fr);
  gap: 5px 16px; margin: 0 0 14px;
  font-size: 14px; line-height: 1.6;
}
.deploy-facts dt {
  font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-mute); padding-top: 4px;
}
.deploy-facts dd { margin: 0; color: var(--ink-soft); }
.deploy-notes {
  font-size: 15px; line-height: 1.72; color: var(--ink); margin: 0 0 14px;
}
.deploy-source {
  font-family: var(--font-mono); font-size: 11px;
  line-height: 1.6; color: var(--ink-mute); margin: 0;
}
.deploy-source a { color: var(--accent); }

.deploy-controls { display: flex; justify-content: flex-end; margin: 0 0 10px; }
.deploy-expand-all {
  font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  letter-spacing: 0.02em; color: var(--ink-soft); background: none;
  border: 1px solid var(--rule); padding: 9px 15px; cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.deploy-expand-all:hover { border-color: var(--accent); color: var(--accent); }

@media (max-width: 720px) {
  .deploy-summary { flex-wrap: wrap; gap: 6px 14px; }
  .deploy-status { flex-basis: 100%; padding-left: 42px; order: 1; }
  .deploy-chevron { position: absolute; top: 17px; right: 16px; }
  .deploy-summary { position: relative; padding-right: 40px; }
  .deploy-detail-head { gap: 6px 14px; }
  .deploy-facts { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .deploy-facts dt { padding-top: 8px; }
  .deploy-notes { font-size: 14.5px; }
}

/* ── SCREEN-READER ONLY ──
   Used by the tracker's table <caption> and the news feed's aria-live
   status region. Both are meant to be announced but not seen. This was
   missing entirely, so both were rendering as visible stray text. The
   clip/clip-path pair is the standard technique: the element keeps its
   place in the accessibility tree (unlike display:none) but occupies no
   visual space. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── BACK TO TOP ──
   The button and its scroll listener (toggling this .visible class) have
   existed in every page's markup and JS since the redesign, but with no
   CSS at all — meaning it rendered as a plain, always-on inline button
   sitting in the document flow rather than the floating, scroll-triggered
   control it's meant to be. This is the missing half of that pair. */
#backToTop {
  position: fixed; right: 20px; bottom: 20px; z-index: 40;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--ink); color: var(--bg); border: none;
  font-size: 18px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
  box-shadow: 0 2px 10px rgba(28, 26, 21, 0.18);
}
#backToTop.visible { opacity: 0.92; visibility: visible; transform: translateY(0); }
#backToTop:hover { opacity: 1; }
[data-theme="ink"] #backToTop { background: var(--accent); color: #14130f; }
@media (prefers-reduced-motion: reduce) { #backToTop { transition: opacity 0.01ms; } }

/* ── BREADCRUMBS ── */
.crumbs {
  font-family: var(--font-mono); font-size: 10.5px;
  color: var(--ink-mute); margin: 22px 0 0; letter-spacing: 0.02em;
}
.crumbs a { color: var(--ink-mute); padding: 6px 2px; margin: -6px -2px; transition: color 0.15s ease; }
.crumbs a:hover { color: var(--accent); }
.crumbs .sep { margin: 0 6px; opacity: 0.5; }

/* ── PAGE HEADER (used on feed/topics/tracker/about) ── */
.page-head { padding: 30px 0 36px; border-bottom: 1px solid var(--rule); }
.page-head h1 {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(30px, 6vw, 42px); line-height: 1.14; letter-spacing: -0.01em;
  color: var(--ink); margin: 12px 0 14px;
}
.page-head .standfirst {
  font-size: 16px; line-height: 1.7; color: var(--ink-soft); margin: 0;
}

/* ── HERO (home only) ── */
.hero { padding: 46px 0 40px; }
/* Selector follows the element: the hero headline became the page's H1
   when the masthead wordmark stopped being one. Left as `.hero h2` it
   matched nothing, the headline fell back to a browser default, and the
   section titles beneath it rendered LARGER than the headline. */
.hero h1, .hero h2 {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(32px, 6.8vw, 53px); line-height: 1.08; letter-spacing: -0.017em;
  color: var(--ink); margin: 14px 0 18px;
  animation: fadeUp 0.35s ease both;
}
.hero .eyebrow, .hero-lede { animation: fadeUp 0.35s ease both; }
.hero-lede { animation-delay: 0.03s; }
@media (prefers-reduced-motion: reduce) {
  .hero h2, .hero .eyebrow, .hero-lede { animation: none !important; opacity: 1 !important; }
}
.hero-lede { font-size: clamp(16px, 3vw, 18.5px); line-height: 1.7; color: var(--ink-soft); margin: 0 0 30px; }

/* Contextual artwork is generated from the page title and canonical URL.
   The shared shell inserts it only when a page has no other visible image,
   giving reference and index pages the same editorial weight as stories
   without duplicating imagery on pages that already contain photographs. */
.page-image-banner {
  margin: 30px 0 4px;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--surface-2);
}
.page-image-banner img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1200 / 630;
  object-fit: cover;
}
@media (max-width: 640px) {
  .page-image-banner { margin-top: 20px; }
}
/* ── STAT STRIP ──
   Previously flex-wrap with border-right removed only on the true last
   child. Once items wrapped to two-per-row on a phone, the item ending
   row one kept its border-right — a vertical line with nothing to its
   right and nothing below it lining up with it. That was the reported
   misalignment.

   Fixed with an actual CSS grid at a fixed column count per breakpoint,
   where EVERY cell draws its own right and bottom edge and the container
   draws the top and left. A grid can never produce a half-finished
   divider the way a wrapping flex row can — every cell's border is
   complete regardless of how many columns there are. Collapses to a
   single column on narrow phones, where there is nothing to misalign
   because there is only one column. */
.stat-strip {
  display: grid; grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--rule); border-left: 1px solid var(--rule);
}
.stat { padding: 16px 18px; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.stat-num { display: block; font-family: var(--font-serif); font-weight: 600; font-size: 30px; color: var(--ink); line-height: 1; }
.stat-label { display: block; margin-top: 5px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-mute); }
@media (max-width: 640px) { .stat-strip { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 380px) { .stat-strip { grid-template-columns: 1fr; } }

/* ── SECTION SHELL ── */
section.block { padding: 40px 0; border-bottom: 1px solid var(--rule); }
section.block:last-of-type { border-bottom: none; }
.block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 22px; flex-wrap: wrap; }
/* Sized to relate to the hero headline rather than to metadata. The hero
   clamps 32-53px; section titles clamp a step below so hierarchy still
   reads, but they are no longer visually closer to body copy than to the
   headline they sit under. */
.block-title {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(26px, 3.6vw, 36px); line-height: 1.12;
  letter-spacing: -0.012em; color: var(--ink); margin: 0;
}
.block-more { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.04em; color: var(--ink-mute); }
.block-more:hover { color: var(--accent); }

/* ── LEAD STORY ── */
.lead { position: relative; display: block; aspect-ratio: 16/9; background: var(--surface-2); overflow: hidden; margin-bottom: 26px; }
.lead img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lead-scrim { position: absolute; inset: 0; background: linear-gradient(to top, rgba(10,9,6,0.86) 0%, rgba(10,9,6,0.35) 45%, transparent 75%); }
/* .lead-text carries its OWN background rather than relying solely on the
   scrim above. That was the actual bug: .lead-text has no fixed height —
   it is bottom-anchored and grows upward as its content grows — while
   .lead-scrim's gradient darkens a FIXED percentage of the image
   regardless of how much text there is. A long headline plus a long dek
   pushes text up past the point where the scrim has already faded to
   transparent, leaving light text on a light photo with nothing behind
   it. A gradient on .lead-text itself doesn't have that failure mode: its
   stops are relative to the text box's own rendered height, which grows
   and shrinks with the content automatically, so the darkening always
   covers exactly what needs covering. */
.lead-text {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 28px 26px 24px;
  background: linear-gradient(to top, rgba(10,9,6,0.94) 0%, rgba(10,9,6,0.94) 55%, rgba(10,9,6,0.75) 85%, rgba(10,9,6,0.35) 100%);
}
.lead .eyebrow { color: #f0d4cd; }
.lead h3 {
  font-family: var(--font-serif); font-weight: 600; font-size: clamp(21px, 4vw, 28px);
  line-height: 1.22; color: #fff; margin: 8px 0 8px;
  /* No max-width here any more: this class is shared by the "Latest
     posts" lead (spans the full wrap width, often 800px or more) and
     smaller card contexts, and a fixed 22ch cap sized for the narrower
     case was leaving roughly half of a wide tile as unused empty space
     next to the text rather than letting it wrap across the width that
     is actually available. The line-clamp below is what actually
     protects against an unreasonably long headline, which is the
     purpose a width cap was standing in for here. */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.lead .dek {
  color: #e6e0d2; font-size: 14.5px; line-height: 1.6; margin: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── SECONDARY / NUMBERED LIST ── */
.secondary-list { border-top: 1px solid var(--rule); }
.secondary-item { display: flex; gap: 16px; padding: 18px 0; border-bottom: 1px solid var(--rule); }
.secondary-num { font-family: var(--font-serif); font-style: italic; font-size: 22px; color: var(--ink-mute); flex-shrink: 0; width: 26px; }
.secondary-body h4 { font-family: var(--font-serif); font-weight: 600; font-size: 17.5px; line-height: 1.32; color: var(--ink); margin: 0 0 6px; }
.secondary-body a:hover h4 { color: var(--accent); }

/* ── RIVER (chronological list — used on feed, topic hub, story-related) ──
   .river itself carries no rules — it is a plain wrapper, and every visual
   property (borders, spacing) lives on .river-item so the list works
   correctly with any number of children. Declared explicitly rather than
   left undefined, so a future reviewer does not need to guess whether the
   absence was deliberate. */
.river { display: block; }
.river-item { display: flex; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--rule); }
.river-item:last-child { border-bottom: none; }
.river-body { flex: 1; min-width: 0; }
.river-title { font-family: var(--font-serif); font-weight: 600; font-size: 16.5px; line-height: 1.36; color: var(--ink); display: block; }
.river-item:hover .river-title { color: var(--accent); }
.river-summary { font-size: 13.5px; line-height: 1.55; color: var(--ink-soft); margin: 6px 0 0; }
.river-meta { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; align-items: center; }
/* Video-type item, sharing river structure with a play affordance */
.play-badge {
  position: relative; width: 84px; height: 84px; flex-shrink: 0; background: var(--surface-2); overflow: hidden;
}
.play-badge img { width: 100%; height: 100%; object-fit: cover; display: block; }
.play-badge::after {
  content: ''; position: absolute; inset: 0; margin: auto; width: 22px; height: 22px;
  background: rgba(28,26,21,0.72); border-radius: 50%;
}
.play-badge::before {
  content: ''; position: absolute; top: 50%; left: 50%; z-index: 1;
  transform: translate(-45%, -50%);
  border-style: solid; border-width: 5px 0 5px 8px; border-color: transparent transparent transparent #fff;
}

/* ── FILTER BAR (feed page) ── */
.filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 20px 0 0; }
.chip {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.04em;
  padding: 7px 13px; border: 1px solid var(--rule); color: var(--ink-soft);
  background: var(--surface);
}
.chip.active { border-color: var(--accent); color: var(--accent); background: var(--accent-tint); }
.chip:hover { border-color: var(--accent); color: var(--accent); }

/* ── DESK (tracker callout / any highlighted panel) ── */
.desk { background: var(--surface); border: 1px solid var(--rule); box-shadow: var(--shadow); padding: 30px 32px; }
@media (max-width: 560px) { .desk { padding: 22px 20px; } }
.desk-standfirst { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 10px 0 24px; }
/* table-layout: fixed makes column widths depend only on these declared
   widths, never on content — this is the actual fix for the mobile
   misalignment bug. Without it, the browser negotiates column widths from
   cell content on every render, and the very long international force
   names added when the tracker went global (e.g. "5,000+ US law
   enforcement agencies", "Ministry of Public Security (national)") made
   that negotiation behave inconsistently at narrow widths — which is what
   produced the ragged, out-of-sync row borders. Fixed layout removes the
   negotiation entirely.

   Only the narrow columns (Country, Status, Since) get an explicit width
   below. Force and Technology are left unset deliberately: this class is
   shared between the full tracker's 5-column table and the home page
   preview's 4-column table (no Since column there), and with
   table-layout: fixed, any unset column automatically shares the space
   left over after the explicit ones — correctly, regardless of whether
   there are two unset columns or a different total column count. Setting
   explicit percentages on every column would need two different sums for
   two different tables, which is exactly the kind of thing that quietly
   goes wrong the next time either table's structure changes. */
.dtable { width: 100%; border-collapse: collapse; table-layout: fixed; }
.dtable th:nth-child(1), .dtable td:nth-child(1) { width: 15%; }
.dtable th:nth-child(4), .dtable td:nth-child(4) { width: 18%; }
.dtable th:nth-child(5), .dtable td:nth-child(5) { width: 12%; }
.dtable th { text-align: left; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; padding: 0 10px 10px 0; border-bottom: 1px solid var(--rule); overflow-wrap: break-word; }
.dtable th:last-child { text-align: right; padding-right: 0; }
.dtable td { padding: 13px 10px 13px 0; border-bottom: 1px solid var(--rule); vertical-align: top; overflow-wrap: break-word; }
.dtable tr:last-child td { border-bottom: none; }
/* ── MOBILE ──
   Previous attempts at this failed because they treated it purely as a
   column-width problem. Two other things were doing more damage:

   1. `.d-tech` sets display:block, and the home page was applying it to
      the <td> itself rather than a <span> inside it. display:block on a
      table cell takes that cell OUT of the table layout, which is what
      actually made the row borders sit at different heights. Fixed in
      the markup, not here.
   2. There is far less width available than the viewport suggests. On a
      390px phone: 390 - 44 (.wrap padding) - 64 (.desk padding) = 282px
      for four columns of real text. The padding is reduced below to give
      the content somewhere to go.

   On the home page preview specifically, every row is operational by
   construction (the code filters for it), so a Status column there shows
   the identical value on every row. It is pure noise on a narrow screen
   and is dropped, which buys the two text-heavy columns real width. The
   full tracker keeps Status, because there it genuinely varies. */
@media (max-width: 640px) {
  /* Reclaim horizontal space before dividing it up. 22px of page padding
     plus 32px of card padding costs 108px on a 390px screen, which is
     most of a column. */
  /* Narrowing the gutter here, on the variable, means .wrap and every
     full-bleed element that cancels it move together. Setting it on
     .wrap alone is what previously left the masthead bleeding 7px past
     the viewport on every phone width. */
  :root, [data-theme="paper"], [data-theme="ink"] { --wrap-pad: 15px; }
  .desk { padding: 22px 15px; }
  .dtable td { padding-right: 7px; }
  .dtable th { font-size: 8.5px; letter-spacing: 0.03em; padding-right: 7px; }

  /* Since: dropped everywhere. Secondary context, not scanned for. */
  .dtable th:nth-child(5), .dtable td:nth-child(5) { display: none; }

  /* Status: dropped as a COLUMN on both tables, because four columns of
     prose cannot fit a phone without breaking words mid-syllable. On the
     full tracker it reappears inside the Force cell via .d-status-mobile,
     so no information is lost — it just stops being a column. On the home
     preview it is genuinely redundant (every row there is operational by
     construction) and is simply dropped.

     width:0 alongside display:none is load-bearing, not belt-and-braces.
     Under table-layout:fixed a hidden column STILL reserves whatever
     width it declares, so hiding two columns that declared 18% and 12%
     silently ate 30% of the table and left the visible text columns at
     76px — narrow enough to break words mid-syllable. Zeroing them, and
     declaring the three remaining widths to sum to exactly 100%, is what
     actually hands that space back. */
  .dtable th:nth-child(4), .dtable td:nth-child(4) { display: none; width: 0; }
  .dtable th:nth-child(5), .dtable td:nth-child(5) { width: 0; }
  .dtable th:nth-child(1), .dtable td:nth-child(1) { width: 16%; }
  .dtable th:nth-child(2), .dtable td:nth-child(2) { width: 46%; }
  .dtable th:nth-child(3), .dtable td:nth-child(3) { width: 38%; }
  .d-status { white-space: normal; }
}
.d-force { font-weight: 700; color: var(--ink); font-size: 14.5px; }
.d-region { display: block; font-size: 12px; color: var(--ink-mute); margin-top: 2px; }
.d-tech { font-size: 13.5px; color: var(--ink-soft); display: block; }
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 7px; }
.dot.live     { background: var(--status-live); }
.dot.trial    { background: var(--status-trial); }
.dot.paused   { background: var(--status-paused); }
.dot.ended    { background: var(--status-ended); }
.dot.proposed { background: var(--status-proposed); }
.d-status { font-size: 12.5px; color: var(--ink-soft); white-space: nowrap; }
.desk-cta { display: inline-block; margin: 22px -4px 0; padding: 8px 4px 11px; font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; letter-spacing: 0.04em; color: var(--accent); border-bottom: 1px solid var(--accent); transition: opacity 0.15s ease; }
.desk-cta:hover { opacity: 0.7; }

/* Tracker page: status summary strip */
/* Same grid technique and the same reason — this strip has FIVE cells,
   an odd number, which is precisely the case that breaks worst under
   flex-wrap (an uneven last row). A grid with every cell fully bordered
   has no such failure mode: an uneven last row just leaves unused grid
   space, never a stray line. */
.status-strip {
  display: grid; grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--rule); border-left: 1px solid var(--rule);
  margin: 22px 0 30px;
}
.status-cell { padding: 14px 14px; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.status-cell .stat-num { font-size: 24px; }
.status-cell.st-live     .stat-num { color: var(--status-live); }
.status-cell.st-trial    .stat-num { color: var(--status-trial); }
.status-cell.st-paused   .stat-num { color: var(--status-paused); }
.status-cell.st-ended    .stat-num { color: var(--status-ended); }
.status-cell.st-proposed .stat-num { color: var(--status-proposed); }
@media (max-width: 640px) { .status-strip { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 420px) { .status-strip { grid-template-columns: repeat(2, 1fr); } }

.callout { background: var(--accent-tint); border-left: 3px solid var(--accent); padding: 15px 20px; margin: 22px 0; }
.callout p { font-size: 13.5px; line-height: 1.65; color: var(--ink-soft); margin: 0; }
.callout strong { color: var(--ink); }

/* ── TOPIC INDEX (grouped) ── */
.topic-group { margin-bottom: 30px; }
.topic-group-title { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin: 0 0 12px; }
.topic-index { columns: 2; column-gap: 34px; }
@media (max-width: 560px) { .topic-index { columns: 1; } }
.topic-row { display: block; padding: 12px 0; border-bottom: 1px solid var(--rule); break-inside: avoid; transition: background-color 0.15s ease; }
.topic-row:hover { background-color: var(--surface-2); }
.topic-row-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.topic-name { font-family: var(--font-serif); font-weight: 600; font-size: 16px; color: var(--ink); }
.topic-row:hover .topic-name { color: var(--accent); }
.topic-count { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-mute); flex-shrink: 0; }
.topic-chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
.topic-chip { font-family: var(--font-mono); font-size: 10.5px; padding: 6px 12px; border: 1px solid var(--rule); color: var(--ink-soft); transition: border-color 0.15s ease, color 0.15s ease; }
.topic-chip:hover { border-color: var(--accent); color: var(--accent); }

/* ── STORY PAGE ── */
.story-head { padding: 26px 0 0; }
.story-head h1 { font-family: var(--font-serif); font-weight: 600; font-size: clamp(26px, 5.4vw, 36px); line-height: 1.2; color: var(--ink); margin: 14px 0 16px; }
.story-source-line { font-size: 13.5px; color: var(--ink-mute); margin: 0 0 22px; }
.story-source-line strong { color: var(--ink); }
.story-image { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; background: var(--surface-2); margin-bottom: 26px; }
.story-summary { font-family: var(--font-serif); font-size: clamp(17px, 3.4vw, 20px); line-height: 1.68; color: var(--ink); margin: 0 0 10px; }
.disclosure { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em; color: var(--ink-mute); }
.disclosure::before { content: '●'; color: var(--accent); margin-right: 6px; font-size: 8px; }
.story-cta { display: inline-flex; align-items: center; gap: 7px; margin: 26px 0 6px; padding: 13px 24px; background: var(--ink); color: var(--bg); font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; }
[data-theme="ink"] .story-cta { background: var(--accent); color: #14130f; }
.story-cta:hover { opacity: 0.88; }
.story-topic-note { font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute); margin: 18px 0 0; }
.story-topic-note a { color: var(--accent); }

/* ── LONG-FORM GUIDE ── */
.guide-toc a:hover { color: var(--accent); }
/* Named .topic-content rather than .guide-body — this is what the nine
   real long-form pages already use as their content wrapper, and every
   paragraph, heading and list inside them is a plain unclassed tag styled
   purely by being a descendant of this one class. Matching the existing
   name means the real 2,000-word pages need no changes to their content
   markup at all, only their surrounding chrome. */
.topic-content { margin: 0; }
.topic-content h2 {
  font-family: var(--font-serif); font-weight: 700; font-size: clamp(22px, 4vw, 27px);
  line-height: 1.28; color: var(--ink); margin: 42px 0 16px;
}
.topic-content h2:first-child { margin-top: 0; }
.topic-content p {
  /* Body copy set in serif deliberately on this page type only — long-form
     reading is where a serif measurably earns its keep; short UI text
     elsewhere stays in Inter. */
  font-family: var(--font-serif); font-size: clamp(16.5px, 3.2vw, 18px);
  line-height: 1.85; color: var(--ink); margin: 0 0 22px;
}
.topic-content ul, .topic-content ol {
  font-family: var(--font-serif); font-size: clamp(16px, 3.1vw, 17.5px);
  line-height: 1.72; color: var(--ink); margin: 0 0 20px; padding-left: 24px;
}
.topic-content li { margin-bottom: 10px; }
.topic-content strong { font-weight: 600; color: var(--ink); }
.topic-content a { color: var(--accent); border-bottom: 1px solid var(--rule); }
.topic-content a:hover { border-bottom-color: var(--accent); }

/* ── "FOLLOW THE COVERAGE" BUTTON ──
   A previous version of this exact button went visually missing in dark
   mode: `.topic-content a` (above) and a same-specificity `.topic-cta`
   rule tied, and whichever happened to sit later in the file won — making
   the label the same colour as its own background. `.topic-content
   a.topic-cta` scores higher than `.topic-content a` regardless of file
   order, which is what actually fixes it rather than relying on being
   declared afterwards. Matches .story-cta's styling for consistency
   between the two primary calls to action on the site. */
.topic-content a.topic-cta {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 10px 0 22px; padding: 13px 24px;
  background: var(--ink); color: var(--bg);
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; text-decoration: none;
  border: none; border-bottom: none;
}
.topic-content a.topic-cta:hover { opacity: 0.88; border-bottom: none; }
[data-theme="ink"] .topic-content a.topic-cta { background: var(--accent); color: #14130f; }

/* ── ABOUT PAGE INDEPENDENCE NOTICE ──
   Sits INSIDE .topic-content in the real markup, so a plain `.about-callout
   p` selector (0,1,0) would tie with `.topic-content p` (0,1,0) — the same
   specificity trap as .topic-cta above, just with smaller consequences
   here (wrong font rather than an invisible button). Using the descendant
   form `.topic-content .about-callout p` (0,2,0) wins outright instead of
   relying on !important or hoping for favourable source order. */
.about-callout {
  background: var(--accent-tint); border-left: 3px solid var(--accent);
  padding: 16px 20px; margin: 22px 0;
}
.topic-content .about-callout p {
  font-family: var(--font-sans); font-size: 13.5px;
  line-height: 1.7; color: var(--ink-soft); margin: 0 0 8px;
}
.topic-content .about-callout p:last-child { margin-bottom: 0; }
.about-callout strong { color: var(--ink); }
/* ── PULL (newsletter) ── */
.pull { border-left: 3px solid var(--accent); padding-left: 22px; }
.pull h3 { font-family: var(--font-serif); font-weight: 500; font-style: italic; font-size: clamp(20px, 3.6vw, 26px); line-height: 1.4; color: var(--ink); margin: 0 0 16px; max-width: 24ch; }
.pull p { font-size: 14.5px; color: var(--ink-soft); line-height: 1.7; margin: 0 0 20px; max-width: 52ch; }
/* The signup form is not wired to anything real yet, so it is not shown
   as one — a working-looking form that silently does nothing is worse
   than being upfront that it is coming. */

footer { padding: 30px 0 44px; text-align: center; font-family: var(--font-mono); font-size: 10px; color: var(--ink-mute); line-height: 1.9; }
footer a { color: inherit; padding: 8px 6px; margin: -8px 0; display: inline-block; transition: color 0.15s ease; }
footer a:hover { color: var(--accent); }

/* ── ANNOTATIONS ── */
body.hide-notes .note { display: none; }

/* ── TRACKER FILTERS ── */
.tracker-filters { display: flex; gap: 10px; flex-wrap: wrap; margin: 22px 0 0; }
.tf-field { flex: 1 1 200px; }
.tf-label {
  display: block; font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-mute); margin-bottom: 6px;
}
.tf-field select, .tf-field input {
  width: 100%; padding: 10px 12px; background: var(--surface);
  border: 1px solid var(--rule); color: var(--ink);
  font-family: var(--font-sans); font-size: 13.5px;
}
.tf-clear {
  align-self: flex-end; padding: 10px 16px; background: transparent;
  border: 1px solid var(--rule); color: var(--ink-mute); cursor: pointer;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em;
}
.tf-clear:hover { border-color: var(--accent); color: var(--accent); }
.tf-empty { padding: 30px 0; text-align: center; color: var(--ink-mute); font-size: 13.5px; display: none; }
.tf-empty.show { display: block; }

/* ═══════════════════════════════════════════════
   PORTED FUNCTIONAL COMPONENTS

   Everything below is used by real rendering functions (render-story.js,
   shell.js) and has no equivalent in the design exploration, which only
   ever mocked static content. render-feed.js and its client template are
   NOT covered here — they keep their current visual design until batch 2,
   so their classes (share-btn, video-embed, direct-read-marker,
   story-image-wrap) are deliberately left untouched for now.
═══════════════════════════════════════════════ */

/* Small mono pill for region/format labels on the story page meta line —
   the same visual language as .eyebrow, applied to inline tags rather
   than section labels. */
.country-tag {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
  background: var(--accent-tint); color: var(--accent);
  padding: 3px 9px; flex-shrink: 0;
}
.video-tag {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
  background: var(--ink); color: var(--bg);
  padding: 3px 9px; flex-shrink: 0;
}
[data-theme="ink"] .video-tag { background: var(--accent); color: #14130f; }

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* A gentle entrance on page load — the same touch the previous design
   used. Applied uniformly rather than staggered per item: none of these
   lists are JS-appended (unlike the old infinite-scroll feed, where a
   staggered delay made sense), so one clean fade for the whole block is
   the right amount of motion for server-rendered content. */
.river-item, .lead, .topic-row, .secondary-item, .desk, .stat {
  animation: fadeUp 0.35s ease both;
}
@media (prefers-reduced-motion: reduce) {
  .river-item, .lead, .topic-row, .secondary-item, .desk, .stat {
    animation: none !important; opacity: 1 !important;
  }
}

/* Pure JS selector hook — the live-coverage-block script on the nine
   long-form guides finds its section via .guide-live and reads its
   data-topic-tag attribute. All visual styling comes from the .block
   class it is paired with in the markup; this carries none of its own,
   declared explicitly so its absence does not look like an oversight. */
.guide-live { display: block; }

/* ── EMPTY / ERROR STATES ──
   Shared by render-story.js (404/503), render-topic.js (no stories yet)
   and render-feed.js (no results / archive unavailable) once that is
   restyled in batch 2 — defined once here so all three agree. */
.state-center { text-align: center; padding: 50px 20px; }
.state-heading {
  font-family: var(--font-serif); font-weight: 600;
  font-size: 20px; color: var(--ink); margin-bottom: 8px;
}
.state-sub { font-size: 14px; color: var(--ink-mute); }
.state-sub a { color: var(--accent); }

/* ═══════════════════════════════════════════════
   THE NEWS FEED (/news)
   Batch 2 of the redesign — the largest, densest page on the site, so it
   builds on the same river visual language used elsewhere but with the
   extra structure a continuous list of 400+ items needs: an image, a
   summary, tags, and a share action, none of which the lighter "latest
   coverage" teasers elsewhere require.
═══════════════════════════════════════════════ */

/* ── FILTER PANEL — collapsible, closed by default ── */
.filter-panel { margin-top: 20px; }
.filter-panel-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 13px 16px; cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink);
  border: 1px solid var(--rule); background: var(--surface-2);
  user-select: none; transition: border-color 0.15s ease, color 0.15s ease;
}
.filter-panel-toggle::-webkit-details-marker { display: none; }
.filter-panel-toggle:hover { border-color: var(--accent); color: var(--accent); }
.filter-panel-chevron { flex-shrink: 0; transition: transform 0.15s ease; }
.filter-panel[open] .filter-panel-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .filter-panel-toggle, .filter-panel-chevron { transition: none; }
}
/* The toggle above already carries its own border on every edge, so the
   panel's own top border directly beneath it would double up as a
   near-adjacent second line when open. Left in place on the bottom,
   which still does useful work separating the filters from the feed. */
.filter-panel .filter-bar { border-top: none; }

.filter-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  padding: 16px 0; margin-bottom: 6px;
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
}
.filter-bar-label {
  font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-mute);
  flex-basis: 100%; margin: 6px 0 -4px;
}
.filter-bar-label:first-child { margin-top: 0; }
.chip-row { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em;
  padding: 6px 12px; border: 1px solid var(--rule); background: var(--surface);
  color: var(--ink-soft); cursor: pointer;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active { border-color: var(--accent); background: var(--accent-tint); color: var(--accent); }
.chip-icon { display: inline-flex; align-items: center; gap: 5px; }
.date-filter-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.date-filter-label {
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-mute);
  display: flex; flex-direction: column; gap: 4px;
}
.date-filter-input {
  padding: 6px 8px; background: var(--surface); border: 1px solid var(--rule);
  color: var(--ink); font-family: var(--font-sans); font-size: 12.5px;
}
.date-filter-clear {
  display: none; padding: 6px 12px; background: transparent; border: 1px solid var(--rule);
  color: var(--ink-mute); cursor: pointer; font-family: var(--font-mono); font-size: 10.5px;
}
.date-filter-clear:hover { border-color: var(--accent); color: var(--accent); }

/* ── DATE DIVIDER ──
   grid-column: 1 / -1 is what makes this span every column of the tile
   grid above each day's group, rather than occupying a single grid cell
   the same size as a story tile -- the one property this needed adding
   to work correctly in a grid rather than a single-column flow. */
.feed-date-divider {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 10px;
  margin: 26px 0 4px; padding-top: 18px;
  border-top: 1px solid var(--rule);
}
.feed-date-divider:first-child { margin-top: 0; border-top: none; padding-top: 0; }
.divider-label {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent); white-space: nowrap;
}
.divider-line { flex: 1; height: 1px; background: var(--rule); }

/* ── FEED ITEM ── */
/* A bordered tile rather than a row in a stacked list, matching the same
   card convention used elsewhere on the site (.blog-card, .news-tile):
   a full border, border-color on hover rather than a background fill,
   and no more last-child special-casing now every tile has all four
   sides. */
.feed-item {
  border: 1px solid var(--rule); border-radius: 0; padding: 18px 20px;
  background: var(--surface, var(--bg));
  animation: fadeUp 0.3s ease both; transition: border-color 0.15s ease;
}
.feed-item:hover { border-color: var(--accent); }
.feed-item-meta {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px;
}
.feed-item-title {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(17px, 3.4vw, 20px); line-height: 1.32;
  color: var(--ink); margin: 0 0 8px;
}
.feed-item-title a { color: inherit; }
.feed-item:hover .feed-item-title a { color: var(--accent); }
.feed-item-image {
  width: 100%; max-height: 280px; object-fit: cover; display: block;
  margin: 4px 0 10px; background: var(--surface-2);
}
/* Title overlaid on its image rather than sitting above it. Applies to
   every card, whether the image is a real publisher photo or one of the
   generated newsprint placeholders — an opaque box behind the text works
   identically either way, which is the point of it: legibility never
   depends on what happens to be under it.

   Fixed black-and-white, not the theme's --ink/--bg tokens, deliberately:
   this reads as a graphic treatment layered on a photo, not as themed UI
   chrome, so it should look the same in both the paper and ink themes
   rather than flip with them.

   box-decoration-break: clone is what gives each wrapped line its own
   tightly-fitted box instead of one rectangle spanning the widest line —
   the display:inline background renders fresh on every line fragment
   rather than stretching a single box around the whole block. */
/* This was a large, magazine-style headline overlaid on the image,
   sized with vw units against the near-full-width single column the
   timeline design gave each story. In a grid tile roughly 300-400px
   wide, the exact same rule computed against the full VIEWPORT width
   rather than the tile's own width -- vw is always relative to the
   viewport, never to a grid track -- so the text overflowed its tile
   both horizontally and vertically. It was also very likely the direct
   cause of the grid rendering as a single wide column rather than
   several: a grid track sized 1fr still respects the minimum intrinsic
   width its content demands, and text this oversized forced every
   track far wider than the 300px minimum, leaving room for only one per
   row regardless of viewport width.

   Neutralised back to a normal, non-overlaid title below the image,
   sized the same as every other story's title -- matching how the
   image-then-title pattern already works elsewhere on this site
   (.blog-card, .news-tile), rather than inventing a third convention. */
.feed-item-image-wrap { margin: 4px 0 10px; }
.feed-item-image-wrap .feed-item-image { margin: 0 0 10px; }
.feed-item-image-wrap .feed-item-title {
  position: static; margin: 0; transform: none;
}
.feed-item-image-wrap .feed-item-title a {
  display: inline; background: none; color: var(--ink); font-weight: 700;
  padding: 0; line-height: 1.32; font-size: clamp(17px, 3.4vw, 20px);
  box-decoration-break: initial; -webkit-box-decoration-break: initial;
}
.feed-item:hover .feed-item-image-wrap .feed-item-title a { color: var(--accent); }
.feed-item-summary {
  font-family: var(--font-serif); font-size: 15.5px;
  line-height: 1.62; color: var(--ink-soft); margin: 0 0 6px;
}
.feed-item-disclosure {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.04em;
  color: var(--ink-mute); display: block; margin-bottom: 4px;
}
.feed-item-marker {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
  color: var(--ink); text-decoration: none; margin: -6px 0 2px -4px; padding: 6px 4px;
  transition: color 0.15s ease;
}
.feed-item-marker:hover { color: var(--accent); }
.feed-item-footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 10px;
}
.feed-item-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tag {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em;
  padding: 3px 9px; border: 1px solid var(--rule); color: var(--ink-mute);
}

/* ── SHARE BUTTON ──
   Deliberately shares a link back to this site rather than the publisher's
   own URL — see buildShareButtonHTML in render-feed.js for the reasoning.
   The icon carries no colour claim of its own; a plain recognised share
   glyph, styled quietly to match the rest of the feed's restraint. */
.share-btn {
  width: 36px; height: 36px; flex-shrink: 0; position: relative;
  background: transparent; border: 1px solid var(--rule); color: var(--ink-mute);
  cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0;
  transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.share-btn:hover, .share-btn:focus-visible { color: var(--accent); border-color: var(--accent); }
.share-btn:active { transform: scale(0.92); }
.share-btn.copied { color: var(--accent); border-color: var(--accent); }
.share-btn.copied::after {
  content: 'Copied'; position: absolute; bottom: 32px; right: 0;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.05em;
  text-transform: uppercase; background: var(--ink); color: var(--bg);
  padding: 3px 8px; white-space: nowrap; animation: fadeUp 0.15s ease both;
}

/* ── VIDEO FACADE + CONSENT ──
   Nothing loads from YouTube until the reader explicitly asks twice — see
   the JS comment in the template for the privacy/performance reasoning.
   This is purely the visual treatment of that mechanism in the new theme. */
.video-embed {
  position: relative; width: 100%; aspect-ratio: 16/9; margin: 4px 0 10px;
  background: var(--surface-2); overflow: hidden;
}
/* The consent notice's text needs more room than a 16:9 box gives it at
   narrow widths, no matter how far the padding and font-size are trimmed
   back — there just isn't enough height in ~220px for a full paragraph
   plus two buttons plus a checkbox. Rather than keep shrinking type until
   it stops being legible, the embed is allowed to grow taller for as
   long as the consent screen (not the thumbnail, not the playing video)
   is what's actually showing. overflow-y:auto on .video-consent itself
   remains as a safety net for anyone with enlarged accessibility text
   settings this still isn't enough for. */
.video-embed:has(.video-consent:not([hidden])) {
  aspect-ratio: unset; min-height: 280px;
}
.video-facade {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: none; padding: 0; cursor: pointer; background: none;
}
.video-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 52px; height: 52px; border-radius: 50%;
  background: rgba(28, 26, 21, 0.72); display: flex; align-items: center; justify-content: center;
}
.video-play::after {
  content: ''; margin-left: 4px;
  border-style: solid; border-width: 9px 0 9px 15px;
  border-color: transparent transparent transparent #fff;
}
.video-views {
  position: absolute; bottom: 10px; right: 10px;
  font-family: var(--font-mono); font-size: 10px;
  background: rgba(28, 26, 21, 0.72); color: #fff; padding: 3px 8px;
}
.video-consent {
  position: absolute; inset: 0; background: var(--ink); color: var(--bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 24px; overflow-y: auto;
}
/* The critical rule. An element's `display` set by an author stylesheet
   overrides the browser's own built-in `[hidden] { display: none }` rule
   at equal specificity — author styles beat user-agent styles regardless
   of declaration order. Without this, .video-consent above renders as a
   visible flex box permanently, hidden attribute or not: the consent
   screen would show unconditionally for every video, on every visit, with
   the thumbnail underneath never actually reachable to click. That in
   turn is why "remember for this session" could never do anything — its
   fast path only runs when the thumbnail itself is clicked. */
.video-consent[hidden] { display: none; }
@media (max-width: 480px) {
  .video-consent { padding: 14px 16px; }
  .video-consent p { font-size: 12px; line-height: 1.5; margin-bottom: 10px; }
  .video-consent-actions { gap: 6px; }
  .video-consent-yes, .video-consent-alt { padding: 8px 12px; font-size: 10px; }
  .video-consent-remember { margin-top: 8px; font-size: 10.5px; }
}
.video-consent p {
  font-family: var(--font-sans); font-size: 13px; line-height: 1.6;
  max-width: 42ch; margin: 0 0 16px;
}
.video-consent-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.video-consent-yes {
  padding: 10px 20px; background: var(--accent); color: var(--bg); border: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: 0.05em; text-transform: uppercase;
}
.video-consent-alt {
  padding: 10px 20px; background: transparent; border: 1px solid rgba(255,255,255,0.3);
  color: var(--bg); font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.03em; text-decoration: none; display: inline-flex; align-items: center;
}
.video-consent-remember {
  margin-top: 14px; font-family: var(--font-sans); font-size: 11.5px;
  display: flex; align-items: center; gap: 6px; cursor: pointer;
}
.video-embed.playing .video-iframe { width: 100%; height: 100%; border: none; display: block; }

/* ── EMPTY / ARCHIVE STATE, feed-specific tweaks ── */
.state-center a { color: var(--accent); }

/* Entries whose source is named but has no direct URL on file. Previously
   these rendered as <a href=""> — a link that silently reloads the page
   instead of going anywhere, which on a site built on verifiability is
   worse than no link at all, because it looks like a citation and isn't
   one. Now the source is named as plain text with the gap stated openly. */
.deploy-source-nolink { color: var(--ink-mute); font-style: italic; }

/* ── COUNTRY PROFILE PAGES ──
   Reuses .deploy / .deploy-summary / .deploy-detail wholesale from the
   tracker, so a deployment looks identical wherever it is shown and there
   is only one component to maintain. Only the page furniture is new. */
.country-h1 {
  font-family: var(--font-serif); font-weight: 700;
  font-size: clamp(28px, 5.4vw, 42px); line-height: 1.12; letter-spacing: -0.015em;
  color: var(--ink); margin: 12px 0 14px;
}
.country-h2 {
  font-family: var(--font-serif); font-weight: 700; font-size: 20px;
  color: var(--ink); margin: 0 0 14px;
}
.country-intro {
  font-size: 16px; line-height: 1.65; color: var(--ink-soft);
  max-width: 68ch; margin: 0 0 22px;
}
.country-intro a { color: var(--accent); border-bottom: 1px solid var(--rule); }
.country-intro a:hover { border-bottom-color: var(--accent); }

.country-stats {
  display: flex; flex-wrap: wrap; gap: 0;
  border: 1px solid var(--rule); margin: 0 0 4px;
}
.country-stat {
  flex: 1 1 120px; padding: 14px 16px;
  border-right: 1px solid var(--rule);
}
.country-stat:last-child { border-right: none; }
.country-stat-n {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-serif); font-weight: 700; font-size: 26px; color: var(--ink);
}
.country-stat-l {
  display: block; margin-top: 3px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-mute);
}
a.deploy-country-full { color: var(--ink-mute); border-bottom: 1px solid var(--rule); }
a.deploy-country-full:hover { color: var(--accent); border-bottom-color: var(--accent); }

@media (max-width: 640px) {
  .country-stat { flex-basis: 50%; border-bottom: 1px solid var(--rule); }
  .country-stat:nth-child(even) { border-right: none; }
}

/* ── TECHNOLOGY PAGE SOURCES ──
   Numbered rather than bulleted so a source can be referred to by number
   from the body text later without restructuring the markup. Where a page
   has no sources yet the absence is stated plainly rather than the section
   being hidden: a missing citation should be visible, not quietly omitted,
   which is the same reasoning behind the tracker's "(no direct link on
   file)" label. */
.tech-sources { margin: 0; padding-left: 20px; }
.tech-sources li {
  font-size: 14px; line-height: 1.6; color: var(--ink-soft);
  margin-bottom: 8px; padding-left: 4px;
}
.tech-sources li a { color: var(--accent); border-bottom: 1px solid var(--rule); }
.tech-sources li a:hover { border-bottom-color: var(--accent); }
.tech-sources-none { font-style: italic; }

/* ── WITHDRAWN DEPLOYMENTS PAGE ── */
.withdrawn-note {
  font-size: 13px; line-height: 1.6; color: var(--ink-mute);
  margin: 14px 0 0; max-width: 68ch;
}
.withdrawn-year {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute);
  white-space: nowrap; flex-shrink: 0;
}
@media (max-width: 720px) { .withdrawn-year { display: none; } }
.deploy-controls { justify-content: space-between; align-items: center; flex-wrap: wrap; row-gap: 8px; }
.deploy-controls-link {
  font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  color: var(--accent); border-bottom: 1px solid var(--rule);
  padding-bottom: 2px; transition: border-color 0.15s ease;
}
.deploy-controls-link:hover { border-bottom-color: var(--accent); }

/* ── STATISTICS PAGE ── */
.stats-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.stats-table th {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-mute); font-weight: 500;
  text-align: left; padding: 0 10px 8px 0; border-bottom: 1px solid var(--rule);
}
.stats-table td { padding: 9px 10px 9px 0; border-bottom: 1px solid var(--rule); color: var(--ink-soft); }
.stats-table tr:last-child td { border-bottom: none; }
.stats-table a { color: var(--ink); border-bottom: 1px solid var(--rule); }
.stats-table a:hover { color: var(--accent); border-bottom-color: var(--accent); }
.stats-n { font-family: var(--font-mono); font-size: 13px; color: var(--ink); width: 90px; }
.stats-year { font-family: var(--font-mono); font-size: 13px; color: var(--ink); width: 70px; }
.stats-bar-h { width: 45%; }
/* Bar is decorative: the number sits beside it, so the bar carries no
   information a screen reader would otherwise miss. Hence aria-hidden. */
.stats-bar {
  display: inline-block; height: 8px; background: var(--accent);
  opacity: 0.55; vertical-align: middle; min-width: 2px;
}
.stats-pct { font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute); margin-left: 8px; }
.deploy-controls-links { display: flex; flex-wrap: wrap; row-gap: 6px; gap: 18px; align-items: center; }
@media (max-width: 640px) {
  .stats-bar-h { display: none; }
  .deploy-controls-links { gap: 12px; }
}

/* ═══════════════════════════════════════════════
   CHARTS
   ═══════════════════════════════════════════════
   All server-rendered inline SVG. No charting library is loaded anywhere
   on this site and none should be: these cost a few hundred bytes each,
   render for crawlers and link previews, and work with JavaScript off.

   Segment colours come from the --status-* variables via lib/charts.js,
   so a chart segment is the same colour as the status dot it refers to,
   and both change together when the ink theme is active. */
.chart-stacked { display: block; width: 100%; height: 26px; margin: 14px 0 0; }
.chart-stacked a { cursor: pointer; }
.chart-stacked a:hover rect { opacity: 1; }
.chart-columns { display: block; width: 100%; height: 120px; }
.chart-timeline { display: block; width: 100%; height: auto; max-height: 60px; margin: 4px 0 0; }
.chart-spark { display: inline-block; width: 74px; height: 20px; vertical-align: middle; margin-left: 8px; }

.chart-prop {
  display: inline-block; width: 100%; max-width: 180px; height: 8px;
  background: var(--rule); vertical-align: middle;
}
.chart-prop-fill { display: block; height: 100%; background: var(--accent); opacity: 0.55; }

/* ── STATUS PICKER (tracker) ── */
.status-bar-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.status-pick {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.03em;
  padding: 7px 11px; border: 1px solid var(--rule); background: transparent;
  color: var(--ink-soft); cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.status-pick:hover { border-color: var(--accent); color: var(--accent); }
.status-pick.active { border-color: var(--accent); color: var(--accent); background: var(--surface-2); }
.status-pick-n { color: var(--ink-mute); }
.status-pick.active .status-pick-n { color: var(--accent); }

/* ── CHART BLOCKS ── */
.chart-block { margin: 0 0 22px; }
.chart-caption {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--ink-mute); margin: 8px 0 0;
}
.chart-axis {
  display: flex; justify-content: space-between;
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-mute); margin-top: 4px;
}
.chart-legend { display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0 0; }
.chart-legend span {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-mute);
}

/* ── COVERAGE PANEL ──
   Charts the completeness of the record itself, not only its contents.
   Deliberately shows gaps rather than hiding them. */
.coverage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 18px; }
.coverage-item { border: 1px solid var(--rule); padding: 14px 16px; }
.coverage-n { font-family: var(--font-serif); font-weight: 700; font-size: 24px; color: var(--ink); }
.coverage-of { font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute); margin-left: 4px; }
.coverage-l {
  display: block; margin: 3px 0 9px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.07em;
  text-transform: uppercase; color: var(--ink-mute);
}

/* ── HEATMAP ── */
.heatmap { width: 100%; border-collapse: collapse; font-size: 12px; }
.heatmap th {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute); font-weight: 500;
  padding: 0 6px 8px; text-align: center;
}
.heatmap th:first-child { text-align: left; }
.heatmap td { padding: 0; border: 1px solid var(--bg); text-align: center; }
.heatmap td:first-child {
  padding: 6px 10px 6px 0; text-align: left; border: none;
  color: var(--ink-soft); white-space: nowrap;
}
.heatmap td:first-child a { color: var(--ink-soft); }
.heatmap td:first-child a:hover { color: var(--accent); }
.heat-cell {
  display: block; padding: 7px 4px;
  font-family: var(--font-mono); font-size: 11px; color: var(--ink);
}
@media (max-width: 640px) {
  .chart-columns { height: 90px; }
  .heatmap { font-size: 11px; }
  .heatmap td:first-child { max-width: 110px; overflow: hidden; text-overflow: ellipsis; }
}
/* The "All" reset in the tracker's status picker. Same shape as the status
   buttons but with no dot, since it represents the absence of a filter
   rather than one of the statuses. */
.status-pick-all { color: var(--ink-mute); }

/* ── SUPPLIER LINK ON TECHNOLOGY PAGES ──
   rel includes nofollow deliberately: these are commercial vendor sites
   linked for the reader's benefit, not endorsements, and this site should
   not be passing ranking signal to suppliers whose systems it also
   reports critically on. */
.tech-vendor-line {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px;
  margin: 10px 0 0; font-size: 13px; color: var(--ink-mute);
}
.tech-vendor-name { color: var(--ink-soft); }
.tech-vendor-link {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em;
  color: var(--accent); border-bottom: 1px solid var(--rule);
}
.tech-vendor-link:hover { border-bottom-color: var(--accent); }

/* Links from an overview page to the individual technologies it covers.
   Reuses .topic-chip so a related-technology link looks the same as any
   other cross-reference on the site. */
.tech-related { margin: 22px 0 0; padding-top: 18px; border-top: 1px solid var(--rule); }

/* ── WHAT CHANGED ──
   An activity stream rather than a card grid: these are short factual
   lines, and putting each in a bordered box would give six routine
   re-checks the visual weight of six headlines. */
.changes-note {
  font-size: 13.5px; line-height: 1.65; color: var(--ink-mute);
  max-width: 72ch; margin: 0 0 18px;
}
.changes-list { list-style: none; margin: 0; padding: 0; }
.change-item {
  display: grid;
  /* The label column sizes to its widest label rather than a fixed 96px,
     which "RECORD CHECKED" overflowed. max-content means adding a longer
     label later cannot reintroduce the bleed. */
  grid-template-columns: max-content minmax(0, 1fr) auto;
  gap: 4px 16px; align-items: baseline;
  padding: 11px 0; border-bottom: 1px solid var(--rule);
}
.change-item:last-child { border-bottom: none; }
.change-label {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-mute);
  border: 1px solid var(--rule); padding: 3px 7px; text-align: center;
  white-space: nowrap;
}
/* The two kinds are distinguished by their label text, not by colour
   alone — the accent border is reinforcement, not the signal. */
.change-coverage .change-label { border-color: var(--accent); color: var(--accent); }
.change-title {
  font-family: var(--font-serif); font-size: 15.5px; line-height: 1.4;
  color: var(--ink); border-bottom: 1px solid transparent;
}
.change-title:hover { border-bottom-color: var(--accent); color: var(--accent); }
.change-meta {
  font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-mute);
  white-space: nowrap;
}
@media (max-width: 640px) {
  .change-item { grid-template-columns: minmax(0, 1fr); gap: 5px; padding: 13px 0; }
  .change-label { justify-self: start; }
  .change-meta { white-space: normal; }
}

/* ── SEARCH ──
   A plain GET form. No JavaScript is involved in submitting or rendering
   results, so search works with scripting disabled and needs no index to
   be kept in sync. */
.search-form { margin: 4px 0 26px; }
.search-label {
  display: block; font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-mute);
  margin-bottom: 8px;
}
.search-row { display: flex; gap: 10px; flex-wrap: wrap; }
.search-input {
  flex: 1 1 280px; min-width: 0; font-family: inherit; font-size: 16px;
  padding: 12px 14px; border: 1px solid var(--rule); background: var(--bg);
  color: var(--ink);
}
.search-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.search-submit {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em;
  padding: 12px 22px; border: 1px solid var(--ink); background: var(--ink);
  color: var(--bg); cursor: pointer; min-height: 44px;
}
.search-submit:hover { background: var(--accent); border-color: var(--accent); }
.search-hint { font-size: 12.5px; color: var(--ink-mute); margin: 8px 0 0; }
.search-count { font-size: 14px; color: var(--ink-soft); margin: 0 0 20px; }
.search-empty { font-size: 15px; line-height: 1.7; color: var(--ink-soft); max-width: 68ch; }
.search-empty a { color: var(--accent); border-bottom: 1px solid var(--rule); }
.search-group { margin: 0 0 28px; }
.search-group-title {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-mute); font-weight: 500;
  padding-bottom: 8px; border-bottom: 1px solid var(--rule); margin: 0 0 4px;
}
.search-results { list-style: none; margin: 0; padding: 0; }
.search-result {
  display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 4px 16px;
  padding: 13px 0; border-bottom: 1px solid var(--rule);
}
.search-result:last-child { border-bottom: none; }
/* The type is spelled out, never signalled by colour alone. */
.search-kind {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-mute);
  border: 1px solid var(--rule); padding: 3px 7px; text-align: center;
  align-self: start; white-space: nowrap;
}
.search-kind-technology, .search-kind-deployment { border-color: var(--accent); color: var(--accent); }
.search-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.search-title {
  font-family: var(--font-serif); font-size: 16px; line-height: 1.35; color: var(--ink);
  border-bottom: 1px solid transparent;
}
.search-title:hover { color: var(--accent); border-bottom-color: var(--accent); }
.search-meta {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-mute);
}
.search-blurb { font-size: 13.5px; line-height: 1.55; color: var(--ink-soft); }

/* ── SOURCE DIRECTORY ── */
.src-kind {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute); border: 1px solid var(--rule);
  padding: 1px 5px; margin-left: 6px;
}
.src-latest { font-family: var(--font-mono); font-size: 10px; color: var(--accent); margin-left: 8px; }
.src-region { font-size: 13px; color: var(--ink-mute); }

@media (max-width: 640px) {
  .search-result { grid-template-columns: minmax(0, 1fr); gap: 6px; }
  .search-kind { justify-self: start; }
  .src-region { display: none; }
}

/* A six-column heatmap genuinely cannot fit a 320px viewport, which is
   the case the mobile guidance calls unavoidable. Containing the scroll
   to the table is the correct fallback: the page itself never scrolls
   sideways, and the region is focusable and labelled so it is reachable
   by keyboard and announced by a screen reader. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Section standfirsts and the tracker credibility line. Sized between
   body and metadata so they explain a section without competing with
   its heading. */
.block-standfirst {
  font-size: 15px; line-height: 1.6; color: var(--ink-soft);
  max-width: 68ch; margin: -4px 0 20px;
}
.desk-credibility {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em;
  color: var(--ink-mute); margin: 8px 0 16px;
}

/* ── HERO CREDIBILITY AND ACTIONS ──
   Four lines of hero copy must not carry equal weight: the H1 leads, the
   lede explains, the credibility line is a trust signal at metadata
   scale, and the actions sit below all three. */
.hero-credibility {
  font-family: var(--font-mono); font-size: 11.5px; line-height: 1.6;
  letter-spacing: 0.02em; color: var(--ink-mute);
  max-width: 60ch; margin: 14px 0 0;
}
/* Bottom margin matters: the stat strip follows immediately, and without
   it the buttons sit flush against the strip's top border.
   flex:1 on each button, rather than a fixed width, is what makes three
   buttons of different label lengths still fill one row evenly on a
   normal browser width, dividing the row equally between them. */
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 22px 0 34px; }
/* All three CTAs are now visually identical — black fill, white text —
   rather than one primary and one secondary style. Two different
   treatments read as "pick this one over that one", which was never the
   intent once there were three equally-weighted destinations. Sentence
   case, never all-caps, per the copy guidance. */
.hero-cta {
  flex: 1 1 0;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 11px 20px; font-size: 15px; font-weight: 500;
  text-align: center; white-space: nowrap;
  background: var(--ink); border: 1px solid var(--ink); color: var(--bg);
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.hero-cta:hover { background: var(--accent); border-color: var(--accent); color: var(--bg); }
/* Below 640px three equal-width buttons get too narrow for their labels
   (see hero-cta white-space:nowrap above), so they stack full-width
   instead of squeezing into one row. */
@media (max-width: 640px) { .hero-cta { flex: 1 1 100%; } }
/* Lead line of the tracker qualification, so the limitation is scannable
   before the fuller methodological explanation beneath it. */
.callout-head { margin-bottom: 10px; }

/* ═══════════════════════════════════════════════
   BLOG
   ═══════════════════════════════════════════════
   Conventional blog tiling: one lead post with real prominence, then an
   even grid. Editorial hierarchy on the page itself, rather than every
   post looking equally important, which is what a flat list says whether
   or not it is true. */
.blog-lead {
  display: grid; grid-template-columns: 1.15fr 1fr; gap: 0;
  border: 1px solid var(--rule); margin: 4px 0 30px;
  background: var(--surface, var(--bg)); overflow: hidden;
  transition: border-color 0.15s ease;
}
/* When a post has no header image, render-blog.js correctly omits the
   .blog-lead-media div entirely rather than rendering an empty one --
   but the grid above still reserved two column tracks regardless, since
   grid-template-columns is unconditional CSS with no way to know from
   here whether a second child actually exists. With only .blog-lead-body
   present, grid auto-placement filled just the first (1.15fr) track and
   left the second (1fr) empty, which is precisely the dead space and
   half-width text this rule fixes: a post with no image gets a single
   full-width column instead of an empty one sitting unused beside it. */
.blog-lead.blog-lead-no-media { grid-template-columns: 1fr; }
.blog-lead:hover { border-color: var(--accent); }
.blog-lead-media { overflow: hidden; background: var(--rule); }
.blog-lead-media img { width: 100%; height: 100%; min-height: 260px; object-fit: cover; display: block; }
.blog-lead-body { padding: 30px 32px; display: flex; flex-direction: column; justify-content: center; }
.blog-lead-title {
  font-size: clamp(24px, 3vw, 32px); line-height: 1.15; font-weight: 700;
  color: var(--ink); margin: 8px 0 12px; letter-spacing: -0.012em;
}
.blog-lead-excerpt { font-size: 15.5px; line-height: 1.65; color: var(--ink-soft); margin: 0 0 16px; }
.blog-readmore {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.05em;
  color: var(--accent); text-transform: uppercase;
}

.blog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 22px; }
.blog-card {
  display: flex; flex-direction: column; border: 1px solid var(--rule);
  background: var(--surface, var(--bg)); overflow: hidden;
  transition: border-color 0.15s ease;
}
.blog-card:hover { border-color: var(--accent); }
.blog-card-media { height: 168px; overflow: hidden; background: var(--rule); }
.blog-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A deliberate typographic block rather than a broken-image icon or an
   unrelated stock photograph, for posts published without an image. */
.blog-card-media-empty {
  height: 88px;
  background: linear-gradient(135deg, var(--rule) 0%, transparent 60%);
}
.blog-card-body { padding: 18px 20px 22px; display: flex; flex-direction: column; flex: 1; }
.blog-card-title { font-size: 18px; line-height: 1.25; font-weight: 700; color: var(--ink); margin: 7px 0 10px; }
.blog-card-excerpt { font-size: 14px; line-height: 1.6; color: var(--ink-soft); margin: 0; }
.blog-meta {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.05em;
  color: var(--ink-mute); text-transform: uppercase;
}
.blog-grid-compact .blog-card-body { padding: 16px 18px; }
.blog-empty { padding: 50px 0; text-align: center; color: var(--ink-mute); }

/* ── SINGLE POST ──
   Constrained measure: long-form prose is unreadable at full page width,
   unlike the reference pages which are scanned rather than read. */
/* Full column width, matching the topic guides and every other content
   page on the site. This was capped at 74ch for prose readability, which
   is defensible in isolation but made blog posts visibly narrower than
   the rest of the site and looked like a mistake rather than a choice. */
.blog-post { margin: 0; }
.blog-post-head { margin-bottom: 26px; }
.blog-post-head h1 { font-size: clamp(28px, 4.4vw, 42px); line-height: 1.12; margin: 10px 0 14px; }
.blog-byline { font-size: 13.5px; color: var(--ink-mute); margin: 12px 0 0; }
.blog-post-image { width: 100%; height: auto; display: block; margin: 0 0 30px; border: 1px solid var(--rule); }

.blog-body { font-size: 17px; line-height: 1.75; color: var(--ink); }
.blog-body p { margin: 0 0 20px; }
.blog-body h2 { font-family: var(--font-serif); font-size: 25px; line-height: 1.2; margin: 36px 0 14px; color: var(--ink); }
.blog-body h3 { font-family: var(--font-serif); font-size: 20px; line-height: 1.25; margin: 30px 0 12px; color: var(--ink); }
.blog-body ul, .blog-body ol { margin: 0 0 20px; padding-left: 24px; }
.blog-body li { margin-bottom: 9px; }
.blog-body a { color: var(--accent); border-bottom: 1px solid var(--rule); }
.blog-body a:hover { border-bottom-color: var(--accent); }
.blog-body blockquote {
  margin: 26px 0; padding: 4px 0 4px 20px;
  border-left: 3px solid var(--accent); color: var(--ink-soft); font-style: italic;
}
.blog-body figure { margin: 28px 0; }
.blog-body img { max-width: 100%; height: auto; display: block; border: 1px solid var(--rule); }
.blog-body figcaption { font-size: 13px; color: var(--ink-mute); margin-top: 8px; }
.blog-body hr { border: none; border-top: 1px solid var(--rule); margin: 32px 0; }
.blog-body table { width: 100%; border-collapse: collapse; margin: 24px 0; font-size: 15px; }
.blog-body th, .blog-body td { border: 1px solid var(--rule); padding: 9px 12px; text-align: left; }
.blog-body th { font-weight: 600; background: var(--surface-2, transparent); }

@media (max-width: 760px) {
  .blog-lead { grid-template-columns: 1fr; }
  .blog-lead-media img { min-height: 200px; }
  .blog-lead-body { padding: 22px 20px; }
  .blog-body { font-size: 16px; }
}

/* ── LATEST NEWS (homepage) ──
   Deliberately separate from .blog-card and the Key Developments lead
   layout: three equal-weight tiles, because this section shows pure
   recency with no ranking, and a lead-plus-secondary layout would imply
   a ranking that does not exist here. */
.news-tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 20px; }
.news-tile {
  display: flex; flex-direction: column; border: 1px solid var(--rule);
  background: var(--surface, var(--bg)); overflow: hidden;
  transition: border-color 0.15s ease;
}
.news-tile:hover { border-color: var(--accent); }
.news-tile-media { height: 140px; overflow: hidden; background: var(--rule); }
.news-tile-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.news-tile-body { padding: 14px 16px 18px; }
.news-tile-body h3 { font-size: 16px; line-height: 1.3; font-weight: 700; color: var(--ink); margin: 6px 0 0; }
@media (max-width: 640px) { .news-tile-grid { grid-template-columns: 1fr; } }

/* ═══════════════════════════════════════════════════════════════════════
   TRACKER & SOURCES — UX REDESIGN (information architecture only)
   ═══════════════════════════════════════════════════════════════════════
   Every rule below reuses the site's existing colour tokens, type scale,
   spacing and border language. Nothing here introduces a new palette,
   new radius, new shadow, or new font. The classes are new because the
   INTERACTIONS are new (a stat that is also a filter button did not
   exist before); the visual vocabulary is not. */

/* ── STAT-FILTER CARDS ──
   Replaces the old pairing of a static .status-strip display with a
   separate, differently-styled row of .status-pick buttons underneath
   it: the same five numbers were rendered twice, once as inert text and
   once as controls. One row now serves both purposes, using the same
   grid geometry .status-strip already had (border-top/border-left with
   each cell closing its own right+bottom edge) so the visual language
   is unchanged, only the redundancy is removed. */
.stat-filter-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  border-top: 1px solid var(--rule); border-left: 1px solid var(--rule);
  margin: 22px 0 0;
}
.stat-filter-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 14px 14px; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  background: var(--bg); cursor: pointer; text-align: left;
  font-family: inherit; transition: background-color 0.15s ease;
}
.stat-filter-card:hover { background: var(--surface-2); }
.stat-filter-card:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.stat-filter-card.active { background: var(--accent-tint); }
.stat-filter-card.active::after {
  content: ''; display: block; height: 2px; width: 100%; margin-top: 6px;
  background: var(--accent);
}
.stat-filter-n { font-family: var(--font-serif); font-weight: 700; font-size: 24px; line-height: 1.1; }
.stat-filter-card.st-live     .stat-filter-n { color: var(--status-live); }
.stat-filter-card.st-trial    .stat-filter-n { color: var(--status-trial); }
.stat-filter-card.st-paused   .stat-filter-n { color: var(--status-paused); }
.stat-filter-card.st-ended    .stat-filter-n { color: var(--status-ended); }
.stat-filter-card.st-proposed .stat-filter-n { color: var(--status-proposed); }
.stat-filter-l {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--ink-mute);
}
.stat-filter-card.active .stat-filter-l { color: var(--ink); }
/* The All card resets every status filter rather than representing one
   itself, so it is deliberately muted relative to the coloured status
   cards beside it -- the same distinction .status-pick-all drew before
   this redesign, carried forward under the new class name. */
.stat-filter-card-all .stat-filter-n { color: var(--ink-mute); }
.stat-filter-card-all.active .stat-filter-n { color: var(--ink); }
@media (max-width: 480px) {
  .stat-filter-row { grid-template-columns: repeat(3, 1fr); }
}

/* ── ABOUT / METHODOLOGY DISCLOSURE ──
   Reuses .filter-panel-toggle's exact visual treatment (same border,
   same mono label, same chevron rotation) so a reader recognises it as
   the same kind of control as the filter drawer elsewhere on the site,
   rather than learning a new affordance. Collapsed by default: the
   methodology is important enough to keep in full, not important
   enough to force past on every visit. */
.about-panel { margin: 18px 0 0; }
.about-panel > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 13px 16px; cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink);
  border: 1px solid var(--rule); background: var(--surface-2);
  user-select: none; transition: border-color 0.15s ease, color 0.15s ease;
}
.about-panel > summary::-webkit-details-marker { display: none; }
.about-panel > summary:hover { border-color: var(--accent); color: var(--accent); }
.about-panel > summary .filter-panel-chevron { flex-shrink: 0; transition: transform 0.15s ease; }
.about-panel[open] > summary .filter-panel-chevron { transform: rotate(180deg); }
.about-panel-body {
  border: 1px solid var(--rule); border-top: none; padding: 18px 20px;
}
@media (prefers-reduced-motion: reduce) {
  .about-panel > summary, .about-panel > summary .filter-panel-chevron { transition: none; }
}

/* ── SEARCH FIELD (tracker + sources) ──
   Sized and bordered exactly like the existing .tf-field selects, so a
   text search sits as a natural sibling of the dropdowns rather than
   looking like an imported component. */
.tf-search { flex: 2 1 260px; position: relative; }
.tf-search input {
  width: 100%; padding: 9px 34px 9px 12px; font-size: 13.5px;
  font-family: inherit; color: var(--ink);
  border: 1px solid var(--rule); background: var(--surface);
}
.tf-search input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.tf-search-icon {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  color: var(--ink-mute); pointer-events: none;
}

/* ── ACTIVE FILTER CHIPS ──
   A removable pill per active filter, using the site's existing .chip
   styling with a small close glyph appended rather than a new shape. */
.active-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px 0 0; }
.active-filters:empty { margin: 0; }
.active-filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em;
  padding: 6px 10px; border: 1px solid var(--accent); background: var(--accent-tint);
  color: var(--accent); cursor: pointer;
}
.active-filter-chip:hover { background: var(--accent); color: var(--bg); }
.active-filter-chip .x { font-size: 13px; line-height: 1; }
.active-filters-clear {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute);
  text-decoration: underline; text-underline-offset: 2px; background: none; border: none;
  cursor: pointer; padding: 6px 4px;
}
.active-filters-clear:hover { color: var(--accent); }

/* ── RESULT COUNT LINE ──
   States what the visitor is currently looking at, since the moment
   filters can hide most of the list, "how many things exist" and "how
   many things are shown" become two different, both useful, numbers. */
.result-count { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-mute); margin: 14px 0 0; }
.result-count strong { color: var(--ink); font-weight: 600; }

/* ── DEPLOY CARD: enriched summary line ──
   Additive to the existing .deploy-summary layout: a second line
   carrying supplier and last-checked, which previously only appeared
   after expanding. Kept small and muted so it reads as metadata
   supporting the headline (force + technology), not competing with it. */
.deploy-summary-meta {
  display: block; font-family: var(--font-mono); font-size: 10.5px;
  color: var(--ink-mute); margin-top: 3px; letter-spacing: 0.01em;
}
.deploy-detail-section { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--rule); }
.deploy-detail-section:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.deploy-detail-heading {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-mute); margin: 0 0 8px;
}

/* ── SOURCES: directory cards (replaces long <table> rows on mobile) ──
   Desktop keeps the existing .stats-table; below the breakpoint each row
   becomes a stacked card using the same visual language (same border
   colour, same muted meta, same accent link) rather than forcing a wide
   table into a narrow viewport. */
.source-card-list { display: none; }
.source-card {
  display: block; padding: 14px 0; border-bottom: 1px solid var(--rule);
}
.source-card:last-child { border-bottom: none; }
.source-card-name { font-weight: 600; color: var(--ink); font-size: 15px; }
.source-card-meta {
  display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 4px;
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute);
}
.source-card-count { color: var(--ink); font-weight: 600; }
.source-card-bar { margin-top: 8px; }
@media (max-width: 700px) {
  .stats-table-wrap { display: none; }
  .source-card-list { display: block; }
}

/* ── SOURCES: tab navigation (Contributing | Monitored | Archive) ──
   Reuses .chip visually but in a role of view-switching rather than
   filtering, so it is sized a little larger and given a bottom-anchored
   active indicator instead of a filled background, to read as tabs
   rather than as another row of filter pills directly above it. */
.source-tabs {
  display: flex; gap: 4px; margin: 20px 0 0; border-bottom: 1px solid var(--rule);
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.source-tab { flex-shrink: 0; }
.source-tab {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.03em;
  padding: 10px 16px; border: none; background: none; color: var(--ink-mute);
  cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.source-tab:hover { color: var(--ink); }
.source-tab.active { color: var(--ink); border-bottom-color: var(--accent); font-weight: 600; }
.source-tab-count { color: var(--ink-mute); font-size: 10.5px; }
.source-view { display: none; }
.source-view.active { display: block; }

/* ── SOURCES: region chip row ── */
.region-chip-row { display: flex; flex-wrap: wrap; gap: 7px; margin: 14px 0 0; }

/* ── SUGGEST A SOURCE (understated CTA, not a banner) ── */
.suggest-source {
  margin: 34px 0 0; padding: 18px 20px; border: 1px solid var(--rule);
  background: var(--surface); text-align: center;
}
.suggest-source p { margin: 0 0 4px; color: var(--ink-soft); font-size: 13.5px; }
.suggest-source a { color: var(--accent); font-weight: 600; }

@media (max-width: 640px) {
  .tracker-filters { flex-direction: column; }
  .tf-search { flex: 1 1 auto; }
  /* .tf-field's flex: 1 1 200px sets a 200px WIDTH basis for the desktop
     horizontal row. flex-basis has no concept of "width" vs "height" of
     its own -- it always means the main-axis size -- so the moment the
     container above switches to flex-direction: column, that same value
     becomes a 200px HEIGHT basis instead, forcing every dropdown into a
     200px-tall box regardless of how short its actual content is. That
     was the large empty gap between each filter on mobile. Reset to
     auto here so each field's height comes from its content again. */
  .tf-field { flex: 1 1 auto; }
}
