/* Bay Area Transit Analysis. House style, matching the author's own sites.
   Tokens and conventions surveyed in docs/STYLE.md; the layout, card, app bar
   and palette rules come from style/DASHBOARD-REFERENCE.md (2026-10-21), which
   studied hsereport.ie and AI-Graph Response-Compare, and are kept in step with
   the car-vs-transit dashboard's site.css so the two read as siblings. Light
   only: the author's sites declare no dark mode, so neither does this one.
   Nothing here changes a number; the data-bearing colours (family, role,
   status, map) survive, pulled toward the same restraint. */

:root {
  /* his tokens, verbatim from /css/index.css */
  --content-max: 66rem;
  --fig-max: 44rem;
  /* 900 px, the card width on his own dashboard (reference rule 1) */
  --card-max: 56.25rem;
  --page-bg: #f5f5f5;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --codefont: Consolas, Menlo, Monaco, "Andale Mono WT", "Lucida Console", "DejaVu Sans Mono", "Liberation Mono", "Courier New", monospace;
  --lightgray: #e0e0e0;
  --gray: #C0C0C0;
  --darkgray: #333;
  --navy: #17050F;
  --blue: #082840;
  --white: #fff;
  --muted-ink: #666;
  --pre-bg: #f6f6f6;
  --warn-bg: #ffc;

  /* dashboard aliases */
  --bg: var(--white);
  --ink: var(--darkgray);
  --muted: var(--muted-ink);
  --panel: var(--white);
  --border: var(--lightgray);
  --grid: var(--lightgray);
  --accent: var(--blue);
  --pending: var(--gray);
  --code: var(--pre-bg);

  /* data-bearing, muted to print weights */
  --density: #8a5a0a;
  --transit: #1f4e79;
  --land: #f4f4f2;
  --water: #eceff2;
  --dais: #4c3575;
  --public: #0f5f57;
  --staff: #7a5230;
  --unknown: var(--gray);
  --ok: #2f6b3a;
  --warn: #8a6d1a;
  --crit: #8c2b23;
  --pilot: var(--dais);
  --chip: var(--warn-bg);
  --chipink: #5c4a00;

  /* ── TYPE SCALE, one size per role (owner, 2026-10-25: "look at the site for
     sizing and redo text for where it is not"). Before this pass the same role
     was set at four different sizes: body prose rendered at 13, 14, 15 and 16 px
     on one page, captions at 12, 12.8 and 13, and an h5 inside a panel fell
     through to the browser's own 0.83em and arrived at 13.28 px. Each rule below
     names the role rather than a number, so a role can only be one size.

     The phone block further down raises the three small tokens, which is what
     puts every floor in reach on a 390 px screen: body prose at 16 px or more,
     captions and table cells at 14 px or more. The desktop values are the ones
     the reference sites set and are unchanged. */
  --fs-body: 1rem;        /* 16 px: prose a reader reads straight through */
  --fs-sec: 0.9375rem;    /* 15 px: secondary prose, small headings, quotes */
  --fs-cap: 0.8125rem;    /* 13 px: captions, notes, metadata, source lines */
  --fs-tbl: 0.8125rem;    /* 13 px: table cells and headers */
  --fs-micro: 0.75rem;    /* 12 px: pills, chips, hashes */
}

/* THE PHONE FLOORS. Below the card's two-column width the three small tokens
   step up, so nothing a reader has to read lands under 14 px and no prose lands
   under 16 px. Sizes are raised here rather than per rule, so a rule that was
   consistent on a desktop cannot drift apart on a phone. */
@media (max-width: 52rem) {
  :root {
    --fs-sec: 1rem;         /* 16 px */
    --fs-cap: 0.875rem;     /* 14 px */
    --fs-tbl: 0.875rem;     /* 14 px */
    --fs-micro: 0.875rem;   /* 14 px */
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--page-bg);
  color: var(--ink);
  font-family: -apple-system, system-ui, sans-serif;
  font-size: 16px;
}

/* ONE WHITE CARD ON A LIGHT GREY PAGE (reference rule 1, his own dashboard's
   shell). Prose is capped narrower than the card; figures may run to the card
   edge, which is why the prose cap is set on the text elements and not here. */
main {
  max-width: var(--card-max);
  margin: 1.25rem auto;
  padding: 1rem 1rem 2.5rem;
  background: var(--bg);
  border-radius: 0.125rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
p, li { line-height: 1.45; }
p { margin: 0 0 0.625rem; }
h1, h2, h3, p, ul, ol { max-width: var(--fig-max); }
/* A repository path or a long command written as prose has no break opportunity
   in it, so it pushed the page wider than the screen: on a 390 px phone the
   Methods page scrolled to 583 px because of one file path in the pooling note.
   Long words break rather than widen the page. */
p, li, dd, dt, td, th, blockquote, h1, h2, h3, h4, h5 { overflow-wrap: break-word; }

/* Typography scale, the car-vs-transit dashboard's, so the two pages set type
   the same way. The reference's own h2 is larger (hsereport sets 1.35rem), but
   every h2 here is a full takeaway sentence rather than a two-word label, and a
   sentence at that size reads as a second headline. */
h1 { font-size: 1.375rem; line-height: 1.3; margin: 0 0 0.25rem; font-weight: 700; }
h2 {
  font-size: 1.0625rem; line-height: 1.35; margin: 2.5rem 0 0.5rem;
  font-weight: 700; border-top: 1px solid var(--border); padding-top: 1.25rem;
  scroll-margin-top: 0.75rem;
}
h3 { font-size: var(--fs-sec); margin: 1.5rem 0 0.35rem; font-weight: 700; }
h4 { font-size: var(--fs-sec); margin: 0 0 0.375rem; }
/* h5 had no rule of its own, so every h5 on the page fell through to the
   browser's 0.83em of whatever it sat in: 13.28 px inside a panel, which is
   under the phone floor and smaller than the paragraph beneath it. */
h5 { font-size: var(--fs-sec); line-height: 1.35; margin: 0 0 0.375rem; font-weight: 700; }

a[href] { color: var(--accent); }
a[href]:visited { color: var(--navy); }

.sub { color: var(--muted); margin: 0 0 0.5rem; font-size: var(--fs-sec); max-width: var(--fig-max); }
.note { font-size: var(--fs-cap); color: var(--muted); }

/* THIN APP BAR (reference rule 2): the site name at the left, the view links at
   the right, the active one the same hue darker. It carries no status pill: the
   standard's rule 12 states exploratory status once, as a formal sentence in the
   title area, with no banner. It carries no second source link either, because
   the footer names the code once (rule 14). His own bar does not wrap, so at 390
   the tab strip here wraps onto its own line instead of scrolling off (reference
   rule 15). */
.appbar {
  min-height: 2.625rem;
  padding: 0.3rem 1rem;
  background: var(--accent);
  color: var(--white);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
}
.appbar .name { font-size: 0.875rem; font-weight: 600; }
.appbar a.name, .appbar a.name:visited { color: var(--white); text-decoration: none; }
.appbar .tabs { margin-left: auto; display: flex; gap: 0.25rem; }
.appbar .tabs a[href], .appbar .tabs a[href]:visited {
  color: #bbb; text-decoration: none; font-size: var(--fs-cap);
  padding: 0.35rem 0.6rem; border-radius: 0.125rem;
}
.appbar .tabs a[href]:hover { color: var(--white); }
.appbar .tabs a[aria-current="page"] { color: var(--white); background: rgba(0, 0, 0, 0.22); }
@media (max-width: 30rem) {
  .appbar .tabs { margin-left: 0; width: 100%; }
}

/* Banner rule (owner, 2026-10-18 21:18, docs/STYLE.md): there is no cream
   EXPLORATORY banner. The word is in the page title and h1; the limitations are
   plain prose on Methods and in each panel's results body. */

/* ── THE FIRST SCREEN ──────────────────────────────────────────────────────
   Owner, 2026-10-21 10:04: "i want the images to start on the first page of the
   website. like when i load it it should be there. so either remove some text or
   put it to the right of the text." So the opening text and the lead chart are
   one two-column band, the text left and the chart right, which is also the
   shape of the author's AI-Graph page (reference section 2: diagram left,
   detail panel right). Below 52rem the band stacks, and the goal, the links and
   the two example comments move below the chart so the chart is still on the
   first screen without being shrunk. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
  gap: 0.5rem 1.75rem;
  align-items: start;
  margin: 0.75rem 0 0;
}
.hero > * { min-width: 0; }

/* The lead chart: its takeaway title, the chart, one sentence under it. The
   chart is drawn client-side from data/panels.json, so the title, the bars and
   the caption all come from the same row and cannot disagree. */
/* The separate definitions paragraph was removed on 2026-10-24 17:05 (owner:
   "just have the definition in the tldr"), so .deflines and its two placement
   rules below went with it. The TLDR bullet carries the definition now and
   needs no styling of its own. */
.leadchart { margin: 0; padding: 0; }
.leadchart h2 {
  border-top: 0; padding-top: 0; margin: 0 0 0.5rem;
  font-size: 1rem; line-height: 1.35; max-width: none;
}
/* The same bar styling serves both charts: the NIMBY split on the first screen and
   the took-a-side pair inside "Who takes a side" (owner, 2026-10-24 16:43). */
svg#leadsvg, svg#sidesvg { display: block; width: 100%; height: auto; max-width: 20rem; }
svg#leadsvg .lead-cat, svg#sidesvg .lead-cat { font-size: 12px; fill: var(--ink); font-weight: 600; }
svg#leadsvg .lead-val, svg#sidesvg .lead-val {
  font-size: 12px; fill: var(--ink); font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* The pad behind the value, drawn by drawBars (index.html). The value reads
   "41 of 100" rather than "41" since 2026-10-25, which is wide enough to reach the
   gridlines behind it, and a gridline was being drawn through the space between
   its words. */
rect.lead-valbg { fill: var(--panel); }
/* The axis numbers and the axis title: 10.5 px here was 10.5 px on screen, since
   this chart's frame is its rendered width, and that is under the floor for text
   a reader has to read. 12 px, which is what the category labels are set at. */
svg#leadsvg .lead-ax, svg#sidesvg .lead-ax { font-size: 12px; fill: var(--tick, var(--muted)); }
svg#leadsvg .lead-base, svg#sidesvg .lead-base { stroke: var(--gray); stroke-width: 1; shape-rendering: crispEdges; }
svg#leadsvg .lead-grid, svg#sidesvg .lead-grid { stroke: var(--grid); stroke-width: 1; shape-rendering: crispEdges; }
rect.lead-bar.public { fill: var(--public); }
rect.lead-bar.dais { fill: var(--dais); }
rect.lead-bar.nimby { fill: var(--nimby); }
rect.lead-bar.yimby { fill: var(--yimby); }
rect.lead-bar.nstance { fill: var(--nstance); }
.sideblock .sidehead { margin: 0.25rem 0 0.4rem; font-size: var(--fs-sec); line-height: 1.35; }
.sideblock .caption { max-width: none; margin: 0.45rem 0 0; font-size: var(--fs-cap); line-height: 1.45; color: #555; }
/* One sentence under every chart, the caption habit of his own dashboard
   (reference rule 5): what is drawn, then the size of the gap with its range. */
.leadchart .caption, .leadchart .leaddef {
  max-width: none; margin: 0.45rem 0 0;
  font-size: var(--fs-cap); line-height: 1.45; color: #555;
}
.leadchart .leaddef { margin-top: 0.5rem; color: var(--muted); }
.leadchart .leadfail { font-size: var(--fs-cap); color: var(--muted); margin: 0; }
/* Desktop placement: the TLDR fills the first row of the left column, the goal
   and the scope the second, and the chart stands beside both. */
@media (min-width: 52.0625rem) {
  .hero > ul.tldr   { grid-column: 1; grid-row: 1; }
  .hero > .herorest { grid-column: 1; grid-row: 2; }
  .hero > .leadchart { grid-column: 2; grid-row: 1 / span 2; }
}
/* Phone: one column, and the chart comes straight after the TLDR. The goal, the
   scope and the exploratory sentence drop below it, and the two links and the
   example comments follow in the normal flow, so the chart is on the first
   screen at full size rather than shrunk to fit above them. */
@media (max-width: 52rem) {
  .hero { grid-template-columns: 1fr; gap: 0.5rem; display: flex; flex-direction: column; }
  /* Phone: the chart comes straight after the TLDR, and the goal, the scope and
     the exploratory sentence drop below it, so the whole chart stands on the first
     screen rather than half of it below the fold (owner, items 10, 17 and 18). */
  .hero > ul.tldr { order: 1; }
  .hero > .leadchart { order: 2; }
  .hero > .herorest { order: 3; }
}

/* THE TLDR BLOCK, his blog pattern: a literal label, then 3 or 4 short bullets.
   It opens the page in place of an intro paragraph (owner, 2026-10-21 08:40: "the
   tldr should replace the first paragraph"), and style/DASHBOARD-LANGUAGE.md
   section "The TLDR" keeps the label visible. These rules came off the stylesheet
   on 2026-10-21 07:14, when the bullets were turned into prose, and are back
   unchanged now that the block is the first thing on the page again. */
ul.tldr::before {
  content: "TLDR:";
  display: block;
  margin: 0 0 0.25rem -1.25rem;
  font-weight: 700;
  color: var(--ink);
}
ul.tldr {
  margin: 0.75rem 0 1.25rem;
  padding-left: 1.25rem;
  max-width: var(--fig-max);
  font-size: var(--fs-body);
}
ul.tldr li { margin: 0.25rem 0; line-height: 1.45; }

/* The goal block under the TLDR: what the classification decides, then the corpus
   scope with its counts. Full body size, because it is the first prose anyone
   reads. Numbers live here, not in the TLDR. */
p.intro {
  margin: 0.5rem 0 0.75rem; max-width: 76ch; font-size: var(--fs-body); line-height: 1.45;
  color: var(--ink);
}
p.intro + p.intro { margin-top: -0.25rem; }

/* The grey exploratory stamp that used to sit under the scope line is gone (owner,
   2026-10-24 18:47, "Remove the Exploratory Analysis grey subtitle"): the goal
   paragraph he dictated ends on that sentence, so rule 12 is met in his own words
   and this rule had no element left to style. */

/* The owner's own sentences: the two links, and why it matters. Body size, same
   measure as the goal block, because they are the page's argument and not notes. */
p.said { margin: 0.5rem 0 1rem; max-width: 76ch; font-size: var(--fs-body); line-height: 1.5; }

/* THE TWO EXAMPLE COMMENTS, one NIMBY beside one YIMBY (owner, 2026-10-21 08:45).
   Side by side on a desktop and stacked on a phone, which .excols already does.
   The side is carried by the quote's own left rule and by the heading colour, so
   the two read as a pair rather than as two unrelated blockquotes. */
.exq { margin: 0.25rem 0 0.75rem; }
.exq h5 { margin: 0 0 0.375rem; font-size: var(--fs-sec); font-weight: 600; }
.exq.yimby h5 { color: var(--yimby); }
.exq.nimby h5 { color: var(--nimby); }
.exq blockquote.quote {
  margin: 0; padding: 0 0 0 0.75rem;
  border-left: 3px solid var(--border);
  font-size: var(--fs-sec); line-height: 1.5;
}
.exq.yimby blockquote.quote { border-left-color: var(--yimby); }
.exq.nimby blockquote.quote { border-left-color: var(--nimby); }
.exq .qmeta { font-size: var(--fs-cap); color: var(--muted); margin: 0.375rem 0 0; }

/* The data-as-of and total-spend line, now the page footer. Dated spend is visible
   by house rule, and it is support rather than a finding -- hence the muted .sub
   treatment, one notch smaller, with a rule above it. */
.asof {
  margin: 1.5rem 0 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
  font-size: var(--fs-cap);
  max-width: var(--fig-max);
}
.asof:empty { display: none; }

/* A panel's opening: one or two short sentences, then an optional line of
   definition. This was a bulleted list under a literal "TLDR:" label until
   2026-10-21; the label was the thing that made every panel read the same. */
.lede { font-size: var(--fs-sec); color: var(--muted); margin: 0 0 0.625rem; max-width: 76ch; line-height: 1.45; }
p.heads {
  margin: 0 0 0.875rem;
  font-size: var(--fs-body); max-width: var(--fig-max); line-height: 1.45;
}

/* Caveats: plain prose in the results body, his bolded run-in lead (§7), no box */
.caveats { margin: 0.875rem 0 0; }
.caveat-prose {
  font-size: 0.875rem; color: var(--ink); line-height: 1.45;
  margin: 0.625rem 0; max-width: 80ch;
}
.caveat-prose b { color: var(--ink); font-weight: 700; }
.caveats p { font-size: 0.875rem; color: var(--ink); margin: 0.4375rem 0 0; max-width: 80ch; }
.caveats table { margin-top: 0.5rem; }
.pfigs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); gap: 1rem; margin: 0.75rem 0; }
.src-note { font-size: var(--fs-cap); color: var(--muted); margin: 0.5rem 0 0; }
.pill.s1 { border-color: var(--dais); color: var(--dais); }

/* Panels: a rule and whitespace, not a card */
.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.75rem 0.875rem;
  margin: 0.625rem 0;
  overflow-x: auto;
}
.wrap { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 24rem); gap: 1rem; align-items: start; }
@media (max-width: 50rem) { .wrap { grid-template-columns: 1fr; } }
/* The Methods flowchart's row. The chart is 720 units wide and the card is 900:
   there is no width left for a 24rem panel beside it, and while there was one the
   chart was scaled to 406 px and its smallest labels rendered at 6.2 px. The step
   panel sits under the chart instead, where it does not need to stick to the
   scroll either. */
.flowwrap { grid-template-columns: 1fr; }
.flowwrap .detail { position: static; max-height: none; overflow: visible; }
/* The phone hint under the flowchart. Hidden until the phone block turns it on. */
.flowhint { display: none; }
.detail { position: sticky; top: 0.75rem; max-height: calc(100vh - 1.5rem); overflow: auto; }
.detail h3 { margin: 0 0 0.375rem; font-size: var(--fs-sec); }
.detail p { margin: 0 0 0.5rem; color: var(--muted); font-size: var(--fs-sec); }
.crumbs { font-size: var(--fs-cap); color: var(--muted); margin: 0 0 0.5rem; }
.crumbs a { color: var(--accent); cursor: pointer; }

/* Code */
pre, code, .detail code, .hash, .files a, svg .code {
  font-family: var(--codefont);
}
code {
  font-size: var(--fs-cap);
  background: var(--pre-bg);
  padding: 0 3px;
  word-break: break-all;
}
.detail code { background: var(--pre-bg); padding: 1px 4px; font-size: var(--fs-cap); }
.hash { font-size: var(--fs-micro); color: var(--muted); word-break: break-all; }
details .verbatim {
  margin: 0;
  padding: 0.625rem 0.875rem 0.875rem;
  border-top: 1px solid var(--border);
  font: 14px/1.375 var(--codefont);
  white-space: pre-wrap;
  background: var(--pre-bg);
}

/* Tables: his are plain, rules under rows only */
table { border-collapse: collapse; width: 100%; font-size: var(--fs-tbl); margin: 1em 0; }
th, td {
  text-align: left;
  padding: 0.3125rem 0.4375rem;
  padding-right: 1em;
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
  vertical-align: top;
}
th { color: var(--muted); font-weight: 500; }
td.num, th.num { text-align: right; }
tr.row { cursor: pointer; }
tr.row:hover td { background: var(--pre-bg); }
tr.best td { background: var(--pre-bg); }
tr.fam-head td {
  border-bottom: 0; padding-top: 0.75rem; color: var(--muted);
  font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: var(--fs-micro);
}

/* Tags and pills, borrowed from his .post-tag */
.pill {
  display: inline-block; font-size: var(--fs-micro); line-height: 1.8;
  padding: 0.0833em 0.3333em; margin: 0;
  border: 1px solid var(--gray); border-radius: 0.25em;
  color: var(--muted); white-space: nowrap; text-transform: uppercase;
}
.pill.density { border-color: var(--density); color: var(--density); }
.pill.transit { border-color: var(--transit); color: var(--transit); }
.pill.both, .pill.done, .pill.OK, .pill.on { border-color: var(--ok); color: var(--ok); }
.pill.located, .pill.cut, .pill.med { border-color: var(--accent); color: var(--accent); }
.pill.mention, .pill.off { color: var(--pending); border-color: var(--pending); }
.pill.WARN, .pill.high { border-color: var(--warn); color: var(--warn); }
.pill.prelim { border-color: var(--warn); color: var(--warn); font-weight: 600; }
.pill.crit { border-color: var(--crit); color: var(--crit); font-weight: 600; }
.pill.pilot { border-color: var(--pilot); color: var(--pilot); }
.badge-row { display: flex; gap: 0.375rem; flex-wrap: wrap; margin: 0 0 0.5rem; }
.chip {
  display: inline-block; font-size: var(--fs-micro); font-weight: 600;
  padding: 0 6px; border-radius: 0.25em;
  background: var(--chip); color: var(--chipink); line-height: 1.5; vertical-align: baseline;
}
.role {
  display: inline-block; font-size: var(--fs-micro); font-weight: 600;
  padding: 0.0833em 0.3333em; border-radius: 0.25em;
  color: var(--white); background: var(--unknown);
}
.role.dais { background: var(--dais); }
.role.public { background: var(--public); }
.role.staff { background: var(--staff); }

/* Role audit (rule ra-1, 2026-10-18): the reader shows the reported role and, where the
   audit disagrees with the diarization-cue label, the cue label struck through plus the
   self-identification that moved it. Muted on purpose -- the reported role is the headline. */
.rolewas { display: inline-block; font-size: 0.625rem; color: var(--muted); }
.rolewas s { text-decoration-thickness: 1px; }
.selfid { display: inline-block; font-size: 0.625rem; color: var(--muted); font-style: italic; }

.corrected {
  display: inline-block; font-size: var(--fs-micro); font-weight: 600; white-space: nowrap;
  padding: 0.0833em 0.3333em; border-radius: 0.25em;
  color: var(--muted); border: 1px solid var(--border);
}
.note.rolenote {
  margin: 0.75rem 0 0; padding: 0.5rem 0.6875rem; max-width: 84ch;
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: 0.1875rem; line-height: 1.5;
}

/* Disclosures (readability review, docs/READABILITY-REVIEW-2026-10-18.md).
   Nothing was deleted to make the page shorter: every sentence, table and figure
   is still here, and only its DEFAULT VISIBILITY moved. A fold is a rule and a
   link, not a card -- the same restraint as the rest of the page. */
details.fold {
  border: 0; border-radius: 0; background: none;
  margin: 0.625rem 0; padding: 0;
}
details.fold > summary {
  padding: 0.3125rem 0; font-size: 0.875rem; color: var(--accent);
  border-bottom: 1px solid var(--border);
}
details.fold > summary:hover { background: none; text-decoration: underline; }
details.fold[open] > summary { margin-bottom: 0.25rem; }
details.fold > .body { padding: 0 0 0.25rem; }
/* The "more" fold at the foot of the page gets a little more presence, because
   it is the one a reader has to decide to open. */
details.fold.more { margin: 2rem 0 0; }
details.fold.more > summary {
  font-size: 1rem; font-weight: 600; color: var(--accent);
  padding: 0.5rem 0; border-top: 1px solid var(--border);
}
/* The plain sentence that stands in for a collapsed table. */
.numlede { font-size: var(--fs-sec); max-width: 80ch; margin: 0.625rem 0 0.125rem; }
/* Every table scrolls inside its own box instead of widening the page. */
.tscroll { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }

/* Reader: the quoted turn leads, everything about it folds away. */
blockquote.leadturn {
  margin: 0.5rem 0 0.375rem; padding: 0 0 0 0.875rem;
  border-left: 3px solid var(--border);
  font-size: 1.0625rem; line-height: 1.5; max-width: var(--fig-max);
}
blockquote.leadturn p { margin: 0 0 0.375rem; }
.leadsrc { font-size: 0.8125rem; color: var(--muted); margin: 0 0 0.75rem; }

/* A data load that fails says so, instead of leaving empty panels. */
.loadfail {
  margin: 0.75rem 0; padding: 0.625rem 0.75rem; max-width: 80ch;
  border: 1px solid var(--border); border-left: 3px solid var(--crit);
  font-size: var(--fs-sec); color: var(--ink);
}

/* Key / legend */
.key {
  display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;
  font-size: var(--fs-cap); color: var(--muted); margin: 0.375rem 0 0.875rem;
}
.key span.sw::before {
  content: ""; display: inline-block; width: 10px; height: 10px;
  border-radius: 50%; margin-right: 6px; vertical-align: middle; background: var(--c);
}
.key span::before { background: var(--c); }
.key .ramp {
  display: inline-block; width: 90px; height: 10px; border-radius: 2px;
  background: linear-gradient(90deg, var(--transit), #9ca3af, var(--density));
  vertical-align: middle; margin: 0 6px;
}

/* Figures: image and caption both capped at his 44rem, caption is muted prose */
.figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); gap: 1rem; }
.fig {
  background: var(--panel); border: 0; border-top: 1px solid var(--lightgray);
  border-radius: 0; padding: 0.75rem 0 0;
}
.fig img {
  display: block; width: 100%; max-width: min(100%, var(--fig-max));
  height: auto; margin: 0 auto; background: var(--white);
}
.fig h4 { margin: 0 0 0.375rem; font-size: var(--fs-sec); }
.fig p, .caption {
  display: block; max-width: min(100%, var(--fig-max)); margin: 0.5rem auto 0;
  font-size: var(--fs-cap); line-height: 1.45; color: var(--muted);
}
#map, #flow, #chart svg { display: block; max-width: 100%; height: auto; }
#chart { text-align: center; }

/* Map and SVG */
svg text { fill: var(--ink); }
svg .lbl { font-size: 10.5px; fill: var(--muted); }
svg .muted { fill: var(--muted); font-size: 12px; }
svg .code { font-size: 11px; fill: var(--muted); }
.county { fill: var(--land); stroke: var(--border); stroke-width: 1; }
/* THE MAP ON A PHONE. The map is drawn in a 640-unit frame and shown 328 px
   wide, so an 11 px label arrives at 5.6 px, and at that width the body names
   also run into each other: "San Francisco" sat across "Sausalito", and
   "SamTrans", "Palo Alto" and "Los Altos" overlapped in one knot. Below 50rem
   the body names come off and the county names are set large enough in frame
   units to land near 11 px on screen. Nothing is lost: a body's name, its type
   and all of its counts are in the hover text, which opens on a tap, and the
   legend under the map says so.
   42rem is the width at which the map starts being scaled down: the card gives it
   640 units of room at 672 px of viewport and less below that. Above it the map is
   drawn at full size and its labels are already the size they are set at. */
@media (max-width: 42rem) {
  #map g.mklbl text.lbl { display: none; }
  #map > g:not(.mklbl) > text.lbl { font-size: 22px; }
}
/* A label that would have been printed across one already placed is not drawn
   (index.html, declutterLabels). */
#map text.lbl[data-hidden] { display: none; }
/* The names are drawn after the circles, so one that falls on a circle needs a
   pad under it to stay readable. */
#map text.lbl { paint-order: stroke; stroke: var(--panel); stroke-width: 3px; stroke-linejoin: round; }
/* One shape for every body, a circle (owner, 2026-10-21 10:06). The dashed ring for
   untranscribed bodies is gone with those bodies (owner, 2026-10-24 19:30, inbox 33).
   `.body.selected` went with the detail panel: there is no selected body any more,
   only a hovered or tapped one. */
.body { stroke: var(--panel); stroke-width: 1.2; cursor: pointer; }
svg .node .box { fill: var(--bg); stroke: var(--pending); stroke-width: 1.5; }
svg .node.done .box { stroke: var(--ok); }
svg .node.active .box { stroke: var(--accent); stroke-width: 2.5; }
svg .node.prelim .box { stroke: var(--warn); }
svg .node.pilot .box { stroke: var(--pilot); }
svg .node.gate .box { stroke-dasharray: 5 3; }
/* A decision box: something was judged here rather than processed. Dashed, as
   in the D3 flow this chart follows (docs/STYLE.md; owner's reference, 2026-10-19). */
svg .node.decision .box { stroke: var(--warn); stroke-dasharray: 5 3; }
svg .node.selected .box { stroke-width: 3; }
svg .node:focus { outline: none; }
svg .node:focus-visible .box { stroke-width: 3; }
svg .lbl { font-size: 11px; fill: var(--muted); }
svg .code { font-size: 11px; fill: var(--muted); }
svg .badge { font-size: 11px; font-weight: 600; letter-spacing: 0.02em; }
svg .num { font-weight: 700; font-size: 13px; fill: var(--accent); }
svg .title { font-weight: 600; font-size: 15px; }
svg .desc { font-size: 12.5px; fill: var(--muted); }
svg .big { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
svg .edge { fill: none; stroke: var(--muted); stroke-width: 2; }
svg .edge.gate { stroke: var(--warn); stroke-dasharray: 6 4; }
svg .elabel { font-size: 12px; fill: var(--muted); }
svg .elabel.gate { fill: var(--warn); }

/* Tooltip */
.tip {
  position: fixed; pointer-events: none; display: none; z-index: 9;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--border); border-radius: 3px;
  padding: 0.375rem 0.5625rem; font-size: var(--fs-cap); white-space: nowrap;
  box-shadow: 0 1px 4px rgba(0,0,0,.12);
}
/* On a phone the map tooltip carries everything the old side panel showed
   (owner, item 13c), and nowrap made that box wider than the screen. Let it
   wrap and cap it to the viewport so a tapped tooltip reads in full. */
@media (max-width: 30rem) {
  .tip { white-space: normal; max-width: calc(100vw - 1rem); }
}

/* Reader */
#reader { margin-top: 1rem; display: none; }
#reader h3 { margin: 0 0 0.25rem; font-size: 1rem; }
#reader .meta { color: var(--muted); font-size: 0.875rem; margin: 0 0 0.625rem; }
.turns td.t { color: var(--muted); white-space: nowrap; width: 62px; }
.turns td.r { width: 64px; }
.turns td.s { width: 110px; color: var(--pending); font-size: 0.75rem; }
.turns td.s.scored { color: var(--ink); }
.turns td.s .k { color: var(--muted); }
.turns td.s .h { display: block; color: var(--muted); font-size: 0.6875rem; }
.turns td.x { line-height: 1.45; }
mark { border-radius: 2px; padding: 0 2px; color: inherit; }
mark.kw-d { background: #f0e2c8; }
mark.kw-t { background: #dde6ee; }
mark.frame { background: none; border-bottom: 2px dotted var(--ok); }
a.src { color: var(--accent); white-space: nowrap; }
button.lnk {
  font: inherit; font-size: 0.75rem; color: var(--accent);
  background: none; border: 1px solid var(--gray); border-radius: 0.25em;
  padding: 1px 8px; cursor: pointer;
}
button.lnk:disabled { color: var(--pending); border-color: var(--lightgray); cursor: default; }

/* Methods page furniture */
.toc { font-size: var(--fs-cap); color: var(--muted); margin: 0 0 1.125rem; }
.toc a { color: var(--accent); }
details { border: 1px solid var(--border); border-radius: 3px; margin: 0.5rem 0; background: var(--panel); }
details > summary { cursor: pointer; padding: 0.5rem 0.75rem; font-size: var(--fs-sec); list-style: none; }
details > summary::-webkit-details-marker { display: none; }
details > summary::before { content: "\25B8 "; color: var(--muted); }
details[open] > summary::before { content: "\25BE "; }
details > summary:hover { background: var(--pre-bg); }
dl.kv, .methods dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 0.875rem; margin: 0.5rem 0; font-size: var(--fs-sec); }
dl.kv dt, .methods dt { font-weight: 600; color: var(--ink); }
dl.kv dd, .methods dd { margin: 0; color: var(--muted); }
.caveat { border-left: 3px solid var(--gray); padding: 2px 0 2px 0.75rem; margin: 0.625rem 0; font-size: var(--fs-sec); }
.caveat.crit { border-left-color: var(--crit); }
.caveat b { display: block; color: var(--ink); }
.lim { margin: 0.625rem 0; font-size: var(--fs-sec); }
.lim b { color: var(--ink); }
.files { list-style: none; padding: 0; margin: 0.375rem 0 0; }
.files li { margin: 2px 0; }
.files a { font-size: var(--fs-cap); color: var(--accent); text-decoration: none; }
.files a:hover { text-decoration: underline; }
.methods, .foot {
  font-size: var(--fs-cap); color: var(--muted); margin-top: 1.75rem;
  border-top: 1px solid var(--lightgray); padding-top: 0.75rem;
}
.methods code, .foot code { font-size: var(--fs-micro); }

/* ── NIMBY/YIMBY scatter: one dot per public speaker cluster ──────────────
   The three label colours are the ones analysis/stance_s1_role_audit_view.py
   already draws the bars with, so a dot and a bar for the same cluster can
   never be different colours. They carry data, which is the one case the house
   style keeps a saturated colour for (docs/STYLE.md §2). */
:root {
  --yimby: #2a7f62;
  --nimby: #b5543a;
  --nstance: #898781;
}
.sctl { font-size: var(--fs-sec); margin: 0.375rem 0 0.5rem; }
.sctl select { font: inherit; font-size: 0.875rem; padding: 0.125rem 0.25rem; }
.sctl button {
  font: inherit; font-size: 0.8125rem; color: var(--accent);
  background: none; border: 0; border-bottom: 1px solid var(--border);
  padding: 0; cursor: pointer;
}
svg#nsc { width: 100%; height: auto; max-width: var(--fig-max); display: block; }
svg#nsc .thr { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
svg#nsc .ax { stroke: var(--border); stroke-width: 1; }
/* 10 px here rendered at 10.5 px on a 1280 desktop and 10.5 px on a 768 tablet,
   where the chart is just under its "narrow" threshold and is drawn in a 640-unit
   frame shown 674 px wide. 11 px clears the floor at every width. */
svg#nsc text { font-size: 11px; fill: var(--muted); }
/* Phone: the viewBox is the rendered width, so one unit is one CSS pixel and these
   are real sizes (readability review §7.5 -- 10px pre-scale arrived at 5px). */
svg#nsc.narrow text { font-size: 12px; }
svg#nsc circle.pt { stroke: var(--white); stroke-width: 0.75; cursor: pointer; }
svg#nsc.narrow circle.pt { stroke-width: 1; }

/* The dot tooltip. It follows the pointer while hovering, so it must not be a
   hover target itself; pinned, it anchors to its dot and becomes clickable, which
   is the only way to reach the "watch at" link inside it. */
.qtip {
  position: fixed; left: 0; top: 0; z-index: 10; display: none;
  pointer-events: none; max-width: min(26rem, calc(100vw - 1.5rem));
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--border); border-left: 3px solid var(--gray);
  border-radius: 3px; padding: 0.5rem 0.625rem;
  box-shadow: 0 2px 10px rgba(0,0,0,.14);
}
.qtip.pinned { pointer-events: auto; border-left-color: var(--accent); }
/* THE QUOTE SCROLLS INSIDE THE POPOVER (owner, 2026-10-25 21:09: "can you make
   the text scrollable when i click on the speaker dot"). The popover used to be
   one scrolling box capped at the full viewport height: a speaker with several
   long statements filled the screen top to bottom, and the source line a reader
   needs, the body, the date and the "watch at" link, was pushed off the end of
   the scroll. Now the box is a column of two parts. The quote is the only part
   that scrolls, and the source line underneath it is pinned, so it is on screen
   whatever the length of the quote.
   The cap is half the viewport on a desktop and three fifths on a phone, where
   there is less around the popover competing for the screen. `overscroll-behavior:
   contain` keeps a touch drag inside the quote from scrolling the page under it
   once the quote reaches its end. */
.qtip {
  flex-direction: column;
  max-height: 50vh; overscroll-behavior: contain;
}
.qtip .qbody {
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  min-height: 0; flex: 1 1 auto;
}
.qtip .qmeta { flex: 0 0 auto; }
@media (max-width: 52rem) { .qtip { max-height: 60vh; } }
.qtip.yimby { border-left-color: var(--yimby); }
.qtip.nimby { border-left-color: var(--nimby); }
.qtip blockquote { margin: 0 0 0.375rem; padding: 0; font-size: var(--fs-sec); line-height: 1.4; }
.qtip .qmeta { font-size: var(--fs-cap); color: var(--muted); margin: 0; line-height: 1.4; }
/* The scrolling quote gets a rule above the pinned source line, so the two parts
   read as two parts and a quote that continues below the fold looks cut rather
   than finished. */
.qtip .qbody + .qmeta { border-top: 1px solid var(--border); padding-top: 0.375rem; margin-top: 0.125rem; }

/* The role dot plot (owner, 2026-10-19): stance up the page, the two benches as
   the two halves of the frame. Same dot, same colours and the same tooltip as the
   scatter above; only the arrangement differs, so the shared rules are repeated on
   this selector rather than generalised -- the two charts are allowed to diverge in
   geometry and must not in palette. */
svg#rdp { width: 100%; height: auto; max-width: var(--fig-max); display: block; }
svg#rdp text { font-size: 10px; fill: var(--muted); }
svg#rdp.narrow text { font-size: 12px; }
svg#rdp text.side { fill: var(--ink); font-weight: 600; font-size: 12px; }
svg#rdp.narrow text.side { font-size: 13px; }
svg#rdp text.zl { font-size: 9px; letter-spacing: 0.04em; text-transform: uppercase; }
svg#rdp.narrow text.zl { font-size: 10px; }
/* The neutral line is the figure's one reference, so it reads as ink, not as grid */
svg#rdp .zero { stroke: var(--darkgray); stroke-width: 1.25; stroke-opacity: 0.6; }
svg#rdp .thr { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; stroke-opacity: 0.6; }
svg#rdp .ax { stroke: var(--border); stroke-width: 1; }
svg#rdp circle.pt { stroke: var(--white); stroke-width: 0.5; cursor: pointer; }
svg#rdp.narrow circle.pt { stroke-width: 0.75; }
svg#rdp circle.pt.on { stroke: var(--darkgray); stroke-width: 1.5; }
/* A caption is muted prose under the thing it describes (docs/STYLE.md §3) */
.cap {
  display: block; max-width: min(100%, var(--fig-max)); margin: 0.5rem 0 0;
  font-size: 0.8125rem; line-height: 1.45; color: var(--muted);
}

/* "Who takes a side": the role gap at full size, then taken apart. A panel rather
   than loose prose because it is one argument in four parts and the parts have to
   stay together -- the headline number is misleading on its own. */
.sideblock { margin-top: 1rem; }
.sidelead { margin: 0 0 0.75rem; max-width: 80ch; font-size: var(--fs-body); line-height: 1.45; }
.sidesteps {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: 0.25rem 1.25rem;
}
.sidesteps h5 { margin: 0 0 0.25rem; font-size: var(--fs-sec); }
.sidesteps p { margin: 0 0 0.625rem; font-size: var(--fs-sec); line-height: 1.45; }
.sideres, .sidewhy {
  max-width: 80ch; font-size: var(--fs-sec); line-height: 1.45; margin: 0.5rem 0 0;
}
.sideres { padding-left: 0.6875rem; border-left: 3px solid var(--accent); }
.sidewhy { color: var(--muted); }

/* The one-paragraph answer to "can the words tell the two benches apart", in the
   Stance panel body where numbers are allowed to live. */
.rolepred {
  max-width: 80ch; margin: 0 0 0.75rem; font-size: var(--fs-sec); line-height: 1.45;
}

/* Topic time's own figure: horizontal bars, drawn client-side (review §7.4). */
svg#rtb { width: 100%; height: auto; max-width: var(--fig-max); display: block; }
svg#rtb text { font-size: 12px; fill: var(--muted); }
svg#rtb text.rl { fill: var(--ink); font-size: 12px; }
svg#rtb text.rl.rt { fill: var(--muted); font-size: 11px; }
svg#rtb text.bv { font-size: 11px; }
svg#rtb line.g { stroke: var(--grid); stroke-width: 1; }
svg#rtb line.ci { stroke: var(--darkgray); stroke-width: 1.25; stroke-opacity: 0.55; }
svg#nsc circle.pt.dim { opacity: 0.1; }
svg#nsc circle.pt.on { stroke: var(--darkgray); stroke-width: 1.5; }
.skey { font-size: var(--fs-cap); color: var(--muted); margin: 0.25rem 0 0; }
.skey i { display: inline-block; width: 0.625rem; height: 0.625rem; border-radius: 50%; margin-right: 0.25rem; vertical-align: -0.0625rem; }

/* The quote card under the scatter stood here. It is gone (owner, 2026-10-24
   16:32: "keep tool tip. remove the under graph quote"): it repeated the quote
   and the metadata line the tooltip already carries. */

/* The examples strip. Each quote is a disclosure, closed at every width
   (readability review §7.1): rendering them open on desktop put 521 words of
   block quote on the page before anyone asked for one. */
.qex { margin: 0.375rem 0; border-bottom: 1px solid var(--border); }
.qex > summary { padding: 0.3125rem 0; font-size: 0.875rem; cursor: pointer; }
.qex.yimby > summary b { color: var(--yimby); }
.qex.nimby > summary b { color: var(--nimby); }
.qex blockquote { margin: 0.125rem 0 0.375rem; padding: 0 0 0 0.75rem; border-left: 3px solid var(--border); font-size: 0.9375rem; line-height: 1.45; }
.qex.yimby blockquote { border-left-color: var(--yimby); }
.qex.nimby blockquote { border-left-color: var(--nimby); }
.qex .qmeta { font-size: 0.8125rem; color: var(--muted); margin: 0 0 0.25rem; }
.excols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1.25rem; }
.excols h5 { margin: 0.625rem 0 0; font-size: var(--fs-sec); font-weight: 600; }

/* The card's own padding, his 1.75rem/2rem once there is room for it
   (reference rule 1). Below this the card keeps the phone padding set above. */
@media (min-width: 46rem) {
  main { padding: 1.75rem 2rem 3rem; margin: 2rem auto; }
  h1 { font-size: 1.7rem; }
}

@media (max-width: 50rem) {
  .figs, .pfigs { grid-template-columns: 1fr; }
  /* Phone: the scatter fits the viewport and the examples become one column of
     tap-to-expand rows -- two columns of quotes do not survive 390 px. */
  .excols { grid-template-columns: 1fr; }
  svg#nsc { max-width: 100%; }
  svg#rdp { max-width: 100%; }
  /* Phone: the two steps of "Who takes a side" stack, and the quoted-number rule
     loses its indent so the paragraph keeps its full measure. */
  .sidesteps { grid-template-columns: 1fr; }
  /* Phone: nothing may widen the page, and the head has to be short enough that
     the map is on the first screen (readability review §3.1). */
  h1 { font-size: 1.25rem; }
  /* The 12 px phone table rule is gone: --fs-tbl already steps up to 14 px here,
     and a table that cannot fit the width scrolls inside its own box rather than
     being set at a size a phone cannot read. */
  th, td { padding-right: 0.5em; }
  /* Every table on a phone scrolls sideways inside its own box. Without this a
     wide Methods table either widened the page or was shrunk until its cells were
     11 px. */
  table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* The Methods flowchart is drawn in a 720-unit frame. Shrunk to a 328 px phone
     its labels arrive at 5 px, which is not readable at any size of type we could
     set. It keeps its own width and scrolls sideways inside its panel instead, so
     the text renders at the 11 to 20 px it is set at. */
  #flow-svg { min-width: 720px; }
  .flowhint { display: block; }
  .detail { position: static; max-height: none; overflow: visible; }
  /* Phone: the TLDR gives up its bottom margin and the lead chart its head margin,
     because those two gaps are the difference between the chart standing on the
     first screen and standing just under it (owner, 2026-10-21 10:04). */
  ul.tldr { margin-bottom: 0.25rem; }
  /* The lead chart's takeaway is a heading a reader reads first, so it keeps the
     body size here rather than dropping under it. */
  .leadchart h2 { margin-bottom: 0.35rem; font-size: var(--fs-body); }
  ul.tldr, ul.heads { padding-left: 1.125rem; }
  blockquote.leadturn { font-size: 1rem; }
}
@media print {
  .appbar, .tip { display: none; }
  main { max-width: none; box-shadow: none; margin: 0; }
}

/* ── the body chart, one row per body ─────────────────────────────────────
   Shipped 2026-10-25 under NET_CHART in index.html. One row per body: the
   difference, YIMBY minus NIMBY, as a dot with its likely range against a zero
   line, in place of an 864x2400 PNG of two bars per body. The 320-unit viewBox
   and the 12 px text are the lead chart's, so the two charts set their labels at
   the same rendered size (A17 §2.3). The chart has no heading of its own: the
   panel's <h2> above it is its takeaway title. */
.netfig { margin: 0.5rem 0 0; }
/* 35rem is the 560-unit cap the draw clamps the viewBox to, so the chart never
   paints wider than the frame it was laid out in. */
svg#netsvg { display: block; width: 100%; height: auto; max-width: 35rem; }
svg#netsvg .net-name { font-size: 12px; fill: var(--muted); }
svg#netsvg .net-row.sig .net-name { fill: var(--ink); font-weight: 700; }
svg#netsvg .net-val {
  font-size: 12px; fill: var(--muted); font-variant-numeric: tabular-nums;
}
svg#netsvg .net-row.sig .net-val { fill: var(--ink); font-weight: 700; }
svg#netsvg .net-ax { font-size: 12px; fill: var(--tick, var(--muted)); }
/* 11 px here rendered at exactly 11 px, which is the floor A17 set rather than a
   size with room in it. Everything the chart prints is 12 px. */
svg#netsvg .net-end, svg#netsvg .net-colh { font-size: 12px; }
/* Zero is the only reference a reader needs, so it is the only rule drawn: the
   gridlines they would read a value off are replaced by the number on each row. */
svg#netsvg .net-zero { stroke: var(--gray); stroke-width: 1; shape-rendering: crispEdges; }
svg#netsvg .net-rng, svg#netsvg .net-cap { stroke: var(--gray); stroke-width: 1.5; }
svg#netsvg .net-row.sig .net-rng, svg#netsvg .net-row.sig .net-cap { stroke: #555; }
svg#netsvg .net-dot { stroke-width: 1.75; opacity: 0.75; }
svg#netsvg .net-row.sig .net-dot { opacity: 1; }
/* The hit target is the row, not the 9-unit dot: about 27 rendered px tall on a
   390 px phone, which a thumb reaches. */
svg#netsvg .net-hit { fill: transparent; cursor: pointer; }
svg#netsvg .net-row:hover .net-hit, svg#netsvg .net-row:focus .net-hit { fill: var(--grid); }
svg#netsvg .net-row:focus { outline: none; }
svg#netsvg .net-row:focus .net-name { text-decoration: underline; }
#net-tabs { display: flex; gap: 0.75rem; margin: 0.25rem 0 0.4rem; }
/* .sctl button sets 0.8125rem, which is 13 px on a phone and under the 14 px
   floor A17 set for text a reader reads on its own. The role token steps to
   14 px below 52rem; naming the role is how that page keeps one size per role. */
#net-tabs button {
  border-bottom: 1px solid transparent; color: var(--muted);
  font-size: var(--fs-cap);
  /* The text alone made a 21 px box, under the 24 px a thumb needs. The padding is
     on the button, so the tap target grows and the label does not move. */
  min-height: 24px; padding: 0.125rem 0 0.1875rem;
}
#net-tabs button.on {
  color: var(--ink); font-weight: 700; border-bottom-color: var(--ink);
}
.netfig .caption {
  max-width: none; margin: 0.5rem 0 0;
  font-size: var(--fs-cap); line-height: 1.45; color: #555;
}
