/* ─────────────────────────────────────────────────────────────
   ARIA SGAI Notebook — shared stylesheet
   Loaded by every page. Ported from the design playground
   (seaofclouds/aria-sgai-blog). Set in PT Serif, PT Sans, and
   PT Mono. Black on white, no dark mode, no colour.

   What lives here is everything not specific to long-form: the
   token system (:root, and the derived colours on html, body),
   the page frame, the masthead, the colophon, hover previews,
   code blocks, and print. Anything a note or an index page could
   want as much as an essay does.

   The essay's own layout — title block, contents, the rail and the
   rule a heading draws, and the marginalia — is in essay.css, which
   loads after this one.

   In order, and the order is the index — every region carries a
   ── banner, and nothing is declared in two of them:

       Faces               Hover preview
       Tokens              Index
       Document            Error page
       Label               Colophon
       Links               Tables
       Page frame          Callouts and quotes
       Environment banner  Breaks
       Masthead            Figures
                           Maths
                           Code
                           Print

   Print sits last because it is the final word on everything
   above it; it used to sit in the middle, before Code, where it
   could not be seen from.

   The playground's type-trial and colour-trial machinery has been
   dropped: it was driven by localStorage written by that project's
   working document, which doesn't exist here. What's left is the
   settled default those trials landed on.
   ───────────────────────────────────────────────────────────── */

/* ── Faces ───────────────────────────────────────────────────────
   Self-hosted, and subsetted to the characters the site actually uses. The build
   collects those from every page it renders, writes them to
   template/static/fonts/chars.txt, and runs hb-subset and woff2_compress over the
   TTFs in fonts/ — 2.5MB of source becomes about 168K of woff2. The .woff2 files
   are build output and are gitignored; fonts/ is the source, and the OFL licences
   travel with them.

   These replace three <link>s to fonts.googleapis.com in head-stuff.html. Same
   files: the vertical metrics of the Google-served faces and of the OFL originals
   are identical, which matters because --font-serif-ascent, --font-serif-descent
   and --font-cap-height below are read off them and the drop cap and the
   marginalia rhythm are derived from those. Measured before the swap: all five
   metric tokens match the TTFs to three decimal places.

   Declared here rather than in essay.css because every page needs the faces, not
   only an essay — the masthead is set in PT Sans. That is the same reason the
   <link>s lived in the shared head include.

   No bold-italic. Google was not serving one either, so the browser has always
   synthesised it; adding one is a change to how the site looks, not part of the
   swap. PT ships one (PT_Serif-Web-BoldItalic) if it is ever wanted.

   font-display: swap matches the &display=swap the Google URL carried, so text is
   readable in a fallback face while the subset loads rather than invisible.
   ───────────────────────────────────────────────────────────── */

@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/PT_Serif-Web-Regular.woff2") format("woff2");
}

@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/PT_Serif-Web-Bold.woff2") format("woff2");
}

@font-face {
  font-family: "PT Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/PT_Serif-Web-Italic.woff2") format("woff2");
}

@font-face {
  font-family: "PT Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/PT_Sans-Web-Regular.woff2") format("woff2");
}

@font-face {
  font-family: "PT Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/PT_Sans-Web-Bold.woff2") format("woff2");
}

@font-face {
  font-family: "PT Sans";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/PT_Sans-Web-Italic.woff2") format("woff2");
}

@font-face {
  font-family: "PT Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/PT_Mono-Web-Regular.woff2") format("woff2");
}



/* ── Tokens ────────────────────────────────────────────────────── */
:root {
  /* v.13 — PT Paratype trio (Pavel Paratype, ParaType, 2009/2010)
     is the publication default. PT Serif for body, PT Sans for
     apparatus, PT Mono for metadata. The Plex / Source families
     stay loaded for the §II.2 type trial in the working document. */
  --text-family-serif: "PT Serif", "Iowan Old Style", Charter, Cambria, Georgia, serif;
  --text-family-sans:  "PT Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --text-family-mono:  "PT Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* v.15 — Colour-system input axes. Mirror of editorial-system.html.
     Five tokens drive the derived OKLCH expressions on
     `html, body` further down. See editorial-system.html for
     the longer comment. */
  --color-hue: 85;
  --color-hue-chroma: 0.010;
  --color-bg-chroma-scale: 0.75;
  --color-accent-chroma: 0.15;
  --color-accent-lightness: 50%;

  /* The page frame: a centred box of this width, with this much padding inside it.
     A token rather than a number on .page because a **full-bleed** element — the
     environment banner — has to work out where the page's content starts in order to
     line its own text up with the masthead, and two places writing 78rem is one place
     too many. */
  --layout-page-width:     78rem;
  --layout-page-padding:   clamp(1.25rem, 4vw, 3rem);

  /* The one corner radius. Everything the publication rounds — a code block, an inline
     chip, an image, an example frame, a hover card — rounds by this much, because a
     page with two radii reads as two systems. 2px is small on purpose: enough to take
     the hardness off a filled rectangle, not enough to become a shape of its own. */
  --radius: 2px;

  /* Page-level grid. The three regions (rail · body · aside) are
     parameterised so the §00 trial figure can switch the split —
     and the column count — by writing these custom properties onto
     <body>. The :root values below are the no-JS default: a twelve-
     column grid split 1 · 8 · 3 (narrow rail, wide body, room for
     marginalia). At 78rem page width with 2rem gutters that is 12
     cells of 4.667rem. The figure can also set --layout-grid-columns to 24
     for half-column granularity; --layout-grid-gap stays 2rem either way,
     so a split re-expressed at 24 is identical to its 12 form. */
  --layout-grid-columns: 12;
  --layout-grid-gap:  2rem;

  --mark-start:  1;  --mark-span:  1;
  --layout-body-start:  2;  --layout-body-span:  7;   /* settled split: 1·7·1·3 */
  --layout-aside-start: 10; --layout-aside-span: 3;
  /* RULE: any non-figure content block (intro lede, TOC, references,
     author bio, etc.) spans rail + body only — i.e. ends at body's
     right edge (col line 9), leaving the gutter and aside columns
     empty. Content NEVER crosses into the aside region unless it is
     the article body laying its rail / column / margin out explicitly. */
  --layout-intro-span:  8;  /* rail + body, the canonical content width */


  /* v.13 — three per-face multipliers (see editorial-system
     for the body.fam-* table). */
  --text-scale-serif: 1;
  --text-scale-sans:  1;
  --text-scale-mono:  1;
  --text-size-display: calc(2.44rem  * var(--text-scale-serif));
  --text-size-h1:      calc(1.56rem  * var(--text-scale-serif));
  --text-size-h3:      calc(1.32rem  * var(--text-scale-serif));
  --text-size-body:    calc(1.125rem * var(--text-scale-serif));
  --text-size-small:   calc(0.88rem  * var(--text-scale-sans));
  --text-size-label:   calc(0.72rem  * var(--text-scale-sans));
  --text-size-mono:    calc(0.88rem  * var(--text-scale-mono));

  /* Maths, in em rather than rem: a formula sizes against the text it is set in, so an
     inline one is right inside a margin note and a table cell as well as in prose.

     KaTeX's own size is 1.21em — TeX's optical correction for a maths face set beside a
     text face. Against PT that is too much twice over: these faces are Computer Modern's
     descendants and already read large, and at 1.21em an inline formula with a subscript
     builds a strut taller than a body line and puts the whole paragraph off the rhythm for
     one `$x$`. Measured at 1.06: every paragraph carrying a formula on /sdcpns/ stays on
     the grid. A display formula has no line to stay on and is *presented* rather than read
     through, so it takes a step up — near KaTeX's own number, arrived at from the other
     side. */
  --text-size-maths:         1.06em;
  --text-size-maths-display: 1.2em;

  --text-line-height-tight: 1.1;          /* h1, display */
  --text-line-height-snug:  1.32;         /* h3, section-q, lede */
  --text-line-height-base:  1.5;          /* body (baseline unit) */
  --text-line-height-loose: 1.55;         /* small */

  /* One body line, 27px at the default scale. This is the document's baseline
     unit, and the vertical rhythms below are fractions of it rather than
     free-standing pixel values. */
  --text-baseline: calc(var(--text-size-body) * var(--text-line-height-base));

  /* The ladder every vertical gap is written on: one to four body lines. A gap is one
     of these and nothing else — one line between paragraphs, around a block set apart
     by a rule and under a heading; two around a block that interrupts, either side of a
     break's mark and above a heading; three above the colophon; four above an essay's
     section, the rule in the middle. A token rather than arithmetic so that a margin reads as the decision it
     is; a fraction of a line that is not a gap — a cell's padding, a label's lift, the
     marginalia's leading — stays as arithmetic on --text-baseline, which is the unit
     and not a gap. Drawn on /styleguide/ under Type. */
  --space-1: var(--text-baseline);
  --space-2: calc(var(--text-baseline) * 2);
  --space-3: calc(var(--text-baseline) * 3);
  --space-4: calc(var(--text-baseline) * 4);

  /* Marginalia takes four-fifths of a body line, so five margin lines occupy
     exactly four body lines and the two columns return to a shared baseline
     every four lines. On --text-size-small that works out at a 1.53 line — the
     small text keeps its own comfortable leading and still shares the rhythm.
     The size itself stays --text-size-small, which the masthead, the references
     and the code blocks also use; only the leading is particular to the margin. */
  --text-line-height-marginal: calc(var(--text-baseline) * 0.8);

  /* Face metrics, per em, read off the PT files the site actually loads:
     PT Serif ascends 1.04 and descends 0.29, PT Sans 1.02 and 0.28, and both
     have a 0.70 cap height and a 0.50 x-height — which is why the
     font-size-adjust below is a no-op for them. These three numbers are what
     let the drop cap be positioned by arithmetic rather than by eye; changing
     the body face means measuring them again. */
  --font-serif-ascent:  1.04;
  --font-serif-descent: 0.29;
  --font-cap-height:    0.70;
  /* PT Sans, measured the same way. The marginalia are set in it, so aligning a
     margin note by anything other than its baseline needs its metrics, not the
     serif's. Cap height happens to agree; the ascent and descent do not. */
  --font-sans-ascent:   1.02;
  --font-sans-descent:  0.28;

  /* Where a line's first baseline falls below the top of its line box: half the
     leading, plus the face's ascent. One for a body line in the serif, one for a
     marginal line in the sans. The styleguide's ruled baseline reads the first; the
     difference between them is what puts a margin note's first line on its paragraph's. */
  --text-first-baseline: calc(
    (var(--text-baseline) - (var(--font-serif-ascent) + var(--font-serif-descent)) * var(--text-size-body)) / 2
    + var(--font-serif-ascent) * var(--text-size-body));
  --text-marginal-first-baseline: calc(
    (var(--text-line-height-marginal) - (var(--font-sans-ascent) + var(--font-sans-descent)) * var(--text-size-small)) / 2
    + var(--font-sans-ascent) * var(--text-size-small));

  /* Where a margin note's first baseline has to sit relative to the first baseline of
     the body line beside it. A sans line at --text-size-small on the marginal leading
     puts its baseline about 4px higher in its box than a serif line at the body size
     does in a body line, so a note positioned at the top edge of a paragraph is moved
     down by the difference and the two first lines share a baseline (essay.css
     § Marginalia). Derived, so a change of face or size re-derives it; the website this
     mechanism comes from hand-tuned the same quantity as -13px. */
  --text-marginal-baseline-offset: calc(var(--text-first-baseline) - var(--text-marginal-first-baseline));

  /* Three more, measured off the loaded PT Serif the same way the ascent and descent
     were — by rendering at 400px and scanning the raster, not by trusting a spec sheet.
     They are what lets a lead-in be arithmetic rather than a size somebody liked.

       x-height          0.512 em  (the top of an `n`)
       lowercase stem    0.090 em  (the ink run through `n` and `l`)
       capital stem      0.0975 em (the ink run through `H` and `I`)

     The last two are the whole reason a lead-in needs a correction at all: **a capital's
     stem is 8.3% heavier than a lowercase one**, so scaling capitals down to small-cap
     size takes them *below* the body's colour. PT has no true small caps — none of the
     three faces carries `smcp`, so `font-variant: small-caps` would be the browser
     scaling capitals and calling them small caps. Real capitals, scaled deliberately and
     corrected, is the honest version of the same thing. */
  --font-x-height:       0.512;
  --font-serif-stem:     0.090;
  --font-serif-stem-cap: 0.0975;

  /* Drop cap. --text-dropcap-lines is the knob; the size and the offset that
     used to be hand-tuned are now derived from it and from the face metrics
     above, so the initial's cap-top lands on the first line's cap-top and its
     baseline lands on the last line's baseline. Both used to be em values
     proportional to the initial's own size, which is why re-sizing the initial
     (as .drop-word does) silently detuned an offset that should have stayed
     put — the cap sat a few pixels high. */
  --text-dropcap-lines:  3;               /* body lines the initial spans */
  --text-dropcap-weight: 200;
  --text-dropcap-line-height: 0.72;
  /* v.15 — per-face dropcap multiplier (mirror of editorial-system.html).
     1 keeps the derivation exact; any other value trades alignment for size. */
  --text-scale-dropcap: 1;

  /* How far below a paragraph's top edge its first line's cap-top sits:
     the line's half-leading, plus the gap between the face's ascender and
     its cap height. */
  --text-dropcap-cap-top: calc(
    (var(--text-baseline) - (var(--font-serif-ascent) + var(--font-serif-descent)) * var(--text-size-body)) / 2
    + (var(--font-serif-ascent) - var(--font-cap-height)) * var(--text-size-body));

  /* The same quantity for the initial itself, as a fraction of its own size —
     it is already inside its float box by this much, so it is subtracted. Kept
     unitless so the rule that uses it can multiply by 1em and have the em
     resolve against the initial rather than against the root. */
  --text-dropcap-glyph-inset: calc(
    (var(--text-dropcap-line-height) - (var(--font-serif-ascent) + var(--font-serif-descent))) / 2
    + (var(--font-serif-ascent) - var(--font-cap-height)));

  /* A raised initial: it sits on the first baseline and rises above the text block
     rather than sinking into it. Nothing wraps beside it except the first line, which is
     what makes it the register where **mixed case works** — there is no silhouette for a
     wrapped line to collide with, so "Mc" and "As" can keep their lowercase.

     Its height is stated in body lines and everything follows. One line puts the cap at
     27px, which is a shade under --text-size-display — the token the styleguide has been
     recording as used by nothing.

     The lift is how far the cap rises above the paragraph's top edge: its own baseline
     within its box, less where the first line's baseline sits. The initial takes it as a
     negative top margin and the paragraph takes it back as padding, so the raised cap has
     somewhere to be and the text block does not move. */
  --text-initial-raise-lines: 1;
  --text-size-initial-raise: calc(
    var(--text-initial-raise-lines) * var(--text-baseline) / var(--font-cap-height));
  --text-initial-raise-lift: calc(
    (var(--text-dropcap-glyph-inset) + var(--font-cap-height)) * var(--text-size-initial-raise)
    - var(--text-dropcap-cap-top)
    - var(--font-cap-height) * var(--text-size-body));

  /* The lead-in: the capitals that carry a reader from the initial back to the body.
     Both values below are derived, so changing the face means re-measuring the three
     metrics above and nothing else.

     Height. A small capital wants to stand above the x-height without reaching the
     cap height — level with either one and it stops reading as its own register. A
     third of the way up from the x-height puts it 1.1px clear of the lowercase at the
     body size, which is the distance the eye reads as deliberate. */
  --text-leadin-scale: calc(
    (var(--font-x-height) + (var(--font-cap-height) - var(--font-x-height)) / 3)
    / var(--font-cap-height));
  --text-size-leadin: calc(var(--text-size-body) * var(--text-leadin-scale));

  /* Weight. The lowercase stem the capital has to match, minus the stem the scaled
     capital actually has. It works out at about a fifth of a pixel — which is the point:
     the size does the work and this only stops the run going pale. If it ever needs more
     than a fraction of a pixel, the scale is wrong rather than the stroke.

     Not halved. `-webkit-text-stroke-width` is the pen width and the pen is centred on
     the outline, so each of a stem's two edges moves out by half of it and the stem
     grows by the whole. Halving it — which the first cut did, reasoning that a stroke is
     drawn on both sides — left the correction at 0.09px against a 0.20px deficit, and
     the measurement is what said so. */
  --text-leadin-stroke: calc(
    (var(--font-serif-stem) - var(--font-serif-stem-cap) * var(--text-leadin-scale))
    * var(--text-size-body));
}



/* --layout-wide-extra lives out here rather than in the :root block above, and the
   selector list is the whole reason.

   A custom property resolves its var() references where it is **declared**, not where
   it is used. :root computes this down to a concrete length and children inherit that
   answer, not the formula — so a register that redefines the body columns inherits an
   extra derived from columns it no longer has. .note drops the rail and takes that
   column back, and before this was understood a wide figure in a note overshot the
   marginalia column's right edge by 56px at 1440. Same family as the 80px undershoot
   this token was written to end, arriving from the opposite direction.

   Listing the selectors is what keeps one formula in one place. **Anything that
   redefines --layout-body-start or --layout-body-span has to be named here**, or its
   wide blocks quietly measure against somebody else's grid.

   How much wider than the body column a `wide` block is: everything from the body's
   right edge to the grid's right edge, which is the gutter column plus the marginalia
   band plus the gaps between them.

   Derived rather than guessed, because the guess was wrong for a long time. The body
   is S columns and S-1 gaps, so body + gap = S × (column + gap); one column plus its
   gap is therefore (100% + gap) / S, and the trailing region is T of them. Written as
   `(100% / S) × asideSpan` — which is what this replaces — a wide figure came out 80px
   short of the marginalia's right edge at 1440, because that expression divides the
   body into S equal parts and quietly hands each of them a share of the six gaps
   inside it.

   `100%` resolves against whatever the block's containing block is, so this is only
   correct on a child of .article-body — which is the only place `wide` means
   anything. */
:root,
.note,
.page-body {
  --layout-wide-extra: calc(
    (100% + var(--layout-grid-gap))
    * (var(--layout-grid-columns) - var(--layout-body-start) + 1 - var(--layout-body-span))
    / var(--layout-body-span));

  /* The same split, as the pieces the page is laid out from (essay.css § Article and
     § Headings). One column and its gap, twice: as a fraction of the page, for the
     article's own paddings, and of the prose column, for a child that reaches out of
     it — a heading's rule, a `wide` figure. The percentage inside each resolves where
     the property is *used*, against that property's own base, so each is right where
     it is read and wrong anywhere else. The rail and the tail are column counts: the
     columns before the prose, and the columns after it to the page's edge. Declared
     here, on the same list as --layout-wide-extra, for the same reason it is. */
  --layout-unit-page: calc((100% + var(--layout-grid-gap)) / var(--layout-grid-columns));
  --layout-unit-body: calc((100% + var(--layout-grid-gap)) / var(--layout-body-span));
  --layout-rail: calc(var(--layout-body-start) - 1);
  --layout-tail: calc(var(--layout-grid-columns) - var(--layout-body-start) + 1 - var(--layout-body-span));
}



/* ── Document ──────────────────────────────────────────────────── */
* { box-sizing: border-box; }



/* v.15 — Derived colour tokens. Declared on html, body so each
   gets its own computed value using its own --color-hue. See
   editorial-system.html for the longer comment. */
html, body {
  --color-paper:      oklch(98.4% calc(var(--color-hue-chroma) * 0.5 * var(--color-bg-chroma-scale)) var(--color-hue));
  /* paper-tint decoupled from bg-chroma-scale per intensity. */
  --color-paper-tint: oklch(95.0% calc(var(--color-hue-chroma) * 2.5) var(--color-hue));
  /* The code surface — a code block's background and an inline chip's, which are one
     material and have to agree. This was `#f6f5f2` hard-coded twice, as the fallback of
     a --color-paper-sunk nothing declared; the comment in § Code had recorded that for a
     while. Written as --color-paper's own expression at 97.0% rather than 98.4%, which
     resolves to exactly the hex it replaces — so nothing moved, and the ramp got its
     missing step back. */
  --color-paper-sunk: oklch(97.0% calc(var(--color-hue-chroma) * 0.5 * var(--color-bg-chroma-scale)) var(--color-hue));

  --color-ink:        oklch(18.0% calc(var(--color-hue-chroma) * 0.6 * var(--color-bg-chroma-scale)) var(--color-hue));
  --color-ink-soft:   oklch(34.0% calc(var(--color-hue-chroma) * 0.6 * var(--color-bg-chroma-scale)) var(--color-hue));
  --color-ink-mute:   oklch(53.0% calc(var(--color-hue-chroma) * 0.8 * var(--color-bg-chroma-scale)) var(--color-hue));
  --color-ink-dim:    oklch(70.0% calc(var(--color-hue-chroma) * 0.8 * var(--color-bg-chroma-scale)) var(--color-hue));

  --color-rule:       oklch(24.0% calc(var(--color-hue-chroma) * 0.4 * var(--color-bg-chroma-scale)) var(--color-hue));
  --color-rule-soft:  oklch(82.0% calc(var(--color-hue-chroma) * 1.0 * var(--color-bg-chroma-scale)) var(--color-hue));
  --color-rule-hair:  oklch(88.0% calc(var(--color-hue-chroma) * 1.2 * var(--color-bg-chroma-scale)) var(--color-hue));

  --color-accent: oklch(var(--color-accent-lightness) var(--color-accent-chroma) var(--color-hue));
}

html { background: var(--color-paper); -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--text-family-serif);
  font-feature-settings: "onum", "kern", "liga";
  font-variant-numeric: oldstyle-nums proportional-nums;
  /* v.11 — body uses --text-size-body, line-height base. Default browser
     root is 16px; --text-size-body 1.125rem = 18px. Line-height 1.5 →
     1 line = 1.6875rem (27px), the document's baseline unit. */
  font-size: var(--text-size-body);
  line-height: var(--text-line-height-base);
  color: var(--color-ink);
  background: var(--color-paper);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  /* v.10 — normalise x-heights across the §II.2 trial so families
     with smaller natural x-heights (Crimson, EB Garamond) don't
     read smaller than families with larger ones (Source, Plex). */
  font-size-adjust: 0.5;
}

.mono { font-family: var(--text-family-mono); font-variant-numeric: lining-nums; font-style: normal; }

/* markdown-it turns ~~this~~ into an <s>, and nothing styled it — so the one
   inline mark the renderer emits that this stylesheet had never named was taking
   whatever the browser felt like. `from-font` asks the face for the stroke rather
   than letting the browser pick one, so the line matches the weight of the text it
   crosses. No colour change: struck text is still text, and dimming it would make
   the deletion look like apparatus. */
s { text-decoration-line: line-through; text-decoration-thickness: from-font; }



/* ── Label ─────────────────────────────────────────────────────── */
/* One voice for every micro-label on the site: the date row, the contents
   header, the colophon keys, a filename bar, an abstract's opener, a label above
   a margin note. It used to be nine near-identical rules in two files, which is
   how two of them drifted — 0.12em in one place and 0.06em in another — without
   anyone being able to see it. Now there is one rule and one place to change.

   Elements that need layout as well keep their own class for that, and carry
   `label` alongside it. */
.label {
  font-family: var(--text-family-sans);
  font-size: var(--text-size-label);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-ink-mute);
}



/* ── Links ─────────────────────────────────────────────────────── */
/* v.13 — Global link style. Ink colour, hairline underline,
   no browser-default blue-and-underline anywhere. Specific
   contexts (.masthead .nav-it, .cite, aside.move-up a,
   etc.) override as needed. */
a {
  color: var(--color-ink);
  text-decoration: none;
  border-bottom: 0.5px solid var(--color-rule-soft);
}

a:hover { border-bottom-color: var(--color-ink); }


/* Nav items in the masthead are pill-like, no underline.
   The publication wordmark (.head-l) also opts out — the
   masthead's own bottom rule does the separating. */
.masthead .nav-it, .masthead .head-l { border-bottom: 0; }



/* ── Page frame ─────────────────────────────────────────────── */
.page {
  max-width: var(--layout-page-width);
  margin: 0 auto;
  padding: 0 var(--layout-page-padding) 4rem;
}



/* ── Environment banner ──────────────────────────────────────────
   A strip across the top of every page of a preview build, saying so. The pattern's
   common name is an **announcement bar**; this one is an **environment banner**, which is
   the precise term — it marks which deployment you are looking at rather than what is new.

   Emitted by {{dev-banner}} on a draft build only (macros.ts), and **all three templates
   carry it** — default.html, essay.html and note.html, each with the macro above
   `<main class="page">`, because the strip is full-bleed and .page is 78rem with padding.
   A fourth template would need the line too; there is nowhere else to put it, since a
   full-width band cannot be emitted from inside the frame.

   Warm rather than loud. It is the publication's own accent at a tenth of its strength on
   the paper tint, in diagonal stripes — the vocabulary a hazard band uses, quietened to
   something that can sit above a research page all day. It is not a warning: nothing is
   wrong, you are simply somewhere other than production. That is also why the accent
   appears here at a tenth and not at full: full is `.caution`, and this is not one.
   ─────────────────────────────────────────────────────────────── */
.dev-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 1.2rem;
  /* A quarter of a body line each side of one line of label — a band, not a bar. Half a
     line each side was two full lines of chrome above a page whose first business is the
     masthead. */
  padding-block: calc(var(--text-baseline) / 4);
  /* **The stripes are full-bleed; the words are on the page's measure.** The band has to
     reach both edges of the window to read as an environment marker, but its two labels
     belong exactly where the wordmark and the end of the masthead rule are — anything
     else reads as a second, wider page.

     .page is a centred box of --layout-page-width with --layout-page-padding inside it,
     so the distance from the window's edge to where its content starts is half the
     leftover plus that padding. `max()` handles the narrow case, where there is no
     leftover to halve and the padding is the whole inset. `100%` is the window's client
     width here, and only because this element is a child of <body> — which is also the
     only place a full-bleed band can be. */
  padding-inline: max(
    var(--layout-page-padding),
    calc((100% - var(--layout-page-width)) / 2 + var(--layout-page-padding))
  );
  background-color: var(--color-paper-tint);
  /* -45deg for `/////`. A gradient's bands run perpendicular to its line, so a line
     pointing north-west puts the bands on the north-east axis. 45deg would give `\\\\\\`.
     color-mix between two opaque colours rather than the accent at an alpha: mixing with
     `transparent` is a different operation and the ramp has no wash step to name. */
  background-image: repeating-linear-gradient(
    -45deg,
    color-mix(in oklch, var(--color-accent) 10%, var(--color-paper-tint)) 0 4px,
    var(--color-paper-tint) 4px 13px
  );
  border-bottom: 0.5px solid var(--color-rule-hair);
}

/* **Required, not defensive.** The browser's own `[hidden] { display: none }` is a bare
   attribute selector and loses to `.dev-banner`'s `display: flex`, so the dismiss button
   would set `hidden` and nothing would happen. Anything given a `display` and hidden by
   attribute needs this line. */
.dev-banner[hidden] { display: none; }

/* Ink-soft rather than the label register's ink-mute. The band is a tint rather than
   paper, and a notice nobody can read at a glance is not doing its job. */
.dev-banner-text {
  margin: 0;
  line-height: var(--text-baseline);
  color: var(--color-ink-soft);
}

/* The only <button> on the site, so there is no button vocabulary to take — it carries
   `.label` for its voice and this strips the browser's chrome off.

   **No underline.** It had the publication's link rule, and a link's hairline is drawn at
   the bottom of the line box — which is a whole body line here against an 11.52px label,
   so the rule sat about ten pixels clear of the word and read as an unrelated line. The
   fix is not to shorten the line box, which would take the band off its own rhythm: this
   is a control rather than a link, and it is the only thing in the band a reader can
   click. Ink-soft matches the notice beside it, so the two read as one register, and
   full ink on hover is the affordance. */
.dev-banner-dismiss {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  line-height: var(--text-baseline);
  color: var(--color-ink-soft);
  cursor: pointer;
}

.dev-banner-dismiss:hover { color: var(--color-ink); }



/* ── Masthead ───────────────────────────────────────────────── */
.masthead {
  /* v.13 — primary nav style. Brand on the left, publication
     nav on the right. Single bottom rule (1.5px); no top
     rule. The active item carries its own short top-tick
     indicator (.nav-it.act), so a second rule above would
     duplicate the signal. */
  /* Wrapping, not a breakpoint. This is the Ink & Switch masthead idiom
     (../website template/static/base.css, `header`): a wrapping flex row, so
     the nav drops to a second line exactly when it stops fitting and at no
     other time. The grid it replaces reserved a column for the wordmark and,
     below 720px, handed the slack to the nav instead — which squeezed the
     brand to 79px at 375px and broke "ARIA SGAI Notebook" across two lines. */
  display: flex;
  flex-flow: row wrap;
  align-items: baseline;
  gap: 0.6rem 2rem;
  padding: 1.2rem 0 0.5rem;
  border-bottom: 2px solid var(--color-rule);
  margin-top: 2rem;
}

.masthead .head-l {
  font-family: var(--text-family-sans);
  font-size: var(--text-size-small);
  letter-spacing: 0.08em;
  color: var(--color-ink);
  text-decoration: none;
  /* 700, not 500: PT Sans ships 400 and 700 only, so a 500 would be rounded to
     400 and the wordmark would read exactly as it did before. The nav items
     beside it stay at 400, which is what makes the wordmark the masthead. */
  font-weight: 700;
}

.masthead a.head-l:hover { color: var(--color-ink-soft); }

/* The wordmark is "ARIA SgAI NOTEBOOK": the name in the case the programme writes
   itself, and the last word in capitals. It used to be set in capitals whole, which hid
   the "Sg" — so the transform moved off the wordmark and onto a span around the last
   word, which {{wordmark}} (macros.ts) wraps from the one plain name in env.ts. The
   share card's masthead draws the same span (share-card.css § Masthead), which is why
   the rule is here rather than scoped to .masthead. */
.wordmark-caps { text-transform: uppercase; }



/* Primary nav on the right. Reuses the §02 nav-bar idiom:
   sans-serif items, active marked by a short top tick above
   the item rather than an underline (the bottom rule on the
   masthead doubles as the underline). */
.masthead .head-nav {
  display: flex;
  align-items: baseline;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 1.6rem;
  font-family: var(--text-family-sans);
  font-size: var(--text-size-small);
  margin: 0;
  padding: 0;
  /* Takes the space left over beside the wordmark, and the whole line once it
     has wrapped below it — its own flex-end keeps the items right-aligned in
     both cases, so the wrapped row reads as the same nav, not a new one. */
  flex: 1 1 auto;
}

/* Every nav item is a link. The masthead used to carry <span> placeholders for
   sections that had no page yet, dressed down with a `muted` modifier — but a
   placeholder in a navigation is an item a reader can try to use and can't, and the
   modifier only made that quieter rather than honest. What has a page is listed;
   what doesn't, isn't. */
.masthead .nav-it {
  color: var(--color-ink);
  padding-bottom: 0.2rem;
  position: relative;
  /* Stated here as well as in the reset above, because these are <a> elements and
     the global link rule gives every anchor a hairline underline. A rule two
     screens away is not where you look when a nav item grows a line under it. */
  text-decoration: none;
  border-bottom: 0;
}


/* PT Sans is not a variable font (only 400 and 700 available);
   a synthesized 500 reads as 400. Active state relies on the
   top tick + the ink colour matching the rest of the nav — same
   pattern as §02 in the working document.

   Hover draws the same tick in the accent, so pointing at an item previews
   exactly the mark that will say "you are here" — one vocabulary for one idea,
   rather than an underline that means something else. The text colour doesn't
   move; the tick is the whole signal. */
.masthead .nav-it.act::before,
.masthead a.nav-it:hover::before {
  content: "";
  position: absolute;
  top: -0.6rem;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--color-ink);
}

.masthead a.nav-it:hover::before { background: var(--color-accent); }

.masthead a.nav-it:hover { color: var(--color-ink); }

/* The narrow-width block that used to sit here is gone. It flipped the grid to
   `1fr auto` and placed .head-c / .head-r — two classes the masthead has never
   emitted. The flip was the only live rule in it, and it was the bug: wrapping
   now handles the same job without a breakpoint. */



/* ── Hover preview (§08) ─────────────────────────────────────────
   A card that reveals the thing a link points at, without leaving the
   paragraph. Two ways in, chosen by what the device can do:

   Hover pointer  — the card fades in under :hover / :focus-within, as it
                    always has.
   No hover       — there is nothing to hover with, so a tap opens the card as
                    a popover instead (citations.js promotes it; it lands in
                    the top layer with light-dismiss and Escape for free).

   Closed, the card is `display: none` — not `opacity: 0`. That is the whole
   fix for the site's horizontal overflow: an invisible card still occupied
   layout, and its ~340–416px pushed the essay's scrollWidth to 541px in a
   375px viewport, and to 1018px at 768px. Nothing else on the site overflowed
   at any width. Keep it display-none; opacity alone will bring the bug back.
   ─────────────────────────────────────────────────────────────── */
.preview { position: relative; display: inline-block; }

.preview > a {
  color: var(--color-ink);
  text-decoration: none;
  border-bottom: 0.5px solid var(--color-ink);
  padding-bottom: 0.05em;
}

.preview-card {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 0;
  width: 22rem;
  max-width: 90vw;
  background: var(--color-paper);
  border: 0.5px solid var(--color-rule);
  padding: 0.9rem 1rem;
  opacity: 0;
  transform: translateY(4px);
  pointer-events: none;
  /* `display` is in the transition so the card can be display:none when closed
     and still fade: transition-behavior flips it at the right end of each
     direction. Kept as a separate declaration rather than an `allow-discrete`
     keyword inside the shorthand — a browser that doesn't know the keyword
     would drop the whole shorthand and lose the fade; one that doesn't know
     this property just ignores the line and the card snaps. */
  transition: opacity 0.18s ease, transform 0.18s ease, display 0.18s;
  transition-behavior: allow-discrete;
  z-index: 10;
  /* v.11 — block so each child (meta / title / dek) sits on its own line with
     proper margin separation, rather than running inline as in the original
     markup. It is the *open* display; closed, the card is removed from layout
     entirely — see the section note above. */
  display: none;
  font-family: var(--text-family-serif);
}

/* Only where there is a pointer that can hover. On a phone this selector never
   matches, so the card stays out of the layout and citations.js opens it on
   tap instead. */
@media (hover: hover) {
  .preview:hover > .preview-card,
  .preview:focus-within > .preview-card {
    display: block;
    opacity: 1;
    transform: translateY(0);
  }

  @starting-style {
    .preview:hover > .preview-card,
    .preview:focus-within > .preview-card { opacity: 0; transform: translateY(4px); }
  }
}

/* Tapped open on a touch device. In the top layer, so it cannot widen the
   document however wide the entry is — the reason this is a popover and not a
   repositioned absolute box. Anchored to the bottom edge rather than to the
   citation: thumb-reachable, and it needs no anchor positioning. */
.preview-card:popover-open {
  display: block;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  position: fixed;
  inset: auto 0 0 0;
  width: auto;
  max-width: none;
  margin: 0;
  border: 0.5px solid var(--color-rule);
  border-inline: 0;
  border-bottom: 0;
  padding: 1.1rem var(--layout-page-padding) calc(1.1rem + env(safe-area-inset-bottom, 0px));
  max-height: 60svh;
  overflow-y: auto;
}

.preview-card::backdrop { background: oklch(18% 0 85 / 0.15); }

/* ── Index ───────────────────────────────────────────────────────
   The list of posts on the front page, and on any section index added later. One item
   is template/includes/post-item.html; the <ol> around it is written on the page,
   because {{index:}} renders that include once per child and joins the results — it
   has no wrapper of its own to give.

   Here rather than in essay.css because an index is not the long form: it is what a
   reader arrives on, alongside the masthead above it and the colophon below. What it
   borrows from the long form is the column. The default template puts its content in
   an .article-body, so the list meets every prose rule that lives there — which is
   what the opt-out in essay.css is about.

   **An item is a section in miniature, and it is built the same way**: four siblings
   and no wrapper, laid out on the page's own columns. The date and the reading time
   take the rail, because they are the item's address rather than part of its sentence —
   the same argument the § number makes on an essay — and the byline, the title and the
   description take the body column beside them. That is the item sketched in tasks.md
   § *The front page*; the `type` and `group` fields it also asks for wait on frontmatter
   no page carries yet.

   Four registers, all of them the publication's: the rail and the byline in the label
   voice, the title in the serif, the description a step back in ink. ────────────── */

/* A bare .post-list is enough, and only because essay.css names it in the opt-out on
   `.article-body ol` — the same way .footnote-list opts out. Out-specifying from this
   side does not work: a `:not()` carries its argument's specificity, so that selector
   is (0,2,1) and a `.article-body .post-list` written to beat it is (0,2,0). */
.post-list {
  list-style: none;
  padding: 0;
  /* **A quarter of a body line inside an item, one and three quarters between two — and
     the pair is why either is a fraction.** The list is the one place on the site that
     spends less than a whole line, because everything whole read wrong: solid, the title
     crowded its standfirst; at a whole line inside and two between, the page was a run of
     separate notices rather than a list.

     A fraction inside a component normally puts everything after it off the baseline for
     the rest of the page — the defect the paragraph rule exists to end. It does not here,
     and only because these two numbers are chosen together. An item is one line of meta,
     its title, **a quarter**, and its description; the gap after it is **one and three
     quarters**; the two fractions sum to a whole, so the pitch from one item's first line
     to the next's is always a whole number of lines. Measured at 1000: item heights of
     6.25, 9.25 and 8.25 lines, gaps of 1.75, pitches of **8 and 11**.

     **Change one and change the other.** The admissible gaps are 0.75, 1.75, 2.75; 0.75
     was tried and runs the posts together. 1.5 looks the same as 1.75 and costs the
     property — each item would start a quarter line below the last.

     Flex `gap` rather than a margin on each item: no adjacent-sibling rule to keep in
     step with a :last-child exception.
     No top margin: the list is the first thing in the flow of the page's .article-body
     — the standfirst before it is an aside, out of the flow — and its margin would
     collapse into .page-body's two lines above and simply not show; stated as none so
     that it never becomes the larger of the two.
     The space under the masthead is .page-body's, in essay.css. */
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: calc(var(--text-baseline) * 1.75);
}

/* Every line of an item is a whole body line tall and carries no margin, so an item
   occupies a whole number of lines however its title and description wrap.

   Each rule below is (0,2,0) by being scoped to .post-item, and that is what it takes
   to beat `.article-body p` — the reason the four fields can be <p> at all. The size on
   the two label rows is the same trap arriving through .label: that carries the
   register's voice at (0,1,0) and loses its font-size to `.article-body p`, so a meta
   row inside the column came out at 18px serif with the tracking and the uppercase
   still on it. */
.post-item .post-meta,
.post-item .post-byline,
.post-item .post-title,
.post-item .post-description { margin: 0; }

.post-item .post-meta,
.post-item .post-byline {
  font-size: var(--text-size-label);
  line-height: var(--text-baseline);
}

/* A wrapping row in both arrangements. In the rail it is turned upright (below); below
   980 it stays a row and the byline joins it. */
.post-item .post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 1.2rem;
  font-variant-numeric: lining-nums;
}

/* A byline can carry a link, because `author:` renders inline markdown. It takes the
   label register whole, underline included — the same argument as the title below. */
.post-item .post-byline a,
.post-item .post-title a { border-bottom: 0; }
.post-item .post-byline a:hover,
.post-item .post-title a:hover { border-bottom: 0.5px solid var(--color-ink); }

/* Not a heading, and that is a constraint rather than a preference: every h2 and h3 in
   an article body is a section of it — the stylesheet numbers it and rules it, and the
   contents lists it — so a title marked up as one would open a rail, take a § number
   and appear in the contents of the page listing it. */
.post-item .post-title {
  font-family: var(--text-family-serif);
  font-size: var(--text-size-h3);
  /* **A quarter of a body line before the description**, which puts the two baselines
     1.25 lines apart — measured, not derived: the browser sets a 21.12px title and an
     18px standfirst on the same 27px line box, so their baselines land on the grid and
     the margin is the whole of the step. Solid is 1.0 and crowds; half a line is 1.5 and
     the title floats between its byline and its standfirst.
     The rest of this number lives in .post-list's `gap`; read the note there before
     changing either. */
  margin-bottom: calc(var(--text-baseline) / 4);
  /* One body line, not --text-line-height-snug. A title that wraps has to hand the
     grid back to the description under it, and at this size that is a 1.28 ratio. */
  line-height: var(--text-baseline);
  letter-spacing: -0.008em;
}

.post-item .post-description {
  font-family: var(--text-family-serif);
  font-size: var(--text-size-body);
  line-height: var(--text-line-height-base);
  color: var(--color-ink-soft);
}

/* The two-column item, on the page's own tracks. .article-body spans the whole of the
   body on this register — .page-body starts it at column 1 — so repeating the body's
   span here reproduces the page's column edges exactly, gaps included, and the item's
   rail lands on the page's rail with its prose beside it.

   `minmax(0, 1fr)` rather than a bare `1fr`, which is `minmax(auto, 1fr)` and would let
   the widest meta row in the list set the rail's width for all of them. The contents
   would then indent differently from one item to the next — the trap /styleguide/ hit
   with a facsimile rail reading "§2.1".

   The rail spans every row, so the meta can be a stacked list beside a title of any
   depth; `align-items: baseline` puts its first line on the byline's. Rows are stated
   rather than auto-placed so that a missing field leaves an empty track rather than
   shuffling the ones below it. */
/* **Below 980 the rail collapses next to the byline**, so the item opens on one line
   reading date · register · reading time · author — its whole apparatus, left to right,
   with the author last because that is the one fact about the piece rather than about
   where it sits. Above 980 the first three go upright in the rail and the byline moves
   to the title's line, which is the same four elements placed twice.

   A wrapping flex row rather than a two-column grid, which is the masthead's argument for
   the same shape: a long affiliation should drop to its own line exactly when it stops
   fitting and at no other width. The meta holds its natural width so the byline is what
   wraps — letting the meta shrink would break the date away from its own reading time
   first, which is the wrong thing to lose.

   Bounded by max-width, not left to source order. It places the same four children the
   grid below places, at the same specificity, so two blocks that are merely ordered would
   leave the later one winning at every width — the trap `.section` has a comment about. */
@media (max-width: 979px) {
.post-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 1.2rem;
  }

.post-item > .post-meta { flex: 0 0 auto; }
.post-item > .post-byline { flex: 0 1 auto; }

.post-item > .post-title,
.post-item > .post-description { flex: 0 0 100%; }

}

@media (min-width: 980px) {
.post-item {
    display: grid;
    grid-template-columns: repeat(var(--layout-body-span), minmax(0, 1fr));
    grid-template-rows: repeat(3, auto);
    column-gap: var(--layout-grid-gap);
    align-items: baseline;
  }

.post-item > .post-meta {
    grid-column: var(--mark-start) / span var(--mark-span);
    grid-row: 1 / -1;
    /* **The rail is one stack, not one line per row.** It followed the item's grid rows
       for a while — `display: contents`, three spans, one per row — which aligned it
       exactly against the byline, the title and the standfirst. Exactly, and only while
       every one of them was a single line: a row's height is set by how the prose in it
       wraps, so a two-line title pushed the third rail line a whole body line down and
       the reading time sat at a different distance from its register in that item than in
       every other. **A rail is an address block, and an address block has one shape.**

       So it is a flex column again, anchored where it was always anchored — its first
       line baseline-aligned with the byline by the item grid — and it steps at its own
       body line from there. The last line takes the same quarter line under the register
       that the title takes under itself, because that is the gap it is standing beside.

       What that costs is exactness, and the amount is worth writing down: measured, the
       register sits **3px** above the title's baseline and the reading time **2px** above
       the standfirst's. The 3px is the depth a 21.12px serif puts its baseline at inside a
       27px line box against an 11.52px sans in the same box, and no arrangement of whole
       lines removes it — the previous version bought it back by following the rows, which
       is the thing that broke. */
    flex-direction: column;

    /* **The rail's line is the prose's first step, and it is 30px.** An even ladder is
       what a rail wants — the reading time the same distance under the register as the
       register is under the date — and this is the only even step that also lands on the
       prose. At the body line's 27 the register sits 3px high and the reading time
       **8.75px** high, which is the gap that started all this. At 33.75 the reading time
       lands on the standfirst but the ladder is visibly uneven, and a wrapping title
       strands it between two lines with nothing to sit beside.

       The 3px is how much lower a 21.12px serif sets its baseline than an 11.52px sans
       inside the same 27px line box. **Measured on the page, not derived:** working it
       out from --font-serif-ascent and --font-sans-ascent gives 3.66 and the browser
       produces exactly 3.00. It is written here rather than as a token because it is
       true of this pair of sizes in this box and of nothing else on the site.

       What it buys: the register sits **exactly** on the title's baseline, the reading
       time 2.75px above the standfirst's, the ladder is even, and none of it moves when
       a title wraps — a two-line title puts the reading time 3px under the title's second
       line, which is the same reading. Re-measure if the faces change, or if either the
       title or the label does.

       **A margin on every line but the first, not a line-height on all of them.** A
       taller line box puts the rail's *first* baseline deeper inside it, and the grid
       aligns that baseline to the byline's — so the box hangs 1.5px above row 1, the row
       grows to fit, and every item gets a pixel taller. That pixel is enough to take the
       list's pitch off whole numbers: 7.037 lines instead of 7. The margin moves the
       lines below without touching where the first one sits in its box. */
  }

.post-item > .post-meta > * + * { margin-top: 3px;
    /* **The date hangs into the gutter, and that is the arrangement rather than a
       defect.** `2026·04·07` has no break opportunity in it — a middle dot is not one —
       so it paints wider than one column and always will. The gutter it hangs into is
       empty by construction, and the byline is the thing it must not reach.
       Measured as painted ink, not as the box, because the box is the track and says
       nothing about where the glyphs end: the date overruns the track by 1.25px at 1440
       and 22.11px at 980, and clears the byline by 30.75px and **9.89px**. 980 is the
       floor — below it the grid collapses and the meta becomes a row — so that 9.89px is
       the worst case there is, and the date is the binding constraint at every width
       since it is always ten characters and the reading time is at most six. */
  }

.post-item > .post-byline      { grid-column: calc(var(--mark-start) + var(--mark-span)) / -1; grid-row: 1; }
.post-item > .post-title       { grid-column: calc(var(--mark-start) + var(--mark-span)) / -1; grid-row: 2; }
.post-item > .post-description { grid-column: calc(var(--mark-start) + var(--mark-span)) / -1; grid-row: 3; }

}



/* ── Error page ──────────────────────────────────────────────────
   content/404.html, and the only error view the site has — Cloudflare's static assets
   serve a 404 page and nothing else, so a 500 has no page to be. The dev server serves
   the same file, which it did not before: it answered a missing url with a status and no
   body at all, and a blank white page is the one thing an error should never be. See
   system/server.ts.

   The message under it needs no rule. It is a paragraph in the prose column and
   `.article-body p` sets it, which is the point of putting the page on the article grid.
   ─────────────────────────────────────────────────────────────── */

/* An h1 with no title block to sit in, so it takes none of .doc-title's placement — only
   the publication's type instead of the browser's bold 2em. --text-size-display, which
   is the size above h1 in the ramp and the one an error code wants: it is the page's
   whole subject. Two body lines of leading and one below, so a page whose only content is
   two elements still sits on the grid the rest of the site does. */
.error-code {
  font-family: var(--text-family-serif);
  font-weight: 400;
  font-size: var(--text-size-display);
  line-height: calc(var(--text-baseline) * 2);
  letter-spacing: -0.012em;
  font-variant-numeric: lining-nums;
  margin: 0 0 var(--space-1);
}



/* ── Colophon (§12) ─────────────────────────────────────────── */
/* **2px, which is the masthead's rule and the section's.** The publication draws one
   structural line and it has one weight; this was 3px, inherited from the playground,
   and a colophon rule a pixel heavier than the masthead rule above it read as two
   systems on one page. The narrow arrangement and the wide one below both take it. */
.colophon {
  /* Three lines above the rule and one below it, less the rule. More than any break in
     the column takes, so the foot of the page reads as the foot rather than as one more
     section; the line under the rule is a heading's, since the label is one. It was 4rem
     and 1.5rem, neither on the ladder — 64px that sat between a break's two lines and a
     section's without being either. */
  margin-top: var(--space-3);
  padding-top: calc(var(--space-1) - 2px);
  border-top: 2px solid var(--color-rule);
}

/* v.13 — colophon follows the content-width rule: rail + body
   span only, ending at body's right edge (col line 9). The
   3px rule above and every list row stop at that line. */
@media (min-width: 980px) {
.colophon {
    display: grid;
    grid-template-columns: repeat(var(--layout-grid-columns), 1fr);
    column-gap: var(--layout-grid-gap);
    border-top: 0;
    padding-top: 0;
    margin-top: var(--space-3);
  }

  /* The rule runs the full width of the page while the colophon under it keeps to
     the content measure. It is the foot of the document rather than part of the
     column, and a line that stops short of the margin reads as another section
     break — which is exactly what it is not. */
.colophon::before {
    content: "";
    grid-column: 1 / -1;
    border-top: 2px solid var(--color-rule);
    margin-bottom: calc(var(--space-1) - 2px);
  }

.colophon > * { grid-column: 1 / span var(--layout-intro-span); }

}


/* v.11 — the colophon is now a vocabulary demonstration: a
   labelled list of who/what/where, framed as the pattern §III.10
   documents. The 2-col layout retires. */
.colophon-header { margin: 0 0 0.6rem; }

.colophon-list {
  list-style: none;
  padding: 0;
  margin: 0 0 2rem;
  max-width: 52rem;
}

.colophon-list li {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  gap: 1.5rem;
  padding: 0.75rem 0;
  border-top: 0.5px solid var(--color-rule-hair);
  align-items: baseline;
}

.colophon-list li:first-child { border-top: 0; padding-top: 0.4rem; }

/* Was tracking at 0.06em where the rest of the register tracks at 0.1em — a
   drift, not a decision, and invisible until /styleguide/ put the nine side by
   side. It takes the shared .label now. */
.colophon-value {
  font-family: var(--text-family-serif);
  font-size: var(--text-size-small);
  line-height: var(--text-line-height-loose);
  color: var(--color-ink-soft);
}

.colophon-value a { color: inherit; border-bottom: 0.5px solid var(--color-rule-soft); text-decoration: none; }

.colophon-value a:hover { border-bottom-color: var(--color-ink); }

@media (max-width: 640px) {
.colophon-list li { grid-template-columns: 1fr; gap: 0.25rem; }

}



/* ── Tables ──────────────────────────────────────────────────────
   markdown-it has emitted GFM tables since the port and nothing had
   ever written one, so until /styleguide/ needed a column of class
   names beside a column of use cases there were no rules here and a
   table came out as browser default: no rules, no register, columns
   sized by whatever the longest cell happened to be.

   The header row speaks the label register the rest of the
   apparatus does. Rows are separated by a hairline rather than
   boxed — the publication has no boxes anywhere else, and a grid of
   borders would be the loudest thing on the page. ──────────────── */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-size-small);
  /* The marginalia's line, like a code block's — five cell lines measure four body lines,
     so a table hands the grid back every fifth line instead of never. It was
     --text-line-height-loose, 21.8px, which is 0.2px from this and has no relationship to
     27 at all. */
  line-height: var(--text-line-height-marginal);
  /* One line above and one below. It used to be one and a half below, which put 1.52
     lines between a table and the paragraph after it — off the rhythm, and read as a
     hole rather than a gap. A table opening a section takes no top margin at all — the
     heading's margin is the whole gap; see essay.css § Headings. */
  margin: var(--space-1) 0;
  /* Fixed, not auto. Auto layout sizes a column to its longest unbreakable run,
     and a cell naming --text-line-height-marginal has no break opportunity in it
     at all — at 375px that pushed the whole document 49px wider than the
     viewport. Fixed layout honours the column widths below and lets the content
     wrap inside them instead. */
  table-layout: fixed;
}

/* The header row speaks the label register; `th` is the one place it is worth
   restating rather than adding a class, because the markup comes from markdown
   and an author cannot put a class on a table cell. */
th {
  font-family: var(--text-family-sans);
  font-size: var(--text-size-label);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-ink-mute);
  text-align: left;
  vertical-align: bottom;
  padding: 0 var(--layout-grid-gap) calc(var(--text-baseline) * 0.4) 0;
  border-bottom: 0.5px solid var(--color-rule);
}

td {
  /* Baseline, not top: the first line of a use case sits on the first line of
     the class name beside it, the same alignment a margin note makes with the
     paragraph beside it. */
  vertical-align: baseline;
  /* A line of padding *including* the hairline, not on top of it. At a flat half-line each
     side the row came to 27.5px of non-content space and every row after the first sat
     half a pixel lower than the last. Padding and border both count towards the grid; only
     margin is outside the box. */
  padding: calc(var(--text-baseline) * 0.5 - 0.5px) var(--layout-grid-gap) calc(var(--text-baseline) * 0.5) 0;
  border-top: 0.5px solid var(--color-rule-hair);
  overflow-wrap: break-word;
}

/* A token name is one long run with no break opportunity in it. `break-word` is not
   enough: it lets a word overflow its line box but does not reduce the element's
   min-content width, which is what a table measures. `anywhere` does both, and is
   scoped to code so prose still breaks only between words. */
td code,
th code { overflow-wrap: anywhere; }

tr > :last-child { padding-right: 0; }

/* A quarter of the table for the name, the rest for what it is for. Stated as a
   fraction rather than a length so it holds at every column width the layout
   gives a table. */
th:first-child,
td:first-child { width: 25%; }

@media (max-width: 979px) {
  th:first-child,
  td:first-child { width: 33%; }
}



/* ── Callouts and quotes ─────────────────────────────────────────
   {{info: …}} and {{caution: …}}. A callout sits *in* the flow and
   interrupts it, which is the whole difference from an aside: an
   aside is for what a reader can skip, a callout for what they
   can't.

   A vertical rule, not horizontal ones. This design already spends
   a lot of horizontal lines — the masthead, every section, every
   table row, the contents — so another pair of them reads as more
   structure rather than as an interruption. A line down the left
   is the one direction nothing else here uses, and it says "this
   block is set apart" without adding a box.

   Adapted from theseus (content/static/content.css), which uses
   the same left rule but fills the block with a tint and takes the
   caution variant to amber. The tint is what does not come across:
   this publication is black on white, and a filled block would be
   the loudest thing on any page carrying one. The colour moves to
   the rule instead, which is the whole signal.

   Two levels, separated by that colour: an info opens on the
   structural neutral, a caution on the accent. That makes a
   caution the accent's second use on the site, after the tick a
   hovered nav item draws.

   A markdown `>` quote takes the same form, and had no rule at all
   before — it rendered at the browser's default 40px indent, which
   is a measure this design does not otherwise use. A quotation and
   a callout are both prose set apart from the argument around it,
   and the left rule is the vocabulary for that; one form, not two
   that nearly match.

   They are not the same *thing*, and the difference is voice: `>`
   quotes somebody else, {{info:}} is the publication interrupting
   in its own. If a reader ever needs to tell them apart, the answer
   is a third weight of this one form rather than a second form —
   the rule already carries the info/caution distinction in its
   colour, and a quote would take the hairline.
   ────────────────────────────────────────────────────────────── */

blockquote,
.info,
.caution {
  /* One body line, where this used to be one and a half. The rule down the left is what
     says "set apart", and it says it on its own — spending 1.52 lines of space saying it
     again put two and a half paragraph-gaps around a block that had already declared
     itself. One line is a visible step up from the 0.59 between paragraphs without
     opening a hole, and it is the same unit a heading takes below it, so the page has one
     separation measure rather than several. */
  margin: var(--space-1) 0;
  /* Half a line top and half a line bottom — one line of padding in total, so the block
     adds a whole line to the column rather than the half it used to. Padding counts
     towards the grid exactly as margin does; the difference is only that it sits inside
     the border rather than outside it. */
  padding: calc(var(--text-baseline) * 0.5) 0 calc(var(--text-baseline) * 0.5) calc(var(--layout-grid-gap) * 0.5);
  border-left: 2px solid var(--color-rule);
}

/* The rule and the words together. Colour is the only thing separating the two
   levels, so a caution says it twice rather than relying on a 2px line to carry the
   whole signal. --color-accent is 50% lightness against paper at 98.4%, which clears
   the 4.5:1 the page targets for body text. */
.caution {
  border-left-color: var(--color-accent);
  color: var(--color-accent);
}

/* A callout is body prose — it is part of the argument, not apparatus — so its
   paragraphs need no reset beyond closing up the last one. (0,1,2) each, which is
   what it takes to beat `.article-body p` at (0,1,1). */
blockquote > p:last-child,
.info > p:last-child,
.caution > p:last-child { margin-bottom: 0; }



/* ── Breaks ──────────────────────────────────────────────────────
   A break inside a section, where a section break is between them.

   `hr` had no rule in either stylesheet and nothing on the site emitted one, so a
   markdown `---` was getting the browser's inset groove at 0.5em auto. Which mark makes
   which break is markdown.ts's: `===` this rule, `---` the hairline, `***` the
   asterism. It takes the
   publication's one structural line — 2px in --color-rule, the weight the masthead,
   a section and the colophon all share — because a second line weight would read as
   a second system. What separates it from a section break is space, not stroke: a
   section spends four body lines with its rule in the middle, a break inside one
   spends two with the rule in the middle of those.

   Two body lines *including the rule*, not either side of it. Padding and border
   count towards the grid exactly as margin does, so the 2px is taken out of the
   margin below rather than added to the pair. Same arithmetic for the hairline.
   ───────────────────────────────────────────────────────────────── */
hr {
  border: 0;
  border-top: 2px solid var(--color-rule);
  margin: var(--space-1) 0 calc(var(--space-1) - 2px);
}

/* The quieter break, for a seam that is not worth a full rule. */
hr.thin {
  border-top: 0.5px solid var(--color-rule-hair);
  margin-bottom: calc(var(--space-1) - 0.5px);
}

/* The asterism — three asterisks arranged as the glyph the face does not carry.

   U+2042 ⁂ is in none of the seven PT faces (checked in the cmap, not assumed), and
   a missing glyph falls back silently to whatever the system has, which is the trap
   CLAUDE.md records for &middot;. So the mark is built from three real asterisks,
   which every face does carry: it stays type, in the publication's own face, on a
   page whose every other mark is type.

   The glyphs live in the markup and never in `content:`. The font pass collects
   characters from page.html, so a glyph that exists only in a stylesheet is never
   subsetted — which is also why {{hr asterism}} cannot emit a bare <hr>: a void
   element has nowhere to put them. role="separator" is <hr>'s own implicit role, so
   the div says the same thing to a screen reader that the rule above does.

   Five body lines: one of mark between two above and two below. More air than the
   rule takes, because a mark this quiet needs the space to read as a break at all — at
   one line each side it read as a paragraph gap with a smudge in it. */
.asterism {
  display: grid;
  /* PT's asterisk carries very little ink — 6.3 x 6.7px at the body size, 37% of its
     own em — so at body size the three of them read as a smudge rather than a mark.
     Set at one body line, where the ink is 9.4 x 10.1 and the triangle stands up.
     The columns are just under the glyph's advance (12.55px at this size), so the
     lower pair sit adjacent the way the real ligature draws them. */
  grid-template-columns: repeat(2, 0.46em);
  grid-template-rows: repeat(2, calc(var(--text-baseline) / 2));
  place-content: center;
  height: var(--text-baseline);
  margin: var(--space-2) 0;
  font-family: var(--text-family-serif);
  font-size: calc(var(--text-size-body) * 2);
  color: var(--color-ink-mute);
}
.asterism > span {
  display: block;
  text-align: center;
  line-height: calc(var(--text-baseline) / 2);
}
.asterism > span:first-child { grid-column: 1 / -1; }



/* ── Figures ─────────────────────────────────────────────────────
   {{figure}} has been in the build since CakeWalk and nothing had
   ever used it, so until /styleguide/ put one on a page there were
   no rules here at all. Three things were wrong with what the
   browser did on its own:

   The caption rendered as body prose. `.article-body p` is a class
   and a type selector, so it reaches into a <figcaption>'s <p> and
   out-specifies any bare class — a caption came out at 18px serif on
   a body line, indistinguishable from the paragraph above it. It
   belongs in the apparatus register, which is what .refs and the
   colophon values already speak.

   An image had no max-width. The 256px placeholder on the styleguide
   fits the column by accident of its size; a real 1600px figure would
   have pushed the document wider than the viewport at every width —
   the same shape of defect as the preview card that was the whole of
   the site's horizontal overflow.

   And <figure>'s browser default indents it 40px from the body
   column, for no reason this design has. ─────────────────────────── */

/* Every image, not only one inside a <figure>. A note written in plain markdown
   reaches for `![alt](img/x.png)`, which markdown-it renders as a bare <img> in a
   <p> — and every sizing rule in this file was scoped to `figure img`, so that
   image had no max-width at all. /alifib/ ships six screenshots 1913px wide,
   which is three times the body column at 1440 and five times the viewport at 375.

   Another rule scoped to a context with no element-level fallback, after
   h2.section-title, .fn-ref and a note's bare headings. The pattern is always the
   same: it works until the markup changes under it. `height: auto` keeps the aspect
   ratio when the width is the binding constraint. */
img {
  max-width: 100%;
  height: auto;
  /* The one radius, the same a code block takes. An image and a code block are the
     two filled rectangles this design puts in the prose column, and a hard corner on
     a screenshot reads as an edge of the page rather than an edge of the picture. */
  border-radius: var(--radius);
}

figure {
  /* Two body lines above and below, and flush with the prose beside it. */
  margin: var(--space-2) 0;
}

figure img,
figure video {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
}

/* The marginalia register exactly — same face, size, leading and colour as a note
   in the margin. A caption annotates the prose in the same voice a side note
   does, and two registers that nearly match read as a mistake. */
figcaption {
  font-family: var(--text-family-sans);
  font-size: var(--text-size-small);
  line-height: var(--text-line-height-marginal);
  color: var(--color-ink-soft);
  margin-top: var(--text-line-height-marginal);
}

/* A caption written in markdown arrives with its prose in a <p>, and that <p> has
   to be reset at a specificity that beats `.article-body p` — a class *and* a type
   selector, (0,1,1), where a bare `figcaption p` is only (0,0,2). Written the weak
   way, every caption the macro produced rendered at body size while a caption
   hand-written without a <p> came out correct, which is a difference nobody would
   think to look for. The third component to be caught by this exact trap, after
   the marginalia column and the {{aside}} macro. */
figcaption p,
.article-body figcaption p { font-size: inherit; line-height: inherit; margin: 0; }
figcaption p + p,
.article-body figcaption p + p { margin-top: var(--text-line-height-marginal); }

/* THE CAPTION GOES IN THE MARGIN, for a figure that spans the prose column.
   A caption and a margin note are already the same voice — same face, size,
   leading and colour, stated above — and putting them in the same place is
   what makes that a register rather than a coincidence. It also gives the
   figure back its own two lines: with the caption out of the flow, the gap
   under an image is the ladder's two lines and not two lines plus however
   many the caption ran to.

   The geometry is the aside's, arrived at from the other side. An aside is
   positioned against .article-body and takes `right: 0`, which is that box's
   padding edge and therefore the band's right edge. A caption cannot do that,
   because its static position is *below* the image and it needs to be level
   with the top of it — so the figure becomes the containing block, `top: 0`
   does the aligning, and the horizontal offsets are written in
   --layout-unit-body instead: the figure's box is the prose column, and one
   column plus its gap is the same length measured either way (§ Article).
   So `right` is the tail, negated, which lands the caption's right edge
   exactly where the aside's `right: 0` lands.

   WHICH FIGURES. Only one that spans the column. A tile is a third of it and
   three of them would stack three captions in one band; a float has prose
   wrapping past it and its caption belongs under it. Both are named rather
   than matched on a substring — `[class*="tile-"]` is the same defect as
   \bwrap\b matching no-wrap, and a class list is a list.

   AND NOT WIDE, which reaches over the band and has nowhere to put it.

   `caption-below` is the escape hatch, and it exists because the band is
   shared. A caption and a margin note pulled level with each other overlap,
   and nothing here moves out of the way — so an author who hits that, or who
   wants a caption long enough to be prose rather than a label, needs a way to
   put one back under its image without also changing the figure's width. The
   three classes that already did it — wide, a tile, a float — each change the
   layout to get there. This changes nothing else: it drops out of the
   selector below and the base rules put the caption back in the flow.

   No media type, so paper takes the margin as the screen does — the same
   choice § Marginalia makes, and for the same reason: one arrangement to keep
   in step rather than two. What it inherits from the aside is the collision:
   a caption and a note pulled level with each other overlap, and nothing
   moves out of the way. */
@media (min-width: 980px) {
  .article-body figure:not(.wide, .tile-2, .tile-3, .tile-4, .float-left, .float-right, .caption-below) {
    position: relative;
  }

  .article-body figure:not(.wide, .tile-2, .tile-3, .tile-4, .float-left, .float-right, .caption-below) > figcaption {
    position: absolute;
    top: 0;
    right: calc(var(--layout-unit-body) * var(--layout-tail) * -1);
    width: calc(var(--layout-unit-body) * var(--layout-aside-span) - var(--layout-grid-gap));
    margin-top: 0;
  }
}

/* The layout vocabulary, adapted from the Ink & Switch reading layout by way of
   theseus (../website-build-system content/static/content.css). One deliberate
   departure: the fractions are expressed in --layout-grid-gap rather than in a
   literal rem, so a row of tiles is separated by the same gutter the page grid
   uses.

   The naming rule is theirs and worth keeping: **modes are words, numeric
   modifiers are bare integers** — `wide` and `float-left`, but `tile-3`.

   All of it is off below the marginalia breakpoint. There is no band to reclaim
   and no room to sit two figures side by side, so everything falls back to block
   flow, which is what a phone wants anyway. */

@media (min-width: 980px) {
  /* WIDE — the body column plus the gutter and the band, the same expression a
     wide code block uses, so the two line up down the page. */
  .article-body figure.wide, .article-body .asterism.wide, .article-body hr.wide {
    width: calc(100% + var(--layout-wide-extra));
    max-width: none;
  }

  /* TILES — 2, 3 or 4 figures side by side, each keeping its own caption.
     Floated rather than inline-block: each {{figure}} emits its own <figure> with
     no wrapper, so an inline-block row has a whitespace character between every
     pair whose width depends on the font, and the arithmetic stops being exact.
     Floats ignore that whitespace. The cost is having to clear them, which the
     change-of-class rule below does. */
  .article-body figure.tile-2,
  .article-body figure.tile-3,
  .article-body figure.tile-4 {
    float: left;
    margin: 0 0 var(--space-2);
  }

  .article-body figure.tile-2 { width: calc((100% - var(--layout-grid-gap)) / 2); }
  .article-body figure.tile-3 { width: calc((100% - var(--layout-grid-gap) * 2) / 3); }
  .article-body figure.tile-4 { width: calc((100% - var(--layout-grid-gap) * 3) / 4); }

  .article-body figure.tile-2 + figure.tile-2,
  .article-body figure.tile-3 + figure.tile-3,
  .article-body figure.tile-4 + figure.tile-4 { margin-left: var(--layout-grid-gap); }

  /* Whatever follows the last tile ends the row. Scoped to a *change* of class so
     it can't fire between two tiles, and can't touch a float, which is meant to
     have text wrapping past it. */
  .article-body figure.tile-2 + :not(figure.tile-2),
  .article-body figure.tile-3 + :not(figure.tile-3),
  .article-body figure.tile-4 + :not(figure.tile-4) { clear: left; }

  /* FLOATS — prose wraps around them. Pair one with a width fraction. */
  .article-body figure.float-left {
    float: left;
    margin: 0 var(--layout-grid-gap) var(--space-1) 0;
  }

  .article-body figure.float-right {
    float: right;
    margin: 0 0 var(--space-1) var(--layout-grid-gap);
  }

  /* The fractions share denominators with the tiles, so a float and a tile of the
     same fraction come out the same visual size. */
  .article-body figure.half    { width: calc((100% - var(--layout-grid-gap)) / 2); }
  .article-body figure.third   { width: calc((100% - var(--layout-grid-gap) * 2) / 3); }
  .article-body figure.quarter { width: calc((100% - var(--layout-grid-gap) * 3) / 4); }
}

/* RATIOS — crop the media to a frame. Mostly for a row of tiles whose sources are
   different shapes, where the mismatch reads as a mistake. Crops from the centre,
   so check nothing important is at an edge. */
figure[class*="ratio-"] img,
figure[class*="ratio-"] video { width: 100%; object-fit: cover; }

figure.ratio-1x1  img, figure.ratio-1x1  video { aspect-ratio: 1 / 1; }
figure.ratio-3x2  img, figure.ratio-3x2  video { aspect-ratio: 3 / 2; }
figure.ratio-4x3  img, figure.ratio-4x3  video { aspect-ratio: 4 / 3; }
figure.ratio-16x9 img, figure.ratio-16x9 video { aspect-ratio: 16 / 9; }
figure.ratio-2x3  img, figure.ratio-2x3  video { aspect-ratio: 2 / 3; }

/* BORDER — a hairline for an image that would otherwise dissolve into the page:
   a pale diagram, a screenshot on white. A real border rather than an inset
   shadow, so with box-sizing: border-box it costs no layout shift. */
figure.border img,
figure.border video,
figure.border .grid > img,
figure.border .grid > video { border: 0.5px solid var(--color-rule-soft); }

/* GRID — several images in ONE figure under ONE caption, for when they are one
   thing: three views of an object, a before and an after. The macro emits a
   single <img>, so this shape needs raw HTML in the markdown. If each image wants
   its own caption, that is a row of tiles instead. */
figure .grid {
  display: grid;
  gap: var(--layout-grid-gap);
}

figure .grid > img,
figure .grid > video { width: 100%; height: auto; }

figure .grid-columns-2 { grid-template-columns: repeat(2, 1fr); }
figure .grid-columns-3 { grid-template-columns: repeat(3, 1fr); }
figure .grid-columns-4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 979px) {
  figure .grid-columns-2,
  figure .grid-columns-3,
  figure .grid-columns-4 { grid-template-columns: 1fr; }
}

/* ── Embedded nets ───────────────────────────────────────────────
   {{petrinaut}}. A figure whose picture is an <iframe>, so everything
   the Figures region already decides — two body lines above and
   below, the caption in the marginalia register, `wide` reaching over
   the band — applies unchanged. What is new is only the box.

   THE HEIGHT IS STATED, because an iframe has no intrinsic size and
   nothing can measure across an origin. It is a whole number of body
   lines (18, then 12 below 980px), which does not put the prose back
   on the grid — a block with an intrinsic height never can, and that
   is the accepted cost the ladder already names for a figure, a table
   and a <pre> — but it does keep the band itself an even multiple, so
   two embeds on one page cannot disagree by a fraction of a line.

   THE FRAME IS BORDERED AND SUNK. An embed is somebody else's surface
   inside our page, and the border says where ours stops. The ground
   is --color-paper-sunk, the same surface a code block takes: both
   are a rectangle of another system's output set into the prose.

   WHAT THIS CANNOT DO, and it needs saying here because the failure is
   silent: a frame the browser refuses to load leaves a bordered empty
   box, and no CSS can put anything in it. There is no fallback content
   in an <iframe>. That is why the macro requires a caption and the
   caption carries a link out — the link is the fallback, and it is the
   same link that serves the feed, paper, and a reader with third-party
   frames turned off. ─────────────────────────────────────────────── */

figure.petrinaut .petrinaut-frame {
  display: block;
  width: 100%;
  /* Twenty-two lines is 594px, and a wide block measures 1053.3px at 1440 — so the box is
     1.773:1 against the 16:9 (1.778) Petrinaut's own page sizes these to, a third of a
     percent out. It is both on the ladder and the shape the embed expects, which is luck
     rather than design and is worth saying so nobody "corrects" one of the two.
     Twenty-one lines was the first guess and came out 1.858, because it was derived against
     1010px — the nominal wide measure rather than the measured one. */
  height: calc(var(--text-baseline) * 22);
  border: 0.5px solid var(--color-rule-soft);
  border-radius: var(--radius);
  background: var(--color-paper-sunk);
  /* A frame is a replaced element and takes the baseline of its box, which leaves a
     descender's worth of white under it inside the figure. Every other block in this
     design sits on its own edges. */
  vertical-align: top;
}

@media (max-width: 979px) {
  figure.petrinaut .petrinaut-frame { height: calc(var(--text-baseline) * 12); }
}

/* Paper and the feed get the caption and its link, and an empty rectangle helps
   nobody — so the box is not printed at all. */
@media print {
  figure.petrinaut .petrinaut-frame { display: none; }
}

/* ── Maths ───────────────────────────────────────────────────────
   `$…$` and `$$…$$`, rendered to static markup by KaTeX at build time
   (markdown.ts § Maths) — no script on the page, the same as Prism.
   The markup and its typesetting are KaTeX's, vendored at
   template/static/katex/katex.css and loaded only by a page that has
   a formula on it (compile-page.ts). What is declared here is the
   publication's half: how large a formula is, and how much room a
   display formula takes.

   **A display formula is a block that interrupts, so it is spaced like
   a figure** — two body lines, the same --space-2, because it is the
   block it most resembles: no frame of its own, and a break in the
   argument either side of it. And like a figure, a table or a code
   block it has an intrinsic height and cannot hand the baseline grid
   back. Everything after one is off grid, which is the accepted cost
   of the block rather than something to tune (CLAUDE.md § Every
   vertical gap in the prose column).

   **Inline maths must not change the line, and that is the thing to
   check rather than assume.** KaTeX sizes itself at 1.21em — TeX's
   optical correction for a maths face set beside a text face — and at
   that size a formula with a subscript builds a strut taller than a
   body line, which would put the whole paragraph off the rhythm for
   one `$x$`. Set against PT Serif the correction is also too much:
   these faces are Computer Modern's descendants and already read
   large. Measured on /sdcpns/ at 1.06em, an inline formula's line box
   is the body's 27px and its baseline sits on the prose's.

   A formula taller than a line — a fraction, a big operator, a sum —
   will still grow its line box, and the answer to that is to write it
   as a display formula rather than to shrink the inline register. ─── */
.katex {
  font-size: var(--text-size-maths);
  /* KaTeX's own faces, so this must not inherit the prose's figures:
     PT's oldstyle numerals are set on the body and a formula wants lining. */
  font-variant-numeric: normal;
}

/* Two body lines, flush with the prose — a figure's spacing exactly. KaTeX's own rule is
   `margin: 1em 0`, which is neither of the gaps this design has.

   **Centred, and a step larger than the prose**, which is the one place this design keeps
   TeX's convention rather than its own. The argument against was that every other block in
   the column is flush with the prose, so a centred formula would be the only centred thing
   on the site — and that these formulae are written as continuations of the sentence above
   them ("evolves continuously through …"), which flush left would let them read as. Both
   are true and neither survived looking at the page: set flush left at the prose's size a
   display formula does not read as a display formula at all. It reads as a paragraph that
   happens to be in italics, and the two lines of air around it look like an accident
   rather than a frame.
   
   What a figure gets and a formula was not getting is *presentation* — the thing is lifted
   out of the argument and shown. Centring and a step up in size are how a formula says that
   about itself; they are the same move a figure makes by having a caption and a picture.
   So this is the block behaving like the figure it is spaced as, rather than a second
   alignment loose on the site: nothing else is centred because nothing else is set apart
   this way. */
.katex-display {
  display: block;
  text-align: center;
  /* A formula wider than the measure scrolls inside itself rather than pushing the
     document wider than the window — the defect the preview card once was, and the
     reason `img` carries a max-width. */
  overflow-x: auto;
  overflow-y: hidden;
  /* **One body line, not the figure's two**, and this is the correction that matters most
     to how a formula reads. A display formula on this site is almost always a continuation
     of the sentence above it — "evolves continuously through …" then the formula then
     "where f(x, p) is …" — and all three on /sdcpns/ are. The ladder spends two lines on a
     block that *interrupts* and one on a block *set apart*, and a sentence running through
     a formula is the second thing: at two the prose came apart either side of it and the
     formula read as a figure dropped into the middle of a clause.

     So the formula is set apart across the measure and joined down it: centred and a step
     larger, one line above and below. TeX makes the same split — a displayed equation is
     centred and its display skip is far less than a paragraph break.

     No padding, and that was checked rather than assumed: `overflow-y: hidden` clips, but
     KaTeX gives the box explicit struts and nothing hangs outside them, so the line of
     padding that used to be here was buying nothing and had to be taken back out of the
     margin to keep the total whole. Margins alone now, and they collapse with the
     paragraph's own line to exactly one. */
  padding: 0;
  margin: var(--space-1) 0;
}

/* KaTeX centres in two places — the wrapper above and the box inside it — and the inner one
   is what actually does it, which is worth knowing in both directions: an alignment set only
   on the wrapper measures as it was written and renders as KaTeX left it, the shape of bug a
   computed-style check passes and a screenshot catches. Centring is KaTeX's own default, so
   there is nothing to say here about it.

   The size is: a display formula sets a step above the prose, where inline maths sets with
   it. The box properties are deliberately NOT shared with the rule above — written as one
   selector list the inner box took the wrapper's margin and padding as well, and every gap
   came out 81px against the 54 it was supposed to be. */
.katex-display > .katex {
  display: block;
  font-size: var(--text-size-maths-display);
  margin: 0;
  padding: 0;
}

/* TeX that KaTeX could not read. The build warns and the page shows the source, the
   way an unknown macro shows {{macro}} rather than a blank — a formula that vanished
   silently would be worse than one that is obviously wrong. It takes --color-accent,
   the same colour a caution's rule takes, because it is the same thing being said. */
.maths-error {
  font-family: var(--text-family-mono);
  font-size: var(--text-size-mono);
  color: var(--color-accent);
  border-bottom: 1px dotted currentColor;
}

@media print {
  /* A formula split across a page break is unreadable, and it is short enough that
     keeping it whole costs nothing. */
  .katex-display { break-inside: avoid; }
}

/* ── Code ────────────────────────────────────────────────────────
   Inline code first, then the block. The build highlights fenced
   blocks with Prism at compile time, so there's no script on the
   page. Every line is its own .code-line div, which is what lets a
   highlighted line take a full-bleed background without anything
   being measured.

   The token colours are deliberately restrained — this publication
   is black on white, and a standard Prism theme's primaries would
   be the loudest thing on the page. Weight and grey do the work.

   There used to be two inline rules here — `code, .code` and, a few
   declarations further down, `:not(pre) > code`. The second won on
   specificity, so the first never applied to a bare <code> at all,
   and the size it set was `0.88em` **of the parent**. That gave
   inline code three different sizes depending on where it sat:
   15.84px in body prose, 12.39px in a table cell or a margin note,
   and 14.08px only inside a <pre>. None of the first two is the
   --text-size-mono the type scale names, and /styleguide/ named it
   in a row whose own sample was set correctly — the page documented
   a size the page did not use.
   The duplicate is gone. A bare `code` governs, at the token, and
   the size is the same wherever a chip appears. */

/* v.12 — inline code style. Use for hex codes, CSS property names,
   short snippets in prose. Always upright (never inherits italic
   from a containing sketch-note); subtle paper tint so it reads
   as code distinct from surrounding mono labels. */
/* `code`, and no longer `code, .code`. Nothing in template/ or content/ ever wrote
   class="code" — but Prism does: a markdown code span inside a ```md block is emitted
   as <span class="token code">, and `pre code` cannot reach it because that is an
   element selector and this is a <span>. So a code span *inside* a code block was
   wearing the inline chip — background, padding and all — which on /styleguide/ put one
   cream rectangle inside one grey one. Dropping the class fixes it and costs nothing,
   because a class nothing uses is not a feature. */
code {
  /* --text-size-mono, absolute rather than an em of whatever contains it, so a chip
     in a table cell, a margin note and a paragraph are all one size — the size the
     type scale names. */
  font-family: var(--text-family-mono);
  font-size: var(--text-size-mono);
  font-style: normal;
  color: var(--color-ink);
  /* The same surface a code block sits on. A chip and a block are one material; they
     were both --color-paper-sunk's #f6f5f2 fallback before the token existed, and
     --color-paper-tint here would have made the chip a warm cream against the block's
     grey the moment the duplicate rule stopped hiding it. */
  background: var(--color-paper-sunk);
  padding: 0.05em 0.35em;
  border-radius: var(--radius);
  font-variant-numeric: lining-nums;
}

/* **A code block sets on the marginalia's line, so it comes back to the grid.** Its own
   leading cannot be a body line without making code absurdly airy, and at 1.5 it was
   21.12px — a number with no relationship to 27 at all, so a block of any length landed
   the prose after it wherever it happened to land.

   --text-line-height-marginal is 0.8 of a body line, so **five code lines measure exactly
   four body lines** — the same trick that lets the margin keep its own leading and still
   meet the prose. The block re-syncs every five lines instead of never. The padding is a
   whole line split between top and bottom, less the two hairlines, so the frame adds a
   line rather than 29.8px of nothing in particular.

   It is not exact for every block, and it cannot be: only a multiple of five code lines
   lands. The alternative is setting code at a full body line — perfectly on grid, and
   about 28% taller. That is a decision about how a code block looks rather than a repair,
   and this is the version that costs nothing to look at. */
pre {
  font-family: var(--text-family-mono);
  font-size: var(--text-size-small);
  line-height: var(--text-line-height-marginal);
  background: var(--color-paper-sunk);
  border: 0.5px solid var(--color-rule-soft);
  border-radius: var(--radius);
  padding: calc((var(--text-baseline) - 1px) / 2) 1.1rem;
  /* One body line, like a callout. It was 1.6rem — 0.96 of a line, near enough to look
     right and not near enough to *be* right, which is how a rhythm goes quietly wrong. */
  margin: var(--space-1) 0;
  overflow-x: auto;
  tab-size: 2;
}

pre code { font: inherit; background: none; padding: 0; border: none; border-radius: 0; }

/* ```md wrap — for a block quoting prose rather than code. `pre-wrap` keeps the block's
   own line breaks and lets a long line fold; `break-word` is what stops a url with no
   spaces in it from pushing the block wide anyway.

   No hanging indent on the folded line, though it was tried: `padding-left` plus a
   negative `text-indent` reads well for prose and moves the *highlighted* line 17.6px
   left of its neighbours, because .highlighted-line takes its full-bleed background
   from its own box. A wrapped continuation starting at the left margin is a small
   ambiguity; a code block whose indentation lies is a bigger one. */
pre.wrap .code-line {
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

.code-line { display: block; min-height: 1lh; }

/* A highlighted line bleeds to the edges of the padding box */
.code-line.highlighted-line,
.code-line.highlighted-red-line {
  margin: 0 -1.1rem;
  padding: 0 1.1rem;
}
.code-line.highlighted-line     { background: var(--color-rule-soft); }
.code-line.highlighted-red-line { background: var(--color-rule-soft); opacity: 0.55; }

/* ```ts title="server.ts" — a sibling of the <pre>, not a wrapper, so it
   has to be widened alongside a wide block rather than inheriting it. */
.code-title {
  border: 0.5px solid var(--color-rule-soft);
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 0.4rem 1.1rem;
  margin: var(--space-1) 0 0;
}

/* The two are one block with a seam across the middle, so the corners round on the
   outside of the pair and stay square where they meet. Give both the full radius and
   the seam grows two notches nothing is behind. */
.code-title + pre { margin-top: 0; border-radius: 0 0 var(--radius) var(--radius); }

/* A ``` wide block is allowed to use the marginalia band as well */
@media (min-width: 980px) {
  .article-body pre.wide,
  .article-body .code-title.wide {
    width: calc(100% + var(--layout-wide-extra));
    max-width: none;
  }
}

/* Prism tokens, in greys and weights rather than hues */
.token.comment, .token.prolog, .token.doctype, .token.cdata { color: var(--color-ink-mute); font-style: italic; }
.token.punctuation { color: var(--color-ink-mute); }
.token.keyword, .token.boolean, .token.important, .token.atrule { font-weight: 700; }
.token.string, .token.char, .token.attr-value, .token.regex { color: var(--color-ink-soft); }
/* Tags, selectors and function names stay at full ink, which is enough to separate
   them from strings (soft) and punctuation and comments (mute). They used to carry
   an underline as well, and in a language made mostly of tags — HTML — that
   underlined almost the whole block. */
.token.function, .token.class-name, .token.tag, .token.selector { color: var(--color-ink); }
.token.number, .token.builtin, .token.constant, .token.symbol { font-variant-numeric: lining-nums; }
.token.operator, .token.entity, .token.url { color: var(--color-ink-soft); }
.token.deleted { text-decoration: line-through; }



/* ── Print ──────────────────────────────────────────────────── */
@media print {
html, body { background: white; }

.page { max-width: none; padding: 0 1in; }

.preview-card { display: none; }

/* A preview build's banner is about the deployment, not the document. Print the page,
   not where you happened to read it. */
.dev-banner { display: none; }

/* No `a { color }` here. There was one, restating the base rule's own value, so it set
   nothing anywhere — and it was outranked by `.colophon-value a { color: inherit }` at
   (0,1,1) anyway, which is the shape of trap the § Marginalia comment in essay.css
   describes. An override that changes nothing is worse than none: it reads as intent and
   has none, so the next person keeps it in step with a rule it never affected. Links
   already print in ink because that is what `a` is set to; the two places that opt out
   with `color: inherit` mean it on paper as much as on screen. */

.section { break-inside: avoid; }

.footnote, .author-bio, .bibliography { break-inside: avoid-page; }

}
