/* blocks.css — AUTHORED. Never generated, never overwritten by a build.
 *
 * S5, layer 3: one rule set per block name in the vocabulary
 * (plans/done/block-vocabulary.md). Keyed entirely on `[data-block="…"]`, which is
 * the whole interface between the generator and the look — the generator picks
 * which blocks a page carries and in what order; this file decides what each
 * one looks like, and knows nothing about which pages exist.
 *
 * Checked by tools/check-block-css.py:
 *   C7  every block in the vocabulary has at least one rule here
 *   C8  no rule is keyed to a block-shaped name outside the vocabulary
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * ⚠️ NINE BLOCKS LEFT THIS FILE ON 2026-09-11, and the reason is not tidying.
 * Under the authoring surface (plans/done/authoring-surface.md) a page holds prose,
 * authored in a markdown file, and tables, generated by a marker. EVERY
 * REMAINING BLOCK IS A TABLE. Prose has no `[data-block]` wrapper any more
 * because prose needs no wrapper: it is the page.
 *
 * THE ONE DECISION THIS FILE MAKES: not everything is a card.
 *
 * The elevation recipe (--ds-surface-card) is the most distinctive thing in
 * the identity, and the temptation is to put every block on one. That would be
 * wrong here, and not as a matter of taste: a card on every block is a card on
 * nothing — it stops being emphasis and becomes the page background.
 *
 * So a card means INTERRUPTION, and NOTHING IN THIS FILE EARNS ONE. That is
 * not an oversight and it is worth stating, because the sentence above reads
 * like a preamble to a list.
 *
 * ⚠️ WHAT WAS REJECTED WAS "EVERYTHING IS A CARD", NOT CARDS. The proposal this
 * paragraph answers put the whole site's content on cards; we declined that and
 * nothing more. This file is keyed entirely on `[data-block="…"]`, every block
 * in the vocabulary is a TABLE, and a table is the document rather than an
 * interruption in it — so the scope of "nothing earns one" is blocks, and it
 * does not reach past this file. The authored CARD FENCE (`:::card{…}`, see
 * project/plans/done/build-the-card-fence.md) is prose the author marked for
 * emphasis, it carries no `data-block`, and its rules live in identity.css
 * beside `img.shot` with the other authored-prose devices. Do not read this
 * header as a reason not to build it.
 *
 * `--ds-surface-card` has zero consumers here. The
 * last name on the list was the provenance stamp, which was deleted on
 * 2026-09-11 when the four trailing bands collapsed into one footer line — and
 * even that never set a card: its rule was a border, a size and a colour, so
 * the list had been empty for longer than it said.
 *
 * The two blocks that used to carry the other cards were `authored-prose` and
 * `gap-notice`, and BOTH ARE GONE RATHER THAN RESTYLED. Authored prose is now
 * the page's own body, so a card around it would be a card around everything;
 * a gap is now an empty cell in the table above, linked to /gaps.md, so its
 * card would be a box drawn around a dash. Everything else — every table, the
 * heading, the footer — is the document itself and stays flat on the page,
 * separated by rules and space rather than by boxes.
 *
 * ⚠️ THE CARD IS SQUARE, and that is a house rule rather than a local taste.
 * A rounded box with a colour bar down one side is banned (Joseph, 2026-09-11,
 * the no-slop pass): pick a colour on all four sides, or pick no radius.
 * `--ds-radius-card` therefore has TWO consumers, and both qualify under the
 * same clause: the `live-preview` figure and the authored picture
 * (`img.shot`, identity.css), each a uniform hairline around a light ground
 * carrying no colour at all.
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * NARROW PANES. plans/done/human-site-structure.md hands S5 a stated requirement:
 * the tables are five to seven columns wide and this site is read in a ~450px
 * side pane while the reader is doing something else. Every wide table stacks
 * into labelled rows below 46rem, using the `data-label` that render-html.py
 * puts on each cell. Columns are never dropped — see the note in `td()` for why
 * a page that silently sheds a column is a page that lies at one window size.
 */

/* ═══ 0. the page's own prose ══════════════════════════════════════════════ */
/* Not a block. This is what authored markdown renders as, and it is most of
 * the site now. It sets a measure and nothing else: a paragraph is a paragraph.
 *
 * ⚠️ THE LEDE RULE WAS HERE AND IS DELETED (Joseph, 2026-09-11). It was
 * `main > p:first-of-type` at H4 in `--content-secondary`, promoting the first
 * paragraph of every page to a deck. Two faults, and the second is the one that
 * matters:
 *
 *   1. It picked by POSITION, so it styled whatever paragraph happened to be
 *      first. Measured over the 958 built pages: 645 lookup stat lines
 *      ("One token. color / vendor / vendor."), 115 "no guidance is written"
 *      apologies promoted to the loudest prose on their page, 30 class-root
 *      stat lines, one body paragraph sitting under an `h2`, and 124 pages
 *      where it never fired at all because their prose is inside a
 *      `<section data-block>` and so is not a child of `main`. It landed on an
 *      authored summary sentence on 37 pages — 4%.
 *   2. THE SOURCE IS MARKDOWN AND ALREADY HAS A WAY TO SAY "heading": `##`.
 *      A rule that infers emphasis from ordinal position is a second, invisible
 *      authoring surface that no author can see, opt into, or opt out of.
 *
 * It was also an unnamed fifth treatment — H4 size, `--content-secondary`, and
 * a hand-typed `line-height: 1.45` that is on no scale — sitting one weight away
 * from the `subhead` role. That is the same hole the four roles in identity.css
 * were minted to close. If a deck is ever wanted back, it is a ROLE the emitter
 * applies, not a paragraph's index. */
main > p, main > ul, main > ol, main > blockquote { max-width: var(--ds-measure); }

/* The footer is authored once, in content/site/footer.md, and appears on every
 * page. It is recognised by the file it came from rather than by a marker —
 * per-page chrome is not per-page content, and it does not need a block name
 * to say so. */
main > footer {
  margin-top: 3em;
  padding-top: 1.2em;
  border-top: 1px solid var(--ds-rule);
  font-size: var(--ds-caption);
  /* 1.45, not the 1.6 that was hand-typed here. The footer is three or four
   * wrapped lines of Caption on all 958 pages, so it is the site's largest
   * single sample of that step -- if Caption's leading is wrong, this is where
   * to change Caption's leading, not where to write a local one. */
  line-height: var(--ds-lh-caption);
  color: var(--content-subtle);
}
main > footer em { font-style: normal; }

/* The one link on a page that does something other than navigate the site: it
 * opens the file this page's prose lives in, ready to edit, or creates it with
 * the frontmatter already filled in. It is styled as an action rather than as
 * a reference, because converting "why is this empty" into a pull request is
 * the single thing this site most wants a reader to do.
 *
 * It appears in two places and inherits the size of whichever it is in: in the
 * footer line, where every page names its own authored file, and at body size
 * inside a generated page that has nothing written about it yet. */
a.authored-file {
  text-decoration: none;
  border-bottom: 1px solid var(--action-rest);
}
a.authored-file code {
  color: var(--action-rest);
  font-size: 1em;
}
a.authored-file:hover code { color: var(--action-hover); }

/* ⚠️ A SECOND RULE STOOD HERE, keyed to a provenance-stamp block, and it was
 * the footer rule above written again with different numbers: 4em rather than
 * 3em, 1.5em rather than 1.2em, and its own border-top in the same colour 4em
 * below this one. Two horizontal rules closing every page, three type sizes
 * between them, 48-98 words of stamp under 40 words of footer. That is the
 * drift check-heading-roles.py exists to stop in headings, shipped in the
 * chrome instead.
 *
 * The stamp's facts — repo, sha, and the date we read it — ride in the footer
 * line now, so there is ONE rule and ONE line below the content. The name is
 * out of the vocabulary in tools/check-block-vocabulary.py and the renderer no
 * longer emits it. See project/plans/done/the-file-owns-the-whole-page.md before
 * putting anything back down here. */

/* ═══ 1. fact-table ════════════════════════════════════════════════════════ */
/* One row per join axis, label left and value right. Two columns, so it does
 * not need the stacking treatment — it needs its label column pinned narrow so
 * the values line up into a scannable second column. */
[data-block="fact-table"] table { font-size: var(--ds-body); }
[data-block="fact-table"] thead:has(th:empty) { display: none; }
[data-block="fact-table"] td:first-child {
  width: 16rem;
  font-family: var(--ds-font-body);
  color: var(--content-subtle);
}
[data-block="fact-table"] td:last-child { color: var(--content-default); }

/* ═══ 2. value-table ═══════════════════════════════════════════════════════ */
/* The token table: name, light, dark, verdict, chain. The identifier is the
 * column a reader scans, so it holds and the rest recedes. */
[data-block="value-table"] td:first-child { white-space: nowrap; }
[data-block="value-table"] td:nth-child(2),
[data-block="value-table"] td:nth-child(3) { color: var(--content-default); }
[data-block="value-table"] td:last-child {
  /* The resolution chain is the answer to "why does this name point there",
   * so it is content: Body. */
  font-size: var(--ds-body);
  color: var(--content-subtle);
}

/* ═══ 3. value-specimen ════════════════════════════════════════════════════ */
/* Render the value itself. HTML-only — it is the block markdown cannot do, and
 * the reason this layer exists at all. Layout is in structure.css; this is the
 * chrome. The border is always the SITE's, never the token's: a specimen must
 * not be able to disguise its own boundary.
 *
 * ⚠️ THE BLOCK IS A LEGEND NOW, NOT A GALLERY. Until 2026-09-11 it was a grid
 * of chips above the value table — the same rows, in the same order, with the
 * names printed twice — so a reader putting a shade next to its hex matched
 * 192 rows by eye. The chips moved into the table's own `light` and `dark`
 * cells (`.sp-cell`), and what is left under this name is prose: one note per
 * table, naming what that table could not draw. That prose is the part that
 * cannot move: *"no specimen rendered" must never be indistinguishable from
 * "nobody built one"*.
 *
 * ⚠️ SO THE BLOCK APPEARS MORE THAN ONCE PER PAGE, and a per-table note sits
 * INSIDE its `value-table` section. Until 2026-09-13 every absence was gathered
 * into the one page-level paragraph, which on `/tokens/other.html` — fifteen
 * dimensions on one route — came out as a nine-clause semicolon run under the
 * `size` heading, reading as a rendering failure. Nothing else nests a block
 * inside a block; this one does because the note is about the rows beneath it.
 * The `.specimens` list below is still reached, by the dimensions whose
 * drawing IS the value's size — a `font-size` sample is 128px when the token
 * is 8rem, and no table column can hold that. */
[data-block="value-specimen"] { margin-bottom: 1em; }
[data-block="value-specimen"] > p {
  font-size: var(--ds-caption);
  color: var(--content-subtle);
}
.sp { border: 1px solid var(--ds-edge); border-radius: 2px; }
.sp-swatch { border-radius: 4px; }
/* Alpha and near-black values need something behind them or they read as empty.
 * A checkerboard is the honest ground: it says "this value is see-through". */
.sp-alpha, .sp-swatch {
  background-image:
    linear-gradient(45deg, #2c333d 25%, transparent 25%, transparent 75%, #2c333d 75%),
    linear-gradient(45deg, #2c333d 25%, transparent 25%, transparent 75%, #2c333d 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
}
.sp-swatch[style*="background"] { background-repeat: no-repeat; }
.sp-alpha { background-color: var(--content-default); }
.sp-corner { background: var(--ds-fill-subtle); border-color: var(--content-secondary); }
/* ⚠️ A SHADOW CHIP USED TO BE STYLED HERE and is not drawn at all any more
 * (2026-09-13). It needed two things a list row could not give it at once: a
 * ground of its own, because the light values resolve to rgba(26,26,26,.1) and
 * vanish on this dark page, and room OUTSIDE the chip, because a shadow is
 * painted outside the box that casts it — `--box-shadow-2xl` throws 50px of
 * blur off a 1.6rem square. `.sp-art` clips that overflow, so the drawing was
 * the clipped corner of a grey patch. The `shadow` table keeps every resolved
 * value; see NO_SPECIMEN_WHY in render-html.py, which now says so on the page. */
.sp-rule { border-top-color: var(--content-secondary); border-left: 0; border-right: 0;
           border-bottom: 0; border-radius: 0; }
/* A length bar is data, not interaction, so it is neutral. */
.sp-length { background: var(--content-secondary); border-color: transparent;
             border-radius: 1px; }
.sp-type, .sp-leading { border: 0; color: var(--content-default); background: none; }
.specimens code { font-size: var(--ds-body); background: none; padding: 0;
                  color: var(--content-default); }
.sp-val { font-family: var(--ds-font-mono); font-size: var(--ds-caption);
          color: var(--content-subtle);
          /* A resolved shadow or font stack is a paragraph, not a label. */
          display: -webkit-box; -webkit-line-clamp: 2;
          -webkit-box-orient: vertical; overflow: hidden; }
/* Caption, and it moved here from structure.css on 2026-09-11, where it was
 * `0.7em` -- 11.2px against a 13px floor. A label ABOUT content is exactly what
 * Caption is for, so the step was never in doubt; the size was simply set in
 * the one file with no type in it. */
.sp-side { font-family: var(--ds-font-mono); font-size: var(--ds-caption);
           color: var(--content-subtle);
           text-transform: uppercase; letter-spacing: var(--ds-track-head); }

/* ═══ 4. live-preview ══════════════════════════════════════════════════════ */
/* Two bounded figures, per plans/done/human-site-structure.md §2 — a Figma contact
 * sheet we downloaded and re-host, and a Storybook story in an iframe.
 * Containment lives in structure.css; everything here is look.
 *
 * ⚠️ THE FRAMES ARE PAINTED LIGHT, ON A DARK SITE, AND THAT IS NOT A MISTAKE.
 * Both artifacts are renderings of the product's LIGHT theme: the Figma sheet is
 * a PNG on a white artboard, and Chromatic's iframe paints its own default
 * ground. Dropping either onto `--ds-ground` gives a white rectangle floating in
 * near-black with a torn edge where the artboard stops. So the frame states the
 * light ground deliberately, the way `value-specimen` paints its light-mode
 * shadow chips light — a specimen is shown against the ground it was authored
 * for, or it is not being shown.
 *
 * ⚠️ NO card gradient and no `--ds-surface-card` here, and the reason is local
 * rather than a site-wide ban: these two figures already interrupt by being
 * pictures on a page of tables. Two interruption signals on one block is one
 * too many. (The authored card fence does earn the recipe — see the header.) */
/* No heading rule: the block carries no heading of its own -- it sits under the
 * page's `Where it exists` heading, beside the fact table. */
[data-block="live-preview"] { margin: 2em 0; }

[data-block="live-preview"] figure {
  margin: 1.5em 0;
  border: 1px solid var(--ds-edge);
  border-radius: var(--ds-radius-card);
  overflow: hidden;                 /* clip the artwork to the rounded corner */
  background: var(--ds-paper);
}
[data-block="live-preview"] .lp-frame,
[data-block="live-preview"] iframe { background: #ffffff; }

[data-block="live-preview"] figcaption {
  font-size: var(--ds-caption);
  line-height: var(--ds-lh-caption);
  color: var(--content-secondary);
  padding: 0.7em 0.9em;
  margin: 0;
  border-top: 1px solid var(--ds-rule);
}
[data-block="live-preview"] figcaption strong {
  color: var(--content-default);
  font-weight: var(--ds-weight-medium);
}
/* The caption is the degradation path the vocabulary requires: if Chromatic is
 * down the iframe is an empty box, and the story id and the link beside it are
 * what keep the page answering the question. So they are content, not chrome. */
[data-block="live-preview"] figcaption code { color: var(--content-default); }

/* ═══ 5. tally-table ═══════════════════════════════════════════════════════ */
/* Counts. The number is the content, so it is the only thing set at the
 * brightest step, and
 * the count column is right-aligned with tabular figures — a column of counts
 * that does not line up on its digits is not worth having as a column. */
[data-block="tally-table"] td:last-child,
[data-block="tally-table"] th:last-child { text-align: right; }
[data-block="tally-table"] td:last-child {
  color: var(--content-default);
  font-variant-numeric: tabular-nums;
  width: 6rem;
}
[data-block="tally-table"] td:first-child { color: var(--content-secondary); }
/* The partition-or-overlap slot is mandatory on this block and says whether the
 * rows sum. A reader who misses it adds up overlapping rows and gets a number
 * that means nothing, so it is set as a statement, not a footnote. */
[data-block="tally-table"][data-slot="partition-or-overlap"] p {
  font-size: var(--ds-caption);
  color: var(--content-secondary);
}

/* ═══ 6. member-list ══════════════════════════════════════════════════════ */
/* Who the members behind a count actually are. These run long and are scanned,
 * not read, so they go multi-column and stay dense. */
[data-block="member-list"] ul {
  columns: 16rem auto;
  column-gap: 2em;
  list-style: none;
  padding: 0;
}
[data-block="member-list"] li {
  break-inside: avoid;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-body);
  color: var(--content-secondary);
}


/* ═══ 7. row-index ════════════════════════════════════════════════════════ */
/* The inventory: 124 rows, seven columns, five of them a single axis mark.
 * Those five are centred so the row reads as a pattern of presence and absence
 * rather than as seven words, which is the only way a 124-row table is
 * scannable at all. */
[data-block="row-index"] td:first-child { white-space: nowrap; }
[data-block="row-index"] td:nth-child(n+4):nth-child(-n+6) { text-align: center; }
[data-block="row-index"] td:last-child {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
[data-block="row-index"] th:nth-child(n+4):nth-child(-n+6) { text-align: center; }
[data-block="row-index"] th:last-child { text-align: right; }

/* ═══ 8. comparison-table ═════════════════════════════════════════════════ */
/* Where Figma and the CSS disagree. Both value columns are set at the
 * brightest step and side
 * by side, because the entire content of the block is a difference between two
 * strings and the reader's job is to spot it. */
[data-block="comparison-table"] td:nth-child(3),
[data-block="comparison-table"] td:nth-child(4) {
  color: var(--content-default);
  white-space: nowrap;
}
[data-block="comparison-table"] td:first-child { white-space: nowrap; }

/* ═══ slots: none ═════════════════════════════════════════════════════════ */
/* There were two, `legend` and `partition-or-overlap`. Both were named regions
 * of the table above them, and both are ordinary authored prose now — in the
 * page class's template, sitting where they always sat. A slot existed because
 * prose emitted from Python needed a name in order to be checked; prose in a
 * markdown file needs no name, because a person can see it. */

/* ═══ narrow panes ═════════════════════════════════════════════════════════ */
/* The requirement inherited from plans/done/human-site-structure.md: this site is
 * read in a ~450px side pane, mouse and keyboard, while the reader is doing
 * something else. A five-column table read through a horizontal scrollbar is
 * not readable, and `.scroll` only stops it breaking the page.
 *
 * So below 46rem the wide tables stop being tables and become one labelled
 * stack per row, using the `data-label` render-html.py puts on every cell.
 * Nothing is hidden: every column that existed at full width is still on the
 * page, with its heading attached to its value. */
@media (max-width: 46rem) {
  [data-block="value-table"] .scroll,
  [data-block="row-index"] .scroll,
  [data-block="comparison-table"] .scroll{ overflow-x: visible; }

  [data-block="value-table"] table, [data-block="value-table"] tbody,
  [data-block="row-index"] table, [data-block="row-index"] tbody,
  [data-block="comparison-table"] table, [data-block="comparison-table"] tbody{
    display: block;
    width: 100%;
  }
  /* The header row is the one thing that DOES go: its content is repeated onto
   * every cell by data-label, so keeping it would print each column name twice.
   * This is the only place a column leaves the page, and it leaves because it
   * has been duplicated, not because it was dropped. */
  [data-block="value-table"] thead, [data-block="row-index"] thead,
  [data-block="comparison-table"] thead{ display: none; }

  [data-block="value-table"] tr, [data-block="row-index"] tr,
  [data-block="comparison-table"] tr{
    display: block;
    padding: 0.6em 0;
    border-bottom: 1px solid var(--ds-rule-strong);
  }
  /* ⚠️ `display: block`, NOT grid or flex, and this is load-bearing. A cell's
   * content is mixed inline markup -- `<em>declared directly</em>` followed by
   * a bare text node `(variables.css:74)`. Grid and flex both promote every
   * child to an item, so the element and the text become two items and the
   * text lands in the LABEL column, one row down. Measured, not theorised.
   * A wrapper element in the renderer would fix it too, at roughly 1.5 MB of
   * extra markup across the site, for a two-column layout in a 450px pane. */
  [data-block="value-table"] td, [data-block="row-index"] td,
  [data-block="comparison-table"] td{
    display: block;
    border: 0;
    padding: 0.2em 0;
    text-align: left !important;
    white-space: normal;
  }
  [data-block="value-table"] td[data-label]::before,
  [data-block="row-index"] td[data-label]::before,
  [data-block="comparison-table"] td[data-label]::before{
    content: attr(data-label);
    display: block;
    font-family: var(--ds-font-mono);
    font-size: var(--ds-caption);
    /* was 1.4, for no reason anyone recorded and 0.05 from the step's own */
    line-height: var(--ds-lh-caption);
    text-transform: uppercase;
    letter-spacing: var(--ds-track-head);
    color: var(--content-subtle);
  }
  /* The identifier leads the stack: it is the row's name, not one of its
   * fields, so it spans both columns and drops its label. */
  [data-block="value-table"] td:first-child,
  [data-block="row-index"] td:first-child,
  [data-block="comparison-table"] td:first-child {
    display: block;
    margin-bottom: 0.3em;
  }
  [data-block="value-table"] td:first-child::before,
  [data-block="row-index"] td:first-child::before,
  [data-block="comparison-table"] td:first-child::before { content: none; }

  /* fact-table is only two columns, so it does not stack — it just gives up the
   * fixed label width that was making its values line up. */
  [data-block="fact-table"] td:first-child { width: 40%; }

  [data-block="member-list"] ul { columns: 1; }
}

/* A short window is the other axis, and it is the one that gets forgotten:
 * terminal-above-browser-below is as common as side-by-side. The only thing on
 * this site that can eat a whole short viewport is a figure sized in dvh, and
 * both of those now live in `live-preview` — so the short-viewport bound moved
 * to structure.css, beside the dvh values it has to stay in step with. It is
 * containment, not look, and splitting the two across the seam is how one of
 * them gets changed alone. Nothing authored belongs here; don't refill it. */
