/* ==========================================================================
   Simplex Blog
   Layout modeled on distill.pub and Anthropic's transformer-circuits.pub:
   full-viewport-width section dividers with content centered in a narrower
   text column.
   Typography: Georgia body, Cardo display.
   ========================================================================== */

:root {
    --color-bg: #ffffff;
    /* Ink ramp (Loren, 2026-08-31): a geometric contrast ramp against the
       white ground, anchored at the accessibility floor — faint 5:1, muted
       10:1, text ~20:1 (each tier twice the contrast of the one below).
       Opaque hexes replace the old alpha blacks so the tiers hold on any
       background. */
    --color-text: #080808;
    --color-text-muted: #424242;
    --color-text-faint: #6f6f6f;
    --color-border: #e5e3df;
    --color-border-dark: #1a1a1a;
    --color-tint: #faf8f4;
    --color-tint-warm: #f5f0e6;
    --color-rule: rgba(0, 0, 0, 0.1);
    /* THE resting underline for every prose/meta link (Loren, 2026-09-02:
       dotted is canonical site-wide; the three link classes are prose =
       dotted token, apparatus = citation gold, navigation = no resting
       underline). */
    --color-link-underline: rgba(0, 0, 0, 0.28);
    --abstract-rule-color: var(--color-rule); /* the abstract/body divider (data-abstract-rule) */
    --endmatter-rule-color: var(--color-rule); /* the body/endmatter divider (data-endmatter-rule) */
    /* metadata (byline) -> abstract air; honest like the other boundary
       tokens: the frame paddings on both sides are zeroed below (Loren,
       2026-09-08). Default matches the old stacked visual (~84px). */
    --byline-abstract-gap: 5rem; /* Loren, 2026-09-08 */
    --abstract-body-gap: 11rem; /* Loren, 2026-09-08 */ /* air between abstract and first prose (was the generic 2.75rem) */
    /* Separator model (Loren, 2026-09-02): the GAP is page rhythm — invariant
       under separator on/off/style — and the POSITION (0..1) is the
       separator's allegiance within it: 0 ends the preceding text, 0.5
       floats, 1 introduces what follows. */
    --abstract-rule-pos: 0.5;
    --endmatter-gap: 11rem;
    --endmatter-rule-pos: 0.5;
    --appendix-gap: 11rem;      /* body -> appendix air (was margin 4rem + padding 2.5rem) */
    --appendix-rule-pos: 0.5;
    --appendix-rule-color: var(--color-rule);
    /* OUR DESIGN (Loren, 2026-08-11, via Color Lab): citation gold — was
       distill blue rgb(0, 0, 163). */
    --color-citation: #b69d20;
    --color-comment-bg: #fdf6d3;
    --color-comment-border: #e8dca0;

    /* ---- Publications design system ------------------------------------
       The type/size/color layer below is the settled post design (developed in
       post-preview/). It replaces the previous Georgia-body / Cardo-display
       pairing site-wide. Two areas are deliberately NOT re-skinned and keep
       Eric's treatment: figure captions and references/citations — see the
       marked sections further down.
       -------------------------------------------------------------------- */
    --font-display: "Cardo", Georgia, "Times New Roman", serif; /* Loren 2026-08-17; Cardo has no 600 face — 600 renders via 700 */
    --font-body: "STIX Two Text", Georgia, "Times New Roman", serif; /* Loren 2026-09-01 (captions tether to body) */
    --font-ui: "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    /* Display heading thickness; single-weight families (Young Serif,
       Instrument Serif) only respond via browser-synthesized bold. */
    --font-display-weight: 600;
    /* --font-wordmark (the header text) is declared in masthead.css. */
    --font-mono: "Commit Mono", ui-monospace, "SF Mono", Menlo, Monaco, monospace;

    /* Per-role font tokens. Each defaults to one of the stacks above, so a
       single role can diverge without disturbing the others. */
    --font-title: var(--font-display);
    --font-headings: var(--font-display);
    --font-byline: var(--font-mono);
    --font-kicker: var(--font-mono);
    --font-captions: var(--font-body);
    --font-code: var(--font-mono);

    /* Text sizes. */
    --size-kicker: 12px;
    --size-title: 56px;
    --title-weight: 400;
    /* Note/Editorial titles may step one modular step down; same by default. */
    --size-body: 17.5px; /* Loren, 2026-09-02 */
    --lh-body: 1.5;
    --size-abstract: var(--size-body); /* abstract copy matches body (Loren, 2026-09-01) */
    --size-meta: 12px;        /* byline / dates / meta */
    /* section numbers on headings (data-secnum): a ratio of the heading's
       OWN font-size (h1/h2/h3/h4 differ a lot), not an absolute size. */
    --size-secnum: 0.8;
    --size-toc: 12.5px;
    --size-code: 13px;
    --size-caption: 15px;    /* figure captions (all kinds, hero included) */
    /* Caption spacing (Loren, 2026-08-16): captions sit TIGHT under their
       figure and keep clear air before the body text resumes. Both ends are
       Size Lab-tunable. --caption-gap is the media->caption gap;
       --figure-space-below is the figure block's bottom margin (caption
       included), i.e. the caption->body gap. Was 0.6rem / 1.75rem. */
    --caption-gap: 0.5rem;
    --figure-space-above: 4rem;
    --figure-space-below: 4rem;
    --size-caption-num: 0.8em;  /* the "Fig. N" label, relative to caption size */
    /* subtitle rides 2px above body by default; the Size Lab overrides in px */
    --size-subtitle: 34px; /* Loren, 2026-09-08 */
    /* mobile (≤720px) caps for the title pair — tokens so the labs can dial
       them from a desktop window (labs hide on phones); the mobile rule is
       min(desktop token, mobile cap) so a smaller desktop value still wins */
    --size-title-mobile: 48px;
    --size-subtitle-mobile: 30px;
    --title-weight-mobile: 400; /* regular (Loren, 2026-09-08); bold via the lab axis */
    /* The subtitle extends the title's name, so it speaks in the title's
       voice: Cardo — whose chancery-descended italic is the system's one
       genuinely calligraphic form (Loren, 2026-09-02). */
    --font-subtitle: var(--font-display);
    --subtitle-style: italic;
    --color-subtitle: var(--color-text-muted);

    /* Section headings follow a modular scale: h_n = base x ratio^(4-n), so
       h3 = base*ratio, h2 = base*ratio^2, h1 = base*ratio^3. Only the two
       tokens are authored; the --size-h* below are the computed layer. */
    --heading-base: 20px;
    --heading-ratio: 1.2;
    --size-h3: calc(var(--heading-base) * var(--heading-ratio));
    --size-h2: calc(var(--size-h3) * var(--heading-ratio));
    --size-h1: calc(var(--size-h2) * var(--heading-ratio));
    --heading-weight: 600;
    /* Multiplier on the space ABOVE each heading; the smaller space below is
       fixed, preserving the asymmetry at any setting. */
    --heading-space: 1.7;   /* baked from dev tuning (Loren, 2026-09-02) */
    /* multiplier on the space BELOW each heading (heading -> its prose);
       below-values raised ~30% 2026-09-02 — the old 0.9/0.75/0.5rem read
       cramped (Loren) */
    --heading-space-below: 1.7;

    /* Byline stack rhythm. */
    /* Per-LINE byline controls (Loren, 2026-08-26): every byline line gets
       its own size + color token, defaulting to the shared tier — nothing
       moves until a lab control diverges a line. Sizes default to the meta
       size; correspondence keeps its 0.9x scale. (The paper credit line from
       the paper-byline branch gets its tokens once both merge.) */
    --size-byline-authors: var(--size-meta);
    --size-byline-affil: var(--size-meta);
    --size-byline-venue: var(--size-meta);
    --size-byline-date: var(--size-meta);
    --size-byline-corr: calc(var(--size-meta) * 0.9);
    /* the "Paper by" credit line tethers to the authors line until tuned */
    --size-byline-paper: var(--size-byline-authors);
    --color-byline-authors: var(--color-byline);
    --color-byline-affil: var(--color-text-faint);
    --color-byline-venue: var(--color-text-muted);
    --color-byline-date: var(--color-text-muted);
    --color-byline-corr: var(--color-text-faint);
    --color-byline-paper: var(--color-byline-authors);
    --byline-lh: 1.85;
    --byline-gap: 0.3rem;

    /* Per-role text colors, each defaulting to a foundation ink. */
    --color-title: var(--color-text);
    --color-headings: var(--color-text);
    --color-byline: var(--color-text);
    --color-byline-secondary: var(--color-text-muted);
    --color-byline-meta: var(--color-text-faint);
    --color-abstract: var(--color-text);
    --color-abstract-label: var(--color-text-faint);
    --color-kicker: var(--color-text-faint);
    --color-caption: var(--color-text-muted); /* re-tethered to the ramp (was pinned #6e6e6e) */ /* figure captions */
    --color-caption-num: var(--color-text-faint); /* re-tethered to the ramp */ /* the mono "Fig. N" label */

    /* Header anchoring inset for the logo on post pages (data-header="viewport"):
       10% of the viewport on wide screens, where that lands well left of the
       centered article column — but never further right than the column's own
       text edge, so once the column fills the viewport (narrow screens) the
       logo lines up with the text at --pad-inline instead of drifting inward. */
    --header-inset: min(10%, max(var(--pad-inline), calc((100% - var(--width-article)) / 2 + var(--pad-inline))));

    --width-container: 1180px;
    --width-wide: 920px;
    /* --content-width is the token the post Size/Layout labs drive; the
       article column follows it so the Frame slider works on built posts. */
    --content-width: 690px; /* ~71 CPL at 17.5px (Loren, 2026-09-03; was 710) */
    --width-article: var(--content-width);
    --width-index: 730px;
    /* Left-gutter TOC box bounds (style.css .post-toc): max caps how wide a
       single-line entry can grow before wrapping; min is the floor the TOC
       hugs down to as the gutter narrows. The grid itself NEVER goes
       asymmetric -- .block-body.has-toc .block-body-inner stays a plain
       symmetric 1fr/article/1fr at every width the TOC is shown inline (see
       that rule below); the TOC's own box just hugs content within
       whatever the (shrinking) left 1fr gutter currently is, independent of
       the grid track. Once that gutter would have to go narrower than
       --toc-min-width to stay symmetric, the TOC moves to the dropdown
       instead of forcing the article off-center -- see toc-sticky.js's
       collapseThreshold(), which is a direct function of this token, so
       tuning it also moves the collapse point live. 280px matches the
       existing expanded dropdown panel's own width, so the inline and
       dropdown TOC read as the same size. Both are Post Layout Lab sliders
       (Notes & TOC group) -- defaults here MUST mirror post-layout-lab.js's
       FRAME array. */
    --toc-min-width: 200px;
    /* the collapsed-TOC dropdown panel's width; also the collapsed BAR's
       width ceiling — the trigger must never be wider than the panel it
       opens (Loren, 2026-09-03) */
    --toc-dropdown-width: 280px;
    --toc-max-width: 280px;
    /* Independent of --toc-max-width above -- a wide but short-in-word-count
       label (e.g. "Appendix: Generators of Data and the Geometry of
       Beliefs") reads more comfortably wrapped a bit narrower than the
       TOC's own nav entries do, which is why this isn't just reusing that
       token. Read by both .endmatter-section > h3's own max-width (below)
       and toc-sticky.js's measureEndmatterLabelSlack(), so the JS threshold
       that decides when a post's endmatter switches to stacked format can
       never drift out of sync with what the CSS will actually render. */
    --endmatter-label-max-width: 240px;
    /* How far the TOC (and the matching empty space on the right, so the
       article stays centered) is allowed to drift from the article on a
       really wide/ultrawide screen. Without this, the gutter is a bare 1fr
       that grows without bound, so the TOC -- pinned to the gutter's own
       left edge -- ends up isolated near the true viewport edge, hundreds
       of px from the text it's a table of contents FOR. 320px leaves ~40px
       of breathing room past --toc-max-width's own 280px cap on the TOC
       box itself. Post Layout Lab slider (Notes & TOC group) -- default
       here MUST mirror post-layout-lab.js's FRAME array. */
    --toc-max-gutter: 340px; /* Loren, 2026-09-08 */
    /* The has-toc grid's own overall width, capped once article + 2 gutters
       would otherwise exceed this -- see .block-body.has-toc
       .block-body-inner below, and the --toc-max-gutter comment above.
       Shared with .block-endmatter .block-inner (has-toc pages only) so
       that cap doesn't drift out of sync with this one, the same reason
       --has-toc-cols below is shared. */
    --has-toc-max-width: calc(var(--width-article) + 2 * var(--toc-max-gutter));
    /* Shared by --toc-fixed-left (below) and the dropdown bar/panel's own
       top offset (.toc-collapsed-bar etc., further down this file) -- the
       same 16px on both axes so the floating dropdown sits an equal
       distance from the top and left edges of the screen, not a
       mismatched pair (Loren's report: top was inheriting
       --toc-sticky-top, 2rem/32px, a value that's actually about the
       UNRELATED pinned-mode sticky offset, not this). */
    --toc-dropdown-inset: 16px;
    /* The dropdown bar/panel are position:fixed, so their own left is
       relative to the true viewport edge -- fine while the has-toc grid
       also spans edge to edge, but once --has-toc-max-width caps and
       centers it (a wide screen), the grid's own left edge moves inward by
       (100vw - has-toc-max-width) / 2 while the fixed dropdown stayed put,
       so the two stop lining up. This tracks the same centering: max(0px, ...)
       matches margin:auto's own behavior (0 once the cap doesn't bind, so
       this is an exact no-op -- still plain --toc-dropdown-inset -- at
       every width below it), and 100vw (not 100%) is deliberate: these
       elements are position:fixed, sized against the viewport already,
       same reference frame the grid's own centering uses. */
    --toc-fixed-left: calc(max(0px, (100vw - var(--has-toc-max-width)) / 2) + var(--toc-dropdown-inset));
    /* Shared by .block-body.has-toc .block-body-inner AND .endmatter-section
       -- both need the EXACT same column formula so the article and the
       References/Footnotes column always land at the same width and
       x-position. A plain constant now (not redefined anywhere) -- an
       earlier version of the has-toc grid went asymmetric in a "squeeze
       zone" and needed this to vary per tier; that's gone (see the has-toc
       grid rule below), but the shared token stays so the two can never
       drift apart again. minmax(0, ...), not a bare var(--width-article) --
       a fixed-length track never yields, so once the viewport itself drops
       below article-width + padding (both 1fr gutters already driven to 0
       well before this point -- see toc-sticky.js's collapseThreshold())
       the middle column needs to be able to shrink too, or it just
       overflows the viewport instead of adapting. */
    --has-toc-cols: 1fr minmax(0, var(--width-article)) 1fr;
    --gutter-aside: 230px;
    --comment-gap: 2rem;        /* gap between the article and a margin comment */
    --comment-max-width: 560px; /* cap so wide screens don't over-stretch lines */

    --pad-inline: 1.5rem;
}

*, *::before, *::after { box-sizing: border-box; }

body {
    font-family: var(--font-body);
    font-size: var(--size-body);
    line-height: var(--lh-body);
    color: var(--color-text);
    background: var(--color-bg);
    margin: 0;
    padding: 0;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* --------------------------------------------------------------------------
   Site header — full-viewport-width border, content centered
   -------------------------------------------------------------------------- */
/* The logo + wordmark themselves are styled in masthead.css (shared with the
   index); only the header's placement in the post layout lives here. */

.site-header-inner {
    max-width: var(--width-container);
    margin: 0 auto;
    /* top 2.6rem matches the index masthead exactly (was 2.2) */
    /* integer px: 2.6rem = 41.6px put the flex-centered masthead on
       half-pixels, smearing the prism slabs' 1px borders on post pages */
    padding: 42px var(--pad-inline) 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    /* body sets 16px/1.6, which inherits here and inflates the strut around
       the inline-flex .logo, dropping the wordmark below where the index puts
       it. Resetting to the index's conditions realigns the two mastheads. */
    font-size: 1rem;
    line-height: normal;
}

/* OUR DESIGN: header anchoring is a layout axis (data-header on <body>):
   - viewport (DEFAULT, per the post-preview design): logo pinned to the
     viewport left at --header-inset, well outside the centered column; the
     nav (which the preview pages lack but posts keep) sits at the right edge.
   - column: logo aligned to the centered article column. */
body:not([data-header]) .site-header-inner,
body[data-header="viewport"] .site-header-inner {
    max-width: none;
    margin: 0;
    padding-left: var(--header-inset);
    padding-right: var(--pad-inline);
}
body[data-header="column"] .site-header-inner {
    max-width: var(--width-article);
    margin: 0 auto;
    padding-left: var(--pad-inline);
    padding-right: var(--pad-inline);
}


/* --------------------------------------------------------------------------
   Links
   -------------------------------------------------------------------------- */
a {
    color: inherit;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-decoration-color: var(--color-link-underline);
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    transition: text-decoration-color 0.15s ease;
}

a:hover { text-decoration-color: var(--color-text); }

/* --------------------------------------------------------------------------
   Index page — the prism system.
   Every post is a rectangular prism in axonometric view; the index reads as a
   categorical distribution where extrusion depth ∝ post weight, and hovering
   conditions the distribution (the hovered prism gains mass, the rest settle).
   Canonical recipe frozen from design-lab/m2-prism-lab — see DESIGN.md. The
   static defaults below ARE canon; the geometry variants (.dir-*, .align-*,
   .w-uniform, .eph-flat) are <body> classes so index.html's dev control panel
   can flip every axis live. Canon's classes are baked into the <body> tag, so
   the page ships canon with no JS at all.
   -------------------------------------------------------------------------- */
@property --d {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

:root {
    /* prism axes (canon values; the index dev panel overrides these inline) */
    --edge-a: 0.5;         /* edge line darkness */
    --face-b-a: 0.13;      /* bottom-face fill darkness */
    --face-r-a: 0.22;      /* side-face fill darkness (canon: bottom × 1.7) */
    --dscale: 0.7;         /* global depth multiplier */
    --speed: 100ms;        /* hover response */
    --shrink-m: 0.8;       /* non-hovered prisms sink to this fraction */
    --grow-m: 1.12;        /* hovered prism grows by this factor... */
    --grow-add: 2px;       /* ...plus this */
    --prism-gap: 4px;      /* spacing between posts */
}

main.index {
    max-width: var(--width-index);
    margin: 0 auto;
    padding: 2.6rem var(--pad-inline) 5rem;
}

.index-intro {
    margin: 0 auto 2.2rem;
    max-width: 540px;
    text-align: center;
    color: var(--color-text-muted);
}
.index-intro p { margin: 0 0 0.8em; }

.year-header {
    display: flex;
    align-items: center;
    gap: 1.2em;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.22em;
    color: var(--color-text-faint);
    margin: 3rem 0 1.8rem;
}
.year-header::before, .year-header::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--color-rule);
}

/* The Official/All selector: a two-bar categorical distribution over views.
   Nearly all mass rests on the committed view; entering the control splits it
   toward 50/50, hovering a specific bar leans the mass toward it (~2:1), and
   clicking collapses it onto the choice. The bars are static canon prisms —
   the top face never moves, only the extrusion behind it grows — with depth
   ∝ p, driven through the registered --d property so plain CSS transitions
   animate it. The CSS fallback depths below show the default (official) state
   before JS arrives; JS-less visitors just see the official view. */
.filter { display: flex; justify-content: center; gap: 4px; margin: 0 0 3rem; }
.fbar {
    --d: 1px;
    position: relative;
    font: inherit;
    font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--color-text-faint);
    background: var(--color-bg);
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    padding: 5px 14px; cursor: pointer;
    transition: --d 150ms ease-out, color 150ms;
}
.fbar::before {   /* bottom face */
    content: ""; position: absolute; pointer-events: none;
    left: 0; top: 100%; width: 100%; height: var(--d);
    background: rgba(26, 24, 20, var(--face-b-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-top: none;
    transform: skewX(45deg); transform-origin: top left;
}
.fbar::after {    /* right face */
    content: ""; position: absolute; pointer-events: none;
    left: 100%; top: 0; width: var(--d); height: 100%;
    background: rgba(26, 24, 20, var(--face-r-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-left: none;
    transform: skewY(45deg); transform-origin: top left;
}
.fbar.on { color: var(--color-text); }
.filter .fbar:first-child { --d: 12.3px; }   /* pre-JS resting state: ~100% official */
.filter .fbar:last-child  { --d: 1.7px; }

/* the official view hides ephemera (and their date labels) */
body.view-official .prism.eph,
body.view-official .list .when:has(+ .prism.eph) { display: none; }

/* Each list row is [date gutter | prism]: posts are labeled by month + year
   off to the left instead of centered year headers. Lists are still one per
   year (the gap between .lists is the year grouping — and the hover
   conditioning stays per-year, since the :hover selectors scope to a .list).
   On wide viewports the gutter hangs into the left margin so the cards keep
   the full column width; below that it shares the column. */
.list {
    display: grid;
    grid-template-columns: 92px 1fr;
    column-gap: 24px;
    row-gap: var(--prism-gap);
}
.list + .list { margin-top: 2.4rem; }
.list .when {
    grid-column: 1;
    text-align: right;
    font-family: var(--font-mono);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    line-height: 1.4;
    color: var(--color-text-faint);
    padding-top: 1.15rem;   /* optically level with the card titles */
}
.list .when:has(+ .prism.minor) { padding-top: 0.85rem; }
.list .prism { grid-column: 2; }

@media (min-width: 1000px) {
    .list { margin-left: -116px; }   /* gutter width + gap: dates hang in the margin */
}
@media (max-width: 640px) {
    .list { grid-template-columns: 1fr; }
    .list .when, .list .prism { grid-column: 1; }
    .list .when { text-align: left; padding: 0 0 0.15rem 0.2rem; }
}

.prism {
    --d0: 10px;
    --d: var(--d0);
    position: relative;
    display: flow-root;  /* contains floated card figures (.fig) */
    text-decoration: none;
    background: var(--color-bg);
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    padding: 1rem 1.2rem;
    transition: --d var(--speed) ease-out, transform var(--speed) ease-out;
    transform: translate(calc(-1 * (var(--d) - var(--d0))), calc(-1 * (var(--d) - var(--d0))));
}
.prism::before {  /* bottom face */
    content: ""; position: absolute;
    left: 0; top: 100%; width: 100%; height: var(--d);
    background: rgba(26, 24, 20, var(--face-b-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-top: none;
    transform: skewX(45deg); transform-origin: top left;
}
.prism::after {   /* right face */
    content: ""; position: absolute;
    left: 100%; top: 0; height: 100%; width: var(--d);
    background: rgba(26, 24, 20, var(--face-r-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-left: none;
    transform: skewY(45deg); transform-origin: top left;
}

/* mirrored projection: extrusion down-left, eye above-right (canon) */
.dir-left .prism {
    transform: translate(calc(var(--d) - var(--d0)), calc(-1 * (var(--d) - var(--d0))));
}
.dir-left .prism::before {
    transform: skewX(-45deg); transform-origin: top right;
}
.dir-left .prism::after {
    left: auto; right: 100%;
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-right: none;
    transform: skewY(-45deg); transform-origin: top right;
}

/* head-on projection: extrusion straight down — no lateral skew, so the
   left AND right edges of every post stay aligned. Only the front face
   shows, as a straight strip below the top face. */
.dir-front .prism {
    transform: translate(0, calc(-1 * (var(--d) - var(--d0))));
}
.dir-front .prism::before { transform: none; }
.dir-front .prism::after { display: none; }

/* viewed from slightly below: the visible strip sits above the face and
   the bar grows upward. Edges likewise stay aligned. */
.dir-back .prism { transform: none; }
.dir-back .prism::before {
    top: auto; bottom: 100%;
    border: 1px solid rgba(0, 0, 0, var(--edge-a)); border-bottom: none;
    transform: none;
}
.dir-back .prism::after { display: none; }

/* base alignment (canon): the prism's BASE sits on the layout line and the
   top face rises above it (bars standing on a baseline), instead of the
   top faces aligning and bases poking into the gap. The base is pinned
   by construction, so hover growth pushes the top face up naturally. */
.align-base .prism { transform: translate(calc(-1 * var(--d)), calc(-1 * var(--d))); }
.align-base.dir-left .prism { transform: translate(var(--d), calc(-1 * var(--d))); }
.align-base.dir-front .prism { transform: translate(0, calc(-1 * var(--d))); }
.align-base.dir-back .prism { transform: translate(0, var(--d)); }

/* static anchoring: the card (top face) never moves at all — only the
   extrusion behind it grows and shrinks. Think of it as viewing the
   categorical bars from the "bottom": hovering pushes a bar's far end
   deeper into the page rather than pulling the card toward the viewer. */
body.align-static .prism { transform: none; }

.eph-flat .eph { transform: none !important; }  /* flat ephemera never shift, whatever the alignment */

/* depth ∝ weight (× the global depth multiplier) */
.d-flag    { --d0: calc(23px  * var(--dscale)); }
.d-paper   { --d0: calc(13px  * var(--dscale)); }
.d-opinion { --d0: calc(7px   * var(--dscale)); }
.d-note    { --d0: calc(5px   * var(--dscale)); }
.d-talk    { --d0: calc(2.5px * var(--dscale)); }

/* conditioning */
.list:hover .prism:not(:hover) { --d: calc(var(--d0) * var(--shrink-m)); }
.list .prism:hover             { --d: calc(var(--d0) * var(--grow-m) + var(--grow-add)); }

/* widths: uniform under canon (body.w-uniform); mixed otherwise */
.w-full { width: 100%; }
.w-mid  { width: 88%; }
.w-min  { width: 76%; }
.w-uniform .w-mid, .w-uniform .w-min { width: 100%; }

/* Ephemera (All-only entries: talks, personal-site posts, substack) are
   tinted faintly purple — a deliberate exception to the monochrome identity,
   so the unofficial register reads at a glance rather than through depth
   alone. The ink triplet feeds the border and both faces at the same alphas
   as the neutral prisms, so the panel's edge/side knobs still apply. */
.prism.eph {
    --eph-ink: 62, 34, 116;
    background: #f8f5fd;
    border-color: rgba(var(--eph-ink), var(--edge-a));
}
.prism.eph::before {
    background: rgba(var(--eph-ink), var(--face-b-a));
    border-color: rgba(var(--eph-ink), var(--edge-a));
}
.prism.eph::after {
    background: rgba(var(--eph-ink), var(--face-r-a));
    border-color: rgba(var(--eph-ink), var(--edge-a));
}

/* ephemera printed flat on the page: ink, no volume */
.eph-flat .eph {
    border-color: transparent; background: transparent;
    padding-left: 0.2rem; padding-right: 0;
}
.eph-flat .eph::before, .eph-flat .eph::after { display: none; }

/* ---- top-face anatomy: title, then authors + date/venue, then description ---- */
.prism .title {
    font-family: var(--font-display); font-weight: 400;
    font-size: 18.5px; line-height: 1.3; margin: 0 0 0.4rem;
}
.prism:hover .title { text-decoration: underline; text-decoration-color: rgba(0, 0, 0, 0.3); text-underline-offset: 3px; }
.prism .meta {
    display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 0.25rem 1rem;
    font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.02em;
    color: var(--color-text-faint); margin: 0 0 0.55rem;
}
.prism .meta .r {
    white-space: nowrap; margin-left: auto;
    font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em;
}
.prism .desc { font-size: 13.5px; line-height: 1.55; color: var(--color-text-muted); margin: 0; }

/* card figure: floats bottom-right of the text block. Authored in source
   AFTER the meta line so it can never rise into it (floats never rise above
   their insertion point) — the description then wraps around it. For figures
   on transparent backgrounds, an inline shape-outside pointing at the same
   image lets the text tuck in around the actual ink (the point cloud) rather
   than the rectangular box; browsers without shapes fall back to the box. */
.prism .fig {
    float: right;
    width: min(170px, 34%);
    margin: 0.1rem 0 0 0.9rem;
    shape-margin: 12px;
}

/* flagship entries: centered, with a hero figure */
.prism.flag { text-align: center; padding: 1.5rem 1.7rem 1.4rem; }
.flag .title { font-size: 23px; max-width: 480px; margin-left: auto; margin-right: auto; }
.flag .meta { display: block; text-align: center; margin-bottom: 1rem; }
.flag .meta .r::before { content: "· "; }  /* inline in the centered line, so separate it from the authors */
.flag img { max-width: min(440px, 100%); max-height: 250px; margin: 0 auto 1rem; display: block; }
.flag .desc { max-width: 460px; margin-left: auto; margin-right: auto; }

/* opinion pieces: italic title */
.op .title { font-style: italic; font-size: 16.5px; }

/* minor entries: talks, notes, short links */
.prism.minor { padding: 0.7rem 1rem; }
.minor .title { font-family: var(--font-body); font-size: 14px; color: var(--color-text-muted); line-height: 1.45; margin: 0 0 0.25rem; }
.minor .meta { margin: 0; }

@media (max-width: 600px) {
    .w-mid, .w-min { width: 100%; }
}

/* --------------------------------------------------------------------------
   Site footer
   -------------------------------------------------------------------------- */
.site-footer {
    width: 100%;
    border-top: 1px solid var(--color-rule);
    margin-top: 2rem;
}

.site-footer-inner {
    max-width: var(--width-container);
    margin: 0 auto;
    padding: 1.5rem var(--pad-inline);
}

.site-footer p {
    margin: 0;
    font-family: var(--font-ui);
    font-size: 12px;
    letter-spacing: 0.02em;
    color: var(--color-text-faint);
}

/* --------------------------------------------------------------------------
   Post page: block layout with full-viewport-width borders
   -------------------------------------------------------------------------- */
/* OUR DESIGN: main.post is a flex column so the front-matter blocks (title /
   hero / byline / abstract) can be reordered by the layout lab via `order`:
   - data-hero: below-title (DEFAULT) | below-byline | hidden
   - data-byline-pos: below-hero (DEFAULT) | above-hero
   Order values are spaced out so a moved block can land between two others
   without tying (a tie silently falls back to DOM order). */
main.post {
    width: 100%;
    padding: 0;
    display: flex;
    flex-direction: column;
}

.block { width: 100%; }

.block-title { order: 0; }
/* Archived-version notice (archive-version.sh → versions.mjs → template): one
   quiet mono line above the title, pointing at the current version. */
.version-notice {
    margin: 0 0 1.25rem;
    font-family: var(--font-byline);
    font-size: var(--size-meta);
    color: var(--color-text-faint);
    letter-spacing: 0.02em;
}
.version-notice a { color: var(--color-text); }
.block-hero { order: 2; }
.block-byline { order: 3; }
.block-abstract { order: 6; }
.block-body { order: 10; }
.block-endmatter { order: 11; }

/* hero placement — 4 slots hero between the byline (3) and the abstract (6) */
body[data-hero="below-byline"] .block-hero { order: 4; }
body[data-hero="hidden"] .block-hero { display: none; }
/* byline placement */
body[data-byline-pos="above-hero"] .block-byline { order: 1; }

/* OUR DESIGN: blocks separate by whitespace rather than full-viewport rules.
   `data-rules="full"` restores the previous hairlines; `data-rules="column"`
   draws the line only as wide as the article column. */
.block + .block { padding-top: 2.75rem; }
body[data-rules="full"] .block + .block {
    padding-top: 0;
    border-top: 1px solid var(--color-rule);
}
body[data-rules="column"] .block + .block { position: relative; padding-top: 0; }
body[data-rules="column"] .block + .block::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: var(--width-article); /* the article BOX width — deliberate: separators span the box, not the inset text measure (Loren, 2026-09-02) */
    max-width: calc(100% - 2 * var(--pad-inline));
    height: 1px;
    background: var(--color-rule);
}

.block-inner {
    max-width: var(--width-article);
    margin: 0 auto;
    padding: 1.25rem var(--pad-inline);
}

.block-inner-wide {
    max-width: var(--width-wide);
    margin: 0 auto;
    padding: 1.5rem var(--pad-inline);
}

/* On a has-toc page, the article's own container has right-padding only
   (deliberate -- see .block-body.has-toc .block-body-inner's own comment),
   which shifts its actual text --pad-inline/2 (12px) left of true viewport
   center. Title, byline, and abstract all still center via plain symmetric
   padding here, so without this they land 12px right of where the article
   body text (and the endmatter, which gets an analogous fix below) actually
   sits -- never noticed until Loren caught the frontmatter reading out of
   alignment. Unlike the endmatter fix, this ISN'T solved by dropping the
   left padding entirely -- these blocks position via plain margin:auto
   centering, not the shared grid, so zeroing padding-left shifts the text
   by the FULL --pad-inline (24px) instead of the 24/2=12px actually needed,
   overshooting in the other direction. A translateX by exactly
   -(--pad-inline/2) shifts the whole (still-symmetrically-padded, so its
   text WIDTH is unchanged) box left by precisely the right amount instead;
   worked out algebraically, the W-dependent terms cancel identically
   whether or not --has-toc-max-width's cap is currently binding, so this
   same constant offset holds at every viewport width. */
/* …but ONLY at widths where that asymmetry exists: below 1140px the mobile
   rules drop the has-toc grid's right-only padding (article back at true
   center), so this compensation would overshoot 12px the other way — the
   exact misalignment it was built to fix (Loren, 2026-09-03). */
@media (min-width: 1141px) {
    body[data-toc="on"] .block-title .block-inner-wide,
    body[data-toc="on"] .block-byline .block-inner,
    body[data-toc="on"] .block-abstract .block-inner {
        transform: translateX(calc(-1 * var(--pad-inline) / 2));
    }
}

.block-byline .block-inner { padding: 1.25rem var(--pad-inline); }
.block-endmatter .block-inner {
    max-width: 1280px;
    padding: 1.5rem var(--pad-inline) 1rem;
}

/* Title block contents */
.post-kicker {
    font-family: var(--font-kicker);
    font-size: var(--size-kicker);
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    /* OUR DESIGN: the kicker carries the post type's tag color (see the
       post-type machinery below) rather than a neutral faint gray. */
    color: var(--color-kicker); /* neutral faint — free-text kickers only */
    margin: 0 0 1.2rem;
    text-align: left;
}

/* (The four-type taxonomy is retired — `type:` is now the internal
   major/minor belief-update bar and renders nothing on post pages.) */

/* OUR DESIGN: the title block is left-aligned and set at a lighter weight
   (was centered, 2.9rem, --font-display-weight). */
.block-title .block-inner-wide {
    max-width: var(--width-article);
    padding: 6rem var(--pad-inline) 1.5rem;
}

.block-title h1 {
    font-family: var(--font-title);
    font-size: var(--size-title);
    font-weight: var(--title-weight);
    line-height: 1.14;
    margin: 0;
    letter-spacing: -0.008em;
    color: var(--color-title);
    text-align: left;
    text-wrap: balance;
}

.block-title .subtitle {
    font-family: var(--font-subtitle);
    font-style: var(--subtitle-style);
    font-size: var(--size-subtitle);
    color: var(--color-subtitle);
    margin: 0.85rem 0 0;
    line-height: 1.4;
    text-align: left;
    max-width: 44em;
}

/* --------------------------------------------------------------------------
   OUR DESIGN: hero figure — its own .block-hero section (was inside
   .block-title) so the data-hero axis can reorder or hide it. The old
   .block-title-scoped selectors are kept alongside for any page built from an
   older template.
   -------------------------------------------------------------------------- */
.block-hero .block-inner-wide {
    max-width: var(--width-article);
    padding: 0.5rem var(--pad-inline) 0.5rem;
}

.block-hero figure.hero,
.block-title figure.hero {
    margin: 0;
    /* hero-width: (front matter / {hero width=…}) caps the figure, centered */
    width: min(var(--hero-width, 100%), 100%);
    margin-left: auto;
    margin-right: auto;
}
.block-title figure.hero { margin-top: 1.75rem; }

.block-hero figure.hero video,
.block-hero figure.hero img,
.block-title figure.hero video,
.block-title figure.hero img {
    display: block;
    width: 100%;
    height: auto;
    max-height: var(--hero-height, 380px); /* hero-height: / {hero height=…} */
    object-fit: contain;
}

/* Hero caption: geometrically identical to every other caption — left-aligned
   on the text column in every option (Loren, 2026-08-13: caption options set
   typography only, never geometry). Never numbered: the hero is an opener,
   not a figure-of-record, and it sits outside <article> so the counter never
   reaches it. Italic follows the option like body captions do. */
.block-hero figure.hero figcaption,
.block-title figure.hero figcaption {
    font-family: var(--font-captions);
    font-size: var(--size-caption);
    font-style: italic;
    color: var(--color-caption);
    margin: var(--caption-gap) 0 0;
    line-height: 1.5;
    max-width: calc(var(--width-article) - 2 * var(--pad-inline));
    text-align: left;
}
body[data-caption-style="upright"] .block-hero figure.hero figcaption,
body[data-caption-style="upright"] .block-title figure.hero figcaption {
    font-style: normal;
}

/* --------------------------------------------------------------------------
   OUR DESIGN: byline stack (default)

   A left-aligned Commit Mono stack — authors / venue+link / date /
   correspondence — the canonical byline (the old authors+published grid was
   retired 2026-08-18; multi-author affiliation needs are covered by the
   data-affil axis).
   -------------------------------------------------------------------------- */
.byline-stack {
    font-family: var(--font-byline);
    font-size: var(--size-meta);
    line-height: var(--byline-lh);
    color: var(--color-byline);
}
.byline-stack p { margin: 0; }
.byline-stack p + p { margin-top: var(--byline-gap); }
.byline-stack .byline-authors-line { font-weight: 700; color: var(--color-byline-authors); font-size: var(--size-byline-authors); }
.byline-stack .byline-venue-line { color: var(--color-byline-venue); font-size: var(--size-byline-venue); }
.byline-stack .byline-venue-line .venue { font-style: italic; font-weight: 400; }
/* venue/arXiv/DOI links take the base dotted treatment (one idiom for every
   byline link — Loren, 2026-09-02); only layout is special here */
.byline-stack .byline-venue-line .venue-link {
    margin-left: 1.4em;
    color: var(--color-byline);
}

/* Paper credit line — posts deeply tied to a paper show both the post's
   writers (the bold authors line, which gains a "Post by" role label) and
   the paper's writers. Wording is the data-paperline axis:
     labeled (default) — "Post by …" / "Paper by …"
     prose             — authors unlabeled; "Based on the paper by …"
   Venue/arXiv/DOI describe the paper, so they render on this line. */
.byline-stack .byline-paper-line { color: var(--color-byline-secondary); font-size: var(--size-byline-paper); }
.byline-stack .byline-paper-line .paper-authors { color: var(--color-byline); }
/* Paper author names take the post-author ink; bold marks NEW names — a
   paper author already on the post line above renders unbolded (dedup).
   data-paper-bold="plain" unbolds everyone (lab toggle). */
.byline-stack .byline-paper-line .pa { color: var(--color-byline-paper, var(--color-byline)); font-weight: 700; }
.byline-stack .byline-paper-line .pa.pa-repeat { font-weight: 400; }
body[data-paper-bold="plain"] .byline-stack .byline-paper-line .pa { font-weight: 400; }

.byline-stack .byline-paper-line .venue { font-style: italic; font-weight: 400; margin-left: 1.4em; }
.byline-stack .byline-paper-line .venue-link { margin-left: 1.4em; }
.byline-role-label,
.paper-label-labeled,
.paper-label-prose { color: var(--color-byline-meta); font-weight: 400; }
/* wording axis */
body:not([data-paperline]) .paper-label-prose,
body[data-paperline="labeled"] .paper-label-prose { display: none; }
body[data-paperline="prose"] .paper-label-labeled { display: none; }
body[data-paperline="prose"] .byline-role-label { display: none; }
/* Prose wording reads as a sentence — hide the scholarly superscripts there;
   the labeled wording keeps full parity with the post authors line. */
body[data-paperline="prose"] .byline-paper-line sup { display: none; }


.byline-stack .byline-venue-line .venue-link:first-child { margin-left: 0; }
.byline-stack .byline-date-line { color: var(--color-byline-date); font-size: var(--size-byline-date); letter-spacing: 0.02em; }
/* read time rides the date line's tokens; hovering swaps it for the word
   count (two spans, CSS-only — the version-line zero-new-tokens precedent) */
.read-time { cursor: default; }
.read-time .rt-words { display: none; }
.read-time:hover .rt-time { display: none; }
.read-time:hover .rt-words { display: inline; }
/* Version line (versions.mjs → template): "v2, revised 3 September 2026.
   Earlier: v1 (27 May 2026)" — same faint mono as the date line. */
.byline-stack .byline-version-line { color: var(--color-byline-date); font-size: var(--size-byline-date); letter-spacing: 0.02em; }
/* version links take the base dotted treatment (was a private --color-rule
   alpha with the codebase's only em-relative offset) */
.byline-stack .byline-version-line a { color: inherit; }
.byline-stack .byline-corr-line {
    margin-top: calc(var(--byline-gap) + 0.4rem);
    font-size: var(--size-byline-corr);
    color: var(--color-byline-corr);
    letter-spacing: 0.01em;
}
.byline-stack sup { font-size: 0.75em; color: var(--color-byline-meta); }

/* Affiliation line — same faint mono treatment as the date line. Shown only in
   the data-affil modes below. */
.byline-stack .byline-affil-line {
    display: none;
    color: var(--color-byline-affil);
    font-size: var(--size-byline-affil);
    letter-spacing: 0.02em;
}

/* ---- affiliations axis (data-affil): none (default) / line / indexed ----
   Superscript markers live in the markup always; they only become visible in
   `indexed` mode, so no JS text mutation is needed. */
sup.affil-idx {
    display: none;
    font-size: 0.75em;
    font-weight: 400;
    color: var(--color-byline-meta);
    padding-left: 1px;
}
body[data-affil="indexed"] sup.affil-idx { display: inline; }
sup.affil-idx-key {
    font-size: 0.75em;
    color: var(--color-byline-meta);
    padding-right: 2px;
}
body[data-affil="line"] .byline-stack .byline-affil-line.affil-plain { display: block; }
body[data-affil="indexed"] .byline-stack .byline-affil-line.affil-indexed { display: block; }

/* one variant renders at a time (grid retired 2026-08-18 — stack is
   canonical; its markup left history in the same commit as this rule) */
body[data-byline="metaline"] .byline-stack { display: none; }

/* ---- metaline — index-style single mono line ---- */
.byline-metaline {
    display: none;
    font-family: var(--font-byline);
    font-size: var(--size-meta);
    line-height: var(--byline-lh);
    margin: 0;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 0.55rem;
    row-gap: 6px;
}
.byline-metaline .author { font-weight: 700; color: var(--color-byline); }
.byline-metaline .date { color: var(--color-byline-meta); letter-spacing: 0.02em; }
.byline-metaline .tag::before,
.byline-metaline .date::before {
    content: "·";
    margin-right: 0.55rem;
    color: var(--color-byline-meta);
    font-weight: 400;
}
body[data-byline="metaline"] .byline-metaline { display: flex; }

/* --------------------------------------------------------------------------
   Byline framing (data-byline-frame): none (DEFAULT) / rules / tint / box / bar
   All applied to .block-byline .block-inner, so every frame is exactly the
   width of the text column and lines up with the title and body.
   -------------------------------------------------------------------------- */
body[data-byline-frame="rules"] .block-byline .block-inner {
    border-top: 1px solid var(--color-rule);
    border-bottom: 1px solid var(--color-rule);
    padding-top: 1rem;
    padding-bottom: 1rem;
}
body[data-byline-frame="tint"] .block-byline .block-inner {
    background: var(--color-tint);
    border-radius: 4px;
    padding: 1rem 1.25rem;
}
body[data-byline-frame="box"] .block-byline .block-inner {
    border: 1px solid var(--color-rule);
    border-radius: 4px;
    padding: 1rem 1.25rem;
}
/* bar — solid left rule; --byline-bar-color is a hook for the tag color later.
   Keep in step with the abstract's bar variant below. */
body[data-byline-frame="bar"] .block-byline .block-inner {
    border-left: 3px solid var(--byline-bar-color, var(--color-text));
    padding-left: 1.25rem;
}

/* --------------------------------------------------------------------------
   Byline — Anthropic transformer-circuits style
   Authors flow inline (comma-separated) on the left; affiliations and
   published date stack on the right.
   -------------------------------------------------------------------------- */
.post-byline {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.5rem 3rem;
    width: 100%;
}

.byline-group h3 {
    font-family: var(--font-ui);
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-text-faint);
    margin: 0 0 0.5rem;
    line-height: 1.4;
}

.byline-group p {
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 1.5;
    margin: 0;
    color: var(--color-text);
}

/* Author cards: each author's name with their affiliation beneath, flowing
   as a grid across the byline width. Ties affiliation to author and keeps
   the block one or two rows tall regardless of author count. */
.byline-author-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.9rem 1.75rem;
}

.byline-author .author-name { margin: 0; }

.byline-author .author-affiliation {
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.4;
    color: var(--color-text-muted);
    margin: 0.15rem 0 0;
}

/* `byline-style: compact` — comma-separated author list with superscript
   affiliation numbers and a deduped affiliation line (byline.lua prepares
   the numbering; the superscripts vanish when all authors share one
   affiliation). */
.byline-authors .byline-author-list { line-height: 1.7; }

.byline-author-list sup,
.byline-affiliation-line sup {
    font-size: 10px;
    color: var(--color-text-muted);
}

.byline-authors .byline-affiliation-line {
    font-family: var(--font-body);
    font-size: 12px;
    line-height: 1.6;
    color: var(--color-text-muted);
    margin: 0.35rem 0 0;
}

.byline-affiliation-line .affil { white-space: nowrap; }

.byline-meta {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

/* base dotted treatment (was a border-bottom fake at the wrong height) */
.byline-group a { color: inherit; }

.byline-footnote {
    grid-column: 1 / -1;
    margin-top: 1rem;
    font-family: var(--font-body);
    font-size: 12px;
    color: var(--color-text-faint);
    line-height: 1.55;
}

/* --------------------------------------------------------------------------
   Abstract — paper-style block between the byline and the body.
   Its own .block, so the full-width rules above and below come for free.
   A narrower centered column than the article, with a small-caps label
   matching the byline group headings.
   -------------------------------------------------------------------------- */
.block-abstract .block-inner { padding: 1.6rem var(--pad-inline) 1.75rem; }

/* OUR DESIGN: the abstract runs the full text column, left-aligned
   (was a 580px centered column). */
.post-abstract {
    max-width: 100%;
    margin: 0;
}

/* "Abstract" names a part of the document (like Contents, References,
   Appendix) rather than a value from a controlled vocabulary (like the
   kicker's post-type tag) -- display face in small caps, same as the TOC
   title, not the kicker's mono treatment. Sized/weighted apart from the
   kicker on purpose (14px Cardo small-caps vs. 12px mono uppercase) so the
   two register as deliberately distinct voices where they sit close
   together at the top of the post, not as one inconsistent label style. */
.post-abstract h3 {
    font-family: var(--font-headings);
    font-size: 14px;
    font-weight: var(--heading-weight);
    font-variant-caps: small-caps;
    letter-spacing: 0.03em;
    color: var(--color-abstract-label);
    text-align: left;
    margin: 0 0 0.7rem;
}

.post-abstract p {
    font-family: var(--font-body);
    font-size: var(--size-abstract);
    line-height: 1.7;
    margin: 0 0 0.9rem;
    color: var(--color-abstract);
}

.post-abstract p:last-child { margin-bottom: 0; }

/* OUR DESIGN: abstract style variants (data-abstract on <body>):
   left (DEFAULT, the base rules above) / centered / lede / boxed / bar. */
body[data-abstract="centered"] .post-abstract {
    max-width: 580px;
    margin: 0 auto;
}
body[data-abstract="centered"] .post-abstract h3 {
    font-size: 12px;
    letter-spacing: 0.03em;
    text-align: center;
    margin-bottom: 0.9rem;
}
body[data-abstract="centered"] .post-abstract p { text-align: center; }

/* lede — label hidden, copy set as a standfirst. Renders at --size-abstract
   exactly (no hidden multiplier): a bigger-than-body standfirst is authored by
   setting the abstract size above the body size. */
body[data-subtitle-style="upright"] { --subtitle-style: normal; }
body[data-title-weight-mobile="400"] { --title-weight-mobile: 400; }

/* the label is governed by data-abstract-label (default on), not the variant */
body[data-abstract-label="off"] .post-abstract h3 { display: none; }
body[data-abstract="lede"] .post-abstract p {
    line-height: 1.55;
    color: var(--color-abstract); /* body ink by default (Loren, 2026-09-02); the muted standfirst look is one Color Lab change away */
}

/* ---- abstract/body divider (data-abstract-rule): off / column / full ----
   A rule ONLY between the abstract and the start of the main text (DOM
   adjacency; the general data-rules axis separates every block instead).
   Style rides data-abstract-rule-style (solid/dotted/dashed); color is the
   --abstract-rule-color token (Color Lab). Column variant uses a border on a
   zero-height ::before, not a background, so dotted/dashed render. */
/* ---- body/endmatter divider (data-endmatter-rule): off / column / full ----
   Marks the register shift from article prose into the apparatus (footnotes,
   references, citation block). Ships ON at column width; style rides
   data-endmatter-rule-style, color is --endmatter-rule-color (Color Lab).
   Same construction as the abstract rule below. */
body[data-endmatter-rule-style="dotted"] { --endmatter-rule-style: dotted; }
body[data-endmatter-rule-style="dashed"] { --endmatter-rule-style: dashed; }
/* --endmatter-gap owns the WHOLE boundary, like the appendix gap: the
   article's bottom padding, the last element's trailing margin (4rem when the
   body ends on a figure), and the endmatter frame's top padding all stacked
   around the token, skewing the rule off the visual midpoint (Loren,
   2026-09-05). Zero every contributor on both sides. */
.block-body article,
.block-body.has-toc:has(+ .block-endmatter) .block-body-inner > article { padding-bottom: 0; } /* :has bumps past the has-toc article rule's specificity tie */
.block-body:has(+ .block-endmatter) { padding-bottom: 0; } /* the block's own 2rem tail */
.block-body article > section:last-child { margin-bottom: 0; }
/* the trailing-margin chain must reach the DEEPEST last element — appendices
   end in nested subsections (measured: 91px of stacked margins got past the
   one-level version, skewing the endmatter rule 113/22 instead of 22/22) */
.block-body article > section:last-child > :nth-last-child(1 of :not(style, script)),
.block-body article > section:last-child > section:last-child > :nth-last-child(1 of :not(style, script)),
.block-body article > section:last-child > section:last-child > section:last-child > :nth-last-child(1 of :not(style, script)) { margin-bottom: 0; }
.block-endmatter .block-inner { padding-top: 0; }
.block-endmatter .endmatter-section:first-child > :first-child { margin-top: 0; }

/* ---- abstract boundary: same honesty treatment as the other two ----------
   --abstract-body-gap owns the whole space: the abstract frame's bottom
   padding, the article's top padding, and the first element's top margin all
   used to stack around the token. */
.block-byline + .block-abstract { padding-top: var(--byline-abstract-gap); }
.block-byline .block-inner { padding-bottom: 0; }
.block-byline + .block-abstract .block-inner { padding-top: 0; }
.block-abstract .post-abstract h3 { margin-top: 0; }
.block-abstract .block-inner { padding-bottom: 0; }
.post-abstract p:last-child { margin-bottom: 0; }
.block-abstract + .block-body article,
.block-abstract + .block-body.has-toc .block-body-inner > article { padding-top: 0; } /* second selector outranks the has-toc article rule */
/* "first" must mean first VISIBLE — posts open with raw <style>/<script>
   runs (per-post widget code), so plain :first-child hit an invisible node
   and the real first heading kept its ~68px margin (Eric's nth-child(1 of …)
   idiom from the TOC first-heading alignment) */
.block-abstract + .block-body article > :nth-child(1 of :not(style, script)),
.block-abstract + .block-body article > section:nth-child(1 of :not(style, script)) > :nth-child(1 of :not(style, script)) { margin-top: 0; }

body[data-endmatter-rule="column"] .block-body + .block-endmatter,
body[data-endmatter-rule="full"] .block-body + .block-endmatter { position: relative; }
body[data-endmatter-rule="column"] .block-body + .block-endmatter::before,
body[data-endmatter-rule="full"] .block-body + .block-endmatter::before {
    content: "";
    position: absolute;
    top: calc(var(--endmatter-gap) * var(--endmatter-rule-pos));
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--width-article); /* the article BOX width — deliberate: separators span the box, not the inset text measure (Loren, 2026-09-02) */
    max-width: calc(100% - 2 * var(--pad-inline));
    height: 0;
    border-top: 1px var(--endmatter-rule-style, solid) var(--endmatter-rule-color);
}
body[data-endmatter-rule="full"] .block-body + .block-endmatter::before {
    width: 100%;
    max-width: 100%;
}

/* ---- "five dots" separator style (data-*-rule-style="dots"): a centered
   asterism replaces the line — width setting becomes moot. Shared by the
   abstract and endmatter rules; color rides each rule's own token. */
body[data-abstract-rule-style="dots"]:is([data-abstract-rule="column"], [data-abstract-rule="full"]) .block-abstract + .block-body { position: relative; border-top: none; }
body[data-abstract-rule-style="dots"]:is([data-abstract-rule="column"], [data-abstract-rule="full"]) .block-abstract + .block-body::before {
    content: "\2022\2002\2022\2002\2022\2002\2022\2002\2022";
    position: absolute;
    top: calc(var(--abstract-body-gap) * var(--abstract-rule-pos));
    left: 50%;
    transform: translate(-50%, -50%);
    width: auto;
    max-width: none;
    height: auto;
    border-top: none;
    font-size: 9px;
    line-height: 1;
    color: var(--abstract-rule-color);
}
body[data-endmatter-rule-style="dots"]:is([data-endmatter-rule="column"], [data-endmatter-rule="full"]) .block-body + .block-endmatter { position: relative; border-top: none; }
body[data-endmatter-rule-style="dots"]:is([data-endmatter-rule="column"], [data-endmatter-rule="full"]) .block-body + .block-endmatter::before {
    content: "\2022\2002\2022\2002\2022\2002\2022\2002\2022";
    position: absolute;
    top: calc(var(--endmatter-gap) * var(--endmatter-rule-pos));
    left: 50%;
    transform: translate(-50%, -50%);
    width: auto;
    max-width: none;
    height: auto;
    border-top: none;
    font-size: 9px;
    line-height: 1;
    color: var(--endmatter-rule-color);
}

body[data-abstract-rule-style="dotted"] { --abstract-rule-style: dotted; }
body[data-abstract-rule-style="dashed"] { --abstract-rule-style: dashed; }
/* the abstract->body gap is its own token (bigger than the generic block
   rhythm); applies regardless of the rule axis */
.block-abstract + .block-body { padding-top: var(--abstract-body-gap); }
.block-body + .block-endmatter { padding-top: var(--endmatter-gap); }

body[data-abstract-rule="column"] .block-abstract + .block-body,
body[data-abstract-rule="full"] .block-abstract + .block-body { position: relative; }
body[data-abstract-rule="column"] .block-abstract + .block-body::before,
body[data-abstract-rule="full"] .block-abstract + .block-body::before {
    content: "";
    position: absolute;
    top: calc(var(--abstract-body-gap) * var(--abstract-rule-pos));
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--width-article); /* the article BOX width — deliberate: separators span the box, not the inset text measure (Loren, 2026-09-02) */
    max-width: calc(100% - 2 * var(--pad-inline));
    height: 0;
    border-top: 1px var(--abstract-rule-style, solid) var(--abstract-rule-color);
}
body[data-abstract-rule="full"] .block-abstract + .block-body::before {
    width: 100%;
    max-width: 100%;
}

body[data-abstract="boxed"] .post-abstract {
    background: var(--color-tint);
    border-radius: 4px;
    padding: 1.1rem 1.25rem 1.2rem;
}

/* bar — the byline frame's bar device reused; applied to .block-inner so both
   bars land on exactly the same x (see the byline bar rule). */
body[data-abstract="bar"] .block-abstract .block-inner {
    border-left: 3px solid var(--abstract-bar-color, var(--color-text));
    padding-left: 1.25rem;
}

/* --------------------------------------------------------------------------
   Article body (inside .block-body)
   With an optional table of contents in the left gutter (transformer-circuits
   style). Layout switches to a grid when .has-toc is present.
   -------------------------------------------------------------------------- */
.block-body { padding-bottom: 2rem; }

.block-body-inner {
    max-width: var(--width-article);
    margin: 0 auto;
}

.block-body article {
    max-width: var(--width-article);
    margin: 0 auto;
    padding: 2rem var(--pad-inline) 1.5rem;
}

/* First heading aligns its top with the TOC "Contents" heading: drop the
   leading top margin so both start at the shared 2rem padding line. */
.block-body article > section:first-child > :first-child,
.block-body article > :first-child {
    margin-top: 0;
}
/* A post that opens with raw <style>/<script> blocks (per-post CSS + JS)
   still needs its first *visible* section aligned — those elements count as
   :first-child despite rendering nothing, so key on the first child that
   isn't one of them. */
.block-body article > section:nth-child(1 of :not(style, script)) > :first-child {
    margin-top: 0;
}

/* With-TOC mode: 3-column grid (left gutter | text column | right gutter),
   ALWAYS symmetric -- this never changes shape at any viewport width the
   TOC is shown inline at (no separate narrow-viewport formula). The article
   stays centered on the viewport; the TOC's own box sits inside the left
   gutter, but does NOT stretch to fill it (see .post-toc's
   justify-self:start + min/max-width below) -- the gutter *track* still
   reaches this container's own edge (the same effect spine/peek/gutter get
   from position:fixed; left:16px, but achieved by widening the grid track
   instead of pulling the TOC out of flow, which would also remove the
   scroll-with-the-page behavior "off" mode depends on), but the TOC box
   itself hugs its own content, capped at .post-toc's max-width -- the extra
   gutter space beyond that is just background, not part of the TOC. As the
   viewport narrows, this gutter naturally shrinks along with it (both 1fr
   tracks shrink equally, keeping the article centered); once the gutter
   would have to go narrower than .post-toc's own min-width, toc-sticky.js
   switches the TOC to the dropdown rather than letting it overflow the
   gutter or forcing the article off-center -- see collapseThreshold() and
   the data-toc-squeeze-collapse rule further down. */
.block-body.has-toc .block-body-inner {
    /* --has-toc-max-width (article + 2 * --toc-max-gutter), not none -- an
       earlier version left this fully uncapped so the gutter (and the TOC
       pinned to its left edge) would grow without bound on a really wide
       screen, isolating the TOC near the true viewport edge instead of
       keeping it a bounded distance from the text. Centered via margin:auto
       once the cap actually binds; below that width this is a no-op and
       the container just fills 100% (unbounded, edge-to-edge gutters)
       exactly like before -- the explicit max-width is still needed rather
       than just omitting it, though, since the base .block-body-inner rule
       (lower specificity, still applies to anything this rule doesn't set)
       caps max-width at var(--width-article) for the no-TOC case, and that
       ~710px cap would otherwise leak in here too. */
    max-width: var(--has-toc-max-width);
    margin: 0 auto;
    display: grid;
    /* --has-toc-cols, not a literal value -- shared with .endmatter-section
       so the two always agree at every tier (see the token comment). */
    grid-template-columns: var(--has-toc-cols);
    gap: 0;
    align-items: start;
    padding-right: var(--pad-inline);
}

.block-body.has-toc .block-body-inner > article {
    grid-column: 2;
    max-width: 100%;
    margin: 0;
    padding: 2rem var(--pad-inline) 1.5rem;
    min-width: 0;
}

.block-body.has-toc .post-toc {
    grid-column: 1;
    /* start, not stretch -- the TOC hugs its own content instead of filling
       the whole (possibly very wide, on an ultrawide monitor) gutter track.
       Without this, .toc-endmatter's divider rule (and the appendix
       separator) would stretch to the full track width too, running far
       past the actual heading text instead of hugging it. */
    justify-self: start;
    width: fit-content;
    /* max-width is what makes a long heading wrap instead of forcing the
       box wider; min-width is the floor toc-sticky.js's collapseThreshold()
       uses to decide when the left gutter has gotten too narrow to hold
       the TOC and it should move to the dropdown instead (see there). */
    min-width: var(--toc-min-width);
    max-width: var(--toc-max-width);
    /* Left padding matches the EXPANDED dropdown panel's own total text
       inset, not just its left:16px fixed offset -- that offset alone left
       the inline TOC's text ~17px left of where the exact same nav reads
       once expanded (16px fixed inset + 1px border + 1rem padding = 33px
       total; the gutter track itself spans all the way to the true
       viewport edge, so nothing else was providing inset here). Matching
       this also means text doesn't visibly jump sideways when collapsing
       into or back out of the dropdown -- collapsed-on-scroll switches
       between rendering this SAME nav element inline vs position:fixed. */
    padding: 2rem 1.75rem 1rem calc(16px + 1px + 1rem);
}

.post-toc { font-family: var(--font-ui); }

/* Faded until engaged, same idea and the same 0.3s ease timing as the
   sidenotes' own resting-opacity/hover tuning (--sn-opacity, elsewhere in
   this file) -- a separate token rather than reusing --sn-opacity directly,
   so the two stay independently tunable even though they start at the same
   value. The inline label + list -- NOT the collapsed dropdown bar, which
   gets an equivalent MUTED look below instead of joining this rule
   directly. :focus-within un-fades for keyboard users tabbing into a
   link, not just :hover. */
:root { --toc-opacity: 0.5; } /* Loren, 2026-09-08 */
.post-toc > h3,
.post-toc > nav[role="doc-toc"] {
    opacity: var(--toc-opacity);
    transition: opacity 0.3s ease;
    /* Safari-only text jitter (Loren) traced here, not to the per-link
       prism hover itself: hovering a single link also hovers its
       ancestor .post-toc, re-triggering THIS opacity fade (0.6 -> 1) at
       the same time as the link's own faster 100ms pop. WebKit switches
       antialiasing mode as opacity crosses certain thresholds, visibly
       shifting glyph rendering mid-transition -- unrelated to how the
       link itself is positioned, which is why neither of the two
       earlier attempts (transform, then top/left on the link) touched
       it. Locking font smoothing keeps the glyph rendering mode
       constant through the opacity transition instead of switching. No
       effect outside WebKit (unsupported property elsewhere). */
    -webkit-font-smoothing: antialiased;
}
.post-toc:hover > h3,
.post-toc:hover > nav[role="doc-toc"],
.post-toc:focus-within > h3,
.post-toc:focus-within > nav[role="doc-toc"] {
    opacity: 1;
}

/* Prism mode only (Loren: "I also wonder if the collapsed block should
   be faded by default kind of like the uncollapsed TOC" -- raised while
   specifically testing data-toc-style="prism", not as a general request;
   an earlier version applied this unconditionally and changed plain
   mode's own look too, unintentionally -- "it doesn't look like the
   plain TOC that is currently deployed"). The actual mute/un-mute rules
   live further down, folded into the prism "TOC style" section's own
   border-color rest/hover rules (search --edge-a * --toc-opacity) --
   not here. A same-specificity (0,4,1) rule placed HERE loses to the
   base plain-mode rule below (also (0,4,1)) on source order regardless
   of how it's written, since this section comes first in the file; only
   a rule positioned AFTER the base rule settles the tie in its own
   favor. Left this comment in place since the reasoning (why fading
   needs color-mix instead of opacity at all) still applies down there
   too. */

/* The display face (Cardo) in small caps -- font-variant-caps, not
   text-transform:uppercase, so the leading capital and the small-capped
   rest stay visually distinct (the classic small-caps look) rather than
   every letter rendering as a full-height capital. */
.post-toc > h3 {
    font-family: var(--font-headings);
    font-size: 14px;
    font-weight: var(--heading-weight);
    font-variant-caps: small-caps;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
    margin: 0 0 0.85rem;
}

.post-toc nav {
    font-size: var(--size-toc);
    line-height: 1.3;
}

.post-toc nav ul {
    list-style: none;
    padding: 0;
    margin: 0;
}

/* nav > .toc-dropdown-scroll > ul, not nav > ul directly -- post-structure.mjs
   wraps nav's real children in a .toc-dropdown-scroll div (see its own
   comment further down, near .toc-dropdown-scroll's rules) so the panel's
   overflow:auto doesn't clip the prism recede faces. That wrapper sits
   between nav and the actual <ul>, so nav > ul stopped matching anything
   the moment the wrapper landed -- these three rules (this one, the one
   right below, and the appendix-separator rule further down) all silently
   lost their top-level-only styling, falling back to unstyled browser
   defaults (lighter inherited weight, default underline) even in plain
   mode (Loren: "the top level sections are now underlined by default...
   the weight... looks lower"). */
.post-toc nav > .toc-dropdown-scroll > ul > li {
    margin-bottom: 0.35rem;
}

.post-toc nav > .toc-dropdown-scroll > ul > li > a {
    font-weight: 600;
    color: var(--color-text);
    text-decoration: none;
    display: block;
}

.post-toc nav ul ul {
    padding-left: 0.85rem;
    margin-top: 0.25rem;
    margin-bottom: 0.2rem;
}

.post-toc nav ul ul li {
    margin-bottom: 0.2rem;
}

.post-toc nav ul ul li a {
    font-weight: 400;
    color: var(--color-text-muted);
    text-decoration: none; /* nav class: no resting underline at any level */
}

/* nav-class hover: brighten to ink + the canonical dotted underline.
   Scoped through .toc-dropdown-scroll (not just .post-toc nav a:hover)
   so its specificity (0,3,2) safely beats the top-level rest rule above,
   which gained the same wrapper class and is now (0,2,4) -- a bare
   .post-toc nav a:hover was only (0,2,2), losing on the element-count
   tiebreak (both tie at 2 classes) and silently keeping text-decoration:
   none on hover for top-level entries only, sub-level/endmatter
   unaffected since their own rest rules never depended on nav > ul.
   Meaning is unchanged -- .toc-dropdown-scroll wraps ALL of nav's real
   children (top list AND endmatter), same as .post-toc nav a:hover did
   before the wrapper existed. Longhand text-decoration-* properties, not
   the multi-value shorthand -- Safari drops an unprefixed multi-value
   text-decoration shorthand ENTIRELY (only the -webkit- prefixed form is
   honored), so the underline silently never applied in Safari at all --
   this alone likely explains most of "sections don't react to hover"
   (color still changed, since that's a separate property in the same
   rule; only the text-decoration value itself was invalid/dropped). */
.post-toc nav .toc-dropdown-scroll a:hover { color: var(--color-text); text-decoration-line: underline; text-decoration-style: dotted; text-decoration-color: var(--color-text); text-decoration-thickness: 1px; text-underline-offset: 3px; }

.post-toc nav ul.toc-endmatter {
    margin-top: 0.85rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--color-rule);
}

.post-toc nav ul.toc-endmatter > li > a {
    font-weight: 400;
    color: var(--color-text-muted);
    text-decoration: none;
}

.post-toc nav ul.toc-endmatter > li > a:hover { color: var(--color-text); }

.post-toc nav li.toc-authored-endmatter > a,
.post-toc nav .toc-dropdown-scroll li.toc-authored-endmatter > a {
    font-weight: 400;
    color: var(--color-text-muted);
}

/* --------------------------------------------------------------------------
   OUR DESIGN: per-entry hover, prism mode only (data-toc-style="prism",
   see the dropdown-style section further down for the token/mechanism
   this reuses). Replaces plain mode's underline + color-darken (the three
   :hover rules just above) with the same small extrude-on-hover prism used
   site-wide -- flat at rest, hovering pops the entry forward with a front-
   face stroke (matching the index page's own cards) plus a bottom+right
   face and a small lift toward the reader (the opposite direction from
   the collapsed bar's recede -- these are small text links, not a box the
   reader clicks, so a lift reads as "select me" rather than needing the
   bar's settle-back treatment).

   The faces live on a plain .toc-prism-face SPAN (toc-sticky.js appends
   one inside every nav link, unconditionally -- see there), not directly
   on <a> or on <li>. Two reasons, one per candidate:
   - Not <a>: .post-toc nav a[data-num]::before (further down) already
     uses ::before for the section-number prefix, so reusing it here for
     the bottom face would silently collide (only one ::before per
     element; the two rules' declarations would merge onto the same
     pseudo-element instead of coexisting).
   - Not <li>: an earlier version put the faces there instead (::before/
     ::after weren't claimed on <li>) -- but a top-level entry's <li> also
     contains its nested <ul> of subsections as a sibling of <a>, so <li>'s
     own box (which percentage-sized faces size against) was the WHOLE
     cluster's height, not just the hovered link's own line. Hovering
     "Nonergodic Generators..." made the box behind ALL of 3.1/3.2 grow
     with it (Loren's report: "the box that bounds all of them gets
     raised"). The span is a CHILD of <a> specifically (not a sibling of
     the nested <ul>, which lives in <li>), so inset:0 sizes it to
     exactly the one link's own line, correctly excluding everything
     nested below it.
   --d and the transition live on <a> itself (a real element can drive
   both its own state AND its descendant span's inherited --d in the
   normal parent-to-child direction -- no :has() needed this time, unlike
   the <li> version, since the span is a genuine descendant now). */
body[data-toc-style="prism"] .post-toc nav a {
    position: relative;
    display: block;
    --d: 0px;
    /* breathing room between the box edge and the text (Loren) --
       --pad-x/-y are read again below by .toc-prism-face, which has to
       stretch back out past this padding (negative inset) so the faces
       still line up with the BORDER edge, not the now-inset text. */
    --pad-x: 0.5rem;
    --pad-y: 0.15rem;
    padding: var(--pad-y) var(--pad-x);
    border: 1px solid transparent; /* reserved so hover adds no layout shift */
    contain: layout; /* isolates <a>'s own box size from its descendants' layout */
    /* --d itself is NOT in this transition list (Loren, round 5: bisected
       via Safari's inspector, forcing :hov and disabling declarations one
       at a time -- removing --d from the transition alone made the jitter
       (text shift, sibling jostle, height growth, all of it) disappear;
       every earlier fix in this saga (will-change, top/left, font-
       smoothing, contain) was accurate about its OWN narrow symptom but
       missed the actual mechanism. Safari's support for transitioning an
       @property-registered custom property is a much shakier code path
       than transitioning an ordinary property directly -- and everything
       downstream of that interpolation (transform here, height/width on
       the face pseudo-elements below) was inheriting whatever instability
       came with it. --d itself now changes as a plain, un-transitioned
       cascade jump; transform is what's actually listed below, and a CSS
       transition triggers on ANY computed-value change regardless of why
       it changed, so transform still animates smoothly on its own --
       through the ordinary, solid transform-interpolation path instead of
       the buggy custom-property one. Same fix mirrored onto the face's
       height/width transitions below, since those depended on --d's
       transition too. */
    transition: transform var(--speed, 100ms) ease-out, border-color var(--speed, 100ms) ease-out;
    transform: translate(calc(-1 * var(--d)), calc(-1 * var(--d)));
}
/* :not([data-toc-state="expanded"]) -- per-link prism-pop applies inline/
   pinned only. Once expanded, links revert to classic underline+darken
   instead (Loren, after review: giving the panel its own recede -- see
   the dropdown-style section -- AND letting individual links inside it
   pop independently read as "boxes popping out of a box"; resolved by
   keeping recede at the container level only and reverting the panel's
   OWN links to the plain-mode hover rule far above, which this exclusion
   lets fall back through to unopposed). */
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:hover,
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:focus-visible {
    --d: 3px;
    border-color: rgba(0, 0, 0, var(--edge-a));
    /* the prism is the only hover signal now -- cancel plain mode's
       brighten-to-ink and dotted underline (both still apply otherwise,
       since this rule doesn't touch color/text-decoration on its own). */
    color: inherit;
    text-decoration: none;
}
body[data-toc-style="prism"] .toc-prism-face {
    position: absolute;
    /* inset:0 -- an absolutely positioned child's offsets resolve against
       the containing block's PADDING box, and the padding box is offset
       from the border box by the BORDER width alone; the padding VALUE
       doesn't move where it sits. A negative-inset "compensation" here
       (an earlier version) was wrong on that basis and shifted the faces
       outward by the padding amount, no longer matching the actual box
       edges (Loren: "the front face... does not match the width and
       height of whichever box is getting the prism treatment"). Plain
       inset:0 was already correct before <a> had any padding at all, and
       adding padding doesn't change that. */
    inset: 0;
    pointer-events: none;
}
body[data-toc-style="prism"] .toc-prism-face::before {
    content: "";
    position: absolute;
    pointer-events: none;
    left: -1px;
    top: calc(100% + 1px);
    width: calc(100% + 2px);
    height: var(--d);
    background: rgba(26, 24, 20, var(--face-b-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    border-top: none;
    transform: skewX(45deg);
    transform-origin: top left;
    opacity: 0; /* a 0-height box still paints its border -- see the collapsed-bar comment above */
    /* height transitions directly, not via --d's own transition -- see
       the reasoning on <a> above. */
    transition: opacity var(--speed, 100ms) ease-out, height var(--speed, 100ms) ease-out;
}
body[data-toc-style="prism"] .toc-prism-face::after {
    content: "";
    position: absolute;
    pointer-events: none;
    left: calc(100% + 1px);
    top: -1px;
    width: var(--d);
    height: calc(100% + 2px);
    background: rgba(26, 24, 20, var(--face-r-a));
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    border-left: none;
    transform: skewY(45deg);
    transform-origin: top left;
    opacity: 0;
    /* width transitions directly, not via --d's own transition -- see
       the reasoning on <a> above. */
    transition: opacity var(--speed, 100ms) ease-out, width var(--speed, 100ms) ease-out;
}
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:hover .toc-prism-face::before,
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:hover .toc-prism-face::after,
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:focus-visible .toc-prism-face::before,
body[data-toc-style="prism"] .post-toc:not([data-toc-state="expanded"]) nav a:focus-visible .toc-prism-face::after {
    opacity: 1;
}

/* --------------------------------------------------------------------------
   OUR DESIGN: TOC sticky (data-toc-sticky): off (DEFAULT) / pinned /
   collapse-on-scroll. .post-toc and <article> land in the same implicit grid
   row (.block-body.has-toc .block-body-inner is `display: grid;
   align-items: start`, no grid-row set on either) -- grid sizes the row to
   the taller item regardless of align-items, so .post-toc's sticky
   containing block already spans the full article height; plain
   `position: sticky` just works here, no restructuring needed.
   -------------------------------------------------------------------------- */
:root { --toc-sticky-top: 2rem; }

/* pinned: tracks scroll for the whole article; internal scroll (never crop)
   once its own content is taller than the viewport allows. */
body[data-toc-sticky="pinned"] .block-body.has-toc .post-toc {
    position: sticky;
    top: var(--toc-sticky-top);
    max-height: calc(100vh - var(--toc-sticky-top) - 2rem);
    overflow-y: auto;
}

/* collapse-on-scroll: toc-sticky.js toggles data-toc-state on .post-toc --
   "inline" (default) | "collapsed" | "expanded". The compact bar only shows
   in the latter two; the full <nav> only changes rendering in "expanded" (a
   floating panel reusing the same element -- no duplicate doc-toc landmark,
   no duplicated ids).

   The bar and expanded panel are position: fixed, not sticky -- the same
   technique post-toc-lab.js's peek mode already uses (its own comment:
   "position:fixed's containing block is the viewport itself, so it escapes
   [ancestor] clipping entirely"). A sticky (or absolute) descendant is
   bounded by its nearest positioned ancestor's OWN box; for a child of
   .post-toc (a CSS grid item with align-items:start), that's .post-toc's
   own short, unstretched box -- NOT the full grid-row height a grid item
   gets when IT is sticky directly, which is what pinned mode relies on.
   That box has typically already scrolled out of view entirely by the
   time collapse triggers, so a sticky child ends up display:flex and
   correctly positioned per state with no visible room left to render in.
   position:fixed sidesteps this whole class of containing-block subtlety.

   left matches spine/peek/gutter's own fixed inset exactly (both now
   --toc-dropdown-inset, see the :root comment) -- same upper-left anchor
   everywhere, desktop or narrow, rather than tracking wherever .post-toc's
   own (now viewport-spanning) column happens to sit.
   The bar's width hugs its own label text (width: fit-content) rather than
   matching that column's width, capped so a long section title truncates
   with an ellipsis (see .toc-collapsed-label) instead of growing the bar
   arbitrarily wide. No JS positioning needed for any of this anymore. */
.post-toc .toc-collapsed-bar { display: none; }

body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="collapsed"] .toc-collapsed-bar,
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="expanded"] .toc-collapsed-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    position: fixed;
    left: var(--toc-fixed-left);
    top: var(--toc-dropdown-inset);
    width: fit-content;
    max-width: min(var(--toc-dropdown-width), calc(100vw - 32px));
    padding: 0.4rem 0.65rem;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 4px;
    font-family: var(--font-ui);
    font-size: var(--size-toc);
    font-weight: 600;
    color: var(--color-text);
    cursor: pointer;
    z-index: 40;
    transition: opacity 0.3s ease;
}

/* Mobile only -- toc-sticky.js's update() only sets data-toc-idle inside
   its isMobileWidth() branch, so this rule never fires on desktop even
   though nothing here checks viewport width itself. Once the compact bar
   is showing, sitting still for a second fades it out rather than leaving
   it on screen indefinitely -- reappears the instant they scroll again
   (toc-sticky.js clears this attribute on every scroll event, not just
   ones that change position). pointer-events:none once faded so it
   doesn't intercept taps on whatever's underneath. */
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-idle="on"] .toc-collapsed-bar {
    opacity: 0;
    pointer-events: none;
}

/* Truncates with an ellipsis instead of wrapping -- a long section title
   would otherwise grow the bar's own height, which should stay constant
   regardless of which section the reader is currently in. min-width: 0
   overrides the flex-item default (auto), which would otherwise let the
   label overflow its allotted space instead of actually shrinking to it. */
.toc-collapsed-label {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Matches the inline TOC's own number treatment exactly (font-family:
   var(--font-code); color: var(--color-text-faint); see the
   data-secnum ::before rule above) so a number reads the same whether it's
   in the static list or the collapsed dropdown label. toc-sticky.js only
   inserts this span when a number is actually showing (data-secnum on). */
.toc-collapsed-num {
    font-family: var(--font-code);
    font-size: 0.85em;
    color: var(--color-text-faint);
    margin-right: 0.3em;
}

.toc-sentinel { height: 0; }

/* the bar's own label already reads "Contents" -- showing the static
   heading above it too would just duplicate that. */
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="collapsed"] > h3,
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="expanded"] > h3 {
    display: none;
}

body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="collapsed"] nav[role="doc-toc"] {
    display: none;
}
/* nav itself is now just the fixed-position frame -- position, size caps,
   background, border. The actual scrolling (and its padding) live on
   .toc-dropdown-scroll, a plain wrapper the build inserts around
   nav's real children -- NOT on nav directly, even though nav is the
   more "natural" place for it. overflow-y:auto forces overflow-x to
   compute as "auto" too (CSS requires both axes to leave "visible"
   together, never just one), which silently clips anything extending
   past the scrolling element's own box -- including the prism recede
   faces' bottom/right extrusion once data-toc-style="prism" is active
   (Loren: "something is still off about... the recede on the dropdown
   menu"). Since nav itself carries no overflow property here anymore,
   its ::before/::after (further down) render against an unclipped box. */
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"] {
    display: block;
    position: fixed;
    left: var(--toc-fixed-left);
    top: calc(var(--toc-dropdown-inset) + 2.6rem);
    width: var(--toc-dropdown-width);
    max-width: calc(100vw - 32px);
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    z-index: 45;
}
body[data-toc-sticky="collapse-on-scroll"] .post-toc[data-toc-state="expanded"] .toc-dropdown-scroll {
    max-height: calc(100vh - var(--toc-dropdown-inset) - 2.6rem - 2rem);
    overflow-y: auto;
    padding: 0.9rem 1rem;
}
/* Prism mode only, same reasoning as the collapsed bar's fade just above
   -- raised while testing prism specifically, an unconditional version
   would silently change plain mode's own (currently deployed) scrollbar
   too. The reader can still scroll it (wheel/trackpad/keyboard) -- only
   the scrollbar's own visual chrome is hidden (Loren: "I don't like
   being able to see the slider on the drop down menu"). Both properties
   are required: scrollbar-width for Firefox, ::-webkit-scrollbar for
   Chrome/Safari -- neither alone covers every engine this site targets. */
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-dropdown-scroll {
    scrollbar-width: none;
}
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-dropdown-scroll::-webkit-scrollbar {
    display: none;
}

/* --------------------------------------------------------------------------
   OUR DESIGN: TOC style (data-toc-style): plain (DEFAULT) / prism.

   "plain" is the flat treatment above, no changes anywhere.

   "prism" is two distinct treatments, not one rule reused three ways
   (an earlier version gave the always-visible inline/pinned list the
   same persistent bordered box as the collapsed bar -- reviewed and
   rejected: the list should read as plain at rest, full stop):

   - The inline/pinned list stays completely plain at rest (no border, no
     background -- literally untouched by this whole section). Only
     individual entries change, and only on hover -- see the per-link
     rules further down, after .post-toc nav a's own definitions.

   - The collapsed bar AND expanded panel BOTH get the sharp-cornered box
     look (border, no radius) and BOTH recede on hover -- reversed from
     an earlier version that gave the panel none of this, after review:
     giving the panel its own recede while ALSO letting individual links
     inside it pop independently read as "boxes popping out of a box"
     (Loren) -- resolved by keeping the container-level recede on both
     bar and panel, but reverting the LINKS inside the panel specifically
     back to classic underline+darken (see the per-link section further
     down, after .post-toc nav a's own definitions, for the :not([data-
     toc-state="expanded"]) scoping that makes this split happen). The
     inline/pinned list's own links are untouched by any of this --
     still plain at rest, still prism-pop on hover.

     Reuses the .fbar/.prism precedent (--edge-a, --face-b-a, --face-r-a,
     the registered --d custom property, the skewed ::before/::after face
     technique). Unlike .fbar/.prism's default "lift toward the viewer"
     direction, this sets no transform at all -- the box itself never
     moves, only the faces grow behind it, the same "static anchoring"
     .align-static .prism uses ("hovering pushes the bar's far end deeper
     into the page rather than pulling it toward the viewer"). Reads as
     the box settling/receding rather than popping out (Loren: "when you
     hover over it the prism recedes behind"). Sharp corners -- a
     border-radius would visually collide with the skewed faces meeting
     the box's edges, so it's zeroed.

     Faces anchor to the BORDER box, not simply top:100%/left:100% (which
     .fbar itself uses, but which lands 1px off here -- Loren: "the faces
     aren't all flush"): those percentages resolve against the PADDING
     box, 1px inside the 1px border, so a face positioned that way starts
     a pixel early/short. The -1px/+1px/+2px terms below are the same
     border compensation the wordmark's own .wm-lt faces and the per-link
     faces already use (see either for the fuller rationale) -- .fbar
     never needed it because nothing had flagged the 1px gap there.

     The faces are OPAQUE (color-mix against --color-bg), not the
     translucent ink wash .fbar/.prism/per-link entries use -- those sit
     against a plain background or empty gutter, but the bar and panel
     both float fixed-position directly over live article text, and a
     true rgba() wash let that text show through, tinted, underneath them
     (Loren: "shouldn't be able to see text from the main text through
     it"). color-mix produces a genuinely opaque result matching what the
     same rgba(26,24,20,--face-b-a/--face-r-a) wash would LOOK like
     against the page background, without any actual transparency --
     calc(var(--face-b-a) * 100%) converts the existing decimal alpha
     token into the percentage color-mix expects, so the two systems
     can't drift out of sync with each other.

     The bar's border is MUTED at rest, full contrast on hover/focus --
     same "faded until engaged" idea the whole TOC uses (see above),
     applied per-channel via color-mix instead of true opacity for the
     same reason the fade section explains. The panel stays full-contrast
     always; once open, the reader is actively using it, not sitting idle
     over it, so muting its border doesn't make sense the way it does for
     the passive bar.

     Both selectors below repeat the [data-toc-state="..."] ancestor
     context from the base display:flex rule above, rather than the bare
     body[data-toc-style="prism"] .toc-collapsed-bar this started as --
     without it, this rule's specificity (0,2,1) loses outright to that
     base rule's (0,4,1) and border-radius:6px keeps winning regardless
     of source order. Matching the shape ties the specificity instead,
     and coming later in the file settles the tie in this rule's favor. */
body[data-toc-style="prism"] .post-toc[data-toc-state="collapsed"] .toc-collapsed-bar,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-collapsed-bar,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"] {
    --d: 0px;
    border-radius: 0;
    /* --d itself is NOT transitioned here (Loren: the dropdown/bar recede
       jitters on hover in Safari too) -- same bug, same fix as the
       per-link hover further up: Safari's support for transitioning an
       @property-registered custom property directly is much shakier than
       transitioning an ordinary property, and this component doesn't even
       need --d's OWN transition -- height/width (below, on the ::before/
       ::after faces, the only things that actually read var(--d)) get
       their own direct transitions instead. */
    transition: border-color var(--speed, 100ms) ease-out;
}
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"] {
    border-color: rgba(0, 0, 0, var(--edge-a)); /* panel: always full contrast */
}
body[data-toc-style="prism"] .post-toc[data-toc-state="collapsed"] .toc-collapsed-bar,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-collapsed-bar {
    /* NOT muted at rest (Loren, option B): an earlier version faded this
       bar's border/text until engaged, same idea as the rest of the TOC
       -- dropped in favor of reading like plain mode at rest (normal
       --color-border stroke, full-contrast text; sharp corners from the
       rule above are prism's only rest-state signature now) and instead
       DARKENING the stroke further on hover, below, rather than un-
       fading it from a muted state. */
    border-color: var(--color-border);
    color: var(--color-text);
    transition: border-color 0.3s ease;
}
/* Same fix as the rest-state rule just above, same reason: a bare
   .toc-collapsed-bar:hover (0,3,1) still loses to that rule's own
   (0,4,1) once it repeats the [data-toc-state="..."] context, so --d
   silently never left 0 on hover -- the faces WERE fading in (opacity's
   own specificity was already state-qualified and unaffected), just at
   zero size, reading as "nothing visibly happens" (Loren's report). */
body[data-toc-style="prism"] .post-toc[data-toc-state="collapsed"] .toc-collapsed-bar:hover,
body[data-toc-style="prism"] .post-toc[data-toc-state="collapsed"] .toc-collapsed-bar:focus-visible,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-collapsed-bar:hover,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] .toc-collapsed-bar:focus-visible,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:hover,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:focus-within {
    --d: 6px;
    /* bar: darkens further from its own rest-state --color-border to this
       deeper prism edge tone (not un-fading from a muted state anymore,
       option B above); panel: already always full contrast, unchanged. */
    border-color: rgba(0, 0, 0, var(--edge-a));
    color: var(--color-text); /* no-op for the bar now (same as its own rest value); kept for the panel, which sets no color of its own */
}
body[data-toc-style="prism"] .toc-collapsed-bar::before,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]::before {
    content: "";
    position: absolute;
    pointer-events: none;
    left: -1px;
    top: calc(100% + 1px);
    width: calc(100% + 2px);
    height: var(--d);
    background: color-mix(in srgb, rgb(26, 24, 20) calc(var(--face-b-a) * 100%), var(--color-bg));
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    border-top: none;
    transform: skewX(45deg);
    transform-origin: top left;
    /* a 0-height box still paints its own border (the bottom edge), a
       visible 1px sliver right under the box even with --d at 0 -- hidden
       at rest via opacity instead, faded in together with the depth grow. */
    opacity: 0;
    /* height transitions directly, not via --d's own transition -- see
       the reasoning on the rest-state rule above. */
    transition: opacity var(--speed, 100ms) ease-out, height var(--speed, 100ms) ease-out;
}
body[data-toc-style="prism"] .toc-collapsed-bar::after,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]::after {
    content: "";
    position: absolute;
    pointer-events: none;
    left: calc(100% + 1px);
    top: -1px;
    width: var(--d);
    height: calc(100% + 2px);
    background: color-mix(in srgb, rgb(26, 24, 20) calc(var(--face-r-a) * 100%), var(--color-bg));
    border: 1px solid rgba(0, 0, 0, var(--edge-a));
    border-left: none;
    transform: skewY(45deg);
    transform-origin: top left;
    opacity: 0; /* see ::before's comment */
    /* width transitions directly, not via --d's own transition -- see style.css */
    transition: opacity var(--speed, 100ms) ease-out, width var(--speed, 100ms) ease-out;
}
body[data-toc-style="prism"] .toc-collapsed-bar:hover::before,
body[data-toc-style="prism"] .toc-collapsed-bar:hover::after,
body[data-toc-style="prism"] .toc-collapsed-bar:focus-visible::before,
body[data-toc-style="prism"] .toc-collapsed-bar:focus-visible::after,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:hover::before,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:hover::after,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:focus-within::before,
body[data-toc-style="prism"] .post-toc[data-toc-state="expanded"] nav[role="doc-toc"]:focus-within::after {
    opacity: 1;
}

/* --------------------------------------------------------------------------
   OUR DESIGN: TOC entry numbers, mirroring the in-article data-secnum
   numbers. CSS counters can't reach here -- .post-toc is a SIBLING of
   <article>, not a descendant, so counter(secnum) (scoped to article's DOM
   subtree) is invisible in this list. crossref.mjs stamps a matching
   data-num on each entry at build time instead; content: attr() reads a
   literal attribute value, so it isn't subject to that scoping limit.
   Inline vs. gutter render identically here (single treatment) -- gutter's
   whole purpose in-article is keeping the number out of the text column's
   line box, which has no counterpart in a narrow nav list. */
body:is([data-secnum="inline"], [data-secnum="gutter"]) .post-toc nav a[data-num]::before {
    content: attr(data-num);
    margin-right: 0.5em;
    font-family: var(--font-code);
    /* Commit Mono reads visually larger than the UI font at the same
       nominal size (bigger x-height) -- scaled down so the number doesn't
       outweigh the heading text next to it, same idea as --size-caption-num
       scaling figure numbers relative to their caption. */
    font-size: 0.85em;
    color: var(--color-text-faint);
    /* Excludes the number from the link's own hover underline (Loren:
       "the section numbers in the TOC are getting underlined on hover").
       text-decoration-line: none ALONE does not do this, despite what its
       own computed style reports -- confirmed by screenshot, not just
       getComputedStyle(), after an earlier attempt with only that
       property still visibly painted the underline straight through the
       number (Loren caught it; computed style lied about what was
       actually rendered). display: inline-block is what actually works --
       an inline-block box establishes its own formatting context, which
       an ancestor's text-decoration line does not paint across, the same
       way it wouldn't cross a real wrapped <span> with the same display
       value. text-decoration-line: none is kept alongside it as the
       already-correct signal of intent, but inline-block is the property
       actually doing the work here. */
    text-decoration-line: none;
    display: inline-block;
}
/* Appendix-descendant entries switch to their letter form live when
   data-appendix-num is on -- crossref.mjs stamps both data-num (whichever
   form matched the toggle AT BUILD TIME) and data-num-letter (always the
   letter form) so this can track the LIVE toggle instead of being frozen to
   build time, matching how the in-article numbers (pure CSS counters) are
   already fully live. Wins over the rule above via the extra attribute
   selector's specificity -- no !important needed. */
body[data-appendix-num="on"]:is([data-secnum="inline"], [data-secnum="gutter"]) .post-toc nav a[data-num-letter]::before {
    content: attr(data-num-letter);
}

/* --------------------------------------------------------------------------
   OUR DESIGN: Appendix TOC entry (data-appendix-num) -- reads as real, bold,
   numbered body content (unlike toc-endmatter's muted tier above) but
   visibly starts a new zone, matching the in-article .is-appendix separator.
   Gated on the same attribute as that in-article separator so the two never
   disagree. Load-bearing on its own, not merely decorative alongside the
   numbers above: data-secnum ships off by default, so whenever numbers are
   off this rule is the ONLY signal distinguishing Appendix from a plain
   top-level entry. */
body[data-appendix-num="on"] .post-toc nav > .toc-dropdown-scroll > ul > li.toc-appendix {
    margin-top: 0.85rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--color-rule);
}

article p { margin: 0 0 1.1rem; }

article h1 {
    font-family: var(--font-headings);
    font-size: var(--size-h1);
    font-weight: var(--heading-weight);
    line-height: 1.15;
    margin: calc(3rem * var(--heading-space)) 0 calc(1.15rem * var(--heading-space-below));
    letter-spacing: -0.005em;
    color: var(--color-headings);
}

article h2 {
    font-family: var(--font-headings);
    font-size: var(--size-h2);
    font-weight: var(--heading-weight);
    line-height: 1.2;
    margin: calc(2.5rem * var(--heading-space)) 0 calc(0.95rem * var(--heading-space-below));
    letter-spacing: -0.005em;
    color: var(--color-headings);
}

article h3 {
    font-family: var(--font-headings);
    font-size: var(--size-h3);
    font-weight: var(--heading-weight);
    line-height: 1.3;
    margin: calc(2rem * var(--heading-space)) 0 calc(0.7rem * var(--heading-space-below));
    letter-spacing: -0.005em;
    color: var(--color-headings);
}

/* Interactive-figure embeds break out of the text column into the wide tier,
   centered on the article column; capped by the viewport on small screens. */
article iframe[data-fit] {
    width: min(var(--width-wide), calc(100vw - 2 * var(--pad-inline)));
    margin-left: 50%;
    transform: translateX(-50%);
}

/* Notion toggles → <details>/<summary> (see _notion/pull.mjs) */
article details {
    margin: 1.5rem 0;
}

article details > summary {
    cursor: pointer;
}

article details > summary > p {
    display: inline;
    margin: 0;
}

article details[open] > summary {
    margin-bottom: 0.75rem;
}

/* toggleable headings: summary styled to match the heading level it replaces */
article details.toggle-heading > summary {
    font-family: var(--font-display);
    font-weight: 700;
    line-height: 1.3;
}

article details.toggle-heading_3 > summary {
    font-size: 1.15rem;
}

article details.toggle-heading_2 > summary {
    font-size: 1.4rem;
    letter-spacing: -0.005em;
}

article details.toggle-heading_1 > summary {
    font-size: 1.6rem;
    letter-spacing: -0.005em;
}

article hr {
    border: none;
    border-top: 1px solid var(--color-rule);
    margin: 2.5rem 0;
}

article blockquote {
    margin: 1.5rem 0 1.5rem 1rem;
    padding: 0 1.25rem;
    color: var(--color-text-muted);
    font-style: italic;
    border-left: 2px solid var(--color-border);
}

article code {
    font-family: var(--font-mono);
    font-size: 0.85em;
    background: #f3f3f0;
    padding: 1px 5px;
    border-radius: 3px;
}

article pre {
    background: #f8f7f3;
    border: 1px solid var(--color-border);
    border-radius: 4px;
    padding: 0.9rem 1rem;
    overflow-x: auto;
    font-family: var(--font-code);
    font-size: var(--size-code);
    line-height: 1.5;
    margin: 1.25rem 0;
}

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

/* --------------------------------------------------------------------------
   OUR DESIGN: math sizing + display-math containment.
   KaTeX ships `.katex { font-size: 1.21em }`, calibrated for its Computer
   Modern faces against generic web fonts — against Libertinus prose it reads
   clearly oversized, especially inline. --math-scale recalibrates it
   (lab-tunable via the post Size Lab; 1.0 = exactly the prose size).
   Display math: KaTeX centers it already, but an equation wider than the
   column just overflowed into the right margin — contain it with a scroll
   instead, so narrower equations visibly center and wide ones stay inside
   the column.
   -------------------------------------------------------------------------- */
article .katex,
.post-abstract .katex {
    font-size: calc(var(--math-scale, 1.05) * 1em);
}
article .katex-display {
    /* contain KaTeX's visually-hidden absolute-positioned MathML: with the
       body blocks now position:relative (separator anchors), the abspos
       mathml escaped this scrollbox to a far ancestor and widened the whole
       PAGE by the equation's width on phones (97px of document scroll) */
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    /* breathing room so glyph ascenders/descenders don't clip against the
       scroll container, and the scrollbar doesn't sit on the equation */
    padding: 2px 1px 4px;
}

/* Sidenotes / asides */
article .aside, article aside {
    float: right;
    clear: right;
    width: 200px;
    margin: 0.3rem calc(-1 * var(--gutter-aside)) 1rem 1.5rem;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-muted);
    font-style: italic;
}
article .aside p, article aside p { margin: 0 0 0.5rem; font-size: 13px; }
article .aside p:last-child, article aside p:last-child { margin: 0; }
article .aside .citation, article aside .citation { font-style: normal; }

/* --------------------------------------------------------------------------
   OUR DESIGN: Tufte sidenotes (data-footnotes on <body>)
       margin (DEFAULT) / endmatter

   sidenotes.js clones each footnote out of the endmatter and drops the clone
   after its reference marker. The clone floats into the right gutter with the
   SAME float/clear/negative-margin mechanics .aside uses, so a run of notes
   (and any .aside they meet) stacks automatically instead of overlapping.
   Both copies live in the DOM; CSS picks which is visible, so switching modes
   or resizing needs no JS. Above 1100px only — the same breakpoint where
   .aside un-floats — so narrow screens keep the endmatter list.
   -------------------------------------------------------------------------- */
article .sidenote { display: none; }

@media (min-width: 1160px) {
    body:not([data-footnotes="endmatter"]) article .sidenote {
        display: block;
        float: right;
        clear: right;
        width: 200px;
        margin: 0.3rem calc(-1 * var(--gutter-aside)) 1rem 1.5rem;
        font-family: var(--font-body);
        font-size: 12.5px;
        font-style: normal;
        line-height: 1.5;
        color: var(--color-text-muted);
    }
    /* The endmatter footnote list ALWAYS renders (Loren, 2026-09-02):
       margin mode adds sidenotes on top rather than replacing the list, so a
       marker's #fn link always lands somewhere visible, and the ↩ backlinks
       always have a home. (This rule used to hide the list in margin mode.) */
}

article .sidenote p { margin: 0 0 0.5rem; font-size: inherit; }
article .sidenote p:last-child { margin: 0; }
article .sidenote .sidenote-number {
    font-family: var(--font-code);
    font-size: 0.85em;
    color: var(--color-text-faint);
    margin-right: 0.5em;
}

/* Comments — boxed in light yellow. Base (narrow screens): an in-flow block
   spanning the text column, sitting between paragraphs. On wide screens the
   media query below lifts it into the right margin. Not italic. */
article .comment {
    margin: 1.25rem 0;
    padding: 0.6rem 0.85rem;
    background: var(--color-comment-bg);
    border: 1px solid var(--color-comment-border);
    border-radius: 3px;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text);
}
article .comment p { margin: 0 0 0.5rem; font-size: 13px; }
article .comment p:last-child { margin: 0; }

/* Wide viewports: lift the comment fully into the whitespace to the right of
   the centered article, out of the text flow, so it never touches the text
   column. Its width fills the available space (capped for readability). The
   article is the positioning context; `left: 100%` puts the box just past the
   article's right edge and `top` stays at the comment's in-flow position.
   Caveat: being out of flow, it reserves no vertical space, so two comments
   placed very close together can overlap. */
@media (min-width: 1400px) {
    .block-body article { position: relative; }
    article .comment {
        position: absolute;
        left: 100%;
        width: calc((100vw - var(--width-article)) / 2 - var(--comment-gap) - var(--pad-inline));
        max-width: var(--comment-max-width);
        margin: 0 0 0 var(--comment-gap);
    }
}

/* Figures inside body */
article figure { margin: var(--figure-space-above) 0 var(--figure-space-below); }

/* Safety net: an image outside any figure (e.g. a caption-less markdown image,
   which pandoc's implicit_figures leaves as a bare <p><img>) must never
   overflow the column at its natural pixel width. */
article img { max-width: 100%; height: auto; }
article iframe { max-width: 100%; }
article table { max-width: 100%; }
/* narrow screens: a wide table scrolls inside its own box rather than
   dragging the whole page sideways */
@media (max-width: 720px) {
    article table { display: block; overflow-x: auto; }
    /* Belt-and-braces on phones: stray horizontal overflow — KaTeX's hidden
       MathML geometry on inline math, 3D widgets mid-rotation — must never
       give the PAGE a sideways scroll. `clip`, not `hidden`: no scroll
       container is created, so sticky/fixed chrome keeps working. Individual
       wide content (tables, display math, code) still scrolls in its own box.
       Scoped to phones — at sidenote widths the gutter overflow is design.
       BOTH html and body: with body alone, overflowing content still widens
       the document's layout box, and iOS Safari sizes its visual viewport
       from that width — the page pans sideways even though nothing scrolls.
       Clipping at the root keeps the document at device width. */
    html, body { overflow-x: clip; }
}

article figure img, article figure svg, article figure canvas, article figure video,
article figure iframe {
    display: block;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
}

article figure figcaption, article .figure-caption {
    font-family: var(--font-captions);
    font-size: var(--size-caption);
    font-style: italic;
    color: var(--color-caption);
    margin: var(--caption-gap) 0 0;
    line-height: 1.5;
    text-align: left;
}

/* OUR DESIGN — caption controls are three INDEPENDENT axes on <body>
   (Loren, 2026-08-13), each applying uniformly to every figure on the page
   (plain, wide/full breakout, hero, one-offs) — within a given setting all
   captions look identical. Toggled live by the localhost-only caption-lab.js.
     data-caption-align: figure (default) | column
         figure — the caption box follows its figure's own left edge
         column — every caption realigns to the text column's left edge
     data-caption-num:   on | off      (mono "Fig. N" label; hero never numbered)
     data-caption-style: italic (default) | upright
   Shipped defaults (template.html): figure / on / italic. The counter resets
   per <article>; increments are gated on data-caption-num, so numbering off
   renders byte-identical to Eric's original. (secnum is the section-number
   counter for the data-secnum axis below.) */
article { counter-reset: figure secnum; }

/* --------------------------------------------------------------------------
   OUR DESIGN: section numbers (data-secnum): off (DEFAULT) / inline / gutter.
   Counters key on section NESTING DEPTH beneath <article> (not heading tags or
   pandoc's level classes — the level that leads varies per document):
       article > section            -> top level   (1, 2, 3 …)
       article > section > section  -> one sublevel (1.1, 1.2 …)
   End-matter excludes itself: inline regions wrap their sections in a div;
   terminal regions and references/footnotes move outside <article>.
   -------------------------------------------------------------------------- */
article > section { counter-increment: secnum; counter-reset: subsecnum; }
article > section > section { counter-increment: subsecnum; }

body[data-secnum="inline"] article > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="inline"] article > section > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4)::before {
    font-family: var(--font-code);
    /* em, not px: resolves against the ::before's host element's OWN
       font-size (the h1/h2/h3/h4 it's attached to), so the number scales
       with whichever heading level it's actually numbering instead of
       rendering at one flat size regardless of h1 vs h3. */
    font-size: calc(var(--size-secnum) * 1em);
    font-weight: 400;
    font-style: normal;
    letter-spacing: 0.06em;
    color: var(--color-text-faint);
}

/* inline: number prefixes the heading text */
body[data-secnum="inline"] article > section > :is(h1, h2, h3, h4)::before {
    content: counter(secnum);
    margin-right: 0.6em;
}
body[data-secnum="inline"] article > section > section > :is(h1, h2, h3, h4)::before {
    content: counter(secnum) "." counter(subsecnum);
    margin-right: 0.6em;
}

/* gutter: number sits left of the text column on the heading's first-line
   baseline; absolute positioning keeps it out of the line box. */
body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4),
body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4) {
    position: relative;
}
body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4)::before {
    position: absolute;
    right: 100%;
    margin-right: 0.9rem;
    text-align: right;
    white-space: nowrap;
    line-height: inherit;
}
body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4)::before {
    content: counter(secnum);
}
body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4)::before {
    content: counter(secnum) "." counter(subsecnum);
}

/* gutter + TOC-on: the left gutter belongs to the TOC, so gutter degrades to
   the inline treatment rather than overlapping the contents list. */
body[data-secnum="gutter"][data-toc="on"] article > section > :is(h1, h2, h3, h4),
body[data-secnum="gutter"][data-toc="on"] article > section > section > :is(h1, h2, h3, h4) {
    position: static;
}
body[data-secnum="gutter"][data-toc="on"] article > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"][data-toc="on"] article > section > section > :is(h1, h2, h3, h4)::before {
    position: static;
    margin-right: 0.6em;
    text-align: left;
}

/* narrow viewports have no gutter either — same degradation */
@media (max-width: 1100px) {
    body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4),
    body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4) {
        position: static;
    }
    body[data-secnum="gutter"] article > section > :is(h1, h2, h3, h4)::before,
    body[data-secnum="gutter"] article > section > section > :is(h1, h2, h3, h4)::before {
        position: static;
        margin-right: 0.6em;
        text-align: left;
    }
}

/* --------------------------------------------------------------------------
   OUR DESIGN: Appendix section (data-appendix-num): off (DEFAULT) / on.
   A top-level section titled exactly "Appendix" gets extra separation from
   whatever precedes it, and — only meaningful once section numbers are shown
   at all — lettered numbering (A, A.1, A.2 …) instead of continuing the
   normal arabic sequence. `.is-appendix` is stamped onto the qualifying
   <section> by crossref.mjs at build time (CSS can't select on heading text).
   Reuses the existing secnum/subsecnum counters — subsecnum already counts
   1, 2, 3… under any `article > section`, appendix or not; only the
   rendered `content` differs here.
   -------------------------------------------------------------------------- */
/* ---- body/appendix boundary (data-appendix-rule): off / column / full ----
   The third boundary joins the abstract and endmatter rules' gap x position
   model (2026-09-03): the GAP is rhythm, the POSITION (0..1) is allegiance,
   style solid/dotted/dashed/dots, color its own token. DECOUPLED from
   data-appendix-num, which now governs lettering only; ships ON (column). */
article > section.is-appendix {
    position: relative;
    padding-top: var(--appendix-gap);
}
/* the gap token owns the WHOLE space — both sides. Above: the heading's own
   top margin (~4.25rem at heading-space 1.7) stacked on the padding, so pos
   0.5 landed ~30% in. Below: the preceding section's trailing margins (p
   1.1rem, figures a full 4rem) stacked UNDER the body text, pushing the rule
   past center the other way. Zero both; --appendix-gap is the exact
   body-text-to-heading distance. (Loren, 2026-09-05) */
article > section.is-appendix > :is(h1, h2, h3, h4):first-child { margin-top: 0; }
article > section:has(+ section.is-appendix) { margin-bottom: 0; }
article > section:has(+ section.is-appendix) > :last-child { margin-bottom: 0; }
body[data-appendix-rule-style="dotted"] { --appendix-rule-style: dotted; }
body[data-appendix-rule-style="dashed"] { --appendix-rule-style: dashed; }
body[data-appendix-rule="off"] article > section.is-appendix::before { content: none; }
article > section.is-appendix::before {
    content: "";
    position: absolute;
    top: calc(var(--appendix-gap) * var(--appendix-rule-pos));
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--width-article); /* the article BOX width, matching the other separators */
    max-width: 100vw;
    height: 0;
    border-top: 1px var(--appendix-rule-style, solid) var(--appendix-rule-color);
}
body[data-appendix-rule="full"] article > section.is-appendix::before {
    width: 100vw;
}
body[data-appendix-rule-style="dots"] article > section.is-appendix::before {
    content: "\2022\2002\2022\2002\2022\2002\2022\2002\2022";
    width: auto;
    max-width: none;
    height: auto;
    border-top: none;
    font-size: 9px;
    line-height: 1;
    color: var(--appendix-rule-color);
}

body[data-secnum="inline"][data-appendix-num="on"] article > section.is-appendix > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"][data-appendix-num="on"] article > section.is-appendix > :is(h1, h2, h3, h4)::before {
    content: "A";
}
body[data-secnum="inline"][data-appendix-num="on"] article > section.is-appendix > section > :is(h1, h2, h3, h4)::before,
body[data-secnum="gutter"][data-appendix-num="on"] article > section.is-appendix > section > :is(h1, h2, h3, h4)::before {
    content: "A." counter(subsecnum);
}

/* Caption GEOMETRY is uniform across every option and every figure kind
   (Loren, 2026-08-13): within a given caption option, all captions on the
   page must look identical — so the options control typography only
   (numbering, italic), never alignment. The old "hybrid" option collapsed
   into "eric-num" once alignment stopped distinguishing them; stored
   "hybrid" states are mapped by caption-lab.js. */
body[data-caption-num="on"] article figure figcaption,
body[data-caption-num="on"] article .figure-caption {
    counter-increment: figure;
}
body[data-caption-style="upright"] article figure figcaption,
body[data-caption-style="upright"] article .figure-caption {
    font-style: normal;
}
body[data-caption-num="on"] article figure figcaption::before,
body[data-caption-num="on"] article .figure-caption::before {
    content: "Fig. " counter(figure);
    font-family: var(--font-code);
    font-size: var(--size-caption-num);
    font-style: normal;
    letter-spacing: 0.02em;
    color: var(--color-caption-num);
    margin-right: 0.6em;
    white-space: nowrap;
}
/* number format axis (data-caption-num-format): fig "Fig. #" (default) /
   figure "Figure #" / caps "FIGURE #" / fig-caps "FIG. #";
   data-caption-num-bold="on" sets the label bold in any format */
body[data-caption-num="on"][data-caption-num-format="figure"] article figure figcaption::before,
body[data-caption-num="on"][data-caption-num-format="figure"] article .figure-caption::before {
    content: "Figure " counter(figure);
}
body[data-caption-num="on"][data-caption-num-format="caps"] article figure figcaption::before,
body[data-caption-num="on"][data-caption-num-format="caps"] article .figure-caption::before {
    content: "Figure " counter(figure);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
/* fig-caps: the abbreviated label uppercased — "FIG. #" */
body[data-caption-num="on"][data-caption-num-format="fig-caps"] article figure figcaption::before,
body[data-caption-num="on"][data-caption-num-format="fig-caps"] article .figure-caption::before {
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
/* bold toggle for the number label */
body[data-caption-num="on"][data-caption-num-bold="on"] article figure figcaption::before,
body[data-caption-num="on"][data-caption-num-bold="on"] article .figure-caption::before {
    font-weight: 700;
}
/* Alignment axis. In "column" mode, breakout captions realign to the TEXT
   COLUMN: a .wide/.full figure is pulled left by half its overhang and the
   figcaption box rides along; pushing it back by the same amount lands its
   left edge on the column, with the measure capped at the prose width. In
   "figure" mode (default) captions follow their figure's own box. */
body[data-caption-align="column"] article figure.wide figcaption {
    margin-left: calc((var(--width-wide) - var(--width-article)) / 2);
    max-width: calc(var(--width-article) - 2 * var(--pad-inline));
}
body[data-caption-align="column"] article figure.full figcaption {
    margin-left: calc((var(--width-container) - var(--width-article)) / 2);
    max-width: calc(var(--width-article) - 2 * var(--pad-inline));
}
/* (The one-off cone-figure overrides that used to sit here are gone: the IBI
   post's Notion raw block now styles only its own media, not the figure box —
   the convention: raw blocks may style their media, never the figure/caption
   chrome the design system owns.) */

/* Cross-references (crossref.mjs fills a.cref anchors at build time). A ref
   whose target id doesn't exist renders as "??" — make it visible in drafts. */
/* Crefs are apparatus references (Loren, 2026-09-02): citation gold, no
   resting underline, gold underline on hover — gold means "navigates the
   scholarly apparatus" (citations, footnote markers, crefs alike). */
a.cref {
    color: var(--color-citation);
    text-decoration: none;
}
a.cref:hover { text-decoration: underline; text-decoration-color: var(--color-citation); text-underline-offset: 3px; text-decoration-thickness: 1px; }
a.cref-broken {
    color: #b42318;
}

/* Breakout figures reach into the right gutter that floated asides occupy,
   so they must drop below any active aside instead of overlapping it. */
article figure.wide {
    width: var(--width-wide);
    margin-left: calc(-1 * (var(--width-wide) - var(--width-article)) / 2);
    max-width: none;
    clear: right;
}

article figure.full {
    width: var(--width-container);
    margin-left: calc(-1 * (var(--width-container) - var(--width-article)) / 2);
    max-width: none;
    clear: right;
}

/* --------------------------------------------------------------------------
   Rosetta widget
   -------------------------------------------------------------------------- */
.rosetta {
    margin: 1.75rem 0;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    background: #fdfcfa;
}

.rosetta-tabs {
    display: flex;
    border-bottom: 1px solid var(--color-border);
    background: #f8f7f3;
    border-radius: 6px 6px 0 0;
}

.rosetta-tab {
    flex: 0 0 auto;
    background: none;
    border: none;
    font-family: var(--font-ui);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.55rem 1.1rem;
    color: var(--color-text-faint);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: color 0.1s ease, border-color 0.1s ease;
}

.rosetta-tab:hover { color: var(--color-text-muted); }

.rosetta-tab.active {
    color: var(--color-text);
    border-bottom-color: var(--color-text);
}

.rosetta-panel { padding: 1rem 1.25rem; }
.rosetta-panel[hidden] { display: none; }
.rosetta-panel > p:first-child { margin-top: 0; }
.rosetta-panel > p:last-child { margin-bottom: 0; }
.rosetta-panel pre { margin: 0; background: transparent; border: none; padding: 0; }
.rosetta-panel .katex-display { margin: 0.5rem 0; }

/* Citations — distill's darkish greyish-blue. The abstract sits outside
   <article> (it's a .block above it) but its citations are the same citeproc
   markup and count toward the same numbering, so they get the same treatment. */
article .citation,
.post-abstract .citation,
.endmatter .citation {
    /* .endmatter too: post-structure.mjs MOVES the footnote/reference sections out
       of <article>, and apparatus links must not decay into prose links there */
    color: var(--color-citation);
    text-decoration: none;
}
article .citation a,
.post-abstract .citation a,
.endmatter .citation a {
    color: inherit;
    text-decoration: none;
    /* OUR DESIGN (Loren, 2026-08-11): no underline at rest on citation marks —
       the gold is the affordance; the underline appears on hover. Real
       text-decoration (2026-09-02) so it sits at the site's 3px offset and
       matches crefs/footnote markers, not a baseline border fake. */
}
article .citation a:hover,
.post-abstract .citation a:hover,
.endmatter .citation a:hover { text-decoration: underline; text-decoration-color: var(--color-citation); text-underline-offset: 3px; text-decoration-thickness: 1px; }
/* Numeric citations are reference marks, not words — set them a step down so
   [4–6] stops competing with the sentence it sits in. Author-date citations
   keep body size: "(Shai et al. 2024)" is read as part of the prose. The
   body class comes from `citation-style:` via template.html. */
.cite-numeric .citation { font-size: 0.86em; }

/* Hover preview for citations and footnotes (hover-preview.js) */
.citation-popover {
    position: absolute;
    z-index: 50;
    max-width: min(460px, calc(100vw - 24px));
    box-sizing: border-box;
    padding: 0.7rem 0.85rem;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 3px;
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text-muted);
}
.citation-popover[hidden] { display: none; }
.citation-popover p { margin: 0; }
.citation-popover p + p { margin-top: 0.4rem; }
/* A grouped citation ([7, 8] or a collapsed [4–6]) previews every entry it
   cites, numbered to match the endmatter — the endmatter's counter is scoped to
   #refs, so hover-preview.js supplies the number. A long group scrolls rather
   than growing past the viewport. */
/* A group of five entries would run off the screen at its natural height, so
   cap it and let it scroll. Width is the shared 460px set above. */
.citation-popover.multi { max-height: 60vh; overflow-y: auto; }
.citation-popover.multi .citation-popover-entry {
    padding-left: 2.2rem; /* matches .csl-entry: room for a two-digit [NN] label */
    position: relative;
}
.citation-popover .citation-popover-entry + .citation-popover-entry {
    margin-top: 0.55rem;
    padding-top: 0.55rem;
    border-top: 1px solid var(--color-border);
}
.citation-popover .citation-popover-entry[data-ref-n]::before {
    content: "[" attr(data-ref-n) "]";
    position: absolute;
    left: 0;
    color: var(--color-text-muted);
    font-family: var(--font-ui);
    font-size: 11px;
}
.citation-popover a {
    color: var(--color-citation);
    word-break: break-word;
    text-decoration: none; /* apparatus class: gold is the affordance */
}
.citation-popover a:hover { text-decoration: underline; text-decoration-color: var(--color-citation); text-underline-offset: 3px; text-decoration-thickness: 1px; }
/* A small pointer toward the reference link. */
.citation-popover::after {
    content: "";
    position: absolute;
    /* hover-preview.js points this at the citation; 50% until it does, and
       whenever the popover is clamped against a viewport edge the arrow stays
       on the reference rather than on the box's centre. */
    left: var(--arrow-left, 50%);
    margin-left: -5px;
    border: 5px solid transparent;
    border-top-color: var(--color-border);
    bottom: -10px;
}
.citation-popover.below::after {
    bottom: auto;
    top: -10px;
    border-top-color: transparent;
    border-bottom-color: var(--color-border);
}

/* --------------------------------------------------------------------------
   Endmatter — two-column: headers (left) and content (right)
   -------------------------------------------------------------------------- */
.endmatter {
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.55;
    color: var(--color-text-muted);
}

/* Mirrors the article's own has-toc grid via --has-toc-cols (shared token,
   see the :root comment) rather than a literal value here, so the two can
   never drift apart. Header goes in the left column (right-aligned),
   content in the column the article itself uses. */
.endmatter-section {
    position: relative;
    display: grid;
    grid-template-columns: var(--has-toc-cols);
    gap: 0;
    margin-bottom: 1.75rem;
    align-items: start;
}

/* The 1fr side columns above are a SHARED formula, but 1fr resolves
   relative to each grid's own container width, not an absolute value --
   .block-endmatter .block-inner has left+right padding (--pad-inline ×2)
   while .block-body-inner (the article's own container) has right padding
   only (deliberately, see its own comment), so the two containers' actual
   available widths differ by one --pad-inline, and identical 1fr formulas
   land 12px off from each other despite matching on paper. Dropping the
   left padding here (has-toc pages only -- this section's whole job is
   tracking the article, unlike title/byline/abstract, which don't try to
   align with a TOC gutter and keep the plain symmetric padding) makes the
   two containers' available widths match exactly.
   UNCONDITIONAL -- applies in the stacked state too (data-endmatter-stack),
   not just the 3-column gutter layout. That state centers its single
   column via justify-content (see that rule), and centering within a
   right-padding-only content-box lands on the exact same point the
   article's own symmetric 1fr/article/1fr grid centers on; restoring
   symmetric padding just for the stacked case (an earlier version of this
   rule did that) put the two centers 12px apart instead of matching. The
   stacked h3's own padding-left (see that rule) is what protects it from
   sitting flush against the true edge in the extreme case where the column
   fills the whole content-box and there's no leftover to center -- not
   this container's padding. max-width is ALSO overridden here (the base
   rule below caps at a flat 1280px) to var(--has-toc-max-width) -- the
   same article + 2*gutter cap .block-body-inner uses, so on a really wide
   screen the two containers' overall widths (and thus their shared center
   point) stay identical instead of diverging once one hits 1280px and the
   other keeps growing. */
body[data-toc="on"] .block-endmatter .block-inner {
    max-width: var(--has-toc-max-width);
    padding-left: 0;
}

.endmatter-section:last-child { margin-bottom: 0; }
.endmatter-section[hidden] { display: none; }
.endmatter-anchor { position: absolute; }

.endmatter-section > h3 {
    grid-column: 1;
    justify-self: end;
    max-width: var(--endmatter-label-max-width);
    padding-right: 1.75rem;
    font-family: var(--font-body);
    font-size: 14px;
    font-weight: 700;
    color: var(--color-text-muted);
    margin: 0;
    line-height: 1.4;
    text-align: right;
    text-transform: none;
    letter-spacing: 0;
}

.endmatter-section > .endmatter-content {
    grid-column: 2;
    min-width: 0;
    padding: 0 var(--pad-inline);
}
.endmatter-section > .endmatter-content > *:first-child { margin-top: 0; }
.endmatter-section > .endmatter-content > *:last-child { margin-bottom: 0; }

.endmatter p { margin: 0 0 0.75rem; font-size: 13px; }

/* Authored end matter may interrupt the article before an appendix. Its
   content already occupies the reading column. Use the footer's stacked
   heading treatment here so labels cannot collide with the article TOC. */
article .endmatter-inline {
    position: relative;
    padding-top: var(--endmatter-gap);
    margin-bottom: 0;
}
article :has(+ .endmatter-inline) { margin-bottom: 0; }
article .endmatter-inline .endmatter-section {
    display: block;
    position: relative;
}
article .endmatter-inline .endmatter-section > h3 {
    position: static;
    max-width: none;
    text-align: left;
    padding: 0;
    margin-bottom: 0.5rem;
}
article .endmatter-inline .endmatter-content { padding: 0; }
body[data-toc="on"][data-endmatter-stack="on"] article .endmatter-inline .endmatter-section > h3 {
    padding-left: 0;
}
article .endmatter-inline .endmatter-section > h3::before,
body[data-caption-num="on"] article .endmatter-inline figcaption::before,
body[data-caption-num="on"] article .endmatter-inline .figure-caption::before { content: none; }
body[data-caption-num="on"] article .endmatter-inline figcaption,
body[data-caption-num="on"] article .endmatter-inline .figure-caption { counter-increment: none; }
body:not([data-endmatter-rule="off"]) article .endmatter-inline::before {
    content: "";
    position: absolute;
    top: calc(var(--endmatter-gap) * var(--endmatter-rule-pos));
    left: calc(-1 * var(--pad-inline));
    right: calc(-1 * var(--pad-inline));
    border-top: 1px var(--endmatter-rule-style, solid) var(--endmatter-rule-color);
}
body[data-endmatter-rule="full"] article .endmatter-inline::before {
    left: 50%;
    right: auto;
    width: 100vw;
    transform: translateX(-50%);
}
body[data-endmatter-rule-style="dots"]:not([data-endmatter-rule="off"]) article .endmatter-inline::before {
    content: "\2022\2002\2022\2002\2022\2002\2022\2002\2022";
    border: none;
    text-align: center;
    font-size: 9px;
    line-height: 1;
    color: var(--endmatter-rule-color);
}

/* Figures relocated into the endmatter (e.g. supplemental figures authored in
   a `::: endmatter` section) sit outside <article>, so the article figure
   rules don't reach them — constrain and center them here. */
.endmatter figure { margin: 1.25rem 0; }

.endmatter figure img,
.endmatter figure svg,
.endmatter figure video {
    display: block;
    max-width: 100%;
    height: auto;
    margin: 0 auto;
}

/* Breakout figures in the endmatter: same geometry as the article rules
   (which don't reach here — the endmatter sits outside <article>). The
   content cell is exactly --width-article wide and centered like the body
   column, so the same half-overhang pull centers the figure over it; the
   grid doesn't clip, so the figure reaches into the 1fr gutters. */
.endmatter .endmatter-content figure.wide {
    width: var(--width-wide);
    margin-left: calc(-1 * (var(--width-wide) - var(--width-article)) / 2);
    max-width: none;
}
.endmatter .endmatter-content figure.full {
    width: var(--width-container);
    margin-left: calc(-1 * (var(--width-container) - var(--width-article)) / 2);
    max-width: none;
}
body[data-caption-align="column"] .endmatter figure.wide figcaption {
    margin-left: calc((var(--width-wide) - var(--width-article)) / 2);
    max-width: calc(var(--width-article) - 2 * var(--pad-inline));
}
body[data-caption-align="column"] .endmatter figure.full figcaption {
    margin-left: calc((var(--width-container) - var(--width-article)) / 2);
    max-width: calc(var(--width-article) - 2 * var(--pad-inline));
}

.endmatter figure figcaption {
    font-family: var(--font-body);
    font-size: 12px;
    font-style: italic;
    color: var(--color-text-muted);
    margin: 0.5rem 0 0;
    line-height: 1.5;
    text-align: left;
}

.endmatter a {
    color: var(--color-text-muted);
    text-decoration-color: var(--color-link-underline);
    /* bare URLs / DOIs in references have no break opportunities; on a phone an
       unbroken link would widen the page past the viewport */
    overflow-wrap: anywhere;
}
/* explicit: .endmatter a above ties with a:hover on specificity and sits
   later in the file, so without this the hover darken never fired here */
.endmatter a:hover { text-decoration-color: var(--color-text); }

/* Citation Information */
.endmatter .cite-row {
    margin-bottom: 1.25rem;
}
.endmatter .cite-row:last-child { margin-bottom: 0; }

.endmatter .cite-label {
    font-family: var(--font-body);
    font-size: 13px;
    color: var(--color-text-muted);
    margin: 0 0 0.5rem;
}

.endmatter .cite-block {
    position: relative;
    background: #f8f7f3;
    border: 1px solid var(--color-border);
    border-radius: 4px;
    padding: 0.65rem 0.9rem;
}

.endmatter .cite-content {
    font-family: var(--font-mono);
    font-size: 10.5px;
    line-height: 1.55;
    color: var(--color-text-muted);
    overflow-x: auto;
    padding-right: 2.6rem; /* room for the copy button */
}

/* Both citation boxes come from a markdown code fence; strip pandoc's inner
   pre/code styling so the content inherits the .cite-block box. The bibtex
   block keeps its default `white-space: pre` (no wrap, horizontal scroll). */
.endmatter .cite-content pre,
.endmatter .cite-content code {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    color: inherit;
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
    border-radius: 0;
}

/* The cite-as line is prose authored on a single line; let it wrap rather than
   scroll, matching how the old paragraph-based box flowed. */
.endmatter .cite-text pre,
.endmatter .cite-text code {
    white-space: pre-wrap;
    word-break: break-word;
}

.endmatter .cite-copy {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    width: 21px;
    height: 21px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--color-rule);
    border-radius: 4px;
    padding: 0;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}

.endmatter .cite-copy:hover {
    color: var(--color-text);
    border-color: var(--color-text-muted);
    background: rgba(255, 255, 255, 0.5);
}

.endmatter .cite-copy .icon-copy,
.endmatter .cite-copy .icon-check {
    display: block;
    width: 11px;
    height: 11px;
}

.endmatter .cite-copy .icon-check { display: none; }

.endmatter .cite-copy.copied {
    color: var(--color-text);
    border-color: var(--color-text-muted);
}

.endmatter .cite-copy.copied .icon-copy { display: none; }
.endmatter .cite-copy.copied .icon-check { display: block; }

/* References (moved from article by post-structure.mjs) */
.endmatter .references { margin: 0; padding: 0; border: none; }
.endmatter .references h2 { display: none; }
.endmatter #refs {
    font-size: 13px;
    color: var(--color-text-muted);
    counter-reset: ref;
}
.endmatter #refs .csl-entry {
    margin-bottom: 0.6rem;
    /* Wide enough that a two-digit [NN] label (~1.65rem at 11px mono) keeps
       a real gap before the entry text. */
    padding-left: 2.2rem;
    position: relative;
    counter-increment: ref;
}
.endmatter #refs .csl-entry::before {
    content: "[" counter(ref) "]";
    position: absolute;
    left: 0;
    color: var(--color-text-muted);
    /* OUR DESIGN (Loren, 2026-08-11): mono index numbers, pairing with the
       mono footnote/sidenote numbers and kicker — was var(--font-ui). */
    font-family: var(--font-code);
    font-size: 11px;
}

/* Footnotes (moved from article by post-structure.mjs) */
.endmatter .footnotes hr { display: none; }
.endmatter .footnotes h2 { display: none; }
.endmatter .footnotes { font-size: 13px; line-height: 1.55; }
.endmatter .footnotes ol {
    padding-left: 0;
    margin: 0;
    list-style: none;
    counter-reset: fn;
}
.endmatter .footnotes li {
    counter-increment: fn;
    padding-left: 1.75rem;
    position: relative;
    margin-bottom: 0.6rem;
}
.endmatter .footnotes li::before {
    content: counter(fn) ".";
    position: absolute;
    left: 0;
    color: var(--color-text-muted);
    /* OUR DESIGN (Loren, 2026-08-11): mono, matching the reference index
       numbers above and the margin-sidenote numbers — was var(--font-ui). */
    font-family: var(--font-code);
    font-size: 11px;
}

a.footnote-ref { text-decoration: none; }
a.footnote-ref sup { font-size: 0.75em; color: var(--color-citation); padding: 0 1px; } /* pairs with reference numbers (Loren, 2026-09-02) */
a.footnote-back { text-decoration: none; margin-left: 0.25em; color: var(--color-text-faint); }
a.footnote-back:hover { color: var(--color-text); }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@media (max-width: 1159px) {
    article .aside, article aside {
        float: none;
        clear: none;
        width: auto;
        margin: 1.25rem 0;
        padding: 0.4rem 0 0.4rem 1rem;
        border-left: 2px solid var(--color-border);
    }

    article figure.wide, article figure.full,
    .endmatter .endmatter-content figure.wide,
    .endmatter .endmatter-content figure.full {
        width: 100%;
        margin-left: 0;
    }

    /* The endmatter 3-col grid needs ~900px+ of viewport; below that,
       collapse to a single column with the header above its content.
       Scoped to :not([data-toc="on"]) -- a has-toc page's endmatter needs
       the SAME kind of bailout eventually (see the data-endmatter-stack
       rule further down), but at a measured, page-specific width rather
       than this fixed guess, since it shares --has-toc-cols with the
       article and stays symmetric with it up to that point. This rule used
       to win over it unconditionally below 1100px (same selector, later in
       the file -- silently stomping the has-toc grid on any page with a
       TOC). minmax(0, var(--width-article)), not minmax(0, 1fr) -- the
       column's container (.block-endmatter .block-inner) caps at 1280px,
       not the article's own 710px, so an unbounded 1fr here let the
       stacked content balloon to nearly the full viewport width the moment
       it stacked, instead of matching the article's own reading width.
       justify-content: center -- a single explicit track narrower than its
       container packs to the start (left) by default; without this the
       leftover width all lands on the right instead of splitting evenly,
       so the column reads as left-aligned rather than centered like the
       article above it. */
    body:not([data-toc="on"]) .endmatter-section {
        grid-template-columns: minmax(0, var(--width-article));
        justify-content: center;
    }
    body:not([data-toc="on"]) .endmatter-section > h3 {
        grid-column: 1;
        justify-self: start;
        max-width: none;
        text-align: left;
        padding-right: 0;
        margin-bottom: 0.5rem;
    }
    body:not([data-toc="on"]) .endmatter-section > .endmatter-content {
        grid-column: 1;
    }
}

/* Endmatter stack: gated on data-endmatter-stack, an attribute
   toc-sticky.js computes and sets live (NOT a static @media breakpoint,
   for the same reason data-toc-squeeze-collapse below isn't one). The
   trigger point is measured, not guessed: toc-sticky.js's
   endmatterCollapseThreshold() reads the actual widest .endmatter-section
   > h3 label ON THIS PAGE (References/Footnotes/Citation/whatever a post's
   own ::: endmatter titles its sections) and solves for the viewport width
   at which the shared gutter (same --has-toc-cols the TOC uses) can no
   longer fit it -- the same algebra as collapseThreshold(), just solved
   for a different left-column minimum. Below that point the label would
   otherwise run past the left edge of the viewport with nothing to stop
   it, unlike the TOC (which has --toc-min-width as an explicit floor).
   Same layout as the non-has-toc fallback above -- label stacks above its
   content instead of sitting in the gutter, capped at minmax(0,
   var(--width-article)) and centered (justify-content) for the same reason
   that fallback is (see its own comment) -- the container caps at 1280px,
   not the article's 710px, and a track narrower than its container packs
   to the left by default rather than centering. */
body[data-toc="on"][data-endmatter-stack="on"] .endmatter-section {
    grid-template-columns: minmax(0, var(--width-article));
    justify-content: center;
}
body[data-toc="on"][data-endmatter-stack="on"] .endmatter-section > h3 {
    grid-column: 1;
    justify-self: start;
    max-width: none;
    text-align: left;
    padding-right: 0;
    /* Own left inset, independent of the container/centering math above --
       matches .endmatter-content's own padding (below), which already
       protects itself the same way. Without this, the extreme case where
       the column fills its entire content-box (no leftover width left to
       center) would leave the label flush against the true left edge,
       since it has no other source of inset at that point. */
    padding-left: var(--pad-inline);
    margin-bottom: 0.5rem;
}
body[data-toc="on"][data-endmatter-stack="on"] .endmatter-section > .endmatter-content {
    grid-column: 1;
}

/* Dropdown-only collapse: gated on data-toc-squeeze-collapse, an attribute
   toc-sticky.js computes and sets live (NOT a static @media breakpoint).
   The trigger point: the left gutter (half of viewport minus the article,
   see .block-body.has-toc .block-body-inner below -- the grid NEVER goes
   asymmetric, so this is always a well-defined single number) would have
   to go narrower than --toc-min-width to keep the article centered. Since
   that bound is a Post Layout Lab slider, the pixel width where this
   actually happens moves whenever it's tuned. A fixed media-query number
   can't track a custom property, so JS measures it instead (see
   toc-sticky.js's collapseThreshold()). toc-sticky.js forces
   data-toc-sticky="collapse-on-scroll" here regardless of what was
   actually configured, but the dropdown bar itself still only appears once
   scrolled past the same sentinel that mode always uses -- an earlier
   version skipped straight to "collapsed" unconditionally, which meant the
   bar sat on top of the page from first load, before the reader had
   scrolled anywhere. The static label/list markup still exists in the DOM (for the
   expanded panel to reveal) but never renders inline; .post-toc's own
   padding is zeroed since it would otherwise render as an empty box now
   that its normal content is hidden; min-width is ALSO zeroed here, not
   just padding -- .post-toc's own min-width (var(--toc-min-width)) is a
   real CSS property on the box itself, so it stays in effect even once its
   children are display:none, and a grid item's own min-width counts toward
   its 1fr track's automatic minimum size. Left at var(--toc-min-width),
   the (now-empty) left column would still be held to at least that width
   while the right 1fr column shrinks to compensate, breaking the exact
   asymmetry this collapse state exists to avoid. The grid itself otherwise
   needs no change here -- .block-body.has-toc .block-body-inner stays the
   same symmetric 1fr/article/1fr whether the left gutter's content is the
   TOC or nothing; an emptied (and now properly min-width:0) 1fr column
   centers the article exactly like before, so .endmatter-section (sharing
   that same --has-toc-cols constant) stays aligned automatically too.
   Caveat: this whole mechanism -- like the collapsed bar itself -- requires
   JS; without it the TOC simply keeps hugging content down to whatever the
   gutter allows instead of collapsing, same as it always did before this
   attribute existed. */
/* .block-body.has-toc chained in, not just body[...] -- needed to outrank
   the base .block-body.has-toc .post-toc rule's own min-width (3 classes)
   in specificity; without it this min-width:0 silently lost. */
body[data-toc-squeeze-collapse="on"] .block-body.has-toc .post-toc { padding: 0; min-width: 0; }
body[data-toc-squeeze-collapse="on"] .post-toc > h3,
body[data-toc-squeeze-collapse="on"] .post-toc > nav[role="doc-toc"] { display: none; }

@media (max-width: 720px) {
    .site-header-inner {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.75rem;
    }

    .post-byline { grid-template-columns: 1fr; gap: 1rem; }

    .block-title .block-inner { padding-top: 1.5rem; }
    /* scale the title/subtitle pair TOGETHER, preserving hierarchy — the old
       hardcoded 2rem title sat at 32px next to a 30px subtitle (Loren,
       2026-09-03). min() keeps lab-tuned sizes authoritative when smaller. */
    /* weight compensates for lost size: regular Cardo reads thin at mobile
       sizes next to the bold authors line; tokens above, lab-tunable */
    .block-title h1 { font-size: min(var(--size-title), var(--size-title-mobile)); font-weight: var(--title-weight-mobile); }
    .block-title .subtitle { font-size: min(var(--size-subtitle), var(--size-subtitle-mobile)); }
}

/* Below the site's small-screen breakpoint the front matter's WIDE frame
   (title, hero — --width-wide) adopts the article column, so title, byline,
   abstract, and body all share one left edge; on wide screens the title's
   left-anchored offset is the design. And the has-toc body grid drops its
   asymmetric right-only padding (the inline TOC is a dropdown at these
   widths anyway), so the body column's RIGHT edge matches the front matter
   too — it ran 24px short on phones. (Loren, 2026-09-03) */
@media (max-width: 1140px) {
    .block-inner-wide { max-width: var(--width-article); }
    .block-body.has-toc .block-body-inner { padding-right: 0; }
}



/* --------------------------------------------------------------------------
   Mobile: dev chrome (labs + drift badge) is desktop tooling — on a phone the
   fixed buttons sit on top of content and there's no room to use the panels
   anyway. !important outranks the labs' injected styles. Also: comfortable
   nav tap targets.
   -------------------------------------------------------------------------- */
@media (max-width: 720px) {
    #fontlab, #sizelab, #hoverlab, #layoutlab, #logolab, #captionlab, #labdrift, #postfontlab, #postsizelab, #postcolorlab, #postlayoutlab { display: none !important; }
}

/* PROTOTYPE (Adam, 2026.08.30): sidenotes and asides rest faded and further
   into the gutter; hovering the note — or its numbered marker in the text —
   brings it to full strength. Knobs are CSS variables driven by the post
   layout lab sliders (post-layout-lab.js FRAME, "Notes & TOC" group). */
:root {
    /* baked from dev tuning (Loren, 2026-09-02); prototype by Adam */
    --sn-opacity: 0.6;   /* resting opacity */
    --sn-shift: 72px;    /* extra distance into the gutter */
    --sn-width: 215px;
    --sn-size: 13px;
    --sn-top: 5px;       /* vertical nudge from the marker line */
    --sn-clamp-lines: 6; /* data-sn-clamp="on": lines shown before hover */
    --sn-edge-gap: 16px; /* the note never comes closer than this to the viewport edge */
    --sn-text-gap: 24px; /* ...nor closer than this to the text column (clamp floor) */
    /* gates at 1160/1159: with default tokens the clamp floor (width 215 +
       text gap 24) stops fitting inside the edge gap at ~1152px (column
       690); below that, notes hide and asides unfloat. */
}
@media (min-width: 1160px) {
    body:not([data-footnotes="endmatter"]) article .sidenote,
    article .aside, article aside {
        opacity: var(--sn-opacity);
        transition: opacity 0.3s ease; /* matches the clamp roll, so un-hover reads as ONE motion */
        width: var(--sn-width);
        font-size: var(--sn-size);
        margin-top: var(--sn-top);
        /* SLIDE with a hard floor (Loren, 2026-09-03): at wide viewports the
           tuned gutter position wins; as the viewport narrows the note slides
           left, holding --sn-edge-gap off the viewport edge — but NEVER
           closer than --sn-text-gap to the text column (the clamp minimum),
           so the float can't start stealing line-box width and thinning the
           prose, whatever the scrollbar or tuned note width does to the
           arithmetic. Below the gate, notes fold into the endmatter. */
        margin-right: calc(-1 * clamp(var(--sn-width) + var(--sn-text-gap), (100vw - var(--width-article)) / 2 + var(--pad-inline) - var(--sn-edge-gap), var(--gutter-aside) + var(--sn-shift)));
    }
    body:not([data-footnotes="endmatter"]) article .sidenote:hover,
    article .aside:hover, article aside:hover,
    a.footnote-ref:hover + .sidenote {
        opacity: 1;
    }
}

/* Sidenote clamp (Adam's prototype, shipped by default 2026-09-02): a note
   shows its first N lines with a soft fade; hovering the note or its marker
   reveals the whole thing. The collapse used to happen in two visible steps —
   mask-image gradients don't transition (the fade SNAPPED back), and a 200vh
   hover ceiling made max-height spend most of its duration collapsing
   invisible headroom before the visible roll. Now: the fade is an OVERLAY
   whose opacity really animates, and with interpolate-size the height
   animates to/from the note's true height — one duration, one motion.
   data-sn-clamp="off" (yaml/lab) disables. */
@media (min-width: 1160px) {
    /* Expansion must NOT change the note's layout height: sidenotes are
       floats in the flow, so a growing box pushes everything after it down —
       worst at the end of a post, where the endmatter visibly jumped on hover
       (Loren, 2026-09-02). The aside's box is capped at the clamp height and
       never grows; the inner .sidenote-body (built by sidenotes.js) does the
       clamping, and on hover expands PAST the cap as an overlay — background
       + soft shadow — painting over whatever sits below. Notes shorter than
       the clamp keep their natural height and gain nothing on hover. */
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote {
        position: relative;
        max-height: calc(var(--sn-clamp-lines) * 1.5 * var(--sn-size));
        overflow: visible;
    }
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote:hover,
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article a.footnote-ref:hover + .sidenote {
        z-index: 3; /* the unfurled overlay covers margin content below it */
    }
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote .sidenote-body {
        position: relative;
        interpolate-size: allow-keywords; /* Chromium: real to/from max-content animation */
        max-height: calc(var(--sn-clamp-lines) * 1.5 * var(--sn-size));
        overflow: hidden;
        /* opaque, so the unfurled body cleanly covers content below — but no
           shadow/card chrome: it should read as the note simply being longer */
        background: var(--color-bg);
        transition: max-height 0.3s ease;
    }
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote .sidenote-body::after {
        content: "";
        position: absolute;
        /* anchored to the CLAMP line, not the box bottom: on a clipped note
           they coincide; on a note SHORTER than the clamp the gradient lands
           below the content and overflow:hidden clips it — so short notes
           never fade (Loren, 2026-09-08: a 2-line note was fading its own
           last line). Top-anchored, so no bottom: */
        left: 0; right: 0;
        top: calc(var(--sn-clamp-lines) * 1.5 * var(--sn-size) - 2.2em);
        height: 2.2em;
        background: linear-gradient(to bottom, transparent, var(--color-bg));
        pointer-events: none;
        transition: opacity 0.3s ease;
    }
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote:hover .sidenote-body,
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article a.footnote-ref:hover + .sidenote .sidenote-body {
        max-height: 60em;         /* fallback ceiling (kept modest to limit dead-time) */
        max-height: max-content;  /* exact with interpolate-size support */
    }
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article .sidenote:hover .sidenote-body::after,
    body[data-sn-clamp="on"]:not([data-footnotes="endmatter"]) article a.footnote-ref:hover + .sidenote .sidenote-body::after {
        opacity: 0;
    }
}

/* --------------------------------------------------------------------------
   Tab groups — a run of Notion toggles titled "Tab: …" (see _notion/pull.mjs)
   becomes div.tracks with one div.track lane per tab, each opening with
   p.track-label. assets/js/tabs.js builds the tab bar and adds .js-tracks;
   without JS the lanes read in series under their labels.
   -------------------------------------------------------------------------- */
article div.tracks { margin: 2.2rem 0; }
article div.tracks .track { min-width: 0; }
article div.tracks .track + .track { margin-top: 1.6rem; }
article div.tracks.js-tracks .track { display: none; margin-top: 0; }
article div.tracks.js-tracks .track.active { display: block; animation: trackfade .28s ease; }
@keyframes trackfade { from { opacity: 0; } to { opacity: 1; } }
article .track-switch {
  display: flex; gap: 1.5rem; align-items: baseline; flex-wrap: wrap;
  font-family: var(--font-ui, sans-serif); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
  border-bottom: 1px solid var(--color-border, #e3e3dc); margin: 0 0 1rem;
}
article .track-switch button {
  all: unset; cursor: pointer; padding: 0 .1em .4rem; margin-bottom: -1px;
  color: var(--color-text-faint, #9a9a92);
  border-bottom: 2px solid transparent;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
}
article .track-switch button.active { color: var(--color-text, #1a1a1a); border-bottom-color: currentColor; }
article .track-switch button:not(.active):hover { color: var(--color-text-muted, #7a7a72); }
article .track-label {
  font-family: var(--font-ui, sans-serif); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--color-text-faint, #9a9a92);
  margin: 0 0 .9rem; padding-bottom: .4rem;
  border-bottom: 1px solid var(--color-border, #e3e3dc);
}
article .track .katex-display { overflow-x: auto; overflow-y: hidden; padding: .2em 0; }

