/* ---------------------------------------------------------------------------
   heroes-lang.org — the one stylesheet, shared by every page.

   Register and rules: site/README.md § Style guide. 70s-glam Bowie with
   discipline — near-black stage / cream daylight, the bolt's red and blue as
   the only loud colours, gold for the quotation marks, poster-condensed caps
   on the system stack (never a webfont), Georgia serif body.

   Why a file and not a <style> block per page: the site is more than a hundred
   pages now, with its documentation written rather than promised, and a
   duplicated palette drifts invisibly, one page's dark mode going stale with
   nothing to fail. There is a build step today and one file still beats it:
   one palette, one place. This is the only cross-page asset; the bolt is
   inline SVG, written once in the layout and served in every page, because it
   is markup.

   The art direction, in one paragraph, so a later edit knows what it is
   editing. The page is a stage: one light source (the red/blue wash behind the
   marquee), one motif (the 1973 bolt, used as mark, as section bullet, as the
   pin on the build log, and once more on the line above the footer), one
   quotation colour (gold, which is why the section nods — song titles, which
   are quotations — are gold and not grey). Everything else is paper and ink.
   Restraint is the whole discipline here: an accent used everywhere is an
   accent used nowhere.

   Sections:
     1  tokens — colour, measure, motion
     2  base — reset, body, selection, texture
     3  shell — the page grid, and the two layouts
     4  nav
     5  headings, marquee, nods
     6  prose — paragraphs, lists, quotes, links
     7  code — panels, syntax, diagnostics, tables
     8  home — hero, demo card, figures, cta, cards
     9  subpage heads, docs, chapters
    10  the build log
    11  the zen
    12  stats, the fixpoint, the panel page
    13  the examples, one page per program, generated from examples/
    14  footer
    15  focus, motion, print
   --------------------------------------------------------------------------- */

/* == 1. tokens ============================================================ */

:root {
  color-scheme: dark light;

  /* Ink and paper. Two greys between background and card, so a panel can sit
     on the stage without a border doing all the work. */
  --bg: #0b0b10;
  --stage: #101017;
  --card: #14141d;
  --line: #262633;
  --line-soft: #1c1c26;
  --fg: #efece4;
  --dim: #9d998f;

  /* The bolt's two colours, plus gold. `--link` is a separate token from
     `--blue` on purpose: the bolt's blue is a graphic colour and fails text
     contrast at body size on this background, so links get a lifted one.
     A colour that carries meaning and a colour that draws a shape are two
     different jobs, and one token cannot hold both. */
  --red: #ef4a52;
  --blue: #3f7bff;
  --link: #7aa2ff;
  --gold: #e8b552;
  /* The display gold is the same ink at quotation-mark size. In dark they are
     one colour (#e8b552 measures 10.44:1 and needs no help); light splits them,
     because a token bound by 11px text is brown at 90px. */
  --gold-display: var(--gold);
  --code: #d6dcf2;

  /* The author's portrait is printed as two plates: `--plate-dark` for the
     shadows, `--plate-light` for the highlights. Blue and cream rather than blue
     and red, because the red/blue pair is the home hero's wash, and § The visual
     system spends it there and nowhere else.

     `--plate-light` is live: it is the ground a cut-out prints on. The dark one
     is recorded here rather than used, because an SVG filter cannot read a custom
     property and the values live in the page's own `#duotone` tables — change
     one and change the other. */
  --plate-dark: #101c38;
  --plate-light: #f2ead9;

  /* Measure and rhythm. Every vertical space on the page is a multiple of
     0.4rem, so nothing is arbitrary. */
  --gutter: clamp(1.15rem, 4vw, 2rem);
  --shell: 64rem;            /* the nav bar's width, constant on every page */
  --radius: 10px;
  --radius-sm: 6px;

  /* The 1973 bolt, as a mask rather than a coloured image: a mask inherits
     `background-color`, so the same declaration follows the theme instead of
     freezing one hex per palette. */
  --bolt: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 32'%3E%3Cpath d='M 14 0 L2 18 h8 L7 32 L22 12 h-9 z'/%3E%3C/svg%3E");

  /* The mark on a nod that is a link, drawn for the same reason the bolt is.
     A note CHARACTER would arrive from whatever face each platform falls back
     to, because the heading stack is condensed Latin text and carries no music
     symbol, and it would be a blank box where none of them carries it. Drawn,
     it inherits `background-color` and no screen reader announces it. */
  /* The `viewBox` is cropped to the ink, and that is the whole finding: at
     `0 0 24 32` the drawing occupied y 5 to 28, so `mask: … / contain` scaled
     to the empty 32 and drew a 6.0px glyph inside a 9.5px box, which survives
     at 2x and is a smudge at 1x. Cropped, the same drawing measures 8.0px in
     the same space, a third more glyph for one string. */
  --note: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0.8 5 22.4 23'%3E%3Cellipse cx='6' cy='24' rx='5.2' ry='4'/%3E%3Cellipse cx='18' cy='21' rx='5.2' ry='4'/%3E%3Cpath d='M 9.5 8h2.5v16H9.5z'/%3E%3Cpath d='M 20.5 5h2.5v16h-2.5z'/%3E%3Cpath d='M 9.5 8 L22.5 5 v3 l-13 3 z'/%3E%3C/svg%3E");

  /* Printed-poster grain. One 160px tile, generated in the file, so it costs
     no request and no JavaScript. Kept under 4% — it should be felt, not
     seen; a visible texture on a page of code is noise in both senses. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-opacity: 0.035;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f7f2e9;
    --stage: #fffdf7;
    --card: #f1ebdd;
    --line: #dbd2be;
    --line-soft: #e7e0d0;
    --fg: #17150f;
    --dim: #6a6559;
    --red: #c2262b;
    --blue: #1f4fd8;
    --link: #1a45c4;
    --gold: #8a5a00;
    --gold-display: #b87d20;
    --code: #23345a;
    --plate-light: #f6efe0;
    --grain-opacity: 0.05;
  }
}

/* == 2. base ============================================================== */

* { box-sizing: border-box; }

/* The stage light behind the hero is absolutely positioned and deliberately
   wider than the page, which makes it part of the document's scroll width:
   without this, a phone scrolls sideways into empty gradient. `clip` rather
   than `hidden` because `hidden` would make the body a scroll container and
   take the sticky nav down with it. */
html, body { overflow-x: clip; }

/* The nav is sticky at `top: 0`, so a fragment scrolled to the top of the
   scrollport arrives UNDERNEATH it: the row a reader was sent to is the one
   row they cannot see. This was true of every anchor on the site before any
   of them was a nod, `/project/#panel` and `/project/#zen` included, and both
   are linked from the page that carries the tracklist.
   TWO VALUES, BECAUSE THE NAV HAS TWO HEIGHTS AND NOTHING BETWEEN THEM: the
   design seat measured it at 18 widths and it is 51.02px from 1400 down to
   801 and 81.16px at 800 and below, which is its own `max-width: 800px` query.
   The first version of this rule was one value, 5.5rem, reasoned from the box
   rather than measured against a scroll, and it left 36.72px of dead space at
   1200 and 6.69px at 390, where the row hugged the nav. These two were
   re-measured from a real fragment scroll and leave one line's leading in each
   band, 16.72px and 18.69px. */
html { scroll-padding-top: 4.25rem; }
@media (max-width: 800px) {
  html { scroll-padding-top: 6.25rem; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 1.0625rem/1.72 Georgia, 'Iowan Old Style', 'Times New Roman', serif;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}
/* Light text on a dark stage renders heavier than it measures; this is the
   one place where smoothing is a typographic decision and not a preference. */
@media (prefers-color-scheme: dark) {
  body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
}

/* The grain sits above everything and receives nothing — a texture that eats
   a click is a bug wearing a costume. */
body::after {
  content: '';
  position: fixed; inset: 0; z-index: 9999;
  background: var(--grain) repeat;
  opacity: var(--grain-opacity);
  pointer-events: none;
}

::selection { background: var(--red); color: #fff; }

/* `height: auto` is not decoration: without it an image whose own rule has not
   loaded takes the column's width from `max-width` and keeps the HEIGHT
   attribute from its tag, which renders it stretched rather than merely large.
   Found on 2026-09-08 on the book cover, 800x1135, against a cached stylesheet.
   `svg` keeps its own sizing, because the inline marks here set width and
   height in CSS on purpose. */
img { max-width: 100%; height: auto; }
svg { max-width: 100%; }

/* == 3. shell ============================================================= */

/* Two layouts, and the difference is deliberate.

   A reading page (why, errors, self-hosted, zen, log, docs, panel) is a column
   of prose at the book measure, with code and figures allowed to bleed out
   symmetrically into a wider track — the editorial move that lets a 70-column
   diagnostic keep its alignment without stretching the prose to 110 characters
   a line.

   The home page is a stage instead: one left-hand axis, everything ranged off
   it, prose held short inside a wide frame. Centring the landing would put the
   hero on one axis and the paragraphs on another. */

/* Three tracks, and the middle one is the only one with a size: the two
   flexible tracks either side are the bleed, and they are what disappears
   first when the screen gets narrow. Sizing the bleed instead — the obvious
   way round — makes a phone keep 88px of empty margin and squeeze the prose
   into what is left, which is what the first cut of this did. */
main {
  --measure: 38rem;          /* the reading column */
  --wide: 49rem;             /* how far a code panel may bleed */
  max-width: calc(var(--wide) + 2 * var(--gutter));
  margin-inline: auto;
  padding: 2.8rem var(--gutter) 6rem;
  display: grid;
  grid-template-columns:
    [wide-start] 1fr
    [text-start] min(100%, var(--measure)) [text-end]
    1fr [wide-end];
}
main > * { grid-column: text; }
main > pre,
main > figure,
main > .stats,
main > ul.cards,
main > ol.chapters,
main > ol.tracks,
main > ul.seats,
main > .next,
main > footer { grid-column: wide; }

/* A section rule and a page title are the page's architecture, so they measure
   the wide track and not the reading column — otherwise every hairline stops
   short of the code blocks under it and the page reads as two grids. The text
   inside them is pushed back onto the reading column by a padding that goes to
   zero the moment the track is narrower than the measure. */
main:not(.home) > h2,
main:not(.home) > .page-head {
  grid-column: wide;
  padding-inline: max(0px, calc((100% - var(--measure)) / 2));
}

/* Reading pages number their own sections, and the number is set large and
   faint in the margin the bleed track leaves free. It is the one thing that
   turns a long page into a page with parts: the eye can see how many there
   are, and where it is. Below the width where that margin exists, it goes. */
main:not(.home) { counter-reset: sec; }
main:not(.home) > h2 { counter-increment: sec; }
main:not(.home) > h2::after {
  content: counter(sec, decimal-leading-zero);
  position: absolute; left: 0.1rem; top: 1.35rem;
  font-weight: 700; font-size: 2.1rem; line-height: 1;
  /* Measured at 1.36:1 dark and 1.30:1 light, which is not a wayfinding
     device: on a dimmed screen it is not there at all. 22% is still quiet. */
  color: color-mix(in srgb, var(--fg) 22%, transparent);
  letter-spacing: -0.02em; pointer-events: none;
}
@media (max-width: 900px) { main:not(.home) > h2::after { display: none; } }

main.home {
  display: block;
  max-width: 60rem;
  margin: 0 auto;
  padding: 0 var(--gutter) 6rem;
}
/* Prose on the stage stays at the book measure even though the frame is wide:
   108 characters a line is not readable, and a landing page is still reading. */
main.home > p,
main.home > blockquote,
main.home > footer p { max-width: 41rem; }

/* == 4. nav =============================================================== */

/* The nav is one component, `src/components/SiteNav.astro`, rather than markup
   repeated in every page. Keep the items and their order in sync there; the
   current page marks itself with class="here". */

nav.site {
  position: sticky; top: 0; z-index: 20;
  border-bottom: 1px solid var(--line-soft);
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
}
nav.site > div {
  max-width: var(--shell); margin: 0 auto;
  padding: 0.65rem var(--gutter);
  display: flex; align-items: center; gap: 1.6rem; flex-wrap: wrap;
}
nav.site .mark {
  display: flex; align-items: center; gap: 0.5rem; text-decoration: none;
  color: var(--fg);
  font-family: 'Avenir Next Condensed', 'HelveticaNeue-CondensedBold', 'Helvetica Neue', 'Roboto Condensed', 'Arial Narrow', sans-serif;
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
  font-size: 1.05rem;
}
nav.site .mark svg {
  width: 0.85rem; height: 1.15rem; display: block;
  transition: transform 160ms ease;
}
nav.site .mark:hover svg { transform: rotate(8deg) scale(1.12); }
nav.site ul {
  display: flex; gap: 1.2rem; list-style: none; margin: 0; padding: 0;
  flex-wrap: wrap;
}
nav.site a:not(.mark) {
  font: 0.775rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); text-decoration: none; letter-spacing: 0.04em;
  padding-bottom: 2px; border-bottom: 1px solid transparent;
  transition: color 120ms ease, border-color 120ms ease;
}
nav.site a:not(.mark):hover { color: var(--fg); border-bottom-color: var(--line); }
/* The current page is named twice — colour and a rule under it — because
   colour alone is a signal a third of readers get less of. */
nav.site a.here { color: var(--red); border-bottom-color: var(--red); }
nav.site .out { margin-left: auto; }

/* The language switch. There are two editions of the same site, so the control
   is a two-letter badge beside the outbound link rather than a tenth item in
   the row: the row is full at nine (see the README), and a language is not a
   page. It takes over the auto margin so the pair sits right together, and it
   keeps a box because a bare `IT` in the dim colour reads as an abbreviation
   in the prose rather than as something you can press. */
nav.site a.lang {
  margin-left: auto;
  padding: 0.16rem 0.42rem; border: 1px solid var(--line); border-radius: 3px;
}
nav.site a.lang:hover { color: var(--red); border-color: var(--red); }
nav.site a.lang + .out { margin-left: 0; }

/* Sticky chrome has a budget on a phone: four wrapped rows was 19% of the
   viewport, and eight items wrap to three rows on their own. So on narrow
   screens the item list takes a single line of its own and scrolls sideways,
   with the mark and GitHub sharing the row above it (`.out` keeps its auto
   margin, `ul` is ordered last and takes the full width). Two fixed rows
   instead of three, no item hidden, and the item clipped at the right edge is
   the affordance that says the row continues. */
@media (max-width: 800px) {
  nav.site > div { gap: 0.45rem 0.9rem; padding-block: 0.55rem; }
  nav.site .mark { font-size: 0.95rem; }
  nav.site a:not(.mark) { font-size: 0.74rem; letter-spacing: 0.02em; }
  nav.site ul {
    order: 3; width: 100%; flex-wrap: nowrap; gap: 1rem;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* The clipped item WAS the affordance, by the note above, and rendered at
       390px it reads as a rendering bug: "Autho", sliced flat. The row
       dissolves at its right edge instead, which is the sentence the code
       blocks already say with their scroll edge. Honest limit: CSS alone
       cannot make this scroll-aware, so once the row is scrolled the left item
       is cut flat again. A right-only fade is the whole of what is available
       without JavaScript. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.2rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.2rem), transparent);
  }
  nav.site ul::-webkit-scrollbar { display: none; }
  nav.site ul li { flex: none; }
}

/* == 5. headings ========================================================== */

/* The poster face. Condensed caps from the system stack — never a webfont,
   so the page paints on the first byte and owes nobody a request. */
h1, h2, h3, .cta a, .stats .n, .figures .n, .seats .who {
  font-family: 'Avenir Next Condensed', 'HelveticaNeue-CondensedBold', 'Helvetica Neue', 'Roboto Condensed', 'Arial Narrow', sans-serif;
}

header { position: relative; }

h1 {
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.01em;
  font-size: clamp(3rem, 9vw, 4.6rem); margin: 0; line-height: 0.95;
  text-wrap: balance;
}
/* Bowie's own quotation marks, in the album's own irony. */
h1 .q {
  /* The DISPLAY gold: `--gold` is bound by 11px nods and reads as mustard at
     90px on paper, measured. In dark the two are one colour. */
  color: var(--gold-display); font-size: 0.82em; vertical-align: 0.05em;
}

.bolt {
  position: absolute; top: -1.6rem; right: 0.2rem; width: 4.4rem; height: auto;
  transform: rotate(8deg); pointer-events: none;
}
.tagline {
  font-size: 1.2rem; color: var(--dim); margin: 0.9rem 0 0; font-style: italic;
}

/* Section headings read as a tracklist: the title on the left, the song nod
   ranged right on the same line, a hairline above. Gold on the nod because a
   song title is a quotation, and gold is this site's quotation colour. */
h2 {
  position: relative;
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;
  font-size: 1.18rem; color: var(--red); margin: 3.4rem 0 1.1rem;
  /* `nowrap`, and the last fix of this got the scope wrong. The heading text
     is an anonymous flex item whose hypothetical size is its max-content
     width, so a heading long enough to wrap became a SECOND flex line and left
     `::before`, the bolt, alone on the first. The repair went into a
     `max-width: 700px` block, and the cause is width-independent: it is alive
     at 1200px on /docs/errors/ section 03. */
  display: flex; align-items: baseline; gap: 0.6rem 1rem; flex-wrap: nowrap;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  /* The rule over a section is not a neutral hairline: it carries the two
     stage colours, fading out before it reaches the end of the measure. */
  border-image: linear-gradient(90deg,
      color-mix(in srgb, var(--red) 70%, transparent),
      color-mix(in srgb, var(--blue) 45%, transparent) 38%,
      var(--line) 72%) 1;
}
main > h2:first-of-type { padding-top: 0; border-top: 0; margin-top: 2.6rem; }
/* The counter is offset from the padding box, and the first heading drops its
   1.5rem of padding without the counter following: measured, `01` sat 24px
   below its heading on every reading page while `02` onward sat level. */
main:not(.home) > h2:first-of-type::after { top: -0.15rem; }
main.home > h2 { margin-top: 4.2rem; }

h2::before {
  content: ''; flex: none; width: 0.7em; height: 0.95em;
  background-color: var(--blue);
  -webkit-mask: var(--bolt) no-repeat center / contain;
  mask: var(--bolt) no-repeat center / contain;
  transform: translateY(0.08em);
}
h2 .nod {
  margin-left: auto; font-size: 0.68rem; letter-spacing: 0.22em;
  color: color-mix(in srgb, var(--gold) 78%, var(--dim));
  font-weight: 400; white-space: nowrap;
}

/* A nod is a link now, and it goes to its own row in the tracklist, where the
   record it comes from is named. The link keeps the nod's gold: a song title is
   a quotation before it is a link, and `--link` here would spend the site's
   navigation colour on an easter egg in twenty-two places. The separators stay
   outside the anchor, so the clickable thing is the words and a reader dragging
   to select the heading has somewhere to start. `text-decoration-line` and not
   the shorthand, which resets the 1px thickness every other link on the site
   sets. `padding-block` grows an 11px hit area to about 24px and changes no
   line box. Every length in `em`, because one rule serves the 0.68rem caps nod
   in a heading and the 0.7rem monospace nod under a page title. */
.nod a {
  color: inherit; text-decoration-line: none; text-underline-offset: 2px;
  padding-block: 0.4rem;
}
.nod a:hover { text-decoration-line: underline; }
.nod a::after {
  content: ''; display: inline-block;
  /* Measured against the cropped `--note`: 8.0px of glyph, its top at cap top
     and its bottom half a pixel under the baseline, with an optical gap of 4px
     to the last letter. `em` and never `rem`, because one rule serves the
     0.68rem caps nod and the 0.7rem monospace one. */
  width: 0.73em; height: 0.75em; margin-left: 0.1em;
  vertical-align: -0.05em;
  background-color: currentColor;
  -webkit-mask: var(--note) no-repeat center / contain;
  mask: var(--note) no-repeat center / contain;
}

h3 {
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  font-size: 0.92rem; color: var(--fg); margin: 2.6rem 0 0.6rem;
}

/* Code keeps its own case inside a heading, and the language decides that
   case: `m[k]` set as `M[K]` and `chars()` as `CHARS()` are not the names the
   compiler answers to. The poster styling is the page's; the spelling is the
   program's, and where they disagree the program wins. */
h1 code, h2 code, h3 code, .cards h3 code, dt code {
  text-transform: none; letter-spacing: 0;
}

/* == 6. prose ============================================================= */

p { margin: 0 0 1.3rem; }

a {
  color: var(--link);
  text-decoration-thickness: 1px; text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--link) 45%, transparent);
  transition: text-decoration-color 120ms ease, color 120ms ease;
}
a:hover { text-decoration-color: currentColor; }

b, strong { font-weight: 700; }

/* The pull quote. The site's one loud typographic gesture, and it earns the
   gold: the album this language is named after wears its title in quotation
   marks, so the marks are the motif, blown up and set behind the text. */
blockquote {
  position: relative;
  margin: 1.8rem 0; padding: 0.2rem 0 0.2rem 1.6rem;
  border-left: 2px solid var(--gold);
  font-size: 1.2rem; line-height: 1.5; font-style: italic;
  text-wrap: pretty;
}
/* The mark sits behind the first line, inside the quote's own box — outside it
   it read as a stray glyph floating in the margin. */
blockquote::before {
  content: '\201C';
  position: absolute; left: 1.1rem; top: -0.28em; z-index: -1;
  font-size: 4.2rem; line-height: 1; color: var(--gold);
  opacity: 0.18; pointer-events: none;
}

ul.pillars { list-style: none; padding: 0; margin: 1.4rem 0; }
/* 1.6rem rather than 1.1: at a 1.72 leading the old gap put 46.5px between
   bullets against 29.5px inside a wrapped one, and four claims read as one
   paragraph with dashes. */
ul.pillars li {
  margin: 1.1rem 0; padding-left: 1.5rem; position: relative;
  text-wrap: pretty;
}
/* A small dot, not a dash. The author did not like the dashes as bullets
   (2026-09-07), and a dash under a hero whose stat cards run links on with
   arrows read as one more separator. */
ul.pillars li::before {
  content: ''; position: absolute; left: 0.15rem; top: 0.55em;
  width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--red);
}
ul.pillars b { font-style: normal; color: var(--fg); }

/* The objections are a list of questions, so they are set as one: numbered,
   ruled off from each other, the question in the poster face and the answer
   in the reading face. Ten paragraphs of prose in a row read as a wall; ten
   numbered exchanges read as something you can enter anywhere. */
dl.why { counter-reset: q; margin: 1.8rem 0 0; }
dl.why dt {
  counter-increment: q; position: relative;
  margin-top: 2.4rem; padding-top: 1.3rem; padding-left: 2.6rem;
  border-top: 1px solid var(--line-soft);
  color: var(--fg); font-size: 1.06rem; font-weight: 700; line-height: 1.35;
  text-wrap: pretty;
}
dl.why dt::before {
  content: counter(q, decimal-leading-zero);
  position: absolute; left: 0; top: 1.45rem;
  font: 0.74rem/1.4 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--red); letter-spacing: 0.06em;
}
dl.why dd {
  margin: 0.55rem 0 0; padding-left: 2.6rem;
  color: color-mix(in srgb, var(--fg) 88%, var(--dim));
}
@media (max-width: 620px) {
  dl.why dt, dl.why dd { padding-left: 0; }
  dl.why dt::before { position: static; display: block; margin-bottom: 0.3rem; }
}

/* The standing notice: the one place on the site where a page says that
   something it promises is not reachable yet. It sits above the section whose
   promise it qualifies, never at the top of a page, because a caveat divorced
   from the sentence it modifies reads as a banner and gets skipped.

   It borrows the gold bar from the pull quote rather than the red, and this is
   the whole of the decision. Gold already means "somebody is speaking" on this
   site, which is what a notice and a quotation both are. Red on a bar means the
   opposite: the SITE'S OWN FURNITURE speaking, the box that says where to go
   next (`.next`) and the box that holds the measured numbers (`.stats`). Two
   bars, two voices, and the design seat's render found the earlier version of
   this comment claiming red would read as an error while `.next` carried it on
   every chapter and every example page. The comment was wrong and the practice
   was right: a reader learns the red box as "the way on" by the second page.
   No icon, no background wash, no uppercase label: the difference from a
   paragraph is a rule and a measure, and that is enough to stop the eye. */
.notice {
  margin: 1.6rem 0;
  padding: 0.1rem 0 0.1rem 1.2rem;
  border-left: 2px solid var(--gold);
  color: color-mix(in srgb, var(--fg) 86%, var(--dim));
  text-wrap: pretty;
}
.notice p { margin: 0.7rem 0; }
.notice b { color: var(--fg); }

/* The author's book, on the two landings and nowhere else. A card rather than a
   bar across the top: this site sells nothing, so the one block that points
   outward sits low on the page, where the author has just been introduced, and
   a reader who does not care scrolls past it. Gold on the left, like a notice,
   because it is an aside and not a claim about the language. */
.book {
  margin: 2.2rem 0;
  padding: 1.1rem 1.35rem;
  background-color: var(--card);
  border: 1px solid var(--line);
  border-left: 2px solid var(--gold);
  border-radius: var(--radius);
}
.book { display: flex; gap: 1.35rem; align-items: flex-start; }
.book > div { flex: 1 1 auto; min-width: 0; }
.book p { margin: 0; text-wrap: pretty; }
.book p + p { margin-top: 0.65rem; }
.book b { color: var(--fg); }
.book-buy { color: var(--dim); }

/* The cover, small on purpose: it is a token of a book and not a poster, and it
   keeps its own aspect so a slow connection reflows nothing. `flex: none` so it
   does not shrink when the paragraph beside it is long, and `aspect-ratio`
   stated as well as the tag's attributes, so no missing stylesheet and no
   later rule can stretch it (author, 2026-09-08: it was too big and out of
   proportion, and both halves of that were true for the same reason). */
.book-cover { flex: none; display: block; line-height: 0; }
.book-cover img {
  width: 72px; height: auto; aspect-ratio: 800 / 1135; display: block;
  border-radius: 2px;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
}

/* The same pair on the About page, where the book has a section of its own:
   no card around it, because it is the page's subject there rather than an
   aside, and a larger cover because there is room for one. */
.book-plate { display: flex; gap: 1.5rem; align-items: flex-start; margin: 1.6rem 0; }
.book-plate > div { flex: 1 1 auto; min-width: 0; }
.book-plate p { margin: 0; text-wrap: pretty; }
.book-plate p + p { margin-top: 0.7rem; }
.book-plate .book-cover img { width: 112px; }

/* Under 34rem the row becomes a column, because 92px of cover beside two
   sentences leaves the sentences about twelve characters wide on a phone. */
@media (max-width: 34rem) {
  .book, .book-plate { display: block; }
  .book-cover { margin-bottom: 0.9rem; }
  .book-cover img, .book-plate .book-cover img { width: 96px; }
}

/* == 7. code ============================================================== */

pre {
  background-color: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.15rem 1.35rem;
  margin: 1.6rem 0;
  overflow-x: auto; overscroll-behavior-x: contain;
  tab-size: 4;
  font: 0.845rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--code);
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  /* Scroll shadows, in CSS alone: two opaque patches pinned to the content
     (`local`) hide two shadows pinned to the frame (`scroll`), so the shadow
     only shows on the side that still has code hiding off-screen. A block
     that scrolls now says so. */
  background-image:
    linear-gradient(to right, var(--card) 40%, transparent),
    linear-gradient(to left,  var(--card) 40%, transparent),
    /* Two things were wrong here and only one of them was the colour.
       The colour: `rgba(0,0,0,0.28)` over a #14141d card is #0e0e15, a black
       shadow on a near-black ground. The edge takes its value from the ink
       instead, so on the stage it is a lift and on paper a shade, one rule for
       both themes.
       The shape, and this was the bigger half: these were RADIAL, so they faded
       vertically as well and a tall block showed its edge only at its own
       mid-height. Fixing the colour alone still measured a luminance delta of
       2.3 at the edge, because the sample was nowhere near that mid-height.
       Linear, and the whole edge says it. Measured over the `interpreter`
       page's tallest scrolling block, 4,689px, at 2%, 50% and 98% of its
       height: dark 41.9 / 40.0 / 42.0, light 41.6 / 41.6 / 41.1. */
    linear-gradient(to left, color-mix(in srgb, var(--fg) 26%, transparent), transparent),
    linear-gradient(to right, color-mix(in srgb, var(--fg) 26%, transparent), transparent);
  background-position: left center, right center, right center, left center;
  background-size: 3rem 100%, 3rem 100%, 1.1rem 100%, 1.1rem 100%;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}
pre::-webkit-scrollbar { height: 8px; }
pre::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }

code {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 0.9em;
}
/* Inline code inside prose is a different object from a code block: it is a
   word in a sentence, so it gets a tint rather than a box. */
p > code, li > code, dd > code, dt > code, figcaption code {
  background: color-mix(in srgb, var(--fg) 7%, transparent);
  border-radius: var(--radius-sm);
  /* THE TINT IS PAINTED, NOT PADDED, and the difference is a word space before
     every comma. 0.3em of side padding widened the inline box, so a chip
     followed by punctuation set as `heroes` , `lex` , `parse` , with the comma
     floating clear of the word. On /start/ that is eleven floating commas, a
     floating colon and a floating full stop in two lines of Georgia; on every
     chapter that names a keyword it is in the tagline under the title.
     A `box-shadow` spread paints outside the inline box without changing its
     advance width, which is the technique `pre.table .hi` already uses. */
  padding: 0.08em 0;
  box-shadow: 0 0 0 0.28em color-mix(in srgb, var(--fg) 7%, transparent);
}

/* ------- syntax colouring, done in markup rather than by a script ----------
   The token classes are the lexer's own tables, not a guess: `.k` is
   `lexer/keywords.rs::keyword`, `.t` is spec § Types. Three accents only, as
   the style guide demands — red for the words the language reserves, blue for
   its types, gold for literals, which is the same gold as the quotation marks.

   A user's own type (`Point`) is deliberately NOT coloured. Doing so would mean
   guessing from the capital letter, and the spec mandates no such convention —
   `snake_case` at spec:136 is about error codes and nothing else. That is a
   premise about the world rather than a fact about the token, so it stays out. */
pre .c { color: var(--dim); }                      /* comments */
pre .k { color: var(--red); }                      /* keywords */
pre .t { color: var(--blue); }                     /* built-in types */
pre .s, pre .n { color: var(--gold); }             /* strings, chars, numbers */
pre .h { color: var(--red); font-style: italic; }  /* ??? — a hole is a question */
/* Since 2026-10-24, the highlighter's four classes more (`src/lib/highlight.ts`).
   A name a declaration gives is bold in the text's own colour, the one word a
   reader looks for in a signature. An escape inside a literal is the literal's
   gold, bold, so `\n` reads as one thing. What the lexer refuses carries the
   one mark every reader already knows for wrong, a wavy red line under the
   text, never a background, so it stays selectable and copies as it is. */
pre .d { font-weight: 700; color: var(--fg); }
pre .e { font-weight: 700; color: var(--gold); }
pre .x { text-decoration: underline wavy var(--red); text-decoration-skip-ink: none; text-underline-offset: 0.2em; }
/* Markdown, on the one page that serves a markdown document verbatim: a heading
   takes the keyword red, because a heading is to a document what a keyword is to
   a program; a code span or fence takes the literal gold it already has inside a
   Heroes block; a table's pipes and rules are dim furniture; and `**bold**` is
   bold, keeping its asterisks, because the page shows the source a reader
   pastes and not a rendering of it (`site/src/lib/markdown.ts`). */
pre .b { font-weight: 700; color: var(--fg); }

/* Real diagnostics have long message lines and short caret lines. Wrapping
   lets the message reflow into the column while the caret rows — which are
   short — keep the alignment that makes them mean anything. */
/* A diagnostic is a figure and not prose: the caret's meaning IS its column,
   so the block scrolls rather than reflows. The rule here used to be
   `pre-wrap`, on the premise that the message lines reflow while the short
   caret lines keep their alignment. Rendered at 390px, that premise is false:
   the caret rows do survive, and the SOURCE line above them does not, so the
   caret ends up under nothing. Found by the design seat, which rendered it. */
/* `text-wrap` applies to a block container, so the declaration that stood on
   `.msg` below did nothing: the design seat served a patched copy and the
   render was byte-identical to the live one, orphan intact (2026-09-10). It
   belongs here, where the message's own line breaks are decided. */
pre.diag { white-space: pre; overflow-x: auto; text-wrap: pretty; }
/* The message line is the product, and at 1200px dark it was cut mid-sentence
   on the page that exists to show it: "so every one of them is named at the
   call" and then the frame. Only the message wraps; the excerpt and caret rows
   below keep their columns, which is the whole reason the block stopped
   wrapping in the first place. The span adds no text byte, so the output stays
   verbatim. */
pre.diag .msg { white-space: pre-wrap; overflow-wrap: anywhere; }

/* A measured table, set as one row of monospace text. `.hi` is the row the
   prose singles out: caught none of the time, in either arm. It is marked
   rather than recoloured, because the number is not an error — it is the
   honest row, and the page says so. */
pre.table { font-size: 0.8rem; }
pre.table .hi {
  background: color-mix(in srgb, var(--red) 16%, transparent);
  box-shadow: 0 0 0 0.22em color-mix(in srgb, var(--red) 16%, transparent);
  border-radius: 2px; color: var(--fg);
}

/* Every code block on this site is a slice of a file that compiles and runs.
   `data-src` (+ optional `data-lines`) is the machine-readable half of that
   promise: M-documentation-site's drift check greps these attributes and diffs
   the block against the file. The figcaption is the same promise, for readers. */
figure.example { margin: 1.8rem 0; }
figure.example pre { margin: 0 0 0.45rem; }
figure.example figcaption {
  font: 0.74rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); text-align: right; text-wrap: balance;
}
figure.example figcaption::before { content: '\21B3\00A0'; color: var(--dim); }

/* A slice of a record rather than of a program: a measurement's transcript, the
   pre-registration's sentences, a digest line (`why/measured.html`). Its lines
   are sentences and pipes, so it wraps at every width for the reason the
   specification block does, and a digest too long for a phone breaks rather
   than pushing the page sideways. The class outranks the narrow-screen
   `figure.example pre` rule below by specificity, so one rule covers both. */
figure.example.record pre { white-space: pre-wrap; overflow-wrap: break-word; }

/* == 8. home ============================================================== */

/* The hero is a two-column stage: the promise on the left, and on the right a
   real program in the page's own furniture. Under it, three numbers that make
   the claim checkable. */
.hero {
  position: relative;
  padding: 3.4rem 0 3rem;
  display: grid; grid-template-columns: 1.02fr 1fr; gap: 3rem;
  align-items: center;
}
.hero > div { min-width: 0; }
.hero > div:first-child { position: relative; }

/* One light source, thrown from the bolt's own colours and raked along the
   bolt's own angle. Two washes and a beam between them — the beam is what
   stops it reading as a generic gradient. */
.hero::before {
  content: ''; position: absolute; z-index: -1;
  top: -7rem; left: -22%; right: -22%; height: 145%;
  background:
    radial-gradient(34rem 20rem at 20% 30%, color-mix(in srgb, var(--red) 22%, transparent), transparent 68%),
    radial-gradient(30rem 19rem at 76% 66%, color-mix(in srgb, var(--blue) 19%, transparent), transparent 68%),
    linear-gradient(97deg, transparent 38%, color-mix(in srgb, var(--red) 7%, transparent) 47%, color-mix(in srgb, var(--blue) 7%, transparent) 53%, transparent 62%);
  pointer-events: none;
}
@media (prefers-color-scheme: light) { .hero::before { opacity: 0.72; } }

/* Says what this is before it says what it is called — a visitor who has never
   heard the name should not have to infer the category. */
.hero .kicker {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 0 0 0.8rem;
  font: 0.75rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  /* 0.16em rather than 0.2: the Italian kicker is six characters longer than
     the English and wrapped, putting COMPILATO alone on a second line under a
     rule that marked only the first. Measured, it was about nine pixels short. */
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--blue);
}
.hero .kicker::before {
  content: ''; width: 1.7rem; height: 1px; background: var(--blue); flex: none;
}
/* The name is set clean: no shadow, no misregistration, nothing behind the
   letters (author instruction 2026-08-18). The two-plate print effect that used
   to sit here — red plate a hair left, blue a hair right — is gone, and with it
   the `h1 .q` override that existed only to keep the gold quotation marks out
   of it. The red and blue still carry the page: they are in the wash behind the
   hero and in the bolt. */
.hero h1 {
  font-size: clamp(3.4rem, 8.4vw, 5.6rem);
}
.hero .promise {
  font-size: clamp(1.3rem, 2.6vw, 1.6rem); line-height: 1.28;
  margin: 1.2rem 0 0.6rem; font-weight: 700; text-wrap: balance;
}
.hero .promise em { color: var(--red); font-style: normal; }
.hero .tagline { font-size: 1.02rem; margin: 0.7rem 0 0; max-width: 30rem; }
.hero .bolt {
  position: static; display: block; width: 2.8rem; height: auto;
  margin: 0 0 1rem; transform: rotate(8deg);
  filter: drop-shadow(0 0 1.6rem color-mix(in srgb, var(--red) 35%, transparent));
}

/* the demo card: a code panel, in the page's own furniture */
.demo {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--stage); overflow: hidden;
  box-shadow: 0 1.4rem 3rem -1.6rem rgba(0,0,0,0.55);
}
.demo .bar {
  display: flex; align-items: center; gap: 0.55rem;
  padding: 0.6rem 0.9rem; border-bottom: 1px solid var(--line-soft);
  background: color-mix(in srgb, var(--card) 60%, transparent);
  font: 0.71rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace; color: var(--dim);
}
/* No window chrome. The bar names the file, and a poster is not a window; the
   three dots also spent gold on decoration, and gold on this site is the
   quotation colour and nothing else (site/README.md § The visual system). One
   red mark stays, because the bolt's red is what marks a panel as this site's. */
.demo .bar::before {
  content: ''; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--red); flex: none;
  margin-right: 0.65rem;
}
/* The demo card's `bar` names the file, so it is that block's caption and the
   figure carries no second one. */
.demo figure.example { margin: 0; }
.demo figure.example figcaption { display: none; }
.demo pre {
  border: 0; border-radius: 0; margin: 0;
  background: transparent; background-image: none;
  font-size: 0.79rem; line-height: 1.55;
}
.demo .under {
  padding: 0 0.95rem 0.85rem; margin: 0;
  font: 0.71rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace; color: var(--dim);
}
.demo .under a { text-decoration: none; }

/* Three numbers, set as a band with hairlines above and below and rules
   between them: a strip of evidence rather than three loose facts. */
.figures {
  display: grid; grid-template-columns: repeat(3, 1fr);
  margin: 1.5rem 0 0; padding: 0; list-style: none;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.figures li {
  margin: 0; padding: 1.15rem 1.4rem 1.15rem 0;
}
.figures li + li {
  border-left: 1px solid var(--line-soft); padding-left: 1.4rem;
}
.figures .n {
  display: block; color: var(--red); font-size: 1.5rem; line-height: 1.1;
  font-weight: 700; letter-spacing: 0.01em; font-variant-numeric: tabular-nums;
}
.figures .what {
  /* Two lines reserved, so a longer label in one edition cannot knock the three
     columns out of line in the other: measured on the Italian home, where the
     first label wraps and the other two do not. */
  min-height: calc(2 * 1.45 * 0.72rem);
  display: block; margin-top: 0.3rem;
  font: 0.72rem/1.45 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim);
  text-wrap: balance;
}
/* Each number says in one sentence what was counted. A bare figure on a
   landing page is a boast; a figure with its method is evidence. */
.figures .how {
  display: block; margin-top: 0.6rem;
  font-size: 0.88rem; line-height: 1.55;
  color: color-mix(in srgb, var(--fg) 76%, var(--dim));
}
/* Stacked as a list rather than run on with a separator: the card is about
   230px wide and the English pair can never share a line, so the middot that
   bound them opened line two, right under a hero whose bullets are dashes. */
.figures .how a { white-space: nowrap; display: block; }
/* The caption under the home's demo is text, then a mark, then a link, so the
   `&nbsp;` pass that bound every arrow to its word could not reach this one:
   the mark's left neighbour is not an element. Under 560px the two halves
   already stack, and a `&middot;` at the end of a line is a mark pointing at
   nothing, so it goes away there (design seat, 2026-09-10). */
.demo .under .sep { white-space: nowrap; }
@media (max-width: 560px) { .demo .under .sep { display: none; } }
/* The method behind the first number, under the band rather than inside its
   cell: in the cell it ran ten lines beside three and six, and the hairline hung
   five empty lines under the third figure (design seat, 2026-09-09). */
.figures-note {
  margin: 0.9rem 0 0; max-width: 41rem;
  font-size: 0.88rem; line-height: 1.55;
  color: color-mix(in srgb, var(--fg) 76%, var(--dim));
}
@media (max-width: 620px) {
  .figures { grid-template-columns: 1fr; }
  .figures li + li { border-left: 0; border-top: 1px solid var(--line-soft); padding-left: 0; }
}

.cta { display: flex; gap: 0.8rem; flex-wrap: wrap; margin: 2rem 0 0; }
.cta a {
  display: inline-block; padding: 0.55rem 1.25rem; border-radius: 999px;
  text-decoration: none; border: 1px solid var(--red);
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  font-size: 0.9rem;
  transition: transform 140ms ease, background-color 140ms ease,
              color 140ms ease, box-shadow 140ms ease;
}
.cta a.primary {
  background: var(--red); color: var(--bg); border-color: var(--red);
}
.cta a.primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 0.7rem 1.6rem -0.6rem color-mix(in srgb, var(--red) 75%, transparent);
}
.cta a.ghost { color: var(--red); }
.cta a.ghost:hover { background: var(--red); color: var(--bg); transform: translateY(-2px); }

.cards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.4rem; margin: 1.6rem 0 0; padding: 0; list-style: none;
}
.cards li {
  margin: 0; padding: 1.4rem 1.4rem 1.2rem; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--card);
  display: flex; flex-direction: column; position: relative; overflow: hidden;
  transition: border-color 160ms ease, transform 160ms ease;
}
/* A card lights up along its top edge on hover — the stage light again, at
   the scale of one object. */
.cards li::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 2px;
  background: linear-gradient(90deg, var(--red), var(--blue));
  opacity: 0; transition: opacity 160ms ease;
}
.cards li:hover { border-color: color-mix(in srgb, var(--red) 55%, var(--line)); transform: translateY(-2px); }
.cards li:hover::before { opacity: 1; }
.cards h3 { margin: 0 0 0.6rem; color: var(--fg); font-size: 0.85rem; letter-spacing: 0.1em; }
.cards p { margin: 0 0 1rem; font-size: 0.95rem; line-height: 1.6; }
.cards a.more {
  margin-top: auto;
  font: 0.73rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  text-decoration: none; letter-spacing: 0.05em;
}
.cards a.more::after { content: ' \2192'; }

/* Under the band of three numbers rather than in the fold, and without the
   version it used to carry (author instruction 2026-09-08): a label over the
   one command that works today, the same lockup at 390 and at 1400. It had no
   rule at all and happened to break into two lines at 1200. */
.whole-language { margin: 1rem 0 0; font-size: 0.95rem; color: var(--dim); }
.whole-language code.cmd { display: block; width: max-content; margin-top: 0.3rem; color: var(--code); }

/* == 9. subpage heads, docs =============================================== */

/* A title, not a marquee: the reading pages announce themselves once and then
   get out of the way. */
/* The bolt, once per reading page, blown up behind the title and cropped by
   the head's own box. Big enough to be architecture, faint enough that the
   title never fights it. */
.page-head {
  position: relative; overflow: hidden;
  padding: 1.6rem 0 1.2rem; border-bottom: 1px solid var(--line);
  margin-bottom: 1.8rem;
}
.page-head::after {
  content: ''; position: absolute; right: 0.5rem; top: -1.4rem;
  width: 7rem; height: 9.5rem; transform: rotate(8deg);
  background-color: var(--red);
  opacity: 0.09;
  -webkit-mask: var(--bolt) no-repeat center / contain;
  mask: var(--bolt) no-repeat center / contain;
  pointer-events: none;
}
@media (max-width: 620px) { .page-head::after { width: 4.5rem; height: 6rem; opacity: 0.07; } }

/* The author's portrait, printed as two inks.

   A real duotone maps every tone to a colour between two plates, and CSS blend
   modes cannot do that — `lighten` and `multiply` only clamp the ends and leave
   the midtones grey, which is a warm photograph rather than a duotone. So the
   mapping is an SVG filter, inline in the page the way the bolt is: one
   `feColorMatrix` takes the photograph to luminance, and one
   `feComponentTransfer` maps 0 to the dark plate and 1 to the light one, per
   channel. No script of the site's own, no request, and the file that ships
   stays the photograph, so replacing it needs no tool.

   A filter cannot read a custom property, so the two inks are written into the
   filter's own tables; `--plate-light` here is the same colour, and it is what
   the transparent corners of a cut-out print on. */
/* The text runs alongside it, which is what a float is for — and a float needs
   a block container, because `main` is a grid and a grid item ignores `float`
   outright. Hence `.byline`, holding the portrait and the lede and nothing else:
   the `h2` under it stays a direct child of `main`, because the section counter
   is `main:not(.home) > h2` and nesting it would drop the number in silence.
   `flow-root` makes the wrapper contain the float without clipping it, so a
   portrait taller than the lede can never reach into the section below. */
.byline { display: flow-root; }
.portrait {
  float: right;
  /* Wider than the space it takes from the prose: it hangs into the right bleed
     by the same amount a code panel does, so its outer edge lands on the line
     every figure on the site is already aligned to, and the reading column only
     gives up the difference. */
  width: 16.5rem;
  margin: 0.35rem calc((var(--measure) - var(--wide)) / 2) 1.5rem 1.9rem;
  border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--line);
  background: var(--plate-light);
  filter: url(#duotone);
}
.portrait img { display: block; width: 100%; height: auto; }
/* Below 900px the bleed tracks are gone, so hanging into them would hang off
   the screen. */
@media (max-width: 900px) { .portrait { width: 13rem; margin-right: 0; } }
/* And on a phone a float would leave the lede in a three-word column. */
@media (max-width: 620px) {
  .portrait { float: none; width: 11rem; margin: 0 0 1.4rem; }
}

/* The filter carries the whole photograph, so a browser that cannot resolve it
   would show it untouched rather than broken — the graceful direction. */
svg.ink { position: absolute; width: 0; height: 0; overflow: hidden; }
}
h1.page {
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em;
  font-size: clamp(2.1rem, 6.5vw, 3rem); margin: 0; line-height: 1.06;
}
.page-head .nod {
  display: block; font: 0.7rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  letter-spacing: 0.22em; margin-top: 0.45rem;
  color: color-mix(in srgb, var(--gold) 78%, var(--dim));
}
.page-head .lede {
  font-size: 1.15rem; color: var(--dim); font-style: italic; margin: 0.8rem 0 0;
}
main p.lede {
  font-size: 1.32rem; line-height: 1.45; color: var(--fg); font-style: normal;
  margin: 0 0 2.4rem; max-width: 33rem; text-wrap: pretty;
}
/* The site's own address does not break at its hyphen: `heroes-` / `lang.org`
   is what the specification page's lede did at every width. */
main p.lede code { white-space: nowrap; }

.crumb {
  font: 0.78rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); margin: 0 0 2.2rem;
}
h1.chapter {
  /* A path is the title on 132 pages, so it has to survive the longest one at
     360px. 5.5vw never reaches the 2rem floor there, so the floor was the bug. */
  font-size: clamp(1.5rem, 7vw, 2.5rem); letter-spacing: 0.005em;
  text-transform: none; line-height: 1.15;
  font-family: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
  font-weight: 700; text-wrap: balance;
  margin: 0 0 0.6rem;
}
/* A chapter's standfirst is a unit with its title, and needs air under it
   before the body starts: without this it reads as the first paragraph. */
h1.chapter + .tagline { margin: 0 0 2rem; }

ol.chapters { list-style: none; padding: 0; margin: 1.6rem 0; counter-reset: ch; }
ol.chapters li {
  counter-increment: ch; position: relative;
  padding: 0.75rem 0 0.75rem 3rem;
  border-bottom: 1px solid var(--line-soft);
  transition: background-color 140ms ease;
}
ol.chapters li:hover { background: color-mix(in srgb, var(--card) 60%, transparent); }
ol.chapters li::before {
  content: counter(ch, decimal-leading-zero);
  position: absolute; left: 0.3rem; top: 0.8rem; color: var(--dim);
  font: 0.78rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
}
ol.chapters .src {
  display: block; font: 0.73rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); margin-top: 0.15rem;
}
ol.chapters .soon {
  font: 0.66rem/1.4 ui-monospace, 'SF Mono', Menlo, monospace;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--dim); margin-left: 0.5rem; white-space: nowrap;
  border: 1px solid var(--line); border-radius: 999px; padding: 0.1rem 0.5rem;
}
ol.chapters li.todo { color: var(--dim); }

.next {
  margin-top: 3.4rem; padding: 1.2rem 1.3rem;
  border: 1px solid var(--line); border-left: 2px solid var(--red);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--card) 55%, transparent);
  font-size: 0.98rem;
}
.next p { margin: 0; }
/* The way back. A chapter is read in order, so the box that offers the next
   one has to offer the previous one too, or the only way back is the browser
   button. Set quieter than the forward link: leaving is not the default. */
/* Two identical red-edged boxes stacked at a chapter's end spend the "next
   thing" signal twice in one viewport, and it stops pointing. The chapter's own
   next keeps the red edge; the three doors become its quieter sibling, grouped
   by the short gap. Where `.away` stands alone nothing changes. */
.next + .next.away {
  margin-top: 0.9rem; border-left-color: var(--line);
  background: transparent; font-size: 0.92rem; color: var(--dim);
}
.next .back {
  display: block; margin-top: 0.75rem;
  padding-top: 0.7rem; border-top: 1px solid var(--line-soft);
  font-size: 0.9rem; color: var(--dim);
}

/* == 10. the build log ==================================================== */

ol.log { list-style: none; padding: 0; margin: 1.6rem 0 0; }
ol.log li {
  margin: 0; padding: 1.4rem 0 1.4rem 1.8rem; position: relative;
  border-left: 1px solid var(--line);
  text-wrap: pretty;
}
/* The bolt pins every entry to the line — the motif doing a job, which is the
   only way it is allowed on this site. */
ol.log li::before {
  content: ''; position: absolute; left: -6px; top: 1.75rem;
  width: 11px; height: 15px;
  background-color: var(--red);
  -webkit-mask: var(--bolt) no-repeat center / contain;
  mask: var(--bolt) no-repeat center / contain;
}
/* The newest entry used to get a GOLD pin, and gold on this site is the
   quotation colour and nothing else (README § The visual system). The lede
   already says newest first. Recency is marked by weight instead. */
ol.log li:first-child .when { color: var(--fg); }
ol.log .when {
  font: 0.74rem/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); display: block;
  text-transform: uppercase; letter-spacing: 0.1em;
  margin-bottom: 0.35rem;
}
/* The lead line is what the page is skimmed by: twenty entries is more than
   anyone reads end to end, so the headline is set above body size and given
   room, and the note under it is read only where the headline earned it. */
ol.log b {
  font-style: normal; color: var(--fg); display: block;
  font-size: 1.08rem; line-height: 1.4; margin-bottom: 0.55rem;
}
/* Its own line: trailing a paragraph it reads as a stray link in the prose. */
ol.log a.more {
  display: block; margin-top: 0.7rem;
  font: 0.73rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  text-decoration: none; letter-spacing: 0.05em;
}
ol.log a.more::after { content: ' \2192'; }

/* == 11. the zen ========================================================== */

/* The compiler's own text (`heroes this`), quoted verbatim — including the
   lyric fragments, which are the binary's packaging and not the site's copy.
   Numbered the way the command numbers them, and set larger than body copy:
   twenty lines are the one page here that is read like a poster. */
ol.zen { list-style: none; padding: 0; margin: 2rem 0; counter-reset: zen; }
ol.zen li {
  counter-increment: zen; position: relative;
  padding: 0.6rem 0 0.6rem 3rem; font-size: 1.1rem; line-height: 1.5;
  text-wrap: pretty;
}
ol.zen li::before {
  content: counter(zen, decimal-leading-zero);
  position: absolute; left: 0; top: 0.85rem; width: 2rem; text-align: right;
  color: color-mix(in srgb, var(--red) 70%, transparent);
  font: 700 0.85rem/1 'Avenir Next Condensed', 'Helvetica Neue', 'Arial Narrow', sans-serif;
  letter-spacing: 0.08em;
  transition: color 140ms ease;
}
ol.zen li:hover::before { color: var(--red); }
/* The first line and the last: the tribute, and the song. */
ol.zen li:nth-child(1), ol.zen li:nth-child(20) {
  color: var(--gold); font-style: italic;
}
ol.zen li:nth-child(1)::before, ol.zen li:nth-child(20)::before { color: var(--gold); }

/* == 12. stats, fixpoint, panel =========================================== */

.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1.6rem 1.2rem; margin: 2.2rem 0; padding: 1.5rem 1.6rem; list-style: none;
  background: color-mix(in srgb, var(--card) 55%, transparent);
  border: 1px solid var(--line-soft); border-left: 2px solid var(--red);
  border-radius: var(--radius);
}
.stats li { margin: 0; }
.stats .n {
  display: block; color: var(--red); font-size: 1.9rem; line-height: 1.05;
  font-weight: 700; letter-spacing: 0.01em; font-variant-numeric: tabular-nums;
}
.stats .what {
  font: 0.73rem/1.45 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); display: block; margin-top: 0.35rem;
  text-wrap: balance;
}

/* Three generations, one hash: the hash is the evidence, so it is the gold. */
.fixpoint .hash {
  color: var(--gold); word-break: break-all; display: block;
  letter-spacing: 0.02em;
}

/* The panel's seats, and the languages this one borrowed from. Both are
   lists of named things with a line of argument each, so they share a shape:
   a name in the poster face, the argument in the reading face. */
ul.seats, ul.lineage { list-style: none; padding: 0; margin: 1.6rem 0; }
ul.seats li {
  margin: 0 0 0.7rem; padding: 1.1rem 1.3rem;
  background: color-mix(in srgb, var(--card) 50%, transparent);
  border: 1px solid var(--line-soft); border-radius: var(--radius-sm);
  display: grid; grid-template-columns: 11rem 1fr; gap: 0.4rem 1.5rem;
  transition: border-color 140ms ease;
}
ul.seats li:hover { border-color: color-mix(in srgb, var(--red) 40%, var(--line)); }
ul.seats .who {
  font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  font-size: 0.86rem; color: var(--fg); padding-top: 0.15rem;
}
ul.seats .veto {
  display: block; margin-top: 0.25rem;
  font: 0.66rem/1.4 ui-monospace, 'SF Mono', Menlo, monospace;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--red);
}
ul.seats .no-veto { color: var(--dim); }
ul.seats p { margin: 0; font-size: 0.98rem; }
@media (max-width: 620px) {
  ul.seats li { grid-template-columns: 1fr; }
}

/* The nods, set like the back of a record sleeve: numbered, the title on the
   left, the record it comes from ranged right, and the row's own rule crossing
   the space between them as the leader. ONE column, because twelve rows of two
   lines flowing into two balanced columns put a hairline at a different height
   either side of the gutter on every row, which reads as a broken table rather
   than a sleeve. The counter follows document order, and that order is the walk
   through the site. Drawn for the book's chapters, which no longer use it.
   THE WRAP BAND THIS COMMENT USED TO CLAIM DOES NOT EXIST. It said the source
   line wraps between about 620px and 780px; the design seat measured the
   longest row, `quicksand, avoided`, at thirteen widths from 800 down to 480 in
   both editions and it stays on one line at every one, going to its own line
   only at 390 and below, where `space-between` puts it at the start. The
   sentence was reasoned and it outlived nothing, because there was nothing.
   `max-width` puts the list on the prose's own measure: unconstrained it ran
   784px against a paragraph's 608px, so the leader gaps reached 599px. */
ol.tracks {
  list-style: none; padding: 0; margin: 1.8rem 0; counter-reset: tr;
  max-width: 38rem;
}
ol.tracks li {
  counter-increment: tr;
  display: flex; flex-wrap: wrap; justify-content: space-between;
  align-items: baseline; gap: 0.2rem 1.5rem;
  position: relative; padding: 0.45rem 0 0.45rem 2.3rem;
  border-bottom: 1px solid var(--line-soft);
  font-size: 1rem; line-height: 1.4;
}
ol.tracks li::before {
  content: counter(tr, decimal-leading-zero);
  /* `0.3rem` and not `0`, so the reserved left border never crosses the `0`. */
  position: absolute; left: 0.3rem; top: 0.62rem;
  font: 0.71rem/1.4 ui-monospace, 'SF Mono', Menlo, monospace;
  /* Mixed toward `--dim`, not toward transparent. At 0.71rem this is normal
     text and owes 4.5:1, and the transparent mix composited on `--bg` measures
     3.59:1 in light and 3.44:1 in dark: under AA in both themes, and unnoticed
     because nothing used this class. Toward `--dim` it measures 5.65:1 and
     5.39:1, and it stops assuming what is behind the row. */
  color: color-mix(in srgb, var(--red) 75%, var(--dim));
  letter-spacing: 0.06em;
}
/* Where the record is named. Under 620px `space-between` leaves it at the start
   of its own line, so this needs no breakpoint of its own. */
ol.tracks .from {
  font: 0.73rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); text-align: right;
}
/* Arrival. A red left edge, and the two objections to one are both answered by
   RESERVING it on every row: a `border-left` added only at `:target` would
   shift the counter, and the `box-shadow: inset` that avoids the shift runs
   through the counter and clips the leading `0` of `04`. Transparent on every
   row costs no layout and moves nothing when it takes colour.
   The version this replaces was a `--card` tint plus the counter at full red,
   and the design seat measured both signals from the pixels: 1.044:1 against a
   neighbouring row in light, 1.051:1 in dark, and a 1.077:1 step on the
   counter, where WCAG 1.4.11 asks 3:1 of a graphic that carries state. Two
   signals that faint are not two signals. The red rule is the same mark the
   callout box on this page already uses, so this spends a vocabulary the
   reader has rather than inventing one. */
ol.tracks li { border-left: 2px solid transparent; }
ol.tracks li:target { border-left-color: var(--red); }
/* Named twice, which is this stylesheet's own principle stated twice over: the
   rule measures 5.05:1 in light and 5.31:1 in dark, so it is a LUMINANCE mark
   that survives without hue discrimination, and the weight is the second
   signal, which is not a colour at all. It shifts nothing, measured: the
   counter is fixed-pitch, so target left, row top and sibling top are all
   unchanged to the hundredth of a pixel. */
ol.tracks li:target::before { color: var(--red); font-weight: 700; }

ul.lineage li {
  margin: 0.85rem 0; padding-left: 1.5rem; position: relative;
  text-wrap: pretty;
}
ul.lineage li::before {
  content: ''; position: absolute; left: 0; top: 0.62em;
  width: 0.7rem; height: 1px; background: var(--blue);
}
ul.lineage b { color: var(--fg); }

/* == 13. the examples ===================================================== */

/* These pages are generated from `examples/` at build time, one per program in
   both editions, so everything here has to hold for 66 pages nobody proofreads
   one by one. Nothing in this section invents a colour or a face: the badges
   borrow the chapter list's pill, the terminal block is § 8's `.demo`, and the
   fingerprint is the gold the fixpoint hash already uses, because gold on this
   site means a quotation and a hash is the page's own evidence. */

ul.badges {
  display: flex; flex-wrap: wrap; gap: 0.4rem 0.45rem;
  list-style: none; padding: 0; margin: 0 0 1.8rem;
}
ul.badges li {
  margin: 0; white-space: nowrap;
  font: 0.66rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim);
  border: 1px solid var(--line); border-radius: 999px; padding: 0.15rem 0.6rem;
}
ul.badges li code {
  background: none; border: 0; padding: 0; font-size: inherit;
  text-transform: none; letter-spacing: 0.02em; color: var(--dim);
}

/* The run block. `.demo` gives it the card, the bar and the caption; what it
   needs on top is a command that reads as one, output that scrolls instead of
   wrapping, and an exit status that is loud when it is not zero. Wrapping a
   transcript would put a false line break inside a printed line, which on a
   page whose whole promise is byte fidelity is the one thing it cannot do. */
.demo.run { margin: 1.6rem 0 2rem; }
.demo.run .bar { color: var(--fg); }
.demo.run pre { overflow-x: auto; white-space: pre; padding: 0.85rem 0.95rem; }
.demo.run .under { padding-top: 0.75rem; border-top: 1px solid var(--line-soft); }
.demo.run .under .status { color: var(--red); }

/* A module after the first is closed, because `examples/interpreter/` is
   eleven modules and 2,636 lines: a page that opens all of it is a page nobody
   scrolls to the end of. Find-in-page opens a closed one in both browsers
   tested, so nothing is hidden from a search. */
details.module {
  margin: 0 0 0.5rem; border-top: 1px solid var(--line-soft);
}
details.module summary {
  cursor: pointer; padding: 0.6rem 0.1rem;
  font: 0.74rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace; color: var(--dim);
}
details.module summary:hover { color: var(--fg); }
details.module summary code {
  background: none; border: 0; padding: 0; font-size: inherit; color: inherit;
}
details.module[open] summary { color: var(--fg); }
details.module figure.example { margin-top: 0.4rem; }

/* A data file is not Heroes, so it is shown uncoloured on purpose: colouring it
   would claim the highlighter understands a format it has never read. */
figure.example.data pre { color: var(--dim); }

/* The index's cards gain one monospace line of facts under the name. */
.cards .meta {
  font: 0.7rem/1.5 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); margin: 0 0 0.7rem;
}

/* The fingerprint, and the command that reproduces it. */
p.fingerprint {
  font: 0.72rem/1.7 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); margin: 2rem 0 0;
}
p.fingerprint .hash {
  color: var(--gold); background: none; border: 0; padding: 0;
  font-size: inherit; letter-spacing: 0.03em;
}

@media (max-width: 620px) {
  ul.badges { margin-bottom: 1.4rem; }
  .demo.run pre { font-size: 0.74rem; }
}

/* The shelf map on the examples index: eight anchors on one line, because the
   button that brings a visitor here says "see the code" and the shelves are
   below three paragraphs of prose. */
p.shelves {
  font: 0.78rem/1.9 ui-monospace, 'SF Mono', Menlo, monospace;
  color: var(--dim); margin: 1.6rem 0 0; text-wrap: pretty;
}
/* A shelf name is one object. Broken across a line, "Files" and "and the
   command line" read as two shelves that do not exist. */
p.shelves a { text-decoration: none; white-space: nowrap; }
p.shelves a:hover { text-decoration: underline; }

/* The specification, shown verbatim in one block. It is markdown rather than
   Heroes, so it is uncoloured, and it is long, so it gets a height and scrolls
   inside its own frame: a reader who wants the whole thing selects it, and a
   reader who is browsing is not made to scroll past 250 lines to reach the
   footer. */
figure.example.spec pre {
  /* A whole number of lines, so the frame never severs one. It used to be a
     round 34rem, which landed mid-glyph on a `64` and showed the reader an
     amputated line on the page the primary button points at. And the scroll
     shadows in § 7 are left and right only, so a block that scrolls DOWN said
     nothing: these are the same technique rotated, the ink-coloured edge from
     the same repair, and LINEAR for the same reason. A radial edge on a block
     this wide is a lens: it says its piece in the middle of the width and
     nothing at either end. Measured across the frame's own 784px at 1200px
     wide, from left edge to right: 25.2 / 22.6 / 22.1 / 23.9 / 23.9. */
  max-height: calc(30 * 0.76rem * 1.55 + 2.3rem);
  overflow-y: auto; overflow-x: hidden; font-size: 0.76rem; line-height: 1.55;
  color: var(--fg);
  /* **It wraps** (author instruction 2026-09-11): one axis of scrolling, not
     two. Every other block on this site scrolls sideways on purpose, because a
     Heroes program's indentation is its structure and a diagnostic's caret is
     its column, so a reflowed line lies. This document is prose in markdown:
     its long lines are sentences and its tables are pipes, both of which read
     fine reflowed, and a reader who has to drag sideways through 289 lines to
     see the right-hand half reads none of it. `break-word` rather than
     `anywhere`, so the break lands between words wherever a word will fit. */
  white-space: pre-wrap; overflow-wrap: break-word;
  /* Eight layers, not four. This rule REPLACES `pre`'s whole background set
     rather than adding to it, so writing only the vertical pair silently
     dropped the horizontal one and the frame said nothing about the axis it
     scrolls furthest on: measured at 390px, a flat 209 to 211 right up to the
     border while lines cut mid-word. Both axes now. */
  /* Four layers, not eight. The horizontal pair shaded an axis that no longer
     moves: with the block wrapping there is nothing to the left or the right of
     the frame, and an edge shadow over a still axis says a thing that is not
     true. The vertical pair stays, and it is the same technique the code blocks
     use rotated. */
  background-image:
    linear-gradient(to bottom, var(--card) 40%, transparent),
    linear-gradient(to top,    var(--card) 40%, transparent),
    linear-gradient(to top,    color-mix(in srgb, var(--fg) 26%, transparent), transparent),
    linear-gradient(to bottom, color-mix(in srgb, var(--fg) 26%, transparent), transparent);
  background-position: top center, bottom center, bottom center, top center;
  background-size: 100% 2.4rem, 100% 2.4rem, 100% 0.8rem, 100% 0.8rem;
  background-repeat: no-repeat;
  background-attachment: local, local, scroll, scroll;
}
figure.example.spec { margin-bottom: 1.4rem; }

/* Two ways to take the block whole, and neither is the only one (site/README.md,
   "JavaScript is never the only way"). Without scripts, `html` carries no `js`
   class and one click selects the whole block, so Cmd+C or Ctrl+C copies the
   specification; the price is that a piece of it cannot be selected on its own,
   and that reader has the guide for a paragraph and `/spec.md` for the file.
   With scripts the class is set, selection is ordinary again, and the button
   below the block does the copying. */
html:not(.js) figure.example.spec pre { -webkit-user-select: all; user-select: all; }

/* The copy button, in the pill the home page's calls to action wear, small, and
   rendered `hidden` until the script that gives it something to do has run. */
figure.example.spec button.copy {
  display: inline-block; margin: 0.35rem 0 0.2rem; padding: 0.35rem 0.95rem;
  border-radius: 999px; border: 1px solid var(--red); background: transparent;
  color: var(--red); font: inherit; font-weight: 700; font-size: 0.78rem;
  text-transform: uppercase; letter-spacing: 0.09em; cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease;
}
figure.example.spec button.copy:hover,
figure.example.spec button.copy:focus-visible { background: var(--red); color: var(--bg); }

/* What the design seat found by rendering at 390px, which nobody had done.
   Each of these was measured before and after in a 390px iframe, because
   Chrome on macOS will not open a window narrower than about 590px and a
   `--window-size=390` screenshot silently renders at 590 and clips. */
@media (max-width: 700px) {
  /* A long heading wraps as one flex item and leaves `::before` behind, so the
     bolt sits alone on a line above its own title. Inline instead, and the nod
     drops below rather than fighting for the same row. */
  h2 { display: block; }
  h2::before { display: inline-block; margin-right: 0.55rem; vertical-align: -0.08em; }
  h2 .nod { display: block; margin: 0.35rem 0 0; }
}

@media (max-width: 620px) {
  /* Stacked, a pill sized by its own text reads as an accident beside a pill
     sized by different text. One column, both full width, and the fill against
     the outline is what says which is which. */
  .cta { display: grid; grid-template-columns: 1fr; gap: 0.7rem; }
  .cta a { text-align: center; }
}

/* The caption under the home's demo broke as "read this one" then a lone
   arrow. An arrow on its own line is not a link, it is a stray glyph. */
.demo .under a { white-space: nowrap; }

/* A paragraph that opens with inline code lost its left margin, because the
   code's own padding pushed the first letter 5px right of every other
   paragraph on the page. Visible on /start/, twice. */
/* This pulled a leading chip back by the padding it used to carry. That
   padding is gone, so the correction has to go with it or the first letter of
   the paragraph sits left of every other paragraph on the page. */
p > code:first-child, li > code:first-child { margin-left: 0; }

/* A path is a title on 132 pages and the longest is 33 characters. At the
   inherited 0.9em it overran the measure by about 30px and the browser took
   the hyphen, so `examples/gallery/06-generics.hero` arrived in two pieces
   with the file's own name split. `overflow-wrap` stays for the phone, where
   wrapping is unavoidable. */
h1.chapter code { overflow-wrap: anywhere; font-size: 0.72em; }

/* A command is one object. `curl -L …` broke after the flag's hyphen on
   /spec/, the page the primary button points at, which put `curl -` at the end
   of one line and `L …` at the start of the next and split the tint in two. */
code.cmd { white-space: nowrap; }

/* == 14. footer =========================================================== */

/* One line, raked from red to blue, with the bolt sitting on it: the last
   appearance of the motif, and the page's full stop. */
footer {
  position: relative; margin-top: 5rem; padding-top: 2.6rem;
  color: var(--dim); font-size: 0.9rem; line-height: 1.6;
}
footer::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--red) 24%, var(--blue) 76%, transparent);
  opacity: 0.5;
}
footer::after {
  content: ''; position: absolute; top: -7px; left: 50%;
  width: 10px; height: 14px; transform: translateX(-50%) rotate(8deg);
  background-color: var(--red);
  -webkit-mask: var(--bolt) no-repeat center / contain;
  mask: var(--bolt) no-repeat center / contain;
}
footer p { margin: 0 0 0.9rem; }
/* No opacity on the copyright line: at 80% the light theme read 3.48:1, under
   AA; the smaller size already marks it as the quiet line. */
footer p:last-child { margin: 0; font-size: 0.82rem; }
footer a { color: var(--link); }

/* == 15. focus, motion, print ============================================ */

/* Nothing on this site was focusable-visible before: a keyboard reader had to
   guess where they were. */
a:focus-visible {
  outline: 2px solid var(--red); outline-offset: 3px; border-radius: 2px;
}
pre:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Narrow screens: a code block scrolls rather than reflows, because wrapping
   4-space indentation destroys the only structure the language has. Shell
   transcripts are the exception, since a clipped command is a command nobody
   can type. Diagnostics are NOT: `pre.diag` below keeps `pre`, wrapping only
   its `.msg` line, because the caret rows point at a column and a wrapped
   source line puts the caret under nothing. */
@media (max-width: 620px) {
  pre { white-space: pre-wrap; }
  figure.example pre, .demo pre, pre.table { white-space: pre; }
  /* The specification keeps wrapping at every width: it is the one block whose
     lines are sentences (author instruction 2026-09-11). */
  figure.example.spec pre { white-space: pre-wrap; }
  .hero { grid-template-columns: 1fr; gap: 2rem; padding: 2.4rem 0 2rem; }
  blockquote::before { display: none; }
}
@media (max-width: 820px) {
  .hero { grid-template-columns: 1fr; gap: 2.2rem; }
  .hero .tagline { max-width: none; }
}

@media print {
  body::after, nav.site, .hero::before { display: none; }
  body { background: #fff; color: #000; font-size: 11pt; }
  pre { background: #f4f4f4; border-color: #ccc; }
  a { color: #000; }
}

/* ------- the chain, on the project page -----------------------------------
   One row per milestone, read out of `ROADMAP.md` § The chain at build
   time (`src/lib/chain.ts`), because a summary written by hand is a summary
   that goes stale against the repository it summarises.

   Two tables, and the split is the reason the section is readable at all:
   twenty-four milestones that have not happened stand open, and the
   forty-six that have are behind a `details` for the reader who wants them.
   Sixty-nine rows in a row read as a wall. */
table.chain {
  width: 100%; border-collapse: collapse; margin: 1rem 0 0;
  font-size: 0.82rem; line-height: 1.5;
}
table.chain th {
  text-align: left; font-weight: 700; color: var(--dim);
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em;
  padding: 0 0.7rem 0.4rem 0; border-bottom: 1px solid var(--line);
}
table.chain td {
  padding: 0.42rem 0.7rem 0.42rem 0; border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
/* The number column is the chain's order, so it is tabular and narrow. */
table.chain td.n {
  color: var(--dim); font-variant-numeric: tabular-nums; width: 2.2rem;
}
table.chain td.when { color: var(--dim); white-space: nowrap; }
table.chain code { font-size: 0.94em; }
/* The name column does not wrap mid-identifier: a milestone's id is one word
   with hyphens, and a hyphen is where a browser would break it. */
table.chain td code { overflow-wrap: normal; word-break: keep-all; }
details.chain { margin: 1.6rem 0 0; }
details.chain > summary {
  cursor: pointer; color: var(--red); font-weight: 700;
  font-size: 0.84rem; letter-spacing: 0.02em;
}
details.chain > summary:hover { text-decoration: underline; }
details.chain[open] > summary { margin-bottom: 0.2rem; }
