/*
 * The whole stylesheet (§5.2): dark, one column, system fonts, no framework, no
 * build step, no web font. Two consequences worth stating because they are
 * decisions rather than omissions:
 *
 *   * No @import and no third-party URL. The CSP is ``default-src 'self'`` and the
 *     privacy posture is "a reader of a public data page is not introduced to
 *     anyone". A web font would break both for a nicer 'g'.
 *   * No horizontal scroll at 360 px. Under 640 px the Mult and Δ24h columns are
 *     hidden and restated under the market name (``.sm-only``), which keeps every
 *     measurement on a phone without a sideways table.
 *
 * ---------------------------------------------------------------------------
 * The design system, and why it is a token layer rather than a set of rules
 * ---------------------------------------------------------------------------
 * Every colour, step of space and size of type below is a **named token** used by
 * the components underneath it. The reason is not tidiness: this page has two HTML
 * surfaces (``/`` and ``/f/{slug}``) that share four partials, and the failure mode
 * of a hand-tuned stylesheet is that the board's card and the farm page's card
 * drift by one pixel of padding per phase until they are visibly different objects.
 * A component that can only spend ``--s-3`` cannot drift.
 *
 * **The palette is the icon's palette.** ``scripts/make_favicon.py`` renders a
 * titanium block on a graphite plate, and its ``DESIGN`` dict is the source of the
 * surface ramp here: ``--surface-3`` is the plate's top (#1c2128), ``--line-strong``
 * is the plate's rim (#3a4350), ``--ink-2`` is the block's lower top-face
 * (#b2c0ce). The tab icon, the Android tile, the address bar (``theme-color``) and
 * the page are therefore one material rather than four things that happen to be
 * dark. ``--bg`` is unchanged (#0d1117) because three shipped artefacts already
 * name it: the manifest's ``background_color``, the ``theme-color`` meta and the
 * plate's own base.
 *
 * **Contrast is measured, not eyeballed.** Against ``--bg`` every token used for
 * text clears WCAG AA at the size it is used: ``--ink`` 15.6:1, ``--ink-2`` 10.4:1,
 * ``--ink-3`` 6.1:1, ``--link`` 8.8:1, ``--up`` 7.4:1, ``--down`` 5.7:1, ``--warn``
 * 7.5:1. ``--ink-edge`` (3.9:1) is the one token that does **not** clear it, which
 * is why it is spent only on rules and borders and never on a character.
 *
 * **Elevation is planes and hairlines, not shadow.** Engineered metal reads as flat
 * faces meeting at a crisp edge with one specular lift — the same three-face logic
 * the icon uses. So a raised surface here is a step up the ramp plus a 1 px inset
 * highlight, never a blurred drop shadow, which is also what keeps the page looking
 * like an instrument rather than a card-based marketing site.
 */

:root {
  color-scheme: dark;

  /* --- surfaces: the graphite plate, in four steps ---------------------- */
  --bg: #0d1117;
  --surface-1: #12171f;
  --surface-2: #171d26;
  --surface-3: #1c2128;

  /* --- edges ------------------------------------------------------------ */
  --line: #232b36;
  --line-strong: #3a4350;
  /* The specular lift along the top of a raised plane. Alpha, not a colour, so it
     reads correctly on every surface step without a token per pairing. */
  --sheen: rgba(255, 255, 255, 0.035);

  /* --- ink: four weights, three of them legal for text ------------------ */
  --ink: #e6edf3;
  --ink-2: #b2c0ce;
  --ink-3: #8b949e;
  --ink-edge: #677382;

  /* --- semantic intent -------------------------------------------------- */
  --up: #3fb950;
  --down: #f85149;
  --warn: #d29922;
  --link: #6cb6ff;
  /* Washes for the same four, at the one opacity that stays legible under the
     ink above. A tint rather than a saturated fill: a red row in a financial
     table means "read me", and a red *block* means "stop", which is a louder
     claim than a flag on one route deserves. */
  --up-wash: rgba(63, 185, 80, 0.13);
  --down-wash: rgba(248, 81, 73, 0.13);
  --warn-wash: rgba(210, 153, 34, 0.12);
  --link-wash: rgba(108, 182, 255, 0.12);
  --focus: #6cb6ff;

  /* --- space: a 4 px scale, and nothing between the steps --------------- */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 22px;
  --s-6: 32px;
  --s-7: 44px;

  /* --- radius: three, because a fourth is a decision nobody can defend --- */
  --r-1: 3px;
  --r-2: 5px;
  --r-3: 8px;

  /* --- type ------------------------------------------------------------- */
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --t-micro: 11px;
  --t-sm: 12px;
  --t-base: 13px;
  --t-md: 15px;
  --t-lg: 19px;
  --t-xl: 24px;
  --track-caps: 0.07em;

  --wrap: 1180px;
  --measure: 66ch;
}

* { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* The anchor targets on this page are section headings under a sticky table
     header; without this a jump lands the heading underneath it. */
  scroll-padding-top: var(--s-6);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--t-base) / 1.5 var(--font-ui);
  /* Every measurement on this page is a column of digits that a reader compares
     down the column, not a sentence they read across. Lining tabular figures are
     the whole difference between a table and a spreadsheet screenshot. */
  font-variant-numeric: tabular-nums lining-nums;
}

::selection { background: var(--link-wash); color: var(--ink); }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: var(--s-5) var(--s-4) var(--s-7); }

/* --- links ---------------------------------------------------------------
   Underline on hover only, but never on the dense table cells, where 59 rows of
   underlined farm names is a texture rather than a set of affordances. */
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

/* --- focus ---------------------------------------------------------------
   One ring, everywhere, and it is *visible on every surface* because it is drawn
   outside the element with a dark offset. ``:focus-visible`` rather than
   ``:focus`` so a mouse click on a sortable header does not leave a ring behind,
   which is the reason authors delete focus styles in the first place. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* The skip link. Off-screen until focused, which is the one control on this page
   that exists solely for a keyboard reader: the board is 59 rows and the table is
   the last landmark, so tabbing to it through the filters is otherwise the cost of
   arriving. */
.skip {
  position: absolute;
  left: var(--s-2);
  top: var(--s-2);
  z-index: 10;
  padding: var(--s-2) var(--s-3);
  background: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-2);
  color: var(--ink);
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

/* Visible to a screen reader, not to an eye. Used for the chip column's meaning
   (a 🔴 with no text is "large red circle" to a reader who cannot see it) and for
   the labels on the filter controls. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

/* --- typography ----------------------------------------------------------
   Three levels and they are genuinely different objects: the page title, the
   section label, and the body. Section labels are small uppercase micro-type with
   a rule running to the right margin — an instrument-panel convention, and the
   reason it is used here rather than a large bold heading is that on this page the
   *data* is the content and a 20 px heading over a 13 px table inverts that. */
h1 {
  font-size: var(--t-xl);
  line-height: 1.15;
  margin: 0;
  letter-spacing: -0.015em;
  font-weight: 650;
}

h2 {
  font-size: var(--t-micro);
  margin: 0;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink-2);
  font-weight: 650;
  white-space: nowrap;
}

section { margin-bottom: var(--s-6); }

/* The rule that turns a micro-label into a section boundary. ``::after`` on the
   flex container rather than a border on the heading, so the rule starts after the
   text at whatever width the text happens to be. */
.sec-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  flex-wrap: wrap;
  margin-bottom: var(--s-3);
}
.sec-head::after {
  content: "";
  flex: 1 1 40px;
  height: 1px;
  background: var(--line);
  align-self: center;
}
.sec-note { margin: 0; color: var(--ink-3); font-size: var(--t-sm); }

.quiet { color: var(--ink-3); font-size: var(--t-base); margin: var(--s-1) 0; }
.sep { color: var(--line-strong); }
.win { text-transform: none; letter-spacing: 0; color: var(--ink-3); font-weight: 400; }

/* --- masthead ------------------------------------------------------------ */
.head {
  display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-5);
  align-items: flex-end; justify-content: space-between;
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--s-4); margin-bottom: var(--s-4);
}
.tagline { margin: var(--s-2) 0 0; color: var(--ink-2); font-size: var(--t-md); max-width: var(--measure); }
.crumb { margin: 0 0 var(--s-2); font-size: var(--t-sm); color: var(--ink-3); }

/* The freshness read-out. It is the page's own claim about itself (P2), so it gets
   the treatment an instrument gives a status light: a bordered plate, monospaced
   digits, and a dot whose colour is the claim. */
.head-meta {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-sm); color: var(--ink-3);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--s-2) var(--s-3);
  box-shadow: inset 0 1px 0 var(--sheen);
}
.age { color: var(--ink); }
.dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--up); flex: none;
  /* The halo is what makes a 6 px dot read as "lit" rather than as a bullet. */
  box-shadow: 0 0 0 3px var(--up-wash);
}
.dot.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-wash); }

/* --- the stale banner (P2 as UI) ---------------------------------------- */
.banner {
  display: flex; gap: var(--s-2); align-items: flex-start;
  background: var(--warn-wash);
  border: 1px solid var(--warn);
  border-left-width: 3px;
  border-radius: var(--r-2);
  color: var(--warn);
  padding: var(--s-3) var(--s-3); margin: 0 0 var(--s-4); font-size: var(--t-base);
}
.banner b { color: var(--warn); font-weight: 650; }

/* --- KPI strip -----------------------------------------------------------
   Four counts the page already knows, promoted out of a parenthetical and onto the
   first screen. This is the "what am I looking at" answer for a reader who arrived
   from a shared link, and it is the one place on the board where a number is
   allowed to be large: they are counts of rows, so no reader can mistake one for a
   measurement of a market.

   No new arithmetic (rule L8) — every figure here is a length the context already
   carried and the footnote of a section heading already printed. */
.kpis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
  list-style: none; margin: 0 0 var(--s-6); padding: 0;
}
.kpi {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: var(--s-3) var(--s-4);
  box-shadow: inset 0 1px 0 var(--sheen);
}
.kpi-v { display: block; font-size: var(--t-lg); font-weight: 650; color: var(--ink); line-height: 1.2; }
.kpi-k { display: block; font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--ink-3); margin-top: var(--s-1); }
.kpi.flagged .kpi-v { color: var(--down); }

/* --- deadline cards (F1) ------------------------------------------------- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: var(--s-2);
  list-style: none; margin: 0; padding: 0;
}
.card {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: var(--r-2);
  padding: var(--s-2) var(--s-3);
  display: flex; flex-direction: column; gap: 2px;
  font-size: var(--t-base);
  box-shadow: inset 0 1px 0 var(--sheen);
}
/* Urgency is the left edge, not the fill: a strip of amber cards would make the
   whole section read as a warning when what is urgent is one date on one farm. */
.card.urgent { border-left-color: var(--warn); }
.card.urgent .card-when { color: var(--warn); }
.card-farm { font-weight: 650; color: var(--ink); }
.card-kind { color: var(--ink-3); text-transform: uppercase; font-size: var(--t-micro); letter-spacing: 0.05em; }
.card-when { color: var(--ink-2); }

.more { margin-top: var(--s-2); }
.more summary {
  color: var(--link); cursor: pointer; font-size: var(--t-base);
  display: inline-block; padding: var(--s-1) 0;
}
.more summary:hover { text-decoration: underline; }
.more > .cards { margin-top: var(--s-2); }

/* --- movers + news (F4) -------------------------------------------------- */
/* The pair carries the section rhythm the two sections inside it give up, so the gap
   before THE BOARD matches every other gap on the page. */
.two-up { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-6); margin-bottom: var(--s-6); }
.two-up > section { margin-bottom: 0; }

.lines { list-style: none; margin: 0; padding: 0; font-size: var(--t-base); }
.lines li {
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-2); align-items: baseline;
  padding: var(--s-2) 0; border-bottom: 1px solid var(--line);
}
.lines li:last-child { border-bottom: 0; }
.mkt { font-weight: 650; min-width: 88px; }
.chg { font-weight: 650; }
.chg.up, .up { color: var(--up); }
.chg.down, .down { color: var(--down); }
.flow { color: var(--ink-3); }
.headline { color: var(--ink-2); flex: 1 1 auto; }

/* --- tables (F2/F3/F8) ---------------------------------------------------
   The board is 59 rows and 11 columns and it is the product, so everything below
   is in service of scanning it: a header that survives the scroll, hairline rows,
   right-aligned figures, and hover as the only decoration. */
.table-wrap {
  /* The escape hatch, and it is *only* an escape hatch: the phone rules further
     down hide two columns and wrap the rest precisely so this never engages at
     360 px. It exists for the viewport between a phone and a laptop where a
     reader would otherwise get a clipped table with no way to see the rest. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

table { width: 100%; border-collapse: collapse; font-size: var(--t-base); }

thead th {
  position: sticky; top: 0; z-index: 2;
  text-align: left; color: var(--ink-2); font-weight: 650; font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.05em;
  /* Opaque, because a sticky header over a transparent background lets 59 rows
     scroll through the column names. */
  background: var(--surface-2);
  border-bottom: 1px solid var(--line-strong);
  padding: var(--s-2);
  white-space: nowrap; user-select: none;
}
thead th:first-child { border-top-left-radius: var(--r-2); }
thead th:last-child { border-top-right-radius: var(--r-2); }

/* The sort control is a real ``<button>`` inside the cell (see board.js): a
   ``<th>`` with a click handler is unreachable by keyboard and announces nothing,
   which is the single largest accessibility defect a sortable table can have. The
   button is styled to disappear into the header so the change is invisible to a
   reader using a mouse. */
thead th .sort-btn {
  all: unset;
  display: flex; align-items: center; gap: var(--s-1);
  width: 100%; cursor: pointer; color: inherit; font: inherit;
  letter-spacing: inherit; text-transform: inherit;
}
th.num .sort-btn { justify-content: flex-end; }
th.flag .sort-btn { justify-content: center; }
thead th .sort-btn:hover { color: var(--ink); }
thead th .sort-btn::after {
  /* Reserved at all times so a column does not shift by 8 px when it is sorted. */
  content: "";
  width: 7px; height: 4px; flex: none;
  clip-path: polygon(50% 100%, 0 0, 100% 0);
  background: var(--ink-edge);
  opacity: 0;
}
thead th[aria-sort="descending"] .sort-btn::after { opacity: 1; }
thead th[aria-sort="ascending"] .sort-btn::after { opacity: 1; transform: rotate(180deg); }
/* ``:not([aria-sort="none"])`` and not simply ``[aria-sort]``: board.js marks every
   sortable header ``none`` so the control announces itself as sortable, and the
   bare attribute selector would then light all eleven columns as the sorted one. */
thead th[aria-sort]:not([aria-sort="none"]) .sort-btn { color: var(--ink); }

tbody td {
  border-bottom: 1px solid var(--line);
  padding: var(--s-2);
  vertical-align: top;
}
tbody tr { transition: background 90ms linear; }
tbody tr:hover { background: var(--surface-1); }
tbody tr:last-child td { border-bottom: 0; }

td.num, th.num { text-align: right; }
td.flag, th.flag { text-align: center; width: 34px; }
td.farm { color: var(--ink-2); white-space: nowrap; }
td.farm a { color: var(--ink); }
td.market { font-weight: 650; }
td.market a { color: var(--ink); }
td.market a:hover { color: var(--link); }
.chain { color: var(--ink-3); font-weight: 400; font-size: var(--t-micro); margin-left: 5px; text-transform: uppercase; letter-spacing: 0.04em; }
.sm-only { display: none; }
tr.hidden { display: none; }

/* The trap reason, under the market name (C6). ``.sm-only``'s slot and ``.sm-only``'s
   muted sub-line metrics — see the 640 px block at the foot of this file, which is
   where those metrics are stated — but shown at every width, because a reason is not
   a restated column: it is the thing the ⚑ column existed to hint at.

   ``var(--down)``, which is the red ``.airdrop-none`` and ``.kpi.flagged .kpi-v``
   already use. No new colour and no new token: the row is already red and this line
   is what the red is about. */
.flag-line {
  display: block; margin-top: 3px;
  color: var(--down); font-weight: 400; font-size: var(--t-micro);
}

/* --- the rank, decomposed (P06-T5) ---------------------------------------
   The disclosure sits *inline*, after the chain, rather than on a line of its own:
   a collapsed ``<details>`` is a block element by default, and a block here would
   add a line box to all 61 rows to say nothing. ``inline-block`` keeps the closed
   state inside the line the market name already occupies — which is the criterion
   this task is measured against — and lets the open state grow downwards.

   The marker is ours: ``display: inline-block`` removes ``list-item``'s own
   triangle in every engine, and a disclosure control with no affordance is worse
   than no control. Text, in the page's own ink, so it needs no image and no CSP
   exception. */
details.factors { float: right; max-width: 100%; font-weight: 400; }
details.factors > summary {
  display: inline-block; list-style: none; cursor: pointer;
  margin-left: 6px;
  color: var(--ink-3); font-weight: 400; font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.04em;
}
details.factors > summary::-webkit-details-marker { display: none; }
details.factors > summary::before { content: "\25B8\00A0"; }
details.factors[open] > summary::before { content: "\25BE\00A0"; }
details.factors > summary:hover { color: var(--link); }

/* One row per factor: label, bar, number. ``.sm-only``'s muted sub-line metrics
   again — this is the same slot and the same register, and a second set of numbers
   under the market name must not compete with the market name. */
.factor-list { list-style: none; margin: 3px 0 0; padding: 0; }
.factor-list li {
  display: flex; align-items: center; gap: var(--s-1);
  color: var(--ink-3); font-weight: 400; font-size: var(--t-micro);
}
.factor-label { flex: 0 0 5.5em; }
.factor-value { flex: 0 0 3em; text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-2); }
/* Fixed width, so five bars in one row and the same bar across two rows are read
   against the same axis — which is the whole reason the width is a share of the
   factor's own weight rather than of the total. */
meter.factor-bar { flex: 0 0 64px; width: 64px; height: 6px; vertical-align: middle; }

/* The one figure the whole table is read for. Given the page's only weight step
   above body text so a reader's eye lands on the column, not on the row. */
td.points { color: var(--ink); font-weight: 650; }

/* The Pts/$ basis marker. Dim and small: it qualifies the number, it does not
   compete with it — the figure is correct either way, and the tooltip on the cell
   carries the sentence. ``cursor: help`` on the whole cell, because on the current
   dataset every row has one and a reader will hover the number, not the dagger. */
.basis-price_only { cursor: help; }
.basis-mark { color: var(--ink-3); font-size: 10px; margin-left: 2px; vertical-align: super; font-weight: 400; }
/* The three-character position mark (C14) sits on the baseline rather than raised:
   it is a small scale, not a footnote reference, and superscripting a "·|·" makes
   the three glyphs illegible at 10px. Everything else — the muted ink, the size, the
   weight — is ``.basis-mark``'s, which the element also carries, so the two markers
   in this cell match each other by construction rather than by two copies of three
   declarations. Tabular so the bar does not shift width with the position it shows. */
.range-mark { vertical-align: baseline; letter-spacing: 1px; font-variant-numeric: tabular-nums; }

/* Dates are tabular so a column of "Aug 12 · 8d" lines up on the separator, which
   is what lets a reader scan for the soonest one without reading any of them. */
td.date, th.date { white-space: nowrap; }
td.airdrop { white-space: nowrap; }

/* Colour follows consequence, not category. ``none`` is the status that makes a
   route a trap (it is in DANGER_CODES), so it is the red one; ``confirmed`` is the
   only green; everything between is the page's ordinary ink, because "expected" is
   a claim and not a promise and colouring it green would say otherwise. */
.airdrop-confirmed { color: var(--up); }
.airdrop-none { color: var(--down); }
.airdrop-rumored, .airdrop-unknown { color: var(--ink-3); }

.chip { cursor: help; font-style: normal; }

/* The legend under the flag column. Three glyphs and three words, printed once,
   because the ⚑ column is the board's most valuable content (P3) and its meaning
   currently lives only in a ``title`` attribute — which a touch device never shows
   and a screen reader announces last. */
.legend {
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4);
  list-style: none; margin: var(--s-3) 0 0; padding: 0;
  font-size: var(--t-sm); color: var(--ink-3);
}
.legend li { display: flex; align-items: center; gap: var(--s-1); }

/* --- filters (W3) --------------------------------------------------------
   ``hidden`` in the markup, revealed by board.js: a control that cannot work
   without JavaScript must not be shown to a reader who has it off. */
.filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3);
  margin-left: auto;
}
.filters[hidden] { display: none; }

/* The row count beside a segment control (P06-T6). Muted and tabular: it qualifies
   the label the way ``.chain`` qualifies a market name, and a column of counts that
   shifts width as it changes is a filter bar that moves while a reader reads it. */
.seg-count { color: var(--ink-3); font-variant-numeric: tabular-nums; }

.toggle {
  display: inline-flex; align-items: center; gap: var(--s-1);
  color: var(--ink-2); font-size: var(--t-sm); cursor: pointer;
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--s-1) var(--s-2);
}
.toggle:hover { border-color: var(--line-strong); color: var(--ink); }
/* The checked state is on the label, so the control reads as pressed rather than
   as a checkbox somebody ticked. ``:has`` degrades to the unstyled-but-working
   checkbox in a browser without it, which is the correct failure. */
.toggle:has(input:checked) { border-color: var(--link); color: var(--ink); background: var(--link-wash); }
.toggle input { accent-color: var(--link); margin: 0; }

.find input {
  background: var(--surface-1); border: 1px solid var(--line); color: var(--ink);
  font: inherit; font-size: var(--t-sm); padding: var(--s-1) var(--s-2);
  /* Wide enough for the placeholder it ships with. A search box that clips its own
     prompt to "filter farm or m" reads as a broken control, and the placeholder is
     the only visible hint of what the box matches on. */
  border-radius: var(--r-2); width: 23ch;
}
.find input::placeholder { color: var(--ink-3); }
.find input:hover { border-color: var(--line-strong); }

/* --- the farm page (§5.3, W3) -------------------------------------------- */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--s-2); margin: 0;
}
.fact {
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--r-2); padding: var(--s-2) var(--s-3);
  box-shadow: inset 0 1px 0 var(--sheen);
}
.fact dt { color: var(--ink-3); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: 0.05em; }
.fact dd { margin: var(--s-1) 0 0; font-size: var(--t-md); white-space: nowrap; color: var(--ink); }

/* --- the /api contract list ----------------------------------------------
   NOT `.facts`, and the difference is why this exists. That grid is
   `auto-fill, minmax(150px, 1fr)` and its cells hold `.fact` *cards* — one card
   per cell, each carrying its own `dt`/`dd` inside itself. `/api` puts bare
   `dt`/`dd` straight into the list, so **every heading and every claim became
   its own grid item**: on a wide viewport the pairs scattered across eight
   tracks, a heading in one cell and its claim three cells away, long prose
   overflowing a 150px column and printing over its neighbour.

   Seventeen paragraphs of contract are a definition list, not a row of chips.
   Two explicit columns, so auto-placement puts every `dt` in the first and its
   `dd` in the second, by construction rather than by luck. */
.claims {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) 1fr;
  column-gap: var(--s-4); row-gap: var(--s-3);
  margin: 0;
}
.claims dt {
  color: var(--ink-3); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.claims dd { margin: 0; color: var(--ink); max-width: 68ch; }
/* A grid item's default `min-width: auto` refuses to shrink below its widest
   unbreakable child, so one long token — `https://creativecommons.org/...`, or a
   `tests/web/...::test_...` node id — widened the whole track and pushed the page
   into a horizontal scroll at 375px. Measured before this pair was added:
   documentElement.scrollWidth 563 against clientWidth 375, four `dd`s overflowing.
   `min-width: 0` lets the track shrink; `overflow-wrap: anywhere` lets the token
   itself break rather than merely being clipped. */
.claims dt, .claims dd { min-width: 0; overflow-wrap: anywhere; }
/* The symbol and its tests are a citation under the claim, not part of it. */
.claims dd .quiet { display: block; margin-top: var(--s-1); font-size: var(--t-sm); }
/* A hairline per claim, suppressed on the first so the list does not open with
   a rule directly under the section head. */
.claims dt, .claims dd { border-top: 1px solid var(--line); padding-top: var(--s-3); }
.claims dt:first-of-type, .claims dt:first-of-type + dd { border-top: 0; padding-top: 0; }

/* One column below the two-column breakpoint: a 14rem label track leaves too
   little for the claim on a phone, which is the failure this file is fixing. */
@media (max-width: 640px) {
  .claims { grid-template-columns: 1fr; row-gap: var(--s-2); }
  .claims dd { padding-top: var(--s-1); border-top: 0; }
}

/* --- empty states --------------------------------------------------------
   A sentence in a bordered plate rather than a bare line of dim text. The
   distinction it has to carry is "this section is empty because nothing happened"
   versus "this section is empty because the page is broken", and an unstyled
   sentence floating under a heading reads as the second one. */
.empty {
  background: var(--surface-1);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-2);
  color: var(--ink-3);
  padding: var(--s-3) var(--s-4);
  margin: 0;
  font-size: var(--t-base);
  max-width: var(--measure);
}

/* --- footer (F5) --------------------------------------------------------- */
.foot { border-top: 1px solid var(--line); padding-top: var(--s-4); margin-top: var(--s-6); }
.srcline {
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3);
  font-size: var(--t-sm); color: var(--ink-3); margin: 0 0 var(--s-3);
}
.src { white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; }
/* A state dot per collector: the freshness footer is the page's evidence for its
   own headline claim, and a reader should be able to find the failing source
   without reading five labels. */
.src::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--up); flex: none;
}
.state-error::before { background: var(--down); }
.state-stale::before, .state-unknown::before { background: var(--warn); }
.state-error { color: var(--down); }
.state-stale, .state-unknown { color: var(--warn); }

/* The "what is this" block (W2). Measure-capped: this is the only prose on the
   page, and a 1180 px line of body text is the one thing here nobody would read.
   Quieter than the board and above the API line, so it explains without competing
   with the content it explains. */
.about { margin: var(--s-5) 0 var(--s-3); max-width: var(--measure); }
.about h2 { margin-bottom: var(--s-2); }
.about p { color: var(--ink-3); font-size: var(--t-base); margin: 0 0 var(--s-3); }

/* --- the machine door ----------------------------------------------------
   ``/api/v1/board.json`` is user #3's whole product (§4 flow 3) and it was a line
   of grey text under the footnotes. Given a plate so a researcher who came for the
   data can find it without reading the page. */
.api {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-2);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: var(--s-2) var(--s-3);
  font-size: var(--t-sm); color: var(--ink-3);
  max-width: var(--measure);
  box-shadow: inset 0 1px 0 var(--sheen);
}
.api a { font-weight: 650; }

/* --- the standalone states (503 / 500 / 404) -----------------------------
   Served by ``web/app.py`` before, or instead of, a board. They share this
   stylesheet so a reader who hits a cold container does not get an unstyled
   ransom note from what is otherwise a finished product. */
.state-page { max-width: var(--measure); padding-top: var(--s-7); }
.state-page h1 { margin-bottom: var(--s-3); }
.state-page p { color: var(--ink-3); font-size: var(--t-md); margin: 0 0 var(--s-4); }
.state-page .mark { display: block; width: 44px; height: 44px; margin-bottom: var(--s-4); }

/* --- tablets ------------------------------------------------------------- */
@media (max-width: 900px) {
  .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .two-up { grid-template-columns: 1fr; gap: var(--s-6); }
}

/* --- phones (§5.2: usable at 360 px, no sideways scroll) ----------------- */
@media (max-width: 640px) {
  .wrap { padding: var(--s-4) var(--s-3) var(--s-6); }
  h1 { font-size: var(--t-lg); }
  .tagline { font-size: var(--t-base); }
  .head { align-items: flex-start; }
  .cards { grid-template-columns: 1fr 1fr; }
  .hide-sm { display: none; }
  .sm-only { display: block; color: var(--ink-3); font-weight: 400; font-size: var(--t-micro); margin-top: 3px; }

  /* Hiding two columns is not enough on its own: a table cannot lay out narrower
     than its widest unbreakable cell, so one ``white-space: nowrap`` sets a floor
     for the whole board. Live farm ids reach ``saturn:susdat-bnb`` and
     ``yuzu-money:yuzu`` — measured 434 px of min-content in a 360 px viewport,
     i.e. the sideways scroll §5.2 forbids, and invisible to the fixtures because
     their labels are short. On a phone the farm cell wraps (and breaks mid-token
     if it must) instead. */
  /* ``anywhere`` is what lowers the column's min-content floor from the widest live
     farm id (``saturn:susdat-bnb``, measured at 434 px of min-content in a 360 px
     viewport — the sideways scroll §5.2 forbids) to a single character. The floor it
     leaves behind is *too* low: an auto-layout table then squeezes the column to
     ~48 px and breaks a nine-character id as "ethen / a:s6". ``min-width`` puts a
     deliberate floor back, wide enough for a typical id on one line and narrow
     enough that a long one still breaks rather than widening the table. */
  td.farm { white-space: normal; overflow-wrap: anywhere; min-width: 5em; }
  td.market { overflow-wrap: anywhere; }
  /* The season badge sits inside ``td.farm`` and inherits the rule above, which
     breaks "settled" into "settl / ed" — a word short enough to always fit on its
     own line, so there is nothing for ``anywhere`` to buy here and a mid-word break
     is all it costs. The floor the rule exists to lower is set by the farm *id*
     (``saturn:susdat-bnb``), not by a six-letter status. */
  td.farm .chain { overflow-wrap: normal; }
  thead th { padding: var(--s-2) 5px; }
  tbody td { padding: var(--s-2) 5px; }

  /* F8's own version of the floor above. ``td.date`` is nowrap so a desktop column
     aligns on the "·"; on a phone the same rule would set a min-content floor of
     "~Sep 01 · 28d" per cell across three columns and reintroduce the sideways
     scroll §5.2 forbids. The milestone table keeps its dates (they are the
     content) and lets them wrap; the *board*'s three F8 columns are ``hide-sm``
     and restated in the ``.sm-only`` line instead. */
  td.date { white-space: normal; }

  .filters { margin-left: 0; width: 100%; }
  .find { flex: 1 1 100%; }
  .find input { width: 100%; }
  .fact dd { white-space: normal; }
  .head-meta { width: 100%; }
}

/* Deliberately **not** a "stack everything" breakpoint. A 360 px column still fits
   two 168 px deadline cards and two KPI tiles side by side, and collapsing them to
   one would double the height of the two sections a phone reader scrolls past to
   reach the board — which is the opposite of what the narrow viewport needs. The
   only thing that has to give at this width is the KPI grid's four columns. */
@media (max-width: 380px) {
  .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- motion --------------------------------------------------------------
   There is almost none to begin with — one 90 ms row hover and one arrow rotation
   — but "almost none" is not a promise, and a reader who has asked their operating
   system for no animation is entitled to the guarantee rather than to a review of
   this file. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Windows high contrast ----------------------------------------------
   Forced-colors mode replaces every colour above with the reader's own palette,
   which silently deletes the two places this page encodes meaning as colour and
   nothing else: the freshness dot and the per-source state dots. ``forced-color-adjust:
   none`` on exactly those two keeps them, and the rest of the page correctly
   becomes the reader's chosen scheme. */
@media (forced-colors: active) {
  .dot, .src::before { forced-color-adjust: none; }
  .toggle:has(input:checked) { border-color: Highlight; }
  thead th { border-bottom: 1px solid CanvasText; }
}

/* --- print ---------------------------------------------------------------
   A farmer taking a deadline calendar into a meeting is a real use of this page,
   and the default print of a dark site is 40 pages of black ink. Inverted to paper,
   controls dropped, table repeated across page breaks with its header. */
@media print {
  :root {
    --bg: #fff; --surface-1: #fff; --surface-2: #fff; --surface-3: #fff;
    --ink: #000; --ink-2: #333; --ink-3: #555; --ink-edge: #888;
    --line: #ccc; --line-strong: #999; --sheen: transparent;
    --link: #000;
  }
  body { font-size: 10pt; }
  .wrap { max-width: none; padding: 0; }
  .filters, .skip, .api, .more summary { display: none; }
  /* The expander's contents are content: on paper there is no click, so the rows
     it hides would simply be missing. */
  details[open] > *, details > .cards { display: grid !important; }
  details { display: block; }
  thead th { position: static; background: transparent; }
  table { page-break-inside: auto; }
  tr { page-break-inside: avoid; }
  thead { display: table-header-group; }
  .card, .kpi, .fact, .head-meta { break-inside: avoid; }
  a[href^="/"]::after { content: ""; }
}
