/* GENERATED FILE — DO NOT EDIT.
 *
 * Concatenated from src/styles/figures/*.css plus src/styles/callout.css by
 * scripts/build-figures-css.mjs; run `make figures` to regenerate.
 * scripts/build-guard.mjs fails the build if this is stale.
 *
 * The figure kit: every markdown figure mode (heatmap, waffle, range, quadrant,
 * timeline, matrix, fold, layers), the hand-written kit components for
 * artifacts and reports, and (riding along, see this script's header comment)
 * the article callout family, so a standalone artifact can write the same
 * <div class="callout callout--note"> markup a post does with no local CSS.
 * Uses tokens.css names only — load /tokens.css first.
 */

/* ── _base.css ───────────────────────────────────────────────────────── */
/*
 * The shell every figure shares — markdown-drawn or hand-written. See
 * src/lib/figures/hast.ts for the markup contract:
 *
 *   figure.abn-fig.abn-fig--<mode>
 *     p.abn-fig__title          (optional)
 *     div.abn-fig__frame        (.is-bare: no surface; .is-scroll: sideways on a phone)
 *     figcaption.abn-fig__caption (optional)
 *
 * TOKENS ONLY. No hex, no redeclared token: this file ships as /figures.css
 * next to /tokens.css, and a local value is the drift build-guard exists for.
 */
.abn-fig {
  margin: 2.25rem 0;
  min-width: 0;
}
.abn-fig__title {
  margin: 0 0 0.75rem;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
}
.abn-fig__frame {
  border: var(--border-width) solid var(--color-line);
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  min-width: 0;
}
.abn-fig__frame.is-bare {
  border: 0;
  background: none;
  padding: 0;
  border-radius: 0;
}
.abn-fig__frame.is-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
/* The site's caption, not a new one: `.prose figcaption` (prose.css) sets
   xs, muted, italic, 10px above, and inside a post it outranks this rule on
   specificity anyway. Stating the same values here makes a figure look the
   same OUTSIDE .prose — an html artifact or a report body — instead of
   depending on which wrapper it landed in. */
.abn-fig__caption {
  margin-top: 10px;
  font-size: var(--text-xs);
  font-style: italic;
  line-height: var(--lh-relaxed);
  color: var(--color-text-muted);
  max-width: 80ch;
}
@media print {
  .abn-fig { break-inside: avoid; }
  .abn-fig__frame.is-scroll { overflow: visible; }
}

/* ── fold.css ────────────────────────────────────────────────────────── */
/*
 * `mode: fold` — see src/lib/figures/fold.ts for the markup this styles:
 *
 *   figure.abn-fig.abn-fig--fold
 *     div.abn-fig__frame.is-bare
 *       ol.abn-fold
 *         li
 *           details.abn-fold__item[id?][open?]      (has a body)
 *             summary.abn-fold__summary.abn-fold__line
 *               span.abn-fold__n / .abn-fold__title / .abn-fold__meta?
 *             div.abn-fold__body
 *               p, ul, ol, pre, …                    (author's own body nodes)
 *               p.abn-fold__label                    (an ALL-CAPS lead strong)
 *           — or, when the item has no body —
 *           div.abn-fold__item.abn-fold__item--plain[id?]  (no indicator)
 *             div.abn-fold__row.abn-fold__line
 *               (same three spans)
 *
 * SPECIFICITY: every selector below is prefixed `.abn-fig--fold`, which is
 * what makes each one (>= 2 of our own classes) beat the single class+type
 * rules prose.css puts on `ol`, `li`, `p` and `strong` regardless of which
 * stylesheet loads second — see AGENTS.md's note on `.prose <tag>` specificity.
 * TOKENS ONLY: no hex, no redeclared token (build-guard enforces this).
 */

.abn-fig--fold .abn-fold {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: none;
  border-top: var(--border-width) solid var(--color-line);
}

.abn-fig--fold .abn-fold > li {
  margin: 0;
  line-height: inherit;
}

.abn-fig--fold .abn-fold__item {
  border-bottom: var(--border-width) solid var(--color-line);
}

.abn-fig--fold .abn-fold__summary,
.abn-fig--fold .abn-fold__row {
  display: grid;
  grid-template-columns: 2.4rem 1fr auto 1.2rem;
  gap: 0.5rem 0.75rem;
  align-items: baseline;
  padding: 0.75rem 0;
}

/* Only the details variant is interactive; the plain row keeps the same
   four-column template (leaving the indicator column empty) so its meta
   lines up with every other row's instead of sitting flush right. */
.abn-fig--fold .abn-fold__summary {
  cursor: pointer;
  list-style: none;
}
.abn-fig--fold .abn-fold__summary::-webkit-details-marker {
  display: none;
}
.abn-fig--fold .abn-fold__summary::marker {
  content: '';
}
.abn-fig--fold .abn-fold__row {
  cursor: default;
}

.abn-fig--fold .abn-fold__n {
  font-family: var(--font-code);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--color-accent);
}

.abn-fig--fold .abn-fold__title {
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  min-width: 0;
  overflow-wrap: anywhere;
}

.abn-fig--fold .abn-fold__meta {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  text-align: right;
  white-space: nowrap;
  grid-column: 3;
}

.abn-fig--fold .abn-fold__summary::after {
  content: '+';
  grid-column: 4;
  grid-row: 1;
  justify-self: end;
  font-family: var(--font-ui);
  font-size: 1.1rem;
  line-height: 1;
  color: var(--color-accent);
}
.abn-fig--fold details[open] > .abn-fold__summary::after {
  content: '\2212';
}

.abn-fig--fold .abn-fold__body {
  padding: 0 0 0.875rem 3.15rem;
  font-size: var(--text-sm);
}
.abn-fig--fold .abn-fold__body p {
  margin: 0 0 0.5rem;
  max-width: none;
}
.abn-fig--fold .abn-fold__body > :last-child {
  margin-bottom: 0;
}
.abn-fig--fold .abn-fold__body ul,
.abn-fig--fold .abn-fold__body ol {
  max-width: none;
}

/* The MITIGATE-style lead: the paragraph keeps body colour, only its leading
   strong is pulled out as apparatus. */
.abn-fig--fold .abn-fold__label strong {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
  color: var(--color-accent-text);
  margin-right: 0.4rem;
}

/* Reference's 560px rule: the meta wraps under the title instead of sharing
   its row, and the body indent follows the now-2-column line. */
@media (max-width: 560px) {
  .abn-fig--fold .abn-fold__summary,
  .abn-fig--fold .abn-fold__row {
    grid-template-columns: 2.2rem 1fr 1.2rem;
  }
  .abn-fig--fold .abn-fold__summary::after {
    grid-column: 3;
  }
  .abn-fig--fold .abn-fold__meta {
    grid-column: 2;
    grid-row: 2;
    text-align: left;
    white-space: normal;
  }
  .abn-fig--fold .abn-fold__body {
    padding-left: 2.9rem;
  }
}

/* ── heatmap.css ─────────────────────────────────────────────────────── */
/*
 * `mode: heatmap` — a scored table, each cell tinted in one of 5 linear steps
 * of the accent, with an optional totals row and a ranked bar list of those
 * totals. See src/lib/figures/heatmap.ts for the markup this styles.
 *
 * TOKENS ONLY, same rule as _base.css: this ships as /figures.css beside
 * /tokens.css and a local value is exactly the drift the build guard exists to
 * catch. `color-mix(in srgb, var(--color-accent) N%, transparent)` composites
 * onto whatever sits behind it (the frame's own --color-surface), which is
 * what makes the same five percentages read correctly in both themes without
 * a second, dark-mode set of them.
 *
 * SPECIFICITY: this sits inside `.prose`, which owns global rules for several
 * of the tags a table needs (figure/figcaption, and potentially table/th/td —
 * see AGENTS.md). Every selector below is written `.abn-fig--heatmap
 * .abn-fig__frame <tag-or-class>` — two classes plus (at most) one type
 * selector, (0,2,1) — matching the convention `.callout .callout__body p`
 * already uses in callout.css to clear a (0,2,0) or lighter prose rule
 * outright, independent of stylesheet import order.
 *
 * INK PER STEP is verified, not eyeballed: test/figures-heatmap.test.ts
 * recomputes the composited background for s1–s5 in both themes from the
 * literal token hex values and checks contrast against the ink used here.
 * --color-heading clears 4.5:1 at every step in both themes (measured
 * minimum 6.74:1); --color-text does not (4.04:1 at s4 in light) — that is
 * why the tinted steps use heading, not text, as their ink.
 */

/* ---- the scroll frame: min-width scales with column count, so a 2-column
   heatmap never scrolls and a 7-column one scrolls only where it must. ---- */
.abn-fig--heatmap .abn-fig__hm-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.abn-fig--heatmap .abn-fig__hm-table {
  width: 100%;
  /* --cols and --col-chars are set by the renderer — computed numbers, never
     author text (see cssVars() in hast.ts). 8rem is the label column's floor.
     --col-chars is the character count of the longest WORD in any data
     column head, so no head ever has to break mid-word (`hyphens: auto`
     below stays only as a defensive fallback). It is a unitless count, not a
     `ch` length, on purpose: the head text is set in --font-ui (mono) at
     --text-xs, not the table's own (Sans, prose-size) font that `ch` would
     resolve against, so this line converts the count into a width using the
     HEAD's actual metrics — 0.6em is a bold monospace character's width, and
     the trailing +0.65rem covers the head cell's own end padding (0.4rem)
     plus a little slack for the width difference between an average
     lowercase letter and the widest glyph actually drawn. `max()` floors the
     result at 5.25rem — the pre-figure-kit column width — so a short head
     ("A", "B") never draws a column narrower than a data value like
     "1250 ms" needs, no matter how small --col-chars comes out. --col-chars
     and --text-xs both fall back for content rendered before this var
     existed. */
  min-width: calc(8rem + var(--cols, 1) *
    max(5.25rem, calc(var(--col-chars, 4) * 0.6 * var(--text-xs, 0.75rem) + 0.65rem)));
  border-collapse: collapse;
  table-layout: fixed;
  /* Row labels are content, not chrome — inherit the article's prose font
     (Sans) rather than the UI mono. Only the column heads and the totals-row
     label below opt back into --font-ui. */
  font-family: inherit;
}
/* Fixed layout takes column widths from the FIRST row only. Give the label
   column an explicit floor; every data column (left unset) shares whatever
   width remains, equally — the same mechanism table.css's own comment on
   `table-layout: fixed` describes for `.abn-tbl`. */
.abn-fig--heatmap .abn-fig__hm-table thead th:first-child {
  width: 8rem;
}

/* ---- header row: chrome — small, muted, sentence case (not a caps kicker;
   the reference sets its column heads in the reading face, unshouted) ---- */
.abn-fig--heatmap .abn-fig__hm-th {
  padding: 0 0.4rem 0.55rem 0;
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  font-family: var(--font-ui);
  color: var(--color-text-muted);
  text-align: center;
  line-height: 1.3;
  border: 0;
  /* The min-width above is sized in the head's own font metrics (mono,
     --text-xs) so a one-word column head should never need to wrap here.
     This stays as a defensive fallback for content rendered by an older
     version of the renderer with no --col-chars var (falls back to the
     5.25rem floor, which a long single word CAN exceed): under
     `table-layout: fixed` a cell does not clip that overflow — it bleeds
     into the next column instead of wrapping. `hyphens: auto` breaks it at a
     syllable rather than mid-letter; `anywhere` (an earlier rule here) broke
     ordinary sentence-case words needlessly even when the column was already
     wide enough. */
  overflow-wrap: break-word;
  hyphens: auto;
}
.abn-fig--heatmap .abn-fig__hm-table thead th:first-child {
  text-align: left;
  padding-left: 0;
}

/* ---- pin the label column when the table scrolls: reading a heatmap needs
   row and column in view together, the same reason .abn-tbl pins its first
   column (table.css `.sticky-col`). Tokens only, opaque background so the
   scrolled data doesn't show through underneath it. ---- */
.abn-fig--heatmap .abn-fig__hm-rowhead,
.abn-fig--heatmap .abn-fig__hm-table thead th:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--color-surface);
}

/* ---- row labels: chrome too, but read as a sentence, not a caps label ---- */
.abn-fig--heatmap .abn-fig__hm-rowhead {
  padding: 0.2rem 0.75rem 0.2rem 0;
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--color-heading);
  text-align: left;
  vertical-align: middle;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* ---- data cells: the tint lives on the chip, not the <td>, so the row
   hairline stays a straight line under every column ---- */
.abn-fig--heatmap .abn-fig__hm-cell {
  padding: 3px;
  text-align: center;
  vertical-align: middle;
}
.abn-fig--heatmap .abn-fig__hm-chip {
  display: block;
  padding: 0.5rem 0;
  border-radius: var(--radius-sm);
  font-family: var(--font-code);
  font-weight: var(--fw-semibold);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-heading);
}
.abn-fig--heatmap .abn-fig__hm-chip.s1 { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.abn-fig--heatmap .abn-fig__hm-chip.s2 { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.abn-fig--heatmap .abn-fig__hm-chip.s3 { background: color-mix(in srgb, var(--color-accent) 38%, transparent); }
.abn-fig--heatmap .abn-fig__hm-chip.s4 { background: color-mix(in srgb, var(--color-accent) 58%, transparent); }
.abn-fig--heatmap .abn-fig__hm-chip.s5 {
  background: var(--color-accent);
  color: var(--color-on-accent);
}

/* ---- totals row: untinted chips, bordered, the winner outlined in accent ---- */
.abn-fig--heatmap .abn-fig__hm-total .abn-fig__hm-rowhead {
  padding-top: 0.65rem;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  border-top: var(--border-width) solid var(--color-line);
}
.abn-fig--heatmap .abn-fig__hm-total .abn-fig__hm-cell {
  padding-top: 0.65rem;
  border-top: var(--border-width) solid var(--color-line);
}
.abn-fig--heatmap .abn-fig__hm-chip.is-total {
  background: transparent;
  border: var(--border-width) solid var(--color-line-strong);
  color: var(--color-heading);
}
.abn-fig--heatmap .abn-fig__hm-chip.is-total.is-win {
  border: var(--border-width-strong) solid var(--color-accent);
  color: var(--color-accent-text);
}

/* ---- blank cells: drawn empty, no chip, no tint ---- */
.abn-fig--heatmap .abn-fig__hm-cell.is-blank {
  background: none;
}

/* ---- rank: bars — a ranked list of the column totals, longest first ---- */
.abn-fig--heatmap .abn-fig__hm-bars {
  list-style: none;
  margin: 1.35rem 0 0;
  padding: 1rem 0 0;
  border-top: var(--border-width) solid var(--color-line);
  /* `.prose ul { max-width: 68ch }` is not overridden by this class alone
     (specificity ties, source order wins), so an unqualified bar list can be
     clipped narrower than its own grid columns need. */
  max-width: none;
}
.abn-fig--heatmap .abn-fig__hm-bar {
  display: grid;
  grid-template-columns: minmax(6rem, 11rem) 1fr 3rem;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.55rem;
  /* Same story for `.prose li { line-height: 1.75 }`: too loose for a single
     line of bar chrome, and not overridden without this. */
  line-height: 1.35;
}
.abn-fig--heatmap .abn-fig__hm-bar:last-child {
  margin-bottom: 0;
}
.abn-fig--heatmap .abn-fig__hm-bar-name {
  font-size: var(--text-sm);
  color: var(--color-text);
  overflow-wrap: anywhere;
}
.abn-fig--heatmap .abn-fig__hm-bar.is-win .abn-fig__hm-bar-name {
  color: var(--color-heading);
  font-weight: var(--fw-semibold);
}
.abn-fig--heatmap .abn-fig__hm-bar-track {
  position: relative;
  height: 0.85rem;
  background: var(--color-surface-sunken);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
/* --fill is a percentage the renderer computed from the totals — never author
   text — passed through cssVars() exactly like the prose figure's own bar
   track (content.ts / table.css `.abn-tbl__track::after`). Linear from zero,
   no headroom: the winner's bar fills in proportion to the maximum POSSIBLE
   total (rows × scale max), not to itself, so a slim lead reads as a slim
   lead. */
.abn-fig--heatmap .abn-fig__hm-bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  width: var(--fill, 0%);
  border-radius: var(--radius-sm);
  background: var(--color-line-strong);
}
.abn-fig--heatmap .abn-fig__hm-bar.is-win .abn-fig__hm-bar-fill {
  background: var(--color-accent);
}
.abn-fig--heatmap .abn-fig__hm-bar-value {
  font-family: var(--font-code);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--color-heading);
}

/* ---- phone: labels get less room, everything else holds its floor ---- */
@media (max-width: 480px) {
  .abn-fig--heatmap .abn-fig__hm-bar {
    grid-template-columns: minmax(5rem, 7.5rem) 1fr 2.6rem;
  }
}

/* No motion here: the bar fill is set once, server-side, from --fill — there is
   no later state change to animate, so a transition would be dead weight
   (and the kit-wide rule is to gate motion behind no-preference, not to invent
   some). Noted so a reviewer does not add one "to match the pattern". */

/* ── kit.css ─────────────────────────────────────────────────────────── */
/*
 * kit.css — hand-written figure components for standalone HTML artifacts
 * (/a/<id>) and long-form reports (/void/reports/<slug>/). There is no
 * renderer and no markdown syntax behind these: a producer copy-pastes the
 * markup from .agents/skills/abn-is-artifact-author/references/
 * figure-kit-components.md straight into a page. This file is concatenated
 * into public/figures.css by scripts/build-figures-css.mjs (`make figures`),
 * the same file the markdown-driven figure modes ship in, so an artifact or
 * report links ONE stylesheet (after /tokens.css) for every figure on the
 * page, drawn or hand-built.
 *
 * TOKENS ONLY — see src/styles/figures/_base.css's header. Every class here
 * is prefixed `abn-` on purpose: this stylesheet is loaded standalone next to
 * whatever CSS the artifact or report page brings of its own (the html
 * starter's `.kicker`, `.tag`, `.note`…), and an unprefixed name would shadow
 * or be shadowed by it. Checked against src/styles/*.css and public/*.css
 * before naming these — `.pill` already exists (admin.css, BEM `.pill--*`),
 * which is why the pill here is `.abn-pill`.
 *
 * Ported from a one-off reference page that redeclared its own tokens and
 * hard-coded #FFD800; every colour below reads a site token instead, see the
 * reference doc's own token-map comment for the correspondence. Reused
 * verbatim: dt/dd field grids, the dashed status strip, the counter-based
 * numbering, the stack-on-phone rule every component here follows.
 */

/* ── Record card — "Fig. 1" ─────────────────────────────────────────────
 * A decision record: who said it, who it's for, whether it still
 * holds. Head carries the id in data font, the record type, and a status
 * pill pinned right; a foot line can show a superseded id struck through.
 */
.abn-record {
  border: var(--border-width) solid var(--color-line-strong);
  border-radius: var(--radius-lg);
  background: var(--color-bg);
  overflow: hidden;
  box-shadow: 0 1px 0 var(--color-line);
}
.abn-record__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-bottom: var(--border-width) solid var(--color-line);
  background: var(--color-surface);
}
.abn-record__id {
  font-family: var(--font-code);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
}
.abn-record__type {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.abn-record__head .abn-pill {
  margin-left: auto;
}
.abn-record__body {
  padding: var(--space-4) var(--space-5) var(--space-1);
}
.abn-record .abn-record__claim {
  font-size: var(--text-lg);
  line-height: var(--lh-relaxed);
  margin: 0 0 var(--space-4);
  color: var(--color-heading);
}
.abn-record__fields {
  display: grid;
  grid-template-columns: minmax(8.5rem, max-content) 1fr;
  gap: 0;
  margin: 0;
  border-top: var(--border-width) solid var(--color-line);
}
.abn-record__fields dt,
.abn-record__fields dd {
  padding: var(--space-3) 0;
  border-bottom: var(--border-width) solid var(--color-line);
  margin: 0;
}
.abn-record__fields dt {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--color-accent-text);
  font-weight: var(--fw-semibold);
  padding-right: var(--space-4);
  padding-top: var(--space-3);
}
.abn-record__fields dd {
  font-size: var(--text-body);
  color: var(--color-text);
}
.abn-record__fields dd > p {
  margin: 0;
}
.abn-record__data {
  display: block;
  margin-top: 2px;
  font-family: var(--font-code);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.abn-record__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-5);
  padding: var(--space-3) var(--space-5) var(--space-4);
  font-family: var(--font-code);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.abn-record__foot s {
  text-decoration-color: var(--color-accent);
}
@media (max-width: 520px) {
  .abn-record__fields {
    grid-template-columns: 1fr;
  }
  .abn-record__fields dt {
    border-bottom: 0;
    padding-bottom: 0;
  }
  .abn-record__fields dd {
    padding-top: var(--space-1);
  }
}
@media print {
  .abn-record {
    break-inside: avoid;
  }
}

/* ── Stage flow — "Fig. 6" ───────────────────────────────────────────────
 * Three stages left to right (top to bottom on a phone), joined by arrows
 * that rotate to point down when stacked. The middle stage is the emphasised
 * "core" — carries the whole idea, so it gets the accent treatment. A dashed
 * "govern" strip of pills sits underneath as the constraints that apply to
 * every stage at once.
 */
.abn-stages {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
  align-items: stretch;
}
@media (min-width: 820px) {
  .abn-stages {
    grid-template-columns: 1fr 34px 1.15fr 34px 1fr;
  }
}
.abn-stage {
  border: var(--border-width) solid var(--color-line-strong);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  padding: var(--space-4);
}
.abn-stage--core {
  border: 2px solid var(--color-accent);
  background: var(--color-accent-soft);
}
.abn-stage h4 {
  margin: 0 0 var(--space-2);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--color-accent-text);
}
.abn-stage ul {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--text-sm);
}
.abn-stage > ul > li {
  padding: var(--space-2) 0;
  border-top: var(--border-width) solid var(--color-line);
  margin: 0;
  line-height: var(--lh-normal);
}
.abn-stage > ul > li:first-child {
  border-top: 0;
}
.abn-stage__note {
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  display: block;
}
.abn-stage__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-accent);
}
.abn-stage__arrow svg {
  width: 22px;
  height: 22px;
  transform: rotate(90deg);
}
@media (min-width: 820px) {
  .abn-stage__arrow svg {
    transform: none;
  }
}
.abn-govern {
  margin-top: var(--space-3);
  border: var(--border-width) dashed var(--color-accent);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  align-items: center;
}
.abn-govern h4 {
  margin: 0 var(--space-2) 0 0;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--color-accent-text);
}
@media print {
  .abn-stages,
  .abn-govern {
    break-inside: avoid;
  }
}

/* ── Lettered options — concepts list ────────────────────────────────────
 * A ranked or enumerated set of options: a big letter, a heading, an
 * optional status pill, prose, and a compact dt/dd facts grid. The
 * recommended (or lead) entry gets the accent letter; every other letter
 * stays muted.
 */
.abn-concepts {
  margin: var(--space-6) 0;
}
.abn-cpt {
  display: grid;
  grid-template-columns: 3.2rem 1fr;
  gap: var(--space-1) var(--space-4);
  padding: var(--space-5) 0;
  border-top: var(--border-width) solid var(--color-line);
}
.abn-cpt:last-child {
  border-bottom: var(--border-width) solid var(--color-line);
}
.abn-cpt__letter {
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: 2.4rem;
  line-height: 1;
  color: var(--color-text-muted);
}
.abn-cpt--lead .abn-cpt__letter {
  color: var(--color-accent-text);
}
.abn-cpt h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-h3);
}
.abn-cpt .abn-pill {
  margin-bottom: var(--space-2);
}
.abn-cpt p {
  margin: 0 0 var(--space-2);
}
.abn-cpt__facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
}
.abn-cpt__facts dt {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  padding-top: 0.15rem;
}
.abn-cpt__facts dd {
  margin: 0;
}
@media (max-width: 560px) {
  .abn-cpt {
    grid-template-columns: 1fr;
  }
  .abn-cpt__facts {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .abn-cpt__facts dd {
    margin-bottom: var(--space-2);
  }
}
@media print {
  .abn-cpt {
    break-inside: avoid;
  }
}

/* ── Numbered phases ──────────────────────────────────────────────────────
 * A sequence of dated phases (a roadmap, a plan). The number is a CSS
 * counter in data font, not typed text, so inserting or removing a phase
 * never leaves a gap or a duplicate. KILL IF / PASS IF / TARGET are inline
 * labels for the sentence that follows — the vocabulary of a plan that
 * expects to be judged against itself.
 */
.abn-phases.abn-phases {
  counter-reset: abn-phase;
  list-style: none;
  padding: 0;
  margin: var(--space-6) 0;
}
.abn-phases > li {
  counter-increment: abn-phase;
  display: grid;
  grid-template-columns: 2.4rem 1fr;
  gap: var(--space-1) var(--space-3);
  padding: var(--space-4) 0;
  border-top: var(--border-width) solid var(--color-line);
  margin: 0;
}
/*
 * ::before is itself a grid item once `content` is set — a bare li with
 * h4 + two <p>s plus this pseudo-element is FIVE items over TWO columns,
 * and auto-placement scatters each paragraph into alternating columns
 * instead of stacking them under the heading. .abn-phases__body is the
 * fix: exactly one content item beside the counter, so the grid only ever
 * has two cells per row and everything inside the body flows normally.
 */
.abn-phases > li::before {
  content: counter(abn-phase, decimal-leading-zero);
  font-family: var(--font-code);
  font-weight: var(--fw-semibold);
  color: var(--color-accent-text);
  padding-top: 0.1rem;
}
.abn-phases__body > h4 {
  margin: 0 0 var(--space-1);
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
}
.abn-phases__body > h4 span {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-regular);
  color: var(--color-text-muted);
  margin-left: var(--space-2);
}
.abn-phases__body > p {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  color: var(--color-text);
}
.abn-phases__label {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-accent-text);
  font-weight: var(--fw-semibold);
  margin-right: var(--space-1);
}
@media print {
  .abn-phases li {
    break-inside: avoid;
  }
}

/* ── Cover block ──────────────────────────────────────────────────────────
 * A dossier's own masthead: kicker, display h1, deck, a meta dl (Filed,
 * Scope, Status, Reading…), and a hero-art slot that carries an
 * illustrative figure (a layered diagram, in the reference) beside the
 * text. The art stacks under the text on a phone.
 */
.abn-cover {
  margin: 0 0 var(--space-8);
}
.abn-cover__grid {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}
@media (min-width: 900px) {
  .abn-cover__grid {
    grid-template-columns: 1.05fr 1fr;
    gap: var(--space-12);
  }
}
.abn-kicker.abn-kicker {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--color-accent-text);
  margin: 0 0 var(--space-2);
}
.abn-cover__title {
  font-family: var(--font-ui);
  font-weight: var(--fw-semibold);
  font-size: clamp(2.4rem, 8vw, var(--text-display));
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-display);
  margin: 0 0 var(--space-5);
  color: var(--color-heading);
}
.abn-cover .abn-cover__deck {
  font-size: var(--text-lg);
  line-height: var(--lh-normal);
  color: var(--color-text);
  margin: 0 0 var(--space-6);
  max-width: 34ch;
}
.abn-cover__meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, max-content));
  gap: var(--space-2) var(--space-8);
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
}
.abn-cover__meta div {
  min-width: 0;
}
.abn-cover__meta dt {
  color: var(--color-text-muted);
}
.abn-cover__meta dd {
  margin: 0;
  color: var(--color-heading);
}
.abn-cover .abn-cover__art {
  margin: 0;
}
.abn-cover__art svg {
  display: block;
  width: 100%;
  height: auto;
}
@media print {
  .abn-cover {
    break-inside: avoid;
  }
}

/* ── Table of contents grid ──────────────────────────────────────────────
 * A kicker/title two-column jump list, rows ruled with a hairline. Collapses
 * to one column on a narrow phone.
 */
.abn-toc {
  list-style: none;
  padding: 0;
  margin: var(--space-5) 0 0;
  border-top: var(--border-width) solid var(--color-line);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
}
.abn-toc > li {
  margin: 0;
  border-bottom: var(--border-width) solid var(--color-line);
}
.abn-toc a {
  display: grid;
  grid-template-columns: 9.5rem 1fr;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  text-decoration: none;
  border-bottom: 0;
  color: var(--color-heading);
}
.abn-toc a span:first-child {
  color: var(--color-accent-text);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  font-size: var(--text-xs);
  padding-top: 0.12rem;
}
.abn-toc a:hover span:last-child {
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (max-width: 520px) {
  .abn-toc a {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

/* ── Pills ─────────────────────────────────────────────────────────────── */
.abn-pill {
  display: inline-block;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  padding: 2px 9px;
  border-radius: var(--radius-pill);
  border: var(--border-width) solid var(--color-line-strong);
  color: var(--color-text);
  white-space: nowrap;
}
.abn-pill--on {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}
.abn-pill--solid {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-on-accent);
}

/*
 * Tone modifiers — the same five fill/line/text triplets already verified
 * for the eval-report `.tag` family (public/eval-report.css) and the review
 * page's `.sev`/`.conf` pills this kit is ported from. `.abn-pill` is already
 * --text-xs (12px, not the reference's 0.7rem/11.2px) so no size change is
 * needed here, only colour. Pick a tone by what it says, not by a fixed
 * meaning — the reference reads danger as P0, pending as P1, info as P2,
 * neutral as P3, and success as a confidence label, but a size chip (S/M/L)
 * or any other tag can use whichever tone fits.
 */
.abn-pill--danger,
.abn-pill--pending,
.abn-pill--info,
.abn-pill--neutral,
.abn-pill--success {
  font-weight: var(--fw-semibold);
}
.abn-pill--danger {
  background: var(--status-danger-fill);
  border-color: var(--status-danger-line);
  color: var(--status-danger-text);
}
.abn-pill--pending {
  background: var(--status-pending-fill);
  border-color: var(--status-pending-line);
  color: var(--status-pending-text);
}
.abn-pill--info {
  background: var(--status-info-fill);
  border-color: var(--status-info-line);
  color: var(--status-info-text);
}
.abn-pill--neutral {
  background: var(--status-neutral-fill);
  border-color: var(--status-neutral-line);
  /*
   * NOT --status-neutral-text: in light mode that pairs #78716C on #F5F5F4 at
   * 4.40:1, just under the 4.5:1 AA floor (measured 2026-09-26 — the other
   * four light-mode tones run 4.57–6.38, all four dark-mode tones 6.85–12.78).
   * --color-text-muted (#57534E, ~7:1 on this fill) is AA-safe in both themes
   * without a new token.
   */
  color: var(--color-text-muted);
}
.abn-pill--success {
  background: var(--status-success-fill);
  border-color: var(--status-success-line);
  color: var(--status-success-text);
}

/* ── Finding card — a record variant ──────────────────────────────────────
 * .abn-record--finding is the same card, re-purposed for a single scored
 * observation: head carries an id, a title and a severity pill; a lens-tags
 * row underneath; the body holds an evidence figure slot (an .abn-shot goes
 * in the frame — a different lane owns that component, so this leaves a
 * plain .abn-fig shell) and reuses .abn-record__fields for the
 * Observed / Why it matters / Fix grid, with Fix's dt carrying a size pill.
 * A caveat is an optional trailing <details>, dashed off from the fields
 * rather than boxed, so it reads as an aside on the evidence, not a fourth
 * field.
 */
.abn-record--finding .abn-record__head {
  border-bottom: 0;
}
.abn-record--finding .abn-record__title {
  flex: 1 1 22ch;
  margin: 0;
  font-size: var(--text-lg);
  line-height: var(--lh-snug);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
}
.abn-record--finding .abn-record__lenses {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: var(--space-3) var(--space-5);
  border-bottom: var(--border-width) solid var(--color-line);
}
.abn-record--finding .abn-record__lenses li {
  margin: 0;
}
.abn-record--finding .abn-record__body .abn-fig {
  margin: 0 0 var(--space-4);
}
.abn-record--finding .abn-record__fields dt .abn-pill {
  margin-left: var(--space-2);
  vertical-align: middle;
}
.abn-record--finding .abn-record__caveat {
  /*
   * No border-top of its own: the last dt/dd pair's solid border-bottom
   * already separates the caveat, and a second (dashed) rule 12px under it
   * showed as a double rule in both themes.
   */
  margin: var(--space-3) 0 0;
}
.abn-record--finding .abn-record__caveat > summary {
  cursor: pointer;
  padding: var(--space-3) 0;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-accent-text);
}
.abn-record--finding .abn-record__caveat[open] > summary {
  padding-bottom: var(--space-2);
}
.abn-record--finding .abn-record__caveat > p {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}
@media (max-width: 520px) {
  .abn-record--finding .abn-record__title {
    flex-basis: 100%;
  }
}

/* ── Stat tiles ────────────────────────────────────────────────────────────
 * A responsive grid of small number callouts: a big value, a label, and an
 * optional third note line for extra context. .is-highlight singles out the
 * tiles worth a reader's attention among a denser row (the reference uses it
 * for a headline result among routine measurements).
 */
.abn-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
}
.abn-tile {
  border: var(--border-width) solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg);
}
.abn-tile__value {
  display: block;
  font-family: var(--font-code);
  font-weight: var(--fw-bold);
  font-size: 1.35rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--color-heading);
}
.abn-tile__label {
  display: block;
  font-size: var(--text-sm);
  line-height: var(--lh-normal);
  color: var(--color-text-muted);
  margin-top: var(--space-1);
}
.abn-tile__note {
  display: block;
  font-size: var(--text-xs);
  line-height: var(--lh-normal);
  color: var(--color-text-faint);
  margin-top: 2px;
}
.abn-tile.is-highlight {
  border-color: var(--color-accent);
}
.abn-tile.is-highlight .abn-tile__value {
  color: var(--color-accent-text);
}
@media print {
  .abn-tiles {
    break-inside: avoid;
  }
}

/* ── Figure numbering, opt-in ─────────────────────────────────────────────
 * Wrap a run of figures in .abn-figs-numbered to have each .abn-fig__caption
 * inside gain a bold "Fig. N" prefix from a CSS counter, matching the
 * reference's `figcaption b`. A page that never applies the container class
 * is completely unaffected — the counter and the ::before both live scoped
 * under .abn-figs-numbered.
 */
.abn-figs-numbered {
  counter-reset: abn-fig-n;
}
.abn-figs-numbered .abn-fig__caption {
  counter-increment: abn-fig-n;
}
.abn-figs-numbered .abn-fig__caption::before {
  content: 'Fig. ' counter(abn-fig-n) ' ';
  font-weight: var(--fw-semibold);
  color: var(--color-accent-text);
  margin-right: 0.4rem;
}

/* ── layers.css ──────────────────────────────────────────────────────── */
/*
 * mode: layers — an isometric plate stack (SVG) beside its legend (<ol>).
 * Markup: src/lib/figures/layers.ts.
 *
 *   div.abn-fig-layers                      (--n: plate count)
 *     svg.abn-fig-layers__art               plates, chips, wires, bracket
 *       g.abn-fig-layers__plate.is-<motif>  (--i: 0 = bottom; --mix: violet %)
 *     ol.abn-fig-layers__legend
 *       li.abn-fig-layers__item[.is-top]    swatch · key (the tag) · desc
 *
 * THE RAMP. A plate's top face is `--mix`% of a violet over a neutral ground;
 * its left and right faces are the top shaded twice. The ground, the violet
 * and the shade are the only things that change with the theme — declared
 * once on the figure as --layers-* (local properties, not tokens) and switched
 * under the two dark selectors below, from token references only. Light shades
 * TOWARDS line-strong (a darker stone); dark shades towards --color-line-strong
 * too (kept from the light default rather than overridden — see the side-face
 * note below).
 *
 * `--mix` is never 0 (see MIN_MIX in layers.ts): a plate whose face is pure
 * ground is, in dark mode with no frame, pure --color-bg too (--layers-ground
 * and --color-bg both resolve to --c-void), so the bottom plate and its L1
 * legend swatch vanished at exactly 1.00:1. The floor and ceiling are picked
 * so every plate's TOP face clears 1.5:1 against BOTH --color-bg and
 * --color-surface, in BOTH themes, AND so every plate's shaded SIDE faces
 * (is-left/is-right below) clear the same floor at every mix `n` (2..6)
 * emits — see the arithmetic in test/figures-layers.test.ts, which checks
 * both, not just the top face.
 *
 * ON-PLATE MARKS are a separate pair of locals, --layers-mark /
 * --layers-mark-accent, because they have to stay legible against a face
 * whose colour they sit ON TOP of, not against the page background the way
 * every other stroke/fill in this kit is measured. See the marks note below
 * the dark blocks.
 *
 * Specificity: every rule leads with `.abn-fig--layers` so it outranks the
 * global `.prose ol` / `.prose li` (0,1,1) that wrap every post.
 */
.abn-fig--layers {
  --layers-ground: var(--color-line);
  --layers-hue: var(--c-violet-300);
  --layers-shade: var(--color-line-strong);
  --layers-top-a: var(--c-violet-600);
  --layers-top-b: var(--c-violet-700);
  --layers-side-l: var(--c-violet-700);
  --layers-side-r: var(--c-violet-800);
  --layers-mark: var(--color-text-muted);
  --layers-mark-accent: var(--color-accent-text);
}
/* Dark climbs in LIGHTNESS, not just hue: the light ramp mixes a violet over
   --color-line (already fairly light), but --color-line is near-black in dark,
   so mixing the same way there left every plate within ~1:1 of the frame
   (SOFT, measured: 1.23-1.26:1 top-to-frame, 1.00-1.02:1 between plates — one
   mud-purple mass). Start darker (surface-sunken) and mix toward a LIGHTER
   violet. Violet-700 (the first fix) was still too dark to clear 1.5:1 at any
   floor low enough to leave room for a visible ramp below MAX_MIX — even at
   100% mix it only reached 2.06:1 against --color-surface, and the floor
   needed to clear 1.5:1 sat ABOVE the ramp's own ceiling. Violet-400 (an
   earlier fix) reached the floor's 1.5:1 by ~40% mix — but violet-400 IS
   `--color-accent-text` in dark, the exact colour the dashed accent link,
   its dots and the plate edge stroke are painted in, so once the ramp got
   near that hue the marks drawn on top of it all but vanished into the face
   (measured 1.19:1 for the accent link at 90% mix, 1.51:1 for the plain
   marks and edge — both well under this kit's 3:1 mark floor). Violet-500
   (this fix) keeps the ramp's own floor/ceiling above 1.5:1 against bg and
   surface while sitting far enough from either mark colour below to leave
   headroom — see the marks note below.
   The SIDE faces (is-left/is-right) used to shade toward
   --color-surface-sunken (== --color-bg in dark, pure void), which put every
   side face's floor under 1.5:1 (right face 1.23:1 on bg at the ramp floor,
   1.38:1 on surface even at n=6's brightest non-top plate) — the fix is
   simply NOT overriding --layers-shade in dark, so it keeps the light
   default (--color-line-strong, which resolves to --c-line-d2 in dark: a
   lighter shade than void that clears the same 1.5:1 floor on every side
   face at every mix this renders). */
/* Both scoped to `screen`: global.css re-asserts LIGHT tokens for
   :root[data-theme="dark"] under @media print, but these --layers-* locals
   are not tokens, so without the guard they kept mixing a dark violet over a
   light ground when printed FROM dark mode — sides lighter than tops. */
@media screen {
  :root[data-theme="dark"] .abn-fig--layers {
    --layers-ground: var(--color-surface-sunken);
    --layers-hue: var(--c-violet-500);
    --layers-top-a: var(--c-violet-500);
    --layers-mark: var(--color-heading);
    --layers-mark-accent: var(--c-violet-50);
  }
}
@media screen and (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .abn-fig--layers {
    --layers-ground: var(--color-surface-sunken);
    --layers-hue: var(--c-violet-500);
    --layers-top-a: var(--c-violet-500);
    --layers-mark: var(--color-heading);
    --layers-mark-accent: var(--c-violet-50);
  }
}

/* ── Layout: stacked on a phone, side by side when the figure is wide ── */
.abn-fig--layers .abn-fig__frame { container-type: inline-size; }
.abn-fig--layers .abn-fig-layers {
  display: grid;
  gap: var(--space-5);
}
.abn-fig--layers .abn-fig-layers__art {
  display: block;
  width: 100%;
  max-width: 26rem;
  height: auto;
  margin: 0 auto;
  overflow: visible;
}
/* Never taller than the viewport: the scroll-driven settle finishes when the
   whole drawing is in view, which a taller one could never be. Floored at
   20rem so a short/landscape viewport (SOFT: measured 10.3px at 348x400)
   cannot shrink the art below the legibility floor on its own — this cap
   exists only for the view-timeline case, so it is scoped to it. */
@supports (animation-timeline: view()) {
  .abn-fig--layers .abn-fig-layers__art { max-height: max(85svh, 20rem); }
}
/* Breakpoint measured against the true minimum column, not the frame width:
   at 34rem the art column was 268px (11.96px text on a 6-plate figure) —
   see test/figures-layers.test.ts. 36rem puts it at ~285px, clearing 12px. */
@container (min-width: 36rem) {
  .abn-fig--layers .abn-fig-layers {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--space-8);
  }
}

/* ── Plates ── */
.abn-fig--layers .abn-fig-layers__plate,
.abn-fig--layers .abn-fig-layers__item {
  --layers-face: color-mix(in srgb, var(--layers-hue) calc(var(--mix, 0) * 1%), var(--layers-ground));
}
/* The hairline was `color-mix(--color-line-strong 70%, transparent)`, which
   composites to under 2:1 against every ground/surface/bg combination in both
   themes (--color-line-strong itself is a subtle-line token, never meant to
   carry edge legibility alone). `--layers-mark` is already used at full
   opacity elsewhere in this drawing (dots, links, tags) and clears >=3:1
   against the plate FACE it actually outlines, at every mix this renders, in
   both themes — see test/figures-layers.test.ts — so the plate silhouette
   reads at every mix percentage, not just where the fill happens to help it. */
.abn-fig--layers .abn-fig-layers__face {
  stroke: var(--layers-mark);
  stroke-width: 1;
  stroke-linejoin: round;
}
.abn-fig--layers .abn-fig-layers__plate:not(.is-graph) .abn-fig-layers__face.is-top { fill: var(--layers-face); }
.abn-fig--layers .abn-fig-layers__face.is-left { fill: color-mix(in srgb, var(--layers-face) 72%, var(--layers-shade)); }
.abn-fig--layers .abn-fig-layers__face.is-right { fill: color-mix(in srgb, var(--layers-face) 52%, var(--layers-shade)); }
/* The top plate: a violet gradient face (its fill is the url() on the path),
   solid violet sides, no hairline — it is the plate the figure points at. */
.abn-fig--layers .abn-fig-layers__plate.is-graph .abn-fig-layers__face { stroke: none; }
.abn-fig--layers .abn-fig-layers__plate.is-graph .abn-fig-layers__face.is-left { fill: var(--layers-side-l); }
.abn-fig--layers .abn-fig-layers__plate.is-graph .abn-fig-layers__face.is-right { fill: var(--layers-side-r); }
.abn-fig--layers .abn-fig-layers__stop-a { stop-color: var(--layers-top-a); }
.abn-fig--layers .abn-fig-layers__stop-b { stop-color: var(--layers-top-b); }

/* ── Marks on the plates — painted with --layers-mark / --layers-mark-accent,
   NOT --color-text-muted / --color-accent-text directly, because these sit ON
   a plate face whose colour climbs the ramp above; in dark that face runs
   toward the same violet the token pair resolves to (see the header note),
   so the mark colour has to be picked against the face, not the page. ── */
.abn-fig--layers .abn-fig-layers__dot { fill: var(--layers-mark); }
.abn-fig--layers .abn-fig-layers__dot-accent { fill: var(--layers-mark-accent); }
.abn-fig--layers .abn-fig-layers__silo {
  fill: none;
  stroke: var(--layers-mark);
  stroke-width: 1.4;
}
.abn-fig--layers .abn-fig-layers__link {
  fill: none;
  stroke: var(--layers-mark);
  stroke-width: 1.6;
  stroke-linejoin: round;
}
.abn-fig--layers .abn-fig-layers__link-accent {
  fill: none;
  stroke: var(--layers-mark-accent);
  stroke-width: 1.6;
  stroke-dasharray: 5 5;
}
.abn-fig--layers .abn-fig-layers__node { fill: var(--c-violet-50); }
.abn-fig--layers .abn-fig-layers__edge {
  fill: none;
  stroke: color-mix(in srgb, var(--c-paper) 72%, transparent);
  stroke-width: 1.8;
}
.abn-fig--layers .abn-fig-layers__ring {
  fill: none;
  stroke: var(--c-yellow-flag);
  stroke-width: 3;
}

/* ── Text in the drawing: 24 user units, ≥ 12px at 380px (see layers.ts) ── */
.abn-fig--layers .abn-fig-layers__tag {
  font-family: var(--font-ui);
  font-size: 24px;
  font-weight: var(--fw-semibold);
  fill: var(--layers-mark);
}
.abn-fig--layers .abn-fig-layers__plate.is-graph .abn-fig-layers__tag { fill: var(--layers-mark-accent); }
.abn-fig--layers .abn-fig-layers__chip {
  fill: var(--color-surface);
  stroke: var(--color-text-muted);
  stroke-width: 1.3;
  stroke-dasharray: 5 4;
}
.abn-fig--layers .abn-fig-layers__chip-text {
  font-family: var(--font-code);
  font-size: 24px;
  fill: var(--color-text-muted);
}
.abn-fig--layers .abn-fig-layers__wire {
  fill: none;
  stroke: var(--color-text-muted);
  stroke-width: 1.3;
  stroke-dasharray: 3 5;
}
.abn-fig--layers .abn-fig-layers__label {
  font-family: var(--font-ui);
  font-size: 24px;
  letter-spacing: 0.04em;
  fill: var(--color-text-muted);
}
.abn-fig--layers .abn-fig-layers__label.is-accent {
  font-weight: var(--fw-semibold);
  fill: var(--color-accent-text);
}
.abn-fig--layers .abn-fig-layers__bracket {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 1.8;
}

/* ── Legend: DOM order now matches this visual order (top plate first,
   layers.ts), so no `order` rewrite is needed here. A subgrid lines the
   tags up. ── */
.abn-fig--layers .abn-fig-layers__legend {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: none;
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  column-gap: var(--space-3);
  row-gap: var(--space-4);
  align-content: center;
}
.abn-fig--layers .abn-fig-layers__item {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: baseline;
  margin: 0;
  font-size: 0.875rem;
  line-height: var(--lh-normal);
}
.abn-fig--layers .abn-fig-layers__swatch {
  display: block;
  width: 1.375rem;
  height: 0.6875rem;
  /* Centred on the FIRST line of its description, not on the whole item. */
  align-self: start;
  margin-top: calc((1lh - 0.6875rem) / 2);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  background: var(--layers-face);
  /* NO rim: an inset box-shadow cannot follow a clip-path — clipped to the
     diamond it survives only as four sub-pixel ticks at the vertices, not a
     visible edge, so it bought nothing but a comment claiming an edge that
     wasn't there. The fill alone clears this kit's 1.5:1 floor at every mix
     this renders (see MIN_MIX in layers.ts) — a real stroke would need this
     swatch drawn as an SVG polygon instead of a clipped <span>. */
}
.abn-fig--layers .abn-fig-layers__item.is-top .abn-fig-layers__swatch {
  background: linear-gradient(135deg, var(--layers-top-a), var(--layers-top-b));
  box-shadow: none;
}
.abn-fig--layers .abn-fig-layers__key {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  white-space: nowrap;
}
.abn-fig--layers .abn-fig-layers__item.is-top .abn-fig-layers__key { color: var(--color-accent-text); }
.abn-fig--layers .abn-fig-layers__desc {
  color: var(--color-text);
  text-wrap: pretty;
}

/* ── Motion: plates drop in bottom-up, then the chips, wires and bracket.
   Time-based on load; where the browser has scroll-driven animations, the
   same keyframes run as the drawing scrolls into view instead. ── */
@media (prefers-reduced-motion: no-preference) {
  .abn-fig--layers .abn-fig-layers__plate {
    animation: abn-layers-settle 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
    animation-delay: calc(var(--i, 0) * 0.12s + 0.05s);
  }
  .abn-fig--layers .abn-fig-layers__rented,
  .abn-fig--layers .abn-fig-layers__owned {
    animation: abn-layers-fade 0.6s ease both;
    animation-delay: calc(var(--n, 4) * 0.12s + 0.2s);
  }
  @supports (animation-timeline: view()) {
    .abn-fig--layers .abn-fig-layers__art { view-timeline: --abn-layers block; }
    .abn-fig--layers .abn-fig-layers__plate {
      animation-duration: auto;
      animation-delay: 0s;
      animation-timeline: --abn-layers;
      animation-range: entry calc(var(--i, 0) * 8%) entry calc(var(--i, 0) * 8% + 60%);
    }
    .abn-fig--layers .abn-fig-layers__rented,
    .abn-fig--layers .abn-fig-layers__owned {
      animation-duration: auto;
      animation-delay: 0s;
      animation-timeline: --abn-layers;
      animation-range: entry 70% entry 100%;
    }
  }
  @keyframes abn-layers-settle {
    from { opacity: 0; transform: translateY(-18px); }
    to { opacity: 1; transform: none; }
  }
  @keyframes abn-layers-fade {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}
@media print {
  .abn-fig--layers .abn-fig-layers__plate,
  .abn-fig--layers .abn-fig-layers__rented,
  .abn-fig--layers .abn-fig-layers__owned { animation: none; }
}

/* ── matrix.css ──────────────────────────────────────────────────────── */
/*
 * `mode: matrix` — src/lib/figures/matrix.ts. A grid of ordinal levels; every
 * selector is scoped under `.abn-fig--matrix` so it clears `.prose a`
 * ((0,1,1)) and friends on specificity ((0,2,0) here) rather than racing them
 * on source order. TOKENS ONLY — see _base.css's header note.
 *
 * The column/row counts are DATA, not CSS: the renderer writes `--mtx-x` and
 * `--mtx-y` (plain integers, via cssVars) on `.abn-mtx__grid`, and `repeat()`
 * below reads them. A 3×3 grid and a 5×4 grid share this one stylesheet.
 */
.abn-fig--matrix .abn-mtx__grid {
  display: grid;
  grid-template-columns: minmax(2.4rem, auto) repeat(var(--mtx-x), minmax(4.75rem, 1fr));
  grid-template-rows: repeat(var(--mtx-y), minmax(64px, auto)) auto;
  gap: var(--space-1);
  /* Room for the id/level text to stay >= 12px even at 5 levels on an axis —
     the frame scrolls sideways (figureShell's `scroll: true`) rather than
     letting the grid squeeze below the legibility floor. */
  min-width: calc(2.4rem + (var(--mtx-x) * 4.75rem));
}

.abn-fig--matrix .abn-mtx__cell {
  border-radius: var(--radius-md);
  padding: var(--space-2);
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: var(--space-1);
}
/* Severity ramp: 5 steps of the SAME token, m0 the grid's own resting surface
   so an empty low-severity cell reads as "no items", not as "a pale item". */
.abn-fig--matrix .abn-mtx__cell--m0 { background: var(--color-surface-sunken); }
.abn-fig--matrix .abn-mtx__cell--m1 { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.abn-fig--matrix .abn-mtx__cell--m2 { background: color-mix(in srgb, var(--color-accent) 24%, transparent); }
.abn-fig--matrix .abn-mtx__cell--m3 { background: color-mix(in srgb, var(--color-accent) 40%, transparent); }
.abn-fig--matrix .abn-mtx__cell--m4 { background: color-mix(in srgb, var(--color-accent) 58%, transparent); }

.abn-fig--matrix .abn-mtx__ylab {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.abn-fig--matrix .abn-mtx__xlab {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  text-align: center;
  padding-top: var(--space-1);
}
.abn-fig--matrix .abn-mtx__corner { min-width: 0; }

/* The chip carries its OWN opaque surface rather than the cell's tint, so it
   stays readable at m4 (~58% accent) without a separate contrast case for
   every severity step — one background, one contrast check, in both themes. */
.abn-fig--matrix .abn-mtx__chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 1.9rem;
  height: 1.9rem;
  padding: 0 var(--space-1);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  border: var(--border-width-strong) solid var(--color-line-strong);
  font-family: var(--font-code);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  text-decoration: none;
}
.abn-fig--matrix a.abn-mtx__chip:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}
/* Rounds the global :focus-visible ring (tokens.css) to match the chip's own
   pill shape instead of its default --radius-md corners. */
.abn-fig--matrix .abn-mtx__chip:focus-visible {
  border-radius: var(--radius-pill);
}

.abn-fig--matrix .abn-mtx__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.abn-fig--matrix .abn-mtx__axes {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-2);
  padding-left: 2.4rem;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

@media (prefers-reduced-motion: no-preference) {
  a.abn-mtx__chip {
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
  }
}

/* ── quadrant.css ────────────────────────────────────────────────────── */
/*
 * `mode: quadrant` — a judged scatter on two 0–100 axes. See
 * src/lib/figures/quadrant.ts for the markup this styles and for the label
 * placement it switches between:
 *
 *   .abn-fig--quadrant__plot            role="img", the size container
 *     .abn-fig--quadrant__head          y title, and the corner's name as a tab
 *     .abn-fig--quadrant__stage         padding for the end labels
 *       .abn-fig--quadrant__axes        the box; tint, end labels, points
 *     p.abn-fig--quadrant__axis-title.is-x
 *     ol.abn-fig--quadrant__legend      names of numbered points (per tier)
 *
 * TOKENS ONLY, same rule as every figure sheet: no hex, no redeclared token.
 * `--fig-x`/`--fig-y` are the renderer's computed position; `--q-*` are this
 * file's own working properties, not site tokens.
 *
 * TIERS. The renderer places every label once per width band of the PLOT
 * (`QUADRANT_TIERS` in quadrant.ts) and writes one class per band onto each
 * point: `q<tier>-<side>` (r, l, a, b, ne, se, nw, sw — the eight spots
 * around its dot), plus `q<tier>-n` where that band shows the point's number instead of its
 * name. The `@container abn-quadrant` blocks at the bottom of this file are
 * those bands, and their edges (19, 25, 34rem) and the stage's horizontal
 * padding (2.4rem + 0.6rem) are numbers quadrant.ts computed against — change
 * them together or the checked layout and the drawn one part ways.
 *
 * SPECIFICITY: the titles are `<p>`s and the legend an `<ol>` of `<li>`s, and
 * `.prose p`/`.prose ol`/`.prose li` are (0,1,1). Every selector touching one
 * of those carries the `.abn-fig--quadrant` ancestor class, (0,2,0) or more,
 * so it wins without `!important`.
 */

.abn-fig--quadrant__plot {
  container: abn-quadrant / inline-size;
  position: relative;
  /* A floor only against a degenerate container: below it the frame
     (`is-scroll`) scrolls sideways rather than squeezing the axes. The
     smallest tier in quadrant.ts starts here. */
  min-width: 15rem;
  /* A plot wider than this does not read better, and capping it bounds the
     widest tier the placement was checked across. */
  max-width: 48rem;
  font-family: var(--font-ui);
}

/* ---- head: y title and corner tab ---------------------------------------
   Columns line up with the stage below: its left padding, the axes' two
   halves, its right padding. The tab sits in the right half, flush on the
   axes' top edge, in the tint's own colour — the tint's heading, wrapped to
   as many lines as it needs, never over a dot that happens to fall in the
   corner. On a narrow plot the y title takes its own full-width row above
   it; from tier 3 up they share one row. */
.abn-fig--quadrant__head {
  display: grid;
  grid-template-columns: 2.4rem 1fr 1fr 0.6rem;
  align-items: end;
}
.abn-fig--quadrant .abn-fig--quadrant__axis-title {
  margin: 0;
  max-width: none;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--color-text-muted);
  overflow-wrap: break-word;
}
.abn-fig--quadrant .abn-fig--quadrant__axis-title.is-y {
  grid-column: 2 / 4;
  grid-row: 1;
  padding-bottom: 0.6rem;
}
.abn-fig--quadrant .abn-fig--quadrant__corner-label {
  grid-column: 3;
  grid-row: 2;
  margin: 0;
  max-width: none;
  padding: 0.4rem 0.5rem 0.3rem;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: var(--color-accent-soft);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent-text);
  overflow-wrap: anywhere;
}

/* ---- stage + axes -------------------------------------------------------
   left: the y end labels ("high"/"low", 4 characters + gap).
   right: room for a label to end a hair past the axes' edge.
   bottom: the x end labels. top: half a line for the y "high" label, which
   is centred on the axes' top edge. */
.abn-fig--quadrant__stage {
  padding: 0.5rem 0.6rem 1.6rem 2.4rem;
}
.abn-fig--quadrant__head + .abn-fig--quadrant__stage {
  padding-top: 0;
}

.abn-fig--quadrant__axes {
  position: relative;
  aspect-ratio: 1;
  border-left: var(--border-width-strong) solid var(--color-line-strong);
  border-bottom: var(--border-width-strong) solid var(--color-line-strong);
}

/* Dashed midlines at 50/50 — a property of the AXES, not an authored mark, so
   drawn on the box itself rather than as a point. */
.abn-fig--quadrant__axes::before,
.abn-fig--quadrant__axes::after {
  content: '';
  position: absolute;
  border: 0 dashed var(--color-line);
}
.abn-fig--quadrant__axes::before {
  left: 50%;
  top: 0;
  bottom: 0;
  border-left-width: 1px;
}
.abn-fig--quadrant__axes::after {
  top: 50%;
  left: 0;
  right: 0;
  border-top-width: 1px;
}

.abn-fig--quadrant__corner {
  position: absolute;
  top: 0;
  right: 0;
  width: 50%;
  height: 50%;
  background: var(--color-accent-soft);
}

.abn-fig--quadrant__end {
  position: absolute;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  line-height: 1.2;
  color: var(--color-text-muted);
  white-space: nowrap;
}
.abn-fig--quadrant__end--x-lo { left: 0; top: 100%; margin-top: 0.35rem; }
.abn-fig--quadrant__end--x-hi { right: 0; top: 100%; margin-top: 0.35rem; }
.abn-fig--quadrant__end--y-lo {
  right: 100%;
  bottom: 0;
  margin-right: 0.4rem;
  transform: translateY(50%);
}
.abn-fig--quadrant__end--y-hi {
  right: 100%;
  top: 0;
  margin-right: 0.4rem;
  transform: translateY(-50%);
}

.abn-fig--quadrant .abn-fig--quadrant__axis-title.is-x {
  padding: 0 0.6rem 0 2.4rem;
  text-align: center;
}

/* ---- points ------------------------------------------------------------- */
.abn-fig--quadrant__point {
  position: absolute;
  left: calc(var(--fig-x) * 1%);
  top: calc(var(--fig-y) * 1%);
  width: 0;
  height: 0;
  /* Above the axes' own dashed midlines (its `::after` paints last, i.e. on
     top of a point born before it in DOM order, without this). */
  z-index: 1;
  /* Distance from the dot's centre to its own label — quadrant.ts's GAP. */
  --q-gap-x: 0.4rem;
  --q-gap-y: 0.3rem;
  --q-gap-d: 0.25rem;
  /* Right of the dot until a tier says otherwise. */
  --q-tx: var(--q-gap-x);
  --q-ty: -50%;
}
.abn-fig--quadrant__point.is-highlight {
  --q-gap-x: 0.8rem;
  --q-gap-y: 0.75rem;
  --q-gap-d: 0.55rem;
}
.abn-fig--quadrant__dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  transform: translate(-50%, -50%);
  background: var(--color-text-muted);
}
/* The highlighted dot is a solid accent fill inside a yellow-flag ring — the
   one mark this kit borrows unchanged from the design brief, because a halo
   is what makes ONE point read as "the pitch" among a dozen muted ones. The
   inner ring layer is --color-surface, not transparent, so the flag colour
   prints as a ring rather than bleeding into the dot itself. Neither ring
   colour alone clears the 3:1 non-text floor; the solid accent fill inside
   (5.3:1) plus the bold accent label carry the mark. */
.abn-fig--quadrant__point.is-highlight .abn-fig--quadrant__dot {
  background: var(--color-accent);
  box-shadow:
    0 0 0 0.15rem var(--color-surface),
    0 0 0 0.32rem var(--c-yellow-flag),
    0 0 0 0.36rem var(--color-line-strong);
}

/* The label: its full name, or — where its tier numbers it — its row
   number. ONE line, never truncated and never wrapped: the renderer counted
   its width (Plex Mono, 0.6em a character) and only chose a side where the
   whole name fits, so `nowrap` here is the promise it was checked against.
   Positioned on the dot's centre and moved to its side by --q-tx/--q-ty. */
.abn-fig--quadrant__label {
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
  font-family: var(--font-ui);
  font-size: var(--q-font, var(--text-xs));
  line-height: 1.2;
  color: var(--color-text-muted);
  transform: translate(var(--q-tx), var(--q-ty));
}
.abn-fig--quadrant__label code {
  font: inherit;
}
.abn-fig--quadrant__num {
  display: none;
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}
.abn-fig--quadrant__point.is-highlight .abn-fig--quadrant__label,
.abn-fig--quadrant__point.is-highlight .abn-fig--quadrant__num {
  color: var(--color-accent-text);
  font-weight: var(--fw-semibold);
}

/* ---- legend: the names behind the numbers -------------------------------
   Hidden unless the current tier numbers at least one point, and then only
   those points' entries. Inside role="img", like everything in the plot:
   the hidden table below carries the data for assistive tech. */
.abn-fig--quadrant .abn-fig--quadrant__legend {
  display: none;
  flex-wrap: wrap;
  gap: 0.3rem 1.1rem;
  list-style: none;
  max-width: none;
  margin: 0.9rem 0 0;
  padding: 0.6rem 0.6rem 0 2.4rem;
  border-top: var(--border-width) solid var(--color-line);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--color-text-muted);
}
.abn-fig--quadrant .abn-fig--quadrant__key {
  display: none;
  margin: 0;
  line-height: 1.35;
}
.abn-fig--quadrant__key-num {
  font-weight: var(--fw-semibold);
  color: var(--color-text);
}
.abn-fig--quadrant .abn-fig--quadrant__key.is-highlight,
.abn-fig--quadrant .abn-fig--quadrant__key.is-highlight .abn-fig--quadrant__key-num {
  color: var(--color-accent-text);
  font-weight: var(--fw-semibold);
}

/* ---- tiers --------------------------------------------------------------
   One block per band of `QUADRANT_TIERS` (quadrant.ts). In each: which side
   each point's label takes, whether it shows its number, which legend
   entries show, and the band's axes aspect and label size.
   Each spot is a translate from the dot's centre — right (gap, -50%), left
   (-100% - gap, -50%), above/below centred, and the diagonals with the
   label's near corner just off the dot — the same rects quadrant.ts's
   `sideRect` checked. */
@container abn-quadrant (width < 19rem) {
  .q1-r { --q-tx: var(--q-gap-x); --q-ty: -50%; }
  .q1-l { --q-tx: calc(-100% - var(--q-gap-x)); --q-ty: -50%; }
  .q1-a { --q-tx: -50%; --q-ty: calc(-100% - var(--q-gap-y)); }
  .q1-b { --q-tx: -50%; --q-ty: var(--q-gap-y); }
  .q1-ne { --q-tx: var(--q-gap-d); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q1-se { --q-tx: var(--q-gap-d); --q-ty: var(--q-gap-d); }
  .q1-nw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q1-sw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: var(--q-gap-d); }
  .q1-n .abn-fig--quadrant__name { display: none; }
  .q1-n .abn-fig--quadrant__num { display: inline; }
  .abn-fig--quadrant .abn-fig--quadrant__legend.q1-n { display: flex; }
  .abn-fig--quadrant .abn-fig--quadrant__key.q1-n { display: block; }
}
@container abn-quadrant (19rem <= width < 25rem) {
  .q2-r { --q-tx: var(--q-gap-x); --q-ty: -50%; }
  .q2-l { --q-tx: calc(-100% - var(--q-gap-x)); --q-ty: -50%; }
  .q2-a { --q-tx: -50%; --q-ty: calc(-100% - var(--q-gap-y)); }
  .q2-b { --q-tx: -50%; --q-ty: var(--q-gap-y); }
  .q2-ne { --q-tx: var(--q-gap-d); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q2-se { --q-tx: var(--q-gap-d); --q-ty: var(--q-gap-d); }
  .q2-nw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q2-sw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: var(--q-gap-d); }
  .q2-n .abn-fig--quadrant__name { display: none; }
  .q2-n .abn-fig--quadrant__num { display: inline; }
  .abn-fig--quadrant .abn-fig--quadrant__legend.q2-n { display: flex; }
  .abn-fig--quadrant .abn-fig--quadrant__key.q2-n { display: block; }
}
@container abn-quadrant (25rem <= width < 34rem) {
  .abn-fig--quadrant__axes { aspect-ratio: 5 / 4; }
  .q3-r { --q-tx: var(--q-gap-x); --q-ty: -50%; }
  .q3-l { --q-tx: calc(-100% - var(--q-gap-x)); --q-ty: -50%; }
  .q3-a { --q-tx: -50%; --q-ty: calc(-100% - var(--q-gap-y)); }
  .q3-b { --q-tx: -50%; --q-ty: var(--q-gap-y); }
  .q3-ne { --q-tx: var(--q-gap-d); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q3-se { --q-tx: var(--q-gap-d); --q-ty: var(--q-gap-d); }
  .q3-nw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q3-sw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: var(--q-gap-d); }
  .q3-n .abn-fig--quadrant__name { display: none; }
  .q3-n .abn-fig--quadrant__num { display: inline; }
  .abn-fig--quadrant .abn-fig--quadrant__legend.q3-n { display: flex; }
  .abn-fig--quadrant .abn-fig--quadrant__key.q3-n { display: block; }
}
@container abn-quadrant (width >= 34rem) {
  .abn-fig--quadrant__axes { aspect-ratio: 4 / 3; --q-font: var(--text-sm); }
  .q4-r { --q-tx: var(--q-gap-x); --q-ty: -50%; }
  .q4-l { --q-tx: calc(-100% - var(--q-gap-x)); --q-ty: -50%; }
  .q4-a { --q-tx: -50%; --q-ty: calc(-100% - var(--q-gap-y)); }
  .q4-b { --q-tx: -50%; --q-ty: var(--q-gap-y); }
  .q4-ne { --q-tx: var(--q-gap-d); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q4-se { --q-tx: var(--q-gap-d); --q-ty: var(--q-gap-d); }
  .q4-nw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: calc(-100% - var(--q-gap-d)); }
  .q4-sw { --q-tx: calc(-100% - var(--q-gap-d)); --q-ty: var(--q-gap-d); }
  .q4-n .abn-fig--quadrant__name { display: none; }
  .q4-n .abn-fig--quadrant__num { display: inline; }
  .abn-fig--quadrant .abn-fig--quadrant__legend.q4-n { display: flex; }
  .abn-fig--quadrant .abn-fig--quadrant__key.q4-n { display: block; }
}
/* From tier 3 up the y title and the corner tab share one row. */
@container abn-quadrant (width >= 25rem) {
  .abn-fig--quadrant .abn-fig--quadrant__head.has-corner .abn-fig--quadrant__axis-title.is-y {
    grid-column: 2;
    padding-right: 1rem;
  }
  .abn-fig--quadrant .abn-fig--quadrant__head .abn-fig--quadrant__corner-label {
    grid-row: 1;
  }
}

/* The real data, for anyone `role="img"` just silenced. Clip-based hiding
   rather than `display: none` so a screen reader still finds it; 1px rather
   than 0 because some AT skips zero-size boxes.
   THIS IS A <div> WRAPPER AROUND THE <table>, NOT THE TABLE ITSELF. A table
   in automatic layout cannot be shrunk below its min-content width (CSS 2.1
   §17.5.2), so `width: 1px` on the `<table>` was a no-op — with `nowrap`
   cells the table's real box rendered as wide as its longest label, escaped
   `.abn-fig__frame`'s `overflow-x: auto`, and scrolled the WHOLE PAGE
   sideways on a phone. A `<div>` has no such minimum, so `overflow: hidden`
   here actually clips — verified at 380px with Playwright:
   `document.documentElement.scrollWidth === window.innerWidth`. */
.abn-fig--quadrant__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  padding: 0;
  margin: -1px;
}

/* ── range.css ───────────────────────────────────────────────────────── */
/*
 * `mode: range` — a dot/span plot: every row is a track on a shared 0..max
 * axis, positioned by cssVars() custom properties (--left, --width, --tick)
 * the renderer computes. TOKENS ONLY — no hex, no redeclared token; see
 * _base.css's header for why.
 *
 * Built entirely from <div>/<span> (no <table>, <ul>/<li>, or <p>) so it never
 * has to out-specify `.prose`'s global td/th/ol/li/p rules — see
 * src/styles/prose.css before reaching for a tag this file doesn't use.
 *
 * NARROW SCREENS: the label/track grid collapses to one column. The axis
 * ITSELF stays — the scale is the only quantitative anchor a sighted reader
 * has besides each row's own printed value, and the default 5 ticks (6
 * labels of at most a few mono characters) fit a ~300px track without
 * crowding. Only the now-redundant label-column spacer above the ticks is
 * hidden. A `ticks:` field above ~6 marks alternate ticks `.is-minor` in the
 * renderer (range.ts) so THOSE can drop here instead of the whole axis.
 */

.abn-fig--range .abn-fig-range__rows {
  display: flex;
  flex-direction: column;
}

.abn-fig-range__row {
  display: grid;
  grid-template-columns: minmax(9rem, 15rem) 1fr;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: var(--border-width) solid var(--color-line);
}
.abn-fig-range__row:last-child {
  border-bottom: 0;
}

.abn-fig-range__label {
  font-size: var(--text-sm);
  line-height: var(--lh-snug);
  color: var(--color-text);
}
.abn-fig-range__name {
  display: block;
}
.abn-fig-range__row.is-highlight .abn-fig-range__name {
  color: var(--color-accent-text);
  font-weight: var(--fw-semibold);
}
.abn-fig-range__value {
  display: block;
  margin-top: 2px;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* The track's faint gridlines repeat every `--tick` (set once, on .rows, and
   inherited by every track below it — one custom property, not one per row). */
.abn-fig-range__track {
  position: relative;
  height: 22px;
  background: repeating-linear-gradient(
    90deg,
    var(--color-line) 0 1px,
    transparent 1px var(--tick, 20%)
  );
}

/* Visually hidden, not display:none — a screen reader still finds it. Same
   clip-rect recipe as the rest of the site's sr-only text. */
.abn-fig-range__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.abn-fig-range__mark {
  position: absolute;
  left: var(--left, 0%);
}
.abn-fig-range__mark.is-dot {
  top: 4px;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  border-radius: var(--radius-pill);
  background: var(--color-text-muted);
  border: 2px solid var(--color-surface);
}
.abn-fig-range__mark.is-span {
  top: 6px;
  height: 10px;
  min-width: var(--space-2);
  width: var(--width, 0%);
  border-radius: var(--radius-pill);
  background: var(--color-text-muted);
}
.abn-fig-range__row.is-highlight .abn-fig-range__mark {
  background: var(--color-accent);
  outline: 3px solid var(--color-highlight);
  outline-offset: 2px;
}
.abn-fig-range__row.is-highlight .abn-fig-range__mark.is-dot {
  border-color: var(--color-surface);
}

.abn-fig-range__scale {
  display: grid;
  grid-template-columns: minmax(9rem, 15rem) 1fr;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-2);
}
.abn-fig-range__ticks {
  position: relative;
  height: 18px;
  font-family: var(--font-code);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.abn-fig-range__tick {
  position: absolute;
  left: var(--left, 0%);
  transform: translateX(-50%);
  white-space: nowrap;
}
/* The end labels anchor INWARD: the first grows right from 0%, the last
   grows left from 100%. Centred, each hung half its own width past the
   scale's edges — the last one ~7px past `.abn-fig-range__scale` at 380px,
   into the frame's padding or off a phone's edge. `:last-child` because the
   renderer marks only the first (range.ts is `is-first` only), and every
   child of `.abn-fig-range__ticks` is a tick. */
.abn-fig-range__tick.is-first {
  transform: none;
}
.abn-fig-range__tick:last-child:not(.is-first) {
  transform: translateX(-100%);
}

@media (max-width: 600px) {
  .abn-fig-range__row,
  .abn-fig-range__scale {
    grid-template-columns: 1fr;
  }
  .abn-fig-range__label {
    margin-bottom: var(--space-1);
  }
  .abn-fig-range__scale-spacer {
    display: none;
  }
  .abn-fig-range__tick.is-minor {
    display: none;
  }
  /* …except the axis end, which is `is-minor` whenever the tick count is
     odd: dropping it left the scale with no label at its own maximum. */
  .abn-fig-range__tick.is-minor:last-child {
    display: block;
  }
}

/* ── shot.css ────────────────────────────────────────────────────────── */
/*
 * shot.css — the "abn-shot" evidence-screenshot component: an <img> with
 * numbered, percentage-positioned callout boxes and an optional pinned tag
 * line. Ported from the ThinkRail research artifacts' `.shot`/`.hl`/`.tagline`
 * pattern (src/artifacts/research/thinkrail-review/) into a site component, so
 * a UI review or bug report gets the same annotated screenshot on a markdown
 * post — via the image card's `marks:`/`tag:` fields, buildImageCard in
 * src/lib/content.ts — and in a hand-written `html` artifact or report, which
 * writes this markup directly (no renderer; see figure-kit-components.md).
 *
 * TOKENS ONLY — see _base.css's header, same rule as every file in this
 * folder. `--x`/`--y`/`--w`/`--h` are bare 0–100 numbers with NO unit
 * appended, the same convention hast.ts's cssVars states: the `* 1%` lives in
 * the property below, not in the custom-property value.
 *
 * THE RING IS DELIBERATELY THEME-INDEPENDENT. A mark sits on top of an
 * arbitrary screenshot, not on the site's own chrome, so it cannot lean on a
 * token that flips with the page's light/dark theme — the pixel underneath it
 * is whatever colour the screenshot is. `--c-yellow-flag` plus a dark ring
 * just outside it and a light ring just inside it (both via color-mix, never a
 * literal hex) sandwich the yellow line so one edge or the other reads against
 * a light UI, a dark one, or a photo. The badge number is what actually
 * carries "there is a mark here" — colour is the flourish, never the only
 * signal — on the same reasoning quadrant.css's highlighted dot states.
 */

.abn-shot {
  position: relative;
  display: block;
  overflow: hidden;
  line-height: 0;
  border: var(--border-width) solid var(--color-line-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  /* Marks number themselves in document order via this counter, reset per
     shot so a second screenshot on the page starts back at ① rather than
     continuing the first one's count. */
  counter-reset: abn-shot-mark;
}
.abn-shot__img {
  display: block;
  width: 100%;
  height: auto;
}
/* `.prose img` (prose.css) is (0,1,1): a 1.5em top/bottom margin and a 60vh
 * `object-fit: contain` cap meant for a bare image sitting loose in an
 * article. Inside `.abn-shot` the image is edge-to-edge in a bordered,
 * overflow-hidden frame, so both rules are wrong here — the margin pushes
 * every mark high by roughly its own height (measured 25.5px at 380px), and
 * the height cap letterboxes a tall screenshot inside a wider box, throwing
 * marks off horizontally too. `.prose .abn-shot__img` is (0,2,0), which beats
 * (0,1,1) on the second component regardless of source order, so this wins
 * without `!important` and without depending on where either stylesheet is
 * imported. Zero margin, no height cap, and no border-radius of its own — the
 * rounding is the frame's (`.abn-shot`, `overflow: hidden`), not the image's;
 * a second radius on the image would show as an uneven inset gap at each
 * corner once the border is added. Verify inside `.prose` with a tall image,
 * at 380 and 1280. */
.prose .abn-shot__img {
  margin: 0;
  max-height: none;
  border-radius: 0;
}

/* One numbered callout box, positioned as a percentage of the image itself —
   NOT of .abn-shot, which is the same box before any letterboxing, but stated
   here because a figure with a `width`/`height` mismatch against the actual
   file would be the one case that drifts. */
.abn-shot__mark {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1%);
  width: calc(var(--w) * 1%);
  height: calc(var(--h) * 1%);
  border-radius: var(--radius-sm);
  box-shadow:
    0 0 0 2px var(--c-yellow-flag),
    0 0 0 3px color-mix(in srgb, var(--c-void) 65%, transparent),
    inset 0 0 0 1px color-mix(in srgb, var(--c-paper) 55%, transparent);
  pointer-events: none;
}
/* The badge: `content: counter(...)` produces the digit, so inserting,
   removing or reordering a mark in the comment never leaves a gap or a
   duplicate — the same device .abn-phases uses for its own numbering
   (kit.css). Sized in rem so it clears the 12px legibility floor at a true
   380px viewport regardless of how small a mark's own box is. */
.abn-shot__mark::before {
  counter-increment: abn-shot-mark;
  content: counter(abn-shot-mark);
  position: absolute;
  top: 2px;
  left: 2px;
  min-width: 1.35em;
  height: 1.35em;
  padding: 0 0.15em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--c-yellow-flag);
  color: var(--color-on-highlight);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  line-height: 1;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-void) 55%, transparent);
}

/* The tag line: one short label pinned to a corner, independent of any mark —
   a headline number, a state, a version. The reference's `.tagline` is
   hand-positioned per instance; this fixes it to the top-right corner
   instead, since a markdown comment has no coordinates to give it — TOP-RIGHT
   rather than top-left because a mark's own numbered badge (above) sits at
   the top-left of ITS box, and a mark drawn near the shot's own top-left
   corner would otherwise land its badge directly under the tag. */
.abn-shot__tag {
  position: absolute;
  right: 10px;
  top: 10px;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  line-height: 1;
  background: var(--c-yellow-flag);
  color: var(--color-on-highlight);
  padding: 4px 7px;
  border-radius: var(--radius-sm);
  pointer-events: none;
}

/* Size modifiers, for a shot that should not run the full column width — a
   phone screen or a small dialog box. Centred so the modifier does not strand
   the frame against the left edge of a wider container. */
.abn-shot--phone {
  max-width: 300px;
  margin-left: auto;
  margin-right: auto;
}
.abn-shot--narrow {
  max-width: 420px;
  margin-left: auto;
  margin-right: auto;
}

/* ── Pair — two shots side by side, each under its own label ──────────────
 * A labelled before/after: one column each, stacking to one on a phone. */
.abn-shot-pair {
  display: grid;
  gap: 14px;
}
@media (min-width: 640px) {
  .abn-shot-pair {
    grid-template-columns: 1fr 1fr;
  }
}
.abn-shot-pair__label {
  margin: 0 0 6px;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* ── Side — a shot beside a facts list ─────────────────────────────────────
 * A fixed-width shot column beside whatever prose or list sits next to it,
 * stacking to one column on a phone (same breakpoint as the pair above). */
.abn-shot-side {
  display: grid;
  gap: 20px;
  align-items: start;
}
@media (min-width: 640px) {
  .abn-shot-side {
    grid-template-columns: 300px 1fr;
  }
}

@media print {
  .abn-shot__mark,
  .abn-shot__mark::before,
  .abn-shot__tag {
    box-shadow: none;
  }
  .abn-shot__mark {
    border: 2px solid var(--c-void);
  }
}

/* ── timeline.css ────────────────────────────────────────────────────── */
/*
 * `mode: timeline` — a roadmap/gantt row per phase on a shared axis. See
 * src/lib/figures/timeline.ts for the markup this styles:
 *
 *   .abn-fig-timeline
 *     .abn-fig-timeline__head (aria-hidden)      one column per axis tick
 *       .abn-fig-timeline__spacer                lines up with the label column
 *       .abn-fig-timeline__track
 *         .abn-fig-timeline__hatch (optional)    beyond `plan-end`
 *         .abn-fig-timeline__tick × N
 *     ol.abn-fig-timeline__rows
 *       li.abn-fig-timeline__row × N
 *         .abn-fig-timeline__label               real text: phase + `unit a–b`
 *         .abn-fig-timeline__track
 *           .abn-fig-timeline__hatch (optional, aria-hidden)
 *           .abn-fig-timeline__bar (aria-hidden, omitted for a milestone row)
 *           .abn-fig-timeline__milestone (aria-hidden)
 *           .abn-fig-timeline__note (optional)   real text
 *
 * TOKENS ONLY (see _base.css's header comment) — this ships as part of
 * /figures.css, and a hex or a redeclared token is the drift the build guard
 * exists to catch. `color-mix()` against a token is fine.
 *
 * PERCENTAGE POSITIONING: every mark's `left`/`width`/`right` rides on a
 * `--tl-*` custom property the renderer computed (0–100, unitless) and this
 * file turns into a percentage with `calc(var(...) * 1%)` — never a literal
 * percentage in the markup, per the kit's one injection-safe pattern
 * (src/lib/figures/hast.ts's `cssVars`).
 *
 * SPECIFICITY: every selector below is scoped through the two classes
 * `.abn-fig--timeline .abn-fig-timeline__…`, (0,2,0) or higher, because
 * `.prose ol`/`.prose li` (src/styles/prose.css) sit at (0,1,1) — one class
 * alone (0,1,0/0,1,1) loses to that on the element-count tiebreak.
 */

.abn-fig--timeline .abn-fig-timeline {
  --tl-label-w: 9.5rem;
  min-width: 40rem; /* ~640px: the reference's own floor for an 18-tick axis to stay legible */
  font-family: var(--font-ui);
}

.abn-fig--timeline .abn-fig-timeline__head,
.abn-fig--timeline .abn-fig-timeline__row {
  display: grid;
  grid-template-columns: var(--tl-label-w) 1fr;
  column-gap: 1rem;
  align-items: center;
}

.abn-fig--timeline .abn-fig-timeline__head {
  padding-bottom: 0.6rem;
  margin-bottom: 0.4rem;
  border-bottom: var(--border-width) solid var(--color-line-strong);
}

.abn-fig--timeline .abn-fig-timeline__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: none; /* .prose ol caps at 68ch; a timeline is meant to run wide */
}

.abn-fig--timeline .abn-fig-timeline__row {
  margin: 0;
  padding: 0.7rem 0;
  border-top: var(--border-width) solid var(--color-line);
}
.abn-fig--timeline .abn-fig-timeline__row:first-child {
  border-top: 0;
}

.abn-fig--timeline .abn-fig-timeline__label {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  line-height: var(--lh-snug);
  padding-right: 0.75rem;
}
.abn-fig--timeline .abn-fig-timeline__sub {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-ui);
  font-weight: var(--fw-regular);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.abn-fig--timeline .abn-fig-timeline__track {
  position: relative;
  min-height: 2.75rem; /* room for a bar/diamond plus a note that wraps to two lines */
  /* Reserves room so a mark drawn exactly at 100% (the end milestone) doesn't
   * overhang the frame's own edge: percentages below are relative to this
   * box, which is now narrower than the grid column by that margin. */
  margin-right: 0.65rem;
}
.abn-fig--timeline .abn-fig-timeline__head .abn-fig-timeline__track {
  min-height: 1.1rem;
}
.abn-fig--timeline .abn-fig-timeline__track.has-below-note {
  min-height: 4.4rem;
}

.abn-fig--timeline .abn-fig-timeline__tick {
  position: absolute;
  top: 0;
  left: calc(var(--tl-left, 0) * 1%);
  padding-left: 0.4rem;
  border-left: var(--border-width) solid var(--color-line-strong);
  font-family: var(--font-code);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  white-space: nowrap;
}
/* The rightmost tick's label runs off whatever text it carries ("beyond
 * plan" is the long case) — anchor its BOX by the right edge instead of the
 * left, so the label grows leftward from the tick mark and never overflows
 * the frame's right edge no matter how long it is. The border flips sides
 * with it so the vertical mark still sits at the exact tick position. */
.abn-fig--timeline .abn-fig-timeline__tick.is-last {
  left: auto;
  right: calc(100% - (var(--tl-left, 0) * 1%));
  padding-left: 0;
  padding-right: 0.4rem;
  border-left: 0;
  border-right: var(--border-width) solid var(--color-line-strong);
  text-align: right;
  /* Its box can grow left over the previous tick when they sit close
   * together (a long "beyond plan" label on a tight tickStep) — an opaque
   * backdrop keeps the two labels from interleaving into unreadable text
   * instead of one legibly winning. */
  background: var(--color-surface);
  z-index: 1;
}

/* `plan-end` — the one boundary the figure cannot afford to lose. The
 * renderer already keeps a minimum gap between this tick and the final one
 * (counted from both labels, `tickLabelPct` in timeline.ts), so the two
 * shouldn't touch; this is the belt-and-braces backstop for the rare case
 * they still sit close — an opaque backdrop plus a z-index above
 * `.is-last`'s own (1) so `plan-end` wins the overlap instead of quietly
 * losing it. */
.abn-fig--timeline .abn-fig-timeline__tick.is-plan-end {
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  background: var(--color-surface);
  z-index: 2;
}

/* Ordinarily `plan-end` sits far enough left of the axis end that a normal
 * left-anchored box (growing right, like any interior tick) has room. When
 * the two ticks are so close together that even the bare final number's box
 * would collide, `plan-end` flips to grow LEFT instead — the same anchor
 * style as `.is-last` — so it only has to clear the final label's own width
 * rather than the sum of both labels' widths (timeline.ts's
 * `planEndEndAnchored`). */
.abn-fig--timeline .abn-fig-timeline__tick.is-plan-end.is-end-anchored {
  left: auto;
  right: calc(100% - (var(--tl-left, 0) * 1%));
  padding-left: 0;
  padding-right: 0.4rem;
  border-left: 0;
  border-right: var(--border-width) solid var(--color-line-strong);
  text-align: right;
}

/* The region past `plan-end`: a diagonal hatch in a muted line tone, never a
 * literal color — `color-mix` against the token keeps both themes' contrast
 * relationship intact without a second value to keep in sync. */
.abn-fig--timeline .abn-fig-timeline__hatch {
  position: absolute;
  inset: 0 0 0 calc(var(--tl-left, 100) * 1%);
  background-image: repeating-linear-gradient(
    135deg,
    transparent 0 6px,
    color-mix(in srgb, var(--color-line) 70%, transparent) 6px 7px
  );
  border-radius: var(--radius-md);
  z-index: 0;
}

.abn-fig--timeline .abn-fig-timeline__bar {
  position: absolute;
  top: 50%;
  left: calc(var(--tl-left, 0) * 1%);
  width: calc(var(--tl-width, 0) * 1%);
  height: 1.5rem;
  transform: translateY(-50%);
  border-radius: var(--radius-md);
  background: var(--color-accent);
  z-index: 1;
}
/* A row with a below-note grows its track to 4.4rem to hold a wrapped note
 * UNDER the bar (`.is-below`, fixed at top: 2.15rem) — so the bar and
 * milestone can no longer sit at the track's 50% (that centre only matches
 * the note's own top in a track this tall), or the note draws straight
 * through them. Pin both to a fixed top instead, high enough that the bar's
 * own bottom edge (top + half its 1.5rem height) clears the note's top with
 * a visible gap. */
.abn-fig--timeline .abn-fig-timeline__track.has-below-note .abn-fig-timeline__bar {
  top: 1.1rem;
}
/* A bar reaching past `plan-end`: outline rather than fill, so "committed"
 * and "beyond the plan" read apart even for a reader who can't see the hatch
 * (e.g. it scrolled past the frame's right edge). */
.abn-fig--timeline .abn-fig-timeline__bar.is-beyond {
  background: transparent;
  border: 2px dashed var(--color-accent);
}

.abn-fig--timeline .abn-fig-timeline__milestone {
  position: absolute;
  top: 50%;
  left: calc(var(--tl-left, 0) * 1%);
  width: 0.75rem;
  height: 0.75rem;
  background: var(--color-surface);
  border: 2.5px solid var(--color-accent);
  transform: translate(-50%, -50%) rotate(45deg);
  z-index: 2;
}
.abn-fig--timeline .abn-fig-timeline__milestone.is-beyond {
  border-style: dashed;
}
/* Same reasoning as the bar's own `.has-below-note` rule above: keep the
 * milestone diamond out of the below-note's line. */
.abn-fig--timeline .abn-fig-timeline__track.has-below-note .abn-fig-timeline__milestone {
  top: 1.1rem;
}

.abn-fig--timeline .abn-fig-timeline__note {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  max-width: 42ch;
  font-size: var(--text-xs);
  line-height: var(--lh-snug);
  color: var(--color-text-muted);
  z-index: 1;
}
.abn-fig--timeline .abn-fig-timeline__note.is-after {
  left: calc(var(--tl-left, 0) * 1%);
  /* Bounded on the right by `plan-end` (the hatch's left edge) for a bar
   * that ends inside the plan, or by the axis end for one already beyond
   * it. The renderer only puts a note here once it has counted that it
   * fits (timeline.ts, `noteLines`), so this edge is where the note WRAPS,
   * never where it is clipped. Unbounded, it ran on into the hatch and off
   * the track, where the frame cut it off. */
  right: calc(var(--tl-right, 0) * 1%);
  /* Fixed length, not a percentage of the track: a percentage gap shrinks
   * along with the frame on a narrow viewport until the diamond's tip
   * touches the note text. */
  padding-left: 0.9rem;
  text-align: left;
}
/* No room after the bar (it ends near the track's right edge): the note sits
 * BEFORE it instead, filling the space back to the track's own left edge and
 * right-aligning up against the bar — the reference's "Launch marketplace" row. */
.abn-fig--timeline .abn-fig-timeline__note.is-before {
  left: 0;
  right: calc(var(--tl-right, 0) * 1%);
  padding-right: 0.9rem;
  text-align: right;
}
/* Neither side has room: its own line under the bar and diamond, full track
 * width, left-aligned like ordinary body text. */
.abn-fig--timeline .abn-fig-timeline__note.is-below {
  position: absolute;
  top: 2.15rem;
  left: 0;
  right: 0;
  max-width: none;
  text-align: left;
  transform: none;
}

/* ── verdict.css ─────────────────────────────────────────────────────── */
/*
 * `mode: verdict` — src/lib/figures/verdict.ts. A categorical comparison
 * matrix: a real <table> of STATES (glyph + word, optional note), a legend of
 * the states used, and an optional derived tally of stacked bars.
 *
 * TOKENS ONLY, same rule as _base.css. Every selector is scoped under
 * `.abn-fig--verdict` so it clears `.prose ul`, `.prose li` and friends on
 * specificity rather than on bundle order.
 *
 * STATE FAMILIES. The renderer tags a cell, a legend swatch and a tally
 * segment with the SAME `abn-vd--<kind>` class, so one rule per kind paints
 * all three. The fills are allowed to sit close together (a partial and an
 * unknown cell are both pale) because the glyph and the word carry the
 * meaning; colour is never the only signal.
 *
 * INK. Words use --color-heading and notes --color-text on every fill.
 * test/figures-verdict.test.ts recomputes the two accent fills (34% and 12%
 * of --color-accent composited onto --color-surface) in both themes from the
 * literal token hexes: the weakest pairing, --color-text on the 34% fill in
 * light, is 6.08:1. --color-accent-text is NOT used on a fill: it drops to
 * 3.26:1 on the 34% fill in dark.
 *
 * LEGIBILITY FLOOR. Nothing here is below --text-xs (12px). The reference's
 * 10.6px state labels were a bug; the table scrolls sideways instead.
 *
 * `.prose code { font-size: 0.875em }` (specificity (0,1,1)) shrinks inline
 * code inside a note, a column header or a tally label below that floor —
 * 13px (--text-sm, a note's own size) x 0.875 measures 11.375px, and a
 * column header or tally label at --text-xs (12px) goes lower still. The
 * rule below scopes at (0,2,1) or higher, so it beats `.prose code`
 * regardless of stylesheet order, and floors the size at --text-xs rather
 * than fighting the em multiplier.
 */

/* ---- legend: only the states the table uses, in vocabulary order ---- */
.abn-fig--verdict .abn-vd__legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
  padding: 0;
  max-width: none;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text);
}
.abn-fig--verdict .abn-vd__legend li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  line-height: 1.3;
}
.abn-fig--verdict .abn-vd__sw {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 30px;
  height: 22px;
  border-radius: var(--radius-sm);
  border: var(--border-width-strong) solid transparent;
}

/* ---- the scroller: focusable so a keyboard can scroll it; positioned so
   the absolute .abn-vd__sr labels stay inside it instead of widening the page ---- */
.abn-fig--verdict .abn-vd__scroll {
  position: relative;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--radius-sm);
}

/* ---- the table ---- */
.abn-fig--verdict .abn-vd__table {
  width: 100%;
  /* --cols is the compared-column count, written by the renderer as a plain
     integer (cssVars). 12rem for the capability column, 9rem per compared
     column: room for a state word and a short note without the note
     wrapping one word to a line. Below that the frame scrolls. */
  min-width: calc(12rem + var(--cols, 1) * 9rem);
  margin: 0;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 3px;
  font-family: inherit;
  font-size: var(--text-sm);
}
.abn-fig--verdict .abn-vd__table thead th {
  padding: 0.3rem 0.55rem 0.45rem;
  border: 0;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  text-align: left;
  vertical-align: bottom;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.abn-fig--verdict .abn-vd__table thead th.abn-vd__corner {
  width: 12rem;
  padding-left: 0.2rem;
  color: var(--color-text-muted);
}
.abn-fig--verdict .abn-vd__table thead th.is-hl {
  color: var(--color-accent-text);
  font-weight: var(--fw-bold);
}

/* ---- the pinned first column. The box-shadow paints over the 3px border
   spacing to its right, so a scrolled cell never shows through the gap. ---- */
.abn-fig--verdict .abn-vd__table .abn-vd__corner,
.abn-fig--verdict .abn-vd__table .abn-vd__rowhead {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--color-surface);
  box-shadow: 3px 0 0 var(--color-surface);
}
.abn-fig--verdict .abn-vd__table .abn-vd__rowhead {
  padding: 0.55rem 0.75rem 0.55rem 0.2rem;
  border: 0;
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  text-align: left;
  vertical-align: top;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* ---- group rows: a caps kicker ruled underneath, its label pinned left
   while the table scrolls ---- */
.abn-fig--verdict .abn-vd__grp th {
  padding: 1.1rem 0 0.35rem;
  border: 0;
  border-bottom: var(--border-width) solid var(--color-line-strong);
  background: var(--color-surface);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-heading);
  text-align: left;
}
.abn-fig--verdict thead + tbody .abn-vd__grp th {
  padding-top: 0.35rem;
}
.abn-fig--verdict .abn-vd__gw {
  position: sticky;
  left: 0;
  display: inline-block;
  padding-left: 0.2rem;
}
.abn-fig--verdict .abn-vd__gn {
  margin-right: 0.45rem;
  color: var(--color-accent-text);
}

/* ---- state cells ---- */
.abn-fig--verdict .abn-vd__cell {
  padding: 0.5rem 0.55rem;
  border: var(--border-width-strong) solid transparent;
  border-radius: var(--radius-sm);
  vertical-align: top;
  line-height: 1.35;
  text-align: left;
}
.abn-fig--verdict .abn-vd__cx {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 7px;
}
.abn-fig--verdict .abn-vd__word {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-heading);
}
.abn-fig--verdict .abn-vd__note {
  flex-basis: 100%;
  font-size: var(--text-sm);
  color: var(--color-text);
  overflow-wrap: anywhere;
}
/* Beats `.prose code`'s (0,1,1) 0.875em rule on specificity, not bundle
   order: a note, a column header or a tally label carrying inline code
   would otherwise shrink below the 12px floor. */
.abn-fig--verdict .abn-vd__note code,
.abn-fig--verdict .abn-vd__table thead th code,
.abn-fig--verdict .abn-vd__tn code {
  font-size: max(0.875em, var(--text-xs));
}
.abn-fig--verdict .abn-vd__cell.is-hl {
  outline: 2px solid var(--color-accent);
  outline-offset: 0;
}

/* A blank ("does not apply") cell's screen-reader-only label. */
.abn-fig--verdict .abn-vd__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---- glyphs: drawn in CSS, aria-hidden (the word beside one says it) ---- */
.abn-fig--verdict .abn-vd__g {
  display: inline-block;
  flex: none;
  box-sizing: border-box;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 2px solid var(--color-heading);
  color: var(--color-heading);
  font-style: normal;
}
.abn-fig--verdict .abn-vd__g--full { background: var(--color-heading); }
.abn-fig--verdict .abn-vd__g--half { background: linear-gradient(90deg, var(--color-heading) 50%, transparent 50%); }
.abn-fig--verdict .abn-vd__g--dash { border-style: dashed; border-color: var(--color-accent-text); }
.abn-fig--verdict .abn-vd__g--bar { height: 3px; border: 0; border-radius: 2px; background: var(--color-heading); }
.abn-fig--verdict .abn-vd__g--arrow {
  width: 0;
  height: 0;
  border-radius: 0;
  border-width: 6px 0 6px 10px;
  border-color: transparent transparent transparent var(--color-heading);
}
.abn-fig--verdict .abn-vd__g--q {
  width: auto;
  height: auto;
  border: 0;
  border-radius: 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
  line-height: 1;
}
.abn-fig--verdict .abn-vd__g--q::before { content: "?"; }
.abn-fig--verdict .abn-vd__g--diamond {
  width: 10px;
  height: 10px;
  margin: 0 1px;
  border-radius: 1px;
  transform: rotate(45deg);
}
.abn-fig--verdict .abn-vd__g--square {
  width: 11px;
  height: 11px;
  border-radius: 1px;
  background: var(--color-heading);
}

/* ---- tally: one derived 100% stacked bar per column, counts in words ---- */
.abn-fig--verdict .abn-vd__tally {
  list-style: none;
  margin: var(--space-5) 0 0;
  padding: var(--space-4) 0 0;
  max-width: none;
  border-top: var(--border-width) solid var(--color-line);
}
.abn-fig--verdict .abn-vd__trow {
  display: grid;
  grid-template-columns: minmax(6rem, 9rem) 1fr;
  gap: 6px 14px;
  align-items: center;
  margin: 0 0 var(--space-3);
  line-height: 1.35;
}
.abn-fig--verdict .abn-vd__trow:last-child { margin-bottom: 0; }
.abn-fig--verdict .abn-vd__tn {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  color: var(--color-heading);
  overflow-wrap: anywhere;
}
.abn-fig--verdict .abn-vd__trow.is-hl .abn-vd__tn { color: var(--color-accent-text); }
.abn-fig--verdict .abn-vd__stack {
  display: flex;
  gap: 2px;
  height: 22px;
  border-radius: var(--radius-sm);
  overflow: hidden;
}
/* --w is the segment's share of the column's rows, a percentage the renderer
   computed. `flex-shrink` lets the 2px gaps come out of every segment in
   proportion instead of pushing the last one off the end. */
.abn-fig--verdict .abn-vd__seg {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  flex: 0 1 var(--w, 0%);
  min-width: 0;
  border: var(--border-width-strong) solid transparent;
  font-family: var(--font-code);
  font-size: var(--text-xs);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  color: var(--color-heading);
  overflow: hidden;
}
.abn-fig--verdict .abn-vd__trow.is-hl .abn-vd__stack {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}
.abn-fig--verdict .abn-vd__tt {
  grid-column: 2;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* ---- fills, one per kind (cell, legend swatch, tally segment). AFTER the
   three carriers' own rules: those set a transparent border at the same
   specificity, and the later rule wins. ---- */
.abn-fig--verdict .abn-vd--full { background: color-mix(in srgb, var(--color-accent) 34%, var(--color-surface)); }
.abn-fig--verdict .abn-vd--half { background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface)); }
.abn-fig--verdict .abn-vd--square { background: color-mix(in srgb, var(--color-accent) 22%, var(--color-surface)); }
.abn-fig--verdict .abn-vd--dash {
  background: var(--color-surface);
  border-color: var(--color-accent);
  border-style: dashed;
}
.abn-fig--verdict .abn-vd--arrow,
.abn-fig--verdict .abn-vd--ring {
  background: var(--color-surface);
  border-color: var(--color-line-strong);
}
.abn-fig--verdict .abn-vd--bar,
.abn-fig--verdict .abn-vd--diamond {
  background: var(--color-surface-sunken);
  border-color: var(--color-line);
}
.abn-fig--verdict .abn-vd--q {
  background: repeating-linear-gradient(135deg, var(--color-surface) 0 6px, var(--color-line) 6px 7px);
  border-color: var(--color-line);
}

/* ---- phone: the capability column gives up width, the tally stacks ---- */
@media (max-width: 560px) {
  .abn-fig--verdict .abn-vd__table { min-width: calc(7.5rem + var(--cols, 1) * 8rem); }
  .abn-fig--verdict .abn-vd__table thead th.abn-vd__corner { width: 7.5rem; }
  .abn-fig--verdict .abn-vd__table .abn-vd__rowhead { padding-right: 0.5rem; }
  .abn-fig--verdict .abn-vd__trow { grid-template-columns: 1fr; }
  .abn-fig--verdict .abn-vd__tt { grid-column: 1; }
}

@media print {
  .abn-fig--verdict .abn-vd__scroll { overflow: visible; }
  .abn-fig--verdict .abn-vd__table { min-width: 0; }
}

/* ── waffle.css ──────────────────────────────────────────────────────── */
/*
 * `mode: waffle` — 100 squares split by largest remainder, a legend beside
 * them (stacking under on narrow screens). See src/lib/figures/waffle.ts for
 * the markup this styles and the shade-ladder reasoning.
 *
 * TOKENS ONLY, same rule as _base.css — this ships as part of /figures.css,
 * next to /tokens.css, on pages that never load prose.css. Every selector is
 * scoped under `.abn-fig--waffle` at two classes deep or more so it beats the
 * `.prose ul/li/p` rules (each (0,1,1)) a figure sitting inside an article can
 * otherwise inherit from.
 */

.abn-fig--waffle .abn-fig-waffle__body {
  display: grid;
  gap: var(--space-6);
  min-width: 0;
}
@media (min-width: 46rem) {
  .abn-fig--waffle .abn-fig-waffle__body {
    grid-template-columns: 1.25fr 1fr;
    align-items: start;
  }
}

/* Kept at 20 columns down to phone width on purpose (AGENTS.md's lane brief):
   the squares shrink, the gap does not, and nothing in the grid carries text,
   so the 12px legibility floor never applies to it — only to the legend. */
.abn-fig-waffle__grid {
  display: grid;
  grid-template-columns: repeat(20, 1fr);
  gap: 3px;
  max-width: 27rem;
}
@media (min-width: 30rem) {
  .abn-fig-waffle__grid {
    gap: 4px;
  }
}
.abn-fig-waffle__cell {
  aspect-ratio: 1;
  border-radius: 2px;
  min-width: 0;
}

/* THE SHADE LADDER — five SOLID tiers per family, one per possible row (a
   family can hold every row: `highlight` naming all 5 labels, or naming none
   of the real labels and leaving all 5 neutral). See the long comment above
   ACCENT_SHADES in waffle.ts for the measured contrast and why the two
   FAMILIES are ACCENT = emphasis and NEUTRAL = recessive bulk, not the other
   way around.

   That "darkest mark in light, brightest in dark" framing describes tier-0
   (--color-accent) ALONE, comparing the two families — it is not a ranking
   that holds down the rest of the accent ladder. Tiers 1-4 trade brightness
   ordering for the thing that matters once every row needs its own tier: no
   two tiers within a family collide (§ladder derivation above), which is a
   PAIRWISE guarantee, not a monotonic one. So a highlighted row on tier 1+
   does not always read more prominent than the neutral bulk — e.g. tier-1 in
   dark (violet-900, 1.36:1 on the surface) sits BELOW neutral-0 (1.60:1), and
   tier-4 in light (violet-50, 1.10:1) is close to invisible against the
   surface on its own. "Accent" here means group membership — which legend
   swatch a cell matches — not a guaranteed emphasis rank past the first row
   of each family; the border below is what keeps every tier legible as a
   mark, independent of how it ranks against the other family.

   Every fill is a solid token/primitive or a `color-mix` between two fixed
   primitives (never `…, N%, transparent`, which blends toward whatever sits
   BEHIND it rather than a known colour — tiers 2 and 3 mix violet-900 and
   violet-50 against EACH OTHER, both known, so the result is still a
   deterministic flat colour). Tiers 1-4 also carry a `--color-text-muted`
   BORDER: two highlighted categories that were both on this ladder used to
   land within 1.2-1.4:1 of each other (the old 600/500/300/200/100 run — see
   waffle.ts), so this ladder trades "every tier independently clears 3:1
   against the surface" for "every PAIR of tiers clears 1.5:1 against each
   other" — violet-900 barely registers on the dark surface and violet-50
   barely registers on the light one, and the border is what keeps each of
   those two legible on its own, the same division of labour the neutral
   family below already uses. */
.abn-fig-waffle__cell.is-shade-0,
.abn-fig-waffle__swatch.is-shade-0 {
  background: var(--color-accent); /* 5.28:1 light / 5.54:1 dark */
}
.abn-fig-waffle__cell.is-shade-1,
.abn-fig-waffle__swatch.is-shade-1 {
  background: var(--c-violet-900); /* 13.29:1 light / 1.36:1 dark — bordered */
  border: 1px solid var(--color-text-muted);
}
.abn-fig-waffle__cell.is-shade-2,
.abn-fig-waffle__swatch.is-shade-2 {
  background: color-mix(in srgb, var(--c-violet-900) 80%, var(--c-violet-50)); /* 8.05:1 light / 2.24:1 dark — bordered */
  border: 1px solid var(--color-text-muted);
}
.abn-fig-waffle__cell.is-shade-3,
.abn-fig-waffle__swatch.is-shade-3 {
  background: color-mix(in srgb, var(--c-violet-900) 30%, var(--c-violet-50)); /* 2.05:1 light — bordered / 8.78:1 dark */
  border: 1px solid var(--color-text-muted);
}
.abn-fig-waffle__cell.is-shade-4,
.abn-fig-waffle__swatch.is-shade-4 {
  background: var(--c-violet-50); /* 1.10:1 light — bordered / 16.38:1 dark */
  border: 1px solid var(--color-text-muted);
}

/* NEUTRAL tiers are deliberately the RECESSIVE family: a light fill stepped
   toward the surface, plus a `--color-line-strong` BORDER on every tier so
   the mark still has a visible edge even where the fill itself measures
   under this kit's 3:1 mark floor (2.52:1 light / 1.60:1 dark for tier 0
   alone) — the edge, not the fill, carries per-mark legibility here, same as
   this kit's other bordered-but-pale UI chips. Stepped with `color-mix`
   toward `--color-surface` (a known, opaque colour, not `transparent`), so
   each step is a deterministic flat colour rather than an alpha blend. */
.abn-fig-waffle__cell.is-neutral-0,
.abn-fig-waffle__swatch.is-neutral-0 {
  background: var(--color-line-strong);
  border: 1px solid var(--color-line-strong);
}
.abn-fig-waffle__cell.is-neutral-1,
.abn-fig-waffle__swatch.is-neutral-1 {
  background: color-mix(in srgb, var(--color-line-strong) 72%, var(--color-surface));
  border: 1px solid var(--color-line-strong);
}
.abn-fig-waffle__cell.is-neutral-2,
.abn-fig-waffle__swatch.is-neutral-2 {
  background: color-mix(in srgb, var(--color-line-strong) 50%, var(--color-surface));
  border: 1px solid var(--color-line-strong);
}
.abn-fig-waffle__cell.is-neutral-3,
.abn-fig-waffle__swatch.is-neutral-3 {
  background: color-mix(in srgb, var(--color-line-strong) 32%, var(--color-surface));
  border: 1px solid var(--color-line-strong);
}
.abn-fig-waffle__cell.is-neutral-4,
.abn-fig-waffle__swatch.is-neutral-4 {
  background: color-mix(in srgb, var(--color-line-strong) 16%, var(--color-surface));
  border: 1px solid var(--color-line-strong);
}

.abn-fig-waffle__side {
  min-width: 0;
}

/* The computed "one square ≈ N" line, shown here ONLY when the author gave
   their own `title:` (otherwise it IS the figure's title, styled by
   `.abn-fig__title` in _base.css instead — see waffle.ts). Data-shaped, so
   the data font, not the chrome one. */
.abn-fig--waffle .abn-fig-waffle__unit {
  margin: 0 0 0.85rem;
  font-family: var(--font-code);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.abn-fig--waffle .abn-fig-waffle__legend {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: none;
}
.abn-fig--waffle .abn-fig-waffle__legend li {
  display: grid;
  grid-template-columns: 0.85rem 1fr auto;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  margin: 0;
  border-bottom: var(--border-width) solid var(--color-line);
}
.abn-fig--waffle .abn-fig-waffle__legend li:last-child {
  border-bottom: 0;
}
.abn-fig-waffle__swatch {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 2px;
  align-self: center;
}
.abn-fig-waffle__label {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--color-text);
  min-width: 0;
  overflow-wrap: anywhere;
}
.abn-fig-waffle__value {
  font-family: var(--font-code);
  font-size: var(--text-sm);
  color: var(--color-text);
  white-space: nowrap;
}
/* A row whose value rounds to fewer than one square: named so it never reads
   as dropped data, not just present in the printed value beside it. */
.abn-fig-waffle__note {
  grid-column: 2 / -1;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--color-text-faint);
}

/* ── callout.css (src/styles/callout.css) ──────────────────────── */
/* ---------------------------------------------------------------------------
   callout.css — the article callout family.

   One stylesheet, two producers:
     • `rehypeCallouts` (src/lib/content.ts) — Ghost/markdown `> [!NOTE]` alerts,
       injected with `set:html` where scoped styles can't reach.
     • `<Callout />` (src/components/Callout.astro) — the same anatomy for
       Astro/MDX-authored asides.
   Keeping the rules here is what lets those two stay one visual family, and it
   retires the third copy of this palette that the design audit flagged
   (.scratch/DESIGN_HANDOFF.md, item A.1).

   Anatomy (shared by the four TINTED variants): full rounded card → uppercase
   tracked-caps eyebrow label → body, and those four change colour only.

   That was once written as a rule for every variant, and it was never true —
   `pull` has always changed padding, font-size, weight, line-height and label
   weight, and carries no left rule at all. The honest rule is narrower: a
   variant may SUBTRACT anatomy only if it carries no fill of its own. That is
   exactly what makes both exceptions fail benignly — strip the stylesheet from
   an aside or a pull card and you get readable prose, not a broken card.

   Palette roles come straight from the canonical --callout-* tokens in
   tokens.css (light + dark, no per-theme override needed here):
     note  → violet    tip  → honey
     info  → cyan      warn → RED (red by intent; amber belongs to STATUS)
   `pull` is the outlier: a fixed brand-violet fill that reads identically in
   both themes, so its three --callout-pull-* tokens are declared in the LIGHT
   block only and have no dark override. It is now reachable from markdown as
   `> [!PULL]` and is emitted as a <blockquote> for the semantic reason (it is a
   quotation), so do not tidy its carrier back to a <div>.

   Each tinted variant also carries a left-rule accent bar using
   --accent-rule (tokens.css defines it as "the violet left-rule on
   callouts" but it had never actually been wired up until this pass — every
   -line/-label token pairing already existed). A top-fade gradient using
   -label at a flat 14% was prototyped alongside it and compared side by
   side against the flat tint, old vs new, per variant, in both themes —
   the left-rule alone read as the clearer signal; the gradient was judged
   an unnecessary addition and dropped.
   --------------------------------------------------------------------------- */

.callout {
  border-radius: var(--radius-xl);
  padding: 22px 26px;
  margin: 30px 0;
  font-size: 15px;
  line-height: 1.7;
  color: var(--color-text);
}

.callout__label {
  /* Was a literal 10.5px — under the 12px legibility floor on every post and,
     via figures.css, every html artifact. --text-xs is 12px (tokens.css);
     letter-spacing stays in em so the tracking scales with it, keeping the
     same visual proportion rather than a now-too-tight fixed value. */
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
}

.callout__body {
  margin: 10px 0 0;
}

/* That 10px assumes a label above. Both labelless variants (aside, and a pull
   card whose eyebrow is omitted — Callout.astro can already emit that shape
   today) otherwise get 38px above and 28px below, so this is a latent bug being
   fixed rather than one being introduced. (0,2,0) to beat `.callout__body`. */
.callout .callout__body:first-child {
  margin-top: 0;
}

/* The Ghost pipeline emits a <div class="callout__body"> whose children may be
   real <p>s (a multi-paragraph blockquote). Those inherit `.prose p` — a 1.5em
   bottom margin that leaves a gap inside the card, and a 68ch measure that lets
   the text run wider than the card's own padding-box.
   NOTE ON SPECIFICITY: `.prose p` and `.callout__body p` both score (0,2,0), so
   a plain override would be decided by whichever stylesheet the bundler emits
   last. These are written at (0,3,0) so they win outright and this file stays
   independent of BaseLayout's import order. Same reason for `.callout … code`
   below. */
.callout .callout__body > :last-child {
  margin-bottom: 0;
}
.callout .callout__body p,
.callout .callout__body ul,
.callout .callout__body ol {
  max-width: none;
}

/* pull quote — violet diagonal gradient, white text, flag-yellow label.
   Fixed brand colours (not --color-accent) — reads identically in both themes. */
/* The shorthand is SPLIT deliberately. `background:` would set colour and image
   in one declaration, so if Lightning CSS ever drops the gradient (the 5c13f18
   failure class) the card loses its fill entirely and white ink lands on the
   page ground — unreadable. Declaring the colour separately means the worst case
   is solid violet with legible white text. */
.callout--pull {
  background-color: #7902AA;
  background-image: var(--callout-pull-bg);
  padding: 28px 30px;
}
.callout--pull,
.callout--pull .callout__body {
  color: var(--callout-pull-ink);
}
.callout--pull .callout__label {
  color: var(--callout-pull-label);
  font-weight: 700;
}
.callout--pull .callout__body {
  font-size: 21px;
  font-weight: 600;
  line-height: 1.4;
}

/* aside — the THIRD prose register, and the one that had no home. Not the site
   flagging a severity (that is a callout) and not someone else speaking (that is
   a blockquote), but the author stepping sideways for a sentence. No fill, no
   eyebrow, a neutral rule.

   Written at (0,2,0) `.callout.callout--aside`, and its body descendants at
   (0,4,0), so nothing here depends on source order — not within this file, and
   not against prose.css, which BaseLayout.astro imports AFTER this one.

   NOT --color-line-strong: tokens.css files #A8A29E as non-text (lines and
   disabled states), and this rule is the component's entire non-typographic
   signal. --color-text-faint is the lightest AA-safe value and is defined in
   all three theme blocks. */
.callout.callout--aside {
  border-left: var(--accent-rule) solid var(--color-text-faint);
  /* The base card is 14px. A radius on a left-only rule curls it into two
     hooks; prose.css squares the blockquote's left edge for the same reason. */
  border-radius: 0;
  padding: 0 0 0 18px;
  margin: 1.5em 0;
  /* The base card is 15px and .prose is 17px. An aside sits IN the paragraph
     flow rather than beside it, so it takes the prose size — which is also what
     makes the 1.5em margin above resolve to the same rhythm as a paragraph. */
  font-size: inherit;
  color: var(--color-text-muted);
  background: none;
}

/* The rule above at :65 sets max-width:none on body children — correct for a
   card whose padding-box supplies the measure. An aside has NO padding-box, so
   without this it becomes the widest block on the page and its right edge
   overhangs every neighbouring paragraph. 68ch as a literal, matching .prose p. */
.callout.callout--aside .callout__body p,
.callout.callout--aside .callout__body ul,
.callout.callout--aside .callout__body ol {
  max-width: 68ch;
}

/* note — violet, soft tint */
.callout--note {
  background: var(--callout-note-bg);
  border: 1px solid var(--callout-note-line);
  border-left: var(--accent-rule) solid var(--callout-note-label);
}
.callout--note .callout__label {
  color: var(--callout-note-label);
}

/* tip — honey, soft tint */
.callout--tip {
  background: var(--callout-tip-bg);
  border: 1px solid var(--callout-tip-line);
  border-left: var(--accent-rule) solid var(--callout-tip-label);
}
.callout--tip .callout__label {
  color: var(--callout-tip-label);
}

/* info — cyan, soft tint */
.callout--info {
  background: var(--callout-info-bg);
  border: 1px solid var(--callout-info-line);
  border-left: var(--accent-rule) solid var(--callout-info-label);
}
.callout--info .callout__label {
  color: var(--callout-info-label);
}

/* warning — red, soft tint */
.callout--warning {
  background: var(--callout-warn-bg);
  border: 1px solid var(--callout-warn-line);
  border-left: var(--accent-rule) solid var(--callout-warn-label);
}
.callout--warning .callout__label {
  color: var(--callout-warn-label);
}

/* Inline code inside a callout: `.prose code` paints a violet-on-violet chip,
   which fights every variant that isn't `note` (violet code on a red warning
   card especially). Drop to a neutral translucent chip tinted by the body text
   colour, so it reads correctly on all five backgrounds and in both themes. */
.callout .callout__body code {
  background: color-mix(in srgb, currentColor 10%, transparent);
  color: inherit;
}
.callout--pull .callout__body code {
  background: rgba(255,255,255,0.16);
}

/* An article block (code, terminal, diagram, table) nested in a callout keeps
   its own card chrome — just tighten the outer margins so it doesn't blow the
   callout's padding out. */
.callout .codeblock,
.callout .term,
.callout .mmd,
.callout .abn-tbl {
  margin: 14px 0;
}
