/* Custom styles for williamdemeo.github.io
 *
 * Everything here is expressed in the tokens from tokens.css; there are no raw
 * colours or sizes below this comment.  That is the property that makes the
 * visual system a system rather than a set of preferences: changing one token
 * moves every place it is used, and nothing else can drift.
 *
 * Material is restyled through its own custom properties rather than by
 * overriding its rules, which is the supported route and survives a theme
 * upgrade.  Where a rule is unavoidable it is because Material hard-codes a
 * value that has no variable.
 *
 * Load order matters and is set in mkdocs.yml: fonts.css declares the faces,
 * tokens.css declares the values, this file applies them.
 */

/* ── Material's variables, expressed in ours ─────────────────────────────────
 *
 * The selector is `[data-md-color-scheme]` rather than `:root` because that
 * attribute lives on <body>, which is also where tokens.css puts the colour
 * tokens.  Mapping on :root would read them from an element that does not
 * have them.
 */

[data-md-color-scheme] {
  /* JuliaMono is the last self-hosted entry in the text stack, not an
     afterthought: no text face carries ∀, ⊢ or ⨅, and a mathematical symbol
     that reaches prose should come from a font this site ships rather than
     from whatever the reader's machine offers.  Material appends its own
     system fallbacks after whatever this names. */
  --md-text-font: var(--font-body), var(--font-mono);
  --md-code-font: var(--font-mono);

  --md-default-bg-color: var(--c-bg);
  --md-default-bg-color--light: color-mix(in srgb, var(--c-bg) 70%, transparent);
  --md-default-bg-color--lighter: color-mix(in srgb, var(--c-bg) 30%, transparent);
  --md-default-bg-color--lightest: color-mix(in srgb, var(--c-bg) 12%, transparent);

  --md-default-fg-color: var(--c-fg);
  --md-default-fg-color--light: var(--c-fg-muted);
  --md-default-fg-color--lighter: var(--c-fg-faint);
  --md-default-fg-color--lightest: var(--c-line);

  --md-typeset-color: var(--c-fg);
  --md-typeset-a-color: var(--c-accent);
  --md-typeset-mark-color: var(--c-accent-wash);
  --md-typeset-del-color: color-mix(in srgb, var(--c-accent) 18%, transparent);
  --md-typeset-ins-color: color-mix(in srgb, var(--c-accent) 18%, transparent);
  --md-typeset-table-color: var(--c-line);
  --md-typeset-kbd-color: var(--c-bg-raised);
  --md-typeset-kbd-border-color: var(--c-line-strong);
  --md-typeset-kbd-accent-color: var(--c-bg);

  /* The header is paper with a hairline under it, not a block of colour.  That
     is most of what separates a 2026 documentation site from a 2016 one. */
  --md-primary-fg-color: var(--c-bg);
  --md-primary-fg-color--light: var(--c-bg-raised);
  --md-primary-fg-color--dark: var(--c-bg-sunken);
  --md-primary-bg-color: var(--c-fg);
  --md-primary-bg-color--light: var(--c-fg-muted);

  --md-accent-fg-color: var(--c-accent-hover);
  --md-accent-fg-color--transparent: var(--c-accent-wash);
  --md-accent-bg-color: var(--c-on-accent);
  --md-accent-bg-color--light: var(--c-on-accent);

  --md-code-bg-color: var(--c-bg-raised);
  --md-code-fg-color: var(--c-fg);

  --md-footer-bg-color: var(--c-bg-sunken);
  --md-footer-bg-color--dark: var(--c-bg-sunken);
  --md-footer-fg-color: var(--c-fg);
  --md-footer-fg-color--light: var(--c-fg-muted);
  /* Material puts the "Made with ..." line in --lighter.  The faint token is
     tuned to clear AA on the page surfaces, not on the darker footer band, so
     the footer gets the muted one instead of a fourth grey. */
  --md-footer-fg-color--lighter: var(--c-fg-muted);

  /* No drop shadows anywhere.  Depth is carried by a one-pixel line and a
     change of surface, which reads the same in both themes and does not need
     a different colour for each. */
  --md-shadow-z1: 0 0 0 1px var(--c-line);
  --md-shadow-z2: 0 0 0 1px var(--c-line);
  --md-shadow-z3: 0 0 0 1px var(--c-line-strong);
}

/* Syntax highlighting.  Material's defaults are tuned to its own palettes and
 * lose contrast against these surfaces, so the eight token colours are set
 * from the palette instead.  Keywords and strings carry the hue; everything
 * else is a grey, because a code block where nine things are coloured is a
 * code block where nothing stands out.  Ratios are checked by
 * `make contrast-audit`, which reads the rendered page rather than this file.
 */

[data-md-color-scheme="default"] {
  --md-code-hl-color: var(--c-accent-wash);
  --md-code-hl-color--light: var(--c-accent-wash);
  --md-code-hl-keyword-color: #7c3aed;
  --md-code-hl-function-color: #1d4ed8;
  --md-code-hl-string-color: #15803d;
  --md-code-hl-number-color: #b45309;
  --md-code-hl-constant-color: #b45309;
  --md-code-hl-special-color: #be123c;
  --md-code-hl-operator-color: var(--c-fg-muted);
  --md-code-hl-punctuation-color: var(--c-fg-muted);
  --md-code-hl-name-color: var(--c-fg);
  --md-code-hl-variable-color: var(--c-fg);
  --md-code-hl-comment-color: var(--c-fg-faint);
  --md-code-hl-generic-color: var(--c-fg-muted);
  /* The typed-proof terminal's verdict green (M3-2d, #96), worn by the ✓
     line and the goal pill at zero.  The pair is the 404 typed-comment's,
     measured there (#98): dark carries the palette's own string green;
     in light that token runs thin on these surfaces, so light gets the
     same deeper leaf of it, 6.0:1 on the raised surface. */
  --proof-ok: #136b34;
}

[data-md-color-scheme="slate"] {
  --md-code-hl-color: var(--c-accent-wash);
  --md-code-hl-color--light: var(--c-accent-wash);
  --md-code-hl-keyword-color: #c4b5fd;
  --md-code-hl-function-color: #93c5fd;
  --md-code-hl-string-color: #86efac;
  --md-code-hl-number-color: #fcd34d;
  --md-code-hl-constant-color: #fcd34d;
  --md-code-hl-special-color: #fda4af;
  --md-code-hl-operator-color: var(--c-fg-muted);
  --md-code-hl-punctuation-color: var(--c-fg-muted);
  --md-code-hl-name-color: var(--c-fg);
  --md-code-hl-variable-color: var(--c-fg);
  --md-code-hl-comment-color: var(--c-fg-faint);
  --md-code-hl-generic-color: var(--c-fg-muted);
  --proof-ok: #86efac;
}

/* ── typography ─────────────────────────────────────────────────────────── */

.md-typeset {
  font-size: var(--type-base);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-body);
}

.md-typeset h1,
.md-typeset h2,
.md-typeset h3,
.md-typeset h4 {
  font-family: var(--font-display), var(--font-body), var(--font-mono), sans-serif;
  color: var(--c-fg);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-heading);
  text-wrap: balance;
}

.md-typeset h1 {
  font-size: var(--type-h1);
  font-weight: var(--display-weight);
  margin: 0 0 var(--space-8);
}

.md-typeset h2 {
  font-size: var(--type-h2);
  font-weight: var(--display-weight);
  margin: var(--space-16) 0 var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: var(--border-width) solid var(--c-line);
}

.md-typeset h3 {
  font-size: var(--type-h3);
  font-weight: var(--strong-weight);
  margin: var(--space-12) 0 var(--space-3);
}

.md-typeset h4 {
  font-size: var(--type-h4);
  font-weight: var(--strong-weight);
  letter-spacing: var(--tracking-body);
  margin: var(--space-8) 0 var(--space-2);
}

.md-typeset h5 {
  font-size: var(--type-small);
  font-weight: var(--strong-weight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--c-fg-muted);
}

.md-typeset strong,
.md-typeset b {
  font-weight: var(--strong-weight);
}

/* Line length.  Only the prose blocks are capped, and only where they are
 * direct children of the article: a paragraph inside a table cell or an
 * admonition already has a container deciding its width. */
.md-typeset > p,
.md-typeset > ul,
.md-typeset > ol,
.md-typeset > dl,
.md-typeset > blockquote {
  max-width: var(--measure);
}

.md-typeset > h1,
.md-typeset > h2,
.md-typeset > h3,
.md-typeset > h4 {
  max-width: var(--measure-heading);
}

.md-typeset p {
  margin: 0 0 var(--space-6);
}

.md-typeset blockquote {
  border-left: 2px solid var(--c-line-strong);
  color: var(--c-fg-muted);
  padding-left: var(--space-4);
}

.md-typeset hr {
  border-bottom: var(--border-width) solid var(--c-line);
  margin: var(--space-16) 0;
}

/* Links: coloured, and underlined only on hover.  A page of prose with a
 * permanent underline under every citation is noisier than it is helpful, but
 * colour alone is not a sufficient affordance, so the underline appears on
 * hover and focus and the colour clears AA on its own. */
.md-typeset a {
  text-decoration: none;
  border-bottom: var(--border-width) solid color-mix(in srgb, var(--c-accent) 35%, transparent);
  transition: border-color 120ms, color 120ms;
}

.md-typeset a:hover,
.md-typeset a:focus-visible {
  color: var(--c-accent-hover);
  border-bottom-color: var(--c-accent);
}

/* Headings own their permalinks; the anchor should not read as body link. */
.md-typeset .headerlink {
  border-bottom: none;
}

:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* ── code ───────────────────────────────────────────────────────────────── */

.md-typeset code,
.md-typeset pre > code,
.md-typeset kbd {
  font-family: var(--font-mono), monospace;
  font-size: var(--type-code);
  /* Only JuliaMono Regular is shipped -- the syntax theme is colour-only, so
     no bold or italic code face is ever needed.  Pinning the weight stops a
     `code` inside a heading asking for 600 and getting a synthesised bold. */
  font-weight: 400;
  font-variant-ligatures: none;
}

.md-typeset code {
  background-color: var(--c-bg-raised);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.3em;
}

.md-typeset pre > code {
  padding: var(--space-4);
  line-height: 1.6;
}

.md-typeset .highlight,
.md-typeset .highlighttable {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.md-typeset .highlight code {
  background: transparent;
}

/* ── mathematics ────────────────────────────────────────────────────────────
 *
 * KaTeX ships `font-size: 1.21em` on `.katex` to compensate for Computer
 * Modern's small x-height.  1.21 is right for the body face KaTeX was designed
 * against, not for the one this site uses; --math-scale is the measured ratio
 * for the active system.  See tokens.css.
 */

.katex {
  font-size: var(--math-scale);
}

.md-typeset .katex-display {
  margin: var(--space-8) 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding: var(--space-1) 0;
}

/* ── chrome ─────────────────────────────────────────────────────────────── */

.md-header {
  box-shadow: none;
  border-bottom: var(--border-width) solid var(--c-line);
}

.md-header--shadow {
  box-shadow: none;
}

.md-tabs {
  border-bottom: var(--border-width) solid var(--c-line);
}

.md-nav {
  font-size: var(--type-ui);
}

.md-nav__title {
  color: var(--c-fg-muted);
  font-weight: var(--strong-weight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  font-size: var(--type-ui-small);
}

/* Search.  Material builds the field out of hsla(0,0%,100%,.12) over the
 * primary colour, which assumes the header is a block of colour.  Against
 * paper it is a grey slab, so the field is restated as a bordered surface. */
.md-search__form {
  background-color: var(--c-bg-raised);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  box-shadow: none;
}

.md-search__form:hover {
  background-color: var(--c-bg-sunken);
}

.md-search__input,
.md-search__icon {
  color: var(--c-fg);
}

.md-search__input::placeholder {
  color: var(--c-fg-muted);
}

[data-md-toggle="search"]:checked ~ .md-header .md-search__form {
  background-color: var(--c-bg);
  border-color: var(--c-line-strong);
}

.md-search__output {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  box-shadow: none;
}

.md-footer-meta {
  border-top: var(--border-width) solid var(--c-line);
}

.md-typeset table:not([class]) {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  font-size: var(--type-small);
}

.md-typeset table:not([class]) th {
  background-color: var(--c-bg-raised);
  font-weight: var(--strong-weight);
}

/* Material renders the search overlay and the admonition titles with a tint
 * derived from the accent; keep those on the palette too. */
.md-typeset .admonition,
.md-typeset details {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  box-shadow: none;
  font-size: var(--type-small);
}

.md-typeset .md-button {
  /* Material builds its buttons out of --md-primary-fg-color, which here is
     the paper colour, so the default rule is white on white.  They are
     restated from the palette rather than patched. */
  color: var(--c-fg);
  border: var(--border-width) solid var(--c-line-strong);
  border-radius: var(--radius-md);
  font-weight: var(--strong-weight);
  padding: var(--space-2) var(--space-4);
  transition: background-color 120ms, color 120ms, border-color 120ms;
}

.md-typeset .md-button--primary,
.md-typeset .md-button:hover,
.md-typeset .md-button:focus-visible {
  background-color: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-on-accent);
}

/* Admonitions.  Material gives each of its dozen types a hard-coded hue, which
 * is a lot of colour for a page that is meant to have one.  The informational
 * types are folded onto the accent; the ones that mean "be careful" keep their
 * semantic colour, because that is what they are for. */
.md-typeset .admonition.note,
.md-typeset .admonition.info,
.md-typeset .admonition.abstract,
.md-typeset .admonition.tip,
.md-typeset .admonition.example,
.md-typeset .admonition.quote,
.md-typeset details.note,
.md-typeset details.info,
.md-typeset details.abstract,
.md-typeset details.tip,
.md-typeset details.example,
.md-typeset details.quote {
  border-color: var(--c-line);
  border-left: 2px solid var(--c-accent);
}

.md-typeset .note > .admonition-title,
.md-typeset .info > .admonition-title,
.md-typeset .abstract > .admonition-title,
.md-typeset .tip > .admonition-title,
.md-typeset .example > .admonition-title,
.md-typeset .quote > .admonition-title,
.md-typeset .note > summary,
.md-typeset .info > summary,
.md-typeset .abstract > summary,
.md-typeset .tip > summary,
.md-typeset .example > summary,
.md-typeset .quote > summary {
  background-color: var(--c-accent-wash);
  color: var(--c-fg);
}

.md-typeset .note > .admonition-title::before,
.md-typeset .info > .admonition-title::before,
.md-typeset .abstract > .admonition-title::before,
.md-typeset .tip > .admonition-title::before,
.md-typeset .example > .admonition-title::before,
.md-typeset .quote > .admonition-title::before,
.md-typeset .note > summary::before,
.md-typeset .info > summary::before,
.md-typeset .abstract > summary::before,
.md-typeset .tip > summary::before,
.md-typeset .example > summary::before,
.md-typeset .quote > summary::before {
  background-color: var(--c-accent);
}

/* ── home hero (M3-2a, #93) ──────────────────────────────────────────────────
 *
 *   <div class="hero" markdown>
 *   <div class="hero-copy" markdown>
 *   eyebrow line {.hero-eyebrow}
 *   # Display claim
 *   One-paragraph claim. {.hero-claim}
 *   [action](…){.md-button .md-button--primary} [action](…){.md-button}
 *   {.hero-actions}
 *   </div>
 *   <div class="hero-side">
 *   <!-- proof-terminal -->
 *   </div>
 *   </div>
 *
 * A grid, not a float.  The second column holds the typed-proof terminal
 * (M3-2d, #96) over the constellation backdrop (M3-2b, #94); the
 * min-block-size holds the stage at the height the portrait used to
 * establish, so a column shorter than the copy reads as canvas rather than
 * collapse, and `align-items: center` seats both columns in it.
 */

.md-typeset .hero {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: var(--space-12);
  align-items: center;
  margin: var(--space-8) 0;
  min-block-size: var(--hero-stage);
}

/* Grid items refuse to shrink below their content's min-width by default;
 * without this a long code line in the second column (M3-2d) would widen the
 * page rather than scroll. */
.md-typeset .hero > * {
  min-width: 0;
}

.md-typeset .hero-eyebrow {
  font-size: var(--type-small);
  font-weight: var(--strong-weight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  margin-bottom: var(--space-2);
}

/* The h1 keeps its type tokens; only its bottom margin tightens, because the
 * hero's vertical rhythm is the grid's business. */
.md-typeset .hero h1 {
  margin-bottom: var(--space-4);
}

.md-typeset .hero-claim {
  color: var(--c-fg-muted);
  max-width: var(--measure);
}

.md-typeset .hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: 0;
}

/* Material's mobile breakpoint, same value the timeline uses.  Stacked, the
 * stage is not held open: a phone screen has no second column to keep clear,
 * so the min-block-size returns to auto and the copy sets the height. */
@media screen and (max-width: 44.9375em) {
  .md-typeset .hero {
    grid-template-columns: 1fr;
    gap: var(--space-6);
    min-block-size: auto;
  }
}

/* ── constellation (M3-2b, #94) ──────────────────────────────────────────────
 *
 * Hasse diagrams drawn as star charts: the site's identity mark, and the
 * first component governed by ADR-009 (motion).  The markup is the inline SVG
 * in docs/_snippets/hero-constellation.html; each line and star carries a
 * stagger index `--i`, and that is the only styling the markup holds.
 *
 * The base rules below are the FINAL state -- lines drawn, stars lit, labels
 * legible.  Motion exists only inside the `no-preference` media query, so
 * reduced-motion readers, crawlers and JS-off readers all get the finished
 * drawing by construction rather than by a fallback path.  No JavaScript
 * anywhere in this component.
 *
 * Only part of the hero is backdrop once the terminal has arrived.  The
 * typed-proof terminal is an opaque panel over the second column, and its
 * left edge falls between x=575 and x=596 of the viewBox at every width
 * where the two columns stay side by side (measured in Chromium at 720,
 * 768, 1024, 1280, 1440 and 1920 CSS pixels).  The drawing spans the whole
 * viewBox anyway, four lattices left to right, because the panel does not
 * enter until --motion-hero-enter: the overture draws the entire chart, L₇
 * included, and the panel then comes down over L₇ and the right half of M₄.
 * Composing inside the clear part instead was tried twice during #140 and
 * rejected both times: three lattices at full size do not fit 575 units
 * with gaps worth the name, and shrunk until they do they read as a box of
 * lattices rather than as the chart the hero stands on.
 *
 *
 * Lines carry pathLength="1", which is what lets one dasharray value draw
 * every line to exactly its own length -- without it, dasharray is measured
 * in user units and any line longer than the value shows a gap in its final
 * state.
 *
 * The labels are real text (JuliaMono; ₅, ₃ and ₇ are in the symbols
 * subset) at full opacity: the contrast audit folds element opacity into
 * the foreground colour, so a faded label would fail AA in a way a faded
 * circle cannot.  Their font-size is a bare 11px on purpose: inside the
 * SVG that is a length in viewBox user units -- geometry, like `r` and
 * `stroke-width`, scaled with the drawing by the viewport transform -- not a
 * step on the type scale.  A --type-* token (em/rem) would resolve against
 * the page's font size and then be rescaled by the same transform, giving a
 * size that tracks neither the type system nor the drawing.
 */

/* The size knob.  `fill-box` makes the origin each group's own bounding box,
 * so a lattice grows about its own centre rather than about the viewBox's,
 * and the three stay where the snippet puts them.  tokens.css carries the
 * value and the ceilings. */
.md-typeset .constellation g {
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(var(--lattice-scale, 1));
}

.md-typeset .constellation line {
  stroke: var(--c-line-strong);
  stroke-width: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
  opacity: var(--lattice-ink, 1);
}

.md-typeset .constellation .star {
  fill: var(--c-accent);
  opacity: calc(0.95 * var(--lattice-ink, 1));
}

.md-typeset .constellation .halo {
  fill: var(--c-accent);
  opacity: calc(0.14 * var(--lattice-ink, 1));
}

.md-typeset .constellation text {
  fill: var(--c-fg-faint);
  font-family: var(--font-mono), monospace;
  font-size: 11px;
}

@media (prefers-reduced-motion: no-preference) {
  .md-typeset .constellation line {
    stroke-dashoffset: 1;
    animation: constellation-draw var(--motion-draw) var(--motion-ease) forwards;
    animation-delay: calc(var(--i) * var(--motion-stagger));
  }

  .md-typeset .constellation .star {
    opacity: 0;
    animation: constellation-pop var(--motion-pop) var(--motion-ease) forwards;
    animation-delay: calc(var(--i) * var(--motion-stagger));
  }

  .md-typeset .constellation .halo {
    opacity: 0;
    animation: constellation-haze var(--motion-pop) var(--motion-ease) forwards;
    animation-delay: calc(var(--i) * var(--motion-stagger) + var(--motion-halo-lag));
  }

  /* A few stars keep shimmering after the draw-in.  The second delay holds
     the twinkle back until the drawing has finished; the token records the
     derivation. */
  .md-typeset .constellation .star.tw {
    animation:
      constellation-pop var(--motion-pop) var(--motion-ease) forwards,
      constellation-twinkle var(--motion-twinkle) ease-in-out infinite;
    animation-delay: calc(var(--i) * var(--motion-stagger)), var(--motion-twinkle-at);
  }

  .md-typeset .constellation text {
    opacity: 0;
    animation: constellation-label var(--motion-label) var(--motion-ease) forwards;
    animation-delay: var(--motion-labels-at);
  }
}

@keyframes constellation-draw { to { stroke-dashoffset: 0; } }
@keyframes constellation-pop { to { opacity: calc(0.95 * var(--lattice-ink, 1)); } }
@keyframes constellation-haze { to { opacity: calc(0.14 * var(--lattice-ink, 1)); } }
@keyframes constellation-label { to { opacity: 1; } }
@keyframes constellation-twinkle {
  0%, 100% { opacity: calc(0.95 * var(--lattice-ink, 1)); }
  50% { opacity: calc(0.35 * var(--lattice-ink, 1)); }
}

/* In the hero the constellation is the background layer: absolutely placed
 * under the two grid columns, cropped by `slice`, and inert to the pointer.
 * The rules target the snippet's wrapper div, because the wrapper is what
 * Markdown guarantees arrives intact (see the comment in docs/index.md). */
.md-typeset .hero {
  position: relative;
}

.md-typeset .hero > .constellation-wrap {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.md-typeset .constellation-wrap .constellation {
  display: block;
  width: 100%;
  height: 100%;
}

.md-typeset .hero > .hero-copy,
.md-typeset .hero > .hero-side {
  position: relative;
  z-index: 1;
}

/* Below the breakpoint the columns stack over the drawing, so it recedes:
 * the whole layer dims and the labels leave rather than fail contrast under
 * an ancestor opacity the audit would fold in. */
@media screen and (max-width: 44.9375em) {
  .md-typeset .hero > .constellation-wrap {
    opacity: 0.35;
  }

  .md-typeset .hero > .constellation-wrap text {
    display: none;
  }
}

/* In the hero the constellation draws itself in and then RESTS: the
 * draw-in is the overture to the typed-proof replay, not a competitor.
 * M3-2d's first cut froze the whole drawing here (ADR-009's amendment of
 * 2026-08-04: two orchestrated timelines in one viewport), but the
 * terminal's entrance delay changed the facts that ruling stood on -- the
 * frame now holds back for --motion-hero-enter while the lattices draw,
 * and the two signatures are sequenced rather than simultaneous; see the
 * ADR's second amendment.  What still yields is the idle twinkle: a
 * shimmer with no end beside a replayable demonstration is two timelines
 * the moment the replay button is pressed, so in the hero the twinkling
 * stars keep their pop and lose the loop.  Equal specificity, later in
 * the file, is what lets this single rule beat the base `.star.tw` rule;
 * the style page's demo and the 404 keep the full behaviour because their
 * rules target `.constellation` and this targets the hero's
 * `.constellation-wrap`. */
@media (prefers-reduced-motion: no-preference) {
  .md-typeset .constellation-wrap .star.tw {
    animation: constellation-pop var(--motion-pop) var(--motion-ease) forwards;
    animation-delay: calc(var(--i) * var(--motion-stagger));
  }
}

/* On the style page the component sits in a framed box at its natural
 * aspect rather than behind content. */
.md-typeset .constellation-demo {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  background-color: var(--c-bg-sunken);
  margin: var(--space-8) 0;
}

.md-typeset .constellation-demo .constellation {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1000 / 420;
}

/* ── evidence strip (M3-2c, #95) ─────────────────────────────────────────────
 *
 * Rendered by scripts/python/evidence_hook.py from docs/assets/evidence.json;
 * the markup never appears in a source file.  Cells are links -- each figure
 * goes to the thing that can check it -- separated by hairlines made of the
 * grid's gap over a line-coloured background, which is this site's depth
 * idiom (ADR-005: no shadows).
 *
 * The count-up in evidence.js rewrites .ev-n's text; everything here styles
 * the resting state, which is also the reduced-motion and no-JS state.
 */

.md-typeset .evidence {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
  gap: var(--border-width);
  background-color: var(--c-line);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin: var(--space-6) 0 var(--space-3);
}

.md-typeset .evidence .ev {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  background-color: var(--c-bg-raised);
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: none;
  transition: background-color 120ms;
}

.md-typeset .evidence .ev:hover,
.md-typeset .evidence .ev:focus-visible {
  background-color: var(--c-accent-wash);
}

.md-typeset .evidence .ev-n {
  font-family: var(--font-display), var(--font-body), sans-serif;
  font-size: var(--type-h2);
  font-weight: var(--display-weight);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-heading);
  font-variant-numeric: tabular-nums;
  color: var(--c-fg);
}

/* The zeros are the claim -- nothing assumed, nothing fetched -- so they are
 * the one place the strip carries the accent. */
.md-typeset .evidence .ev-n.ev-zero {
  color: var(--c-accent);
}

.md-typeset .evidence .ev-l {
  font-size: var(--type-small);
  font-weight: var(--strong-weight);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--c-fg-muted);
}

.md-typeset .evidence .ev-d {
  font-size: var(--type-small);
  color: var(--c-fg-muted);
}

.md-typeset .ev-caption {
  font-size: var(--type-small);
  color: var(--c-fg-muted);
  max-width: var(--measure);
}

/* ── typed-proof terminal (M3-2d, #96) ───────────────────────────────────────
 *
 * Rendered by scripts/python/proof_hook.py from docs/assets/proof.json; the
 * markup never appears in a source file, and the sessions it shows -- one
 * per tab, one lemma each -- are ones `make proof` recorded from a real
 * Agda (ADR-009: motion is evidence-honest).  Everything below styles the
 * FINAL state -- completed proof, zero goals, ✓ -- which is also the
 * reduced-motion, JS-off and crawler rendering; proof.js only rewinds it
 * and types it back in.  The tab bar ships `hidden` (proof.js reveals it,
 * like the replay button: no dead controls), so the JS-off rendering is a
 * single finished terminal, the first lemma's.
 *
 * The frame is the site's hairline box, deliberately: #98 tried
 * terminal-window chrome (title bar, dots) on the 404's goal and reverted
 * it.  The bar here earns its place because each of its three occupants is
 * load-bearing -- the source link is provenance, the replay control is
 * ADR-009's keyboard requirement, the goal pill is the state the session is
 * about.  No dots.
 *
 * Colour comes from the code-block tokens: the hook emits Pygments' Agda
 * vocabulary (`c1`, `nf`, `ow` -- the 404's precedent), so the mappings
 * below are the same three every real code block uses; the hole wears the
 * 404 goal's exact recipe (wash, 80/20 ink mix, hairline ring -- the ratios
 * are recorded there); the verdict green is --proof-ok, pinned per scheme
 * beside the syntax colours above.  Nothing here invents a hue.
 */

.md-typeset .proof {
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  background-color: var(--c-bg-sunken);
  font-family: var(--font-mono), monospace;
  font-size: var(--type-ui);
  font-variant-ligatures: none;
}

/* The terminal holds back while the hero's words land, then fades in and
 * the replay begins (proof.js waits for this animation's end).  CSS-only,
 * so a JS-off reader still gets the frame -- just late; a reduced-motion
 * reader gets it immediately, because the entrance exists only inside this
 * media query.  `visibility` rides along so the held-back frame cannot be
 * clicked or tabbed into before it exists visually. */
@media (prefers-reduced-motion: no-preference) {
  .md-typeset .proof {
    animation: proof-enter var(--motion-check-beat) var(--motion-ease)
      var(--motion-hero-enter) both;
  }
}

@keyframes proof-enter {
  from { opacity: 0; visibility: hidden; }
  to   { opacity: 1; visibility: visible; }
}

/* One tab per lemma, labelled with the definiendum the transcript records.
 * The row needs its own [hidden] guard: `display: flex` would out-rank the
 * user-agent's [hidden] rule, and this bar ships hidden (see above). */
.md-typeset .proof-tabs {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
  padding: var(--space-2) var(--space-4);
  border-bottom: var(--border-width) solid var(--c-line);
  font-size: var(--type-ui-small);
}

.md-typeset .proof-tabs[hidden] {
  display: none;
}

/* Real <button>s, like the replay control: Tab reaches the selected one
 * (proof.js keeps the others at tabindex -1, arrow keys walk the row), the
 * global :focus-visible ring applies.  Selection is an accent underline --
 * the site's link grammar -- not a filled chip; the bar stays quiet. */
.md-typeset .proof-tab {
  appearance: none;
  background: none;
  font: inherit;
  color: var(--c-fg-muted);
  border: none;
  border-bottom: var(--border-width) solid transparent;
  padding: 0;
  cursor: pointer;
  transition: color 120ms, border-color 120ms;
}

.md-typeset .proof-tab:hover {
  color: var(--c-accent-hover);
}

.md-typeset .proof-tab[aria-selected="true"] {
  color: var(--c-fg);
  border-bottom-color: var(--c-accent);
}

.md-typeset .proof-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
  border-bottom: var(--border-width) solid var(--c-line);
  font-size: var(--type-ui-small);
  color: var(--c-fg-muted);
}

/* The filename, in the bar's own muted colour: it names what was checked
 * and is not a link (ADR-011 -- the source repository is private).  All it
 * learns here is to give way, so the pill and the control never wrap. */
.md-typeset .proof-src {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-right: auto;
}

/* A real <button>, like the 404's search affordance: Tab reaches it, Enter
 * and Space fire it, the global :focus-visible ring applies.  It ships
 * `hidden` -- proof.js reveals it only when a replay can actually run, so
 * neither a JS-off page nor a reduced-motion one shows a dead control. */
.md-typeset .proof-replay {
  appearance: none;
  background: none;
  font: inherit;
  color: var(--c-fg-muted);
  border: var(--border-width) solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  white-space: nowrap;
  cursor: pointer;
  transition: color 120ms, border-color 120ms;
}

.md-typeset .proof-replay:hover,
.md-typeset .proof-replay:focus-visible {
  color: var(--c-accent-hover);
  border-color: var(--c-accent);
}

.md-typeset .proof-goals {
  border: var(--border-width) solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: color 120ms, border-color 120ms;
}

/* Emptied by the replay's rewind; visibility keeps the box so the bar does
 * not reflow when the count arrives. */
.md-typeset .proof-goals:empty {
  visibility: hidden;
}

.md-typeset .proof-goals-zero {
  color: var(--proof-ok);
  border-color: color-mix(in srgb, var(--proof-ok) 45%, transparent);
}

/* The floor is the tallest session's height -- six code rows (Induction has
 * a base case the others don't) plus the HUD row and its rule -- so
 * switching tabs never moves the page below the terminal.  Shorter sessions
 * carry one row of air instead; a future module taller than six rows would
 * show up immediately as a jump when its tab is chosen. */
.md-typeset .proof-body {
  padding: var(--space-4) var(--space-4) var(--space-3);
  overflow-x: auto;
  min-height: calc(7 * 1em * var(--leading-body)
    + var(--space-3) + var(--space-2) + var(--space-4) + var(--space-3));
}

/* Every line holds its height from birth: the replay empties them all and
 * types back in, and a terminal that grew a row per line would pump the
 * page's layout (the 404's typed comment solves the same problem with a
 * clip; here the row count is the thing that must not change). */
.md-typeset .proof-line {
  white-space: pre;
  line-height: var(--leading-body);
  min-height: calc(1em * var(--leading-body));
}

.md-typeset .proof .c1 { color: var(--md-code-hl-comment-color); }
.md-typeset .proof .nf { color: var(--md-code-hl-function-color); }
.md-typeset .proof .ow { color: var(--md-code-hl-operator-color); }

/* The 404 goal-hole's recipe, by the same numbers: agda2-mode paints an
 * interactive hole green, this site's holes wear the accent, and the 20%
 * lean toward the ink is what clears 5.8:1 on the wash in both themes. */
.md-typeset .proof-hole {
  background-color: var(--c-accent-wash);
  color: color-mix(in srgb, var(--c-accent) 80%, var(--c-fg));
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 var(--border-width)
    color-mix(in srgb, var(--c-accent) 45%, transparent);
  padding: 0 var(--space-1);
}

.md-typeset .proof-check {
  color: var(--proof-ok);
}

.md-typeset .proof-check .proof-dim {
  color: var(--c-fg-muted);
}

/* The goals HUD: agda-mode's information buffer, one line tall, under a
 * dashed rule so it reads as the tool speaking rather than the file. */
.md-typeset .proof-hud {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: var(--border-width) dashed var(--c-line);
  color: var(--c-fg-faint);
  white-space: pre;
  min-height: calc(1em * var(--leading-body));
}

/* The caret exists only while proof.js is typing a line, which it never is
 * under reduced motion; the media query states that rather than assumes it.
 * A drawn box, not a glyph, so no font subset owes it a character. */
@media (prefers-reduced-motion: no-preference) {
  .md-typeset .proof-typing::after {
    content: "";
    display: inline-block;
    width: 0.5em;
    height: 1em;
    vertical-align: text-top;
    background-color: var(--c-accent-hover);
    animation: proof-caret var(--motion-caret) steps(1) infinite;
  }
}

@keyframes proof-caret {
  50% { opacity: 0; }
}

/* Below Material's breakpoint the hero stacks and the terminal takes the
 * full column under the copy; the UI size keeps the session's longest line
 * inside a 390px viewport without a scrollbar (overflow-x above stays as
 * the safety net, per the code-block convention: wrapping Agda is worse). */
@media screen and (max-width: 44.9375em) {
  .md-typeset .proof {
    font-size: var(--type-ui-small);
  }
}

/* On the style page the component sits framed at the width the hero column
 * gives it, so the demo previews what the home page ships. */
.md-typeset .proof-demo {
  max-width: calc(var(--measure) * 0.8);
  margin: var(--space-8) 0;
}

/* ── components ─────────────────────────────────────────────────────────────
 *
 * Five components carry most of the site: a project card, a publication entry,
 * a timeline entry, a talk entry (#19) and a secondary-project entry (#28).
 * They are defined once here so that M4 and M5 assemble pages from decided
 * parts rather than hand-styling each occurrence, which is what makes a site
 * read as designed rather than assembled.  docs/design/style.md renders a live
 * example of every one, including the degraded cases, so a component that
 * breaks breaks visibly on one page.
 *
 * Authoring form is Markdown plus `attr_list` and `md_in_html`, never a raw
 * HTML block: a wrapper `<div class="…" markdown>` around ordinary Markdown,
 * and `{.class}` where a specific line needs naming.  What that means in
 * practice, per component, is on the style page.
 *
 * Two conventions used throughout, stated once:
 *
 * 1.  The type tokens are `em`-relative to the typeset base, so they compose:
 *     an entry set at --type-small (0.8125em) whose title is --type-h3
 *     (1.1875em) puts the title at 0.965em -- just under body size -- against
 *     14px metadata.  That compounding is deliberate and is the only way to
 *     get a size hierarchy inside a component without inventing a token.
 *
 * 2.  No component wraps itself in a link.  A card whose whole surface is
 *     clickable has to nest the title and action links inside that link, which
 *     is invalid, unreachable by keyboard in the way it looks, and destroys
 *     text selection.  Cards are <div>s; the title and the action links are
 *     ordinary sibling links, and each gets the global :focus-visible ring.
 *     Nothing here sets `overflow: hidden`, so that ring is never clipped.
 */

/* ── project card ─────────────────────────────────────────────────────────
 *
 *   <div class="project-grid" markdown>
 *   <div class="project-card" markdown>
 *   **[title](url)**
 *
 *   One-line summary.
 *
 *   `Agda`{.tag} `Nix`{.tag}
 *
 *   [Source](url) · [Docs](url)
 *   {.project-links}
 *   </div>
 *   </div>
 *
 * Every part after the title is optional.  The title is the first paragraph,
 * so it needs no class of its own; the links row needs one because it is
 * pinned to the bottom of the card, which is what keeps a row of cards of
 * different lengths aligned along their actions.
 */

.md-typeset .project-grid {
  display: grid;
  /* Half a text column is the point at which a card stops being a card and
     starts being a paragraph with a border, so that is the track minimum.
     `min(100%, …)` keeps a single card from overflowing a narrow viewport. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(var(--measure) / 2)), 1fr));
  gap: var(--space-4);
  margin: var(--space-8) 0;
}

.md-typeset .project-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  /* The anchor for the preview below (M3-3b, #97), which is a descendant of
     the card rather than a sibling: hovering a descendant hovers its
     ancestor whatever box it sits in, and that is what lets the pointer
     travel from the card onto the preview without the preview closing under
     it (WCAG 1.4.13, "hoverable"). */
  position: relative;
  background-color: var(--c-bg-raised);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  font-size: var(--type-small);
  color: var(--c-fg-muted);
  transition: border-color 120ms;
}

/* Hover and keyboard focus are deliberately different colours: hover is a
   passing pointer, focus is where the keyboard actually is. */
.md-typeset .project-card:hover {
  border-color: var(--c-line-strong);
}

.md-typeset .project-card:focus-within {
  border-color: var(--c-accent);
}

/* The open card paints over the ones after it.  Without this the preview,
   which is a positioned descendant, is painted in tree order and therefore
   *under* every later card in the grid -- so the preview of a card in the
   first row disappeared behind the second row rather than over it.  Only
   the open card is raised, so nothing else in the grid changes layer. */
.md-typeset .project-card:hover,
.md-typeset .project-card:focus-within {
  z-index: 2;
}

/* The flex `gap` owns the vertical rhythm inside a card; Material's paragraph
   margins would otherwise be added to it. */
.md-typeset .project-card > * {
  margin: 0;
}

.md-typeset .project-card > p:first-child {
  font-family: var(--font-display), var(--font-body), sans-serif;
  font-size: var(--type-h3);
  font-weight: var(--display-weight);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-heading);
  color: var(--c-fg);
}

/* The title is authored as `**…**`, so the paragraph's weight is not the one
   that lands: `.md-typeset strong` sets its own, and it wins over inheritance.
   Under Constellation both tokens are 600 and nothing shows; under Meridian
   --display-weight is 500, and the card title would be the one component title
   that ignored it.  Measured, not assumed: forcing --display-weight to 500
   moves the publication title, the talk title and every heading to 500 and
   leaves this one at 600 without this rule. */
.md-typeset .project-card > p:first-child > strong {
  font-weight: var(--display-weight);
}

/* A title that links somewhere reads as a link; one that does not is still a
   title.  The bottom rule under body links is noise at this size. */
.md-typeset .project-card > p:first-child a {
  border-bottom: none;
}

.md-typeset .project-links {
  /* Pushed to the bottom, so cards in a row line their actions up. */
  margin-top: auto;
  padding-top: var(--space-2);
}

/* ── card preview ─────────────────────────────────────────────────────────
 *
 *   <div class="project-card" markdown>
 *   …
 *   <!-- preview: agda-algebras -->
 *   </div>
 *
 * The marker is expanded by scripts/python/preview_hook.py from
 * docs/assets/previews.json, which `make previews` writes: a 480x270
 * thumbnail of the card's artifact -- a paper's first page, or the front
 * page of the site the card points at -- the facts gen_previews.py derived
 * about it, and a line saying what was photographed and when.  A card with
 * no marker is exactly the card above, which is the floor this degrades to.
 *
 * Three decisions are worth stating, because each is the answer to a way
 * this component can be got wrong.
 *
 * 1.  **It opens below the card, never over it, and in one column.**  A
 *     preview that covered the card would cover the card's own links, which
 *     is the one thing it must not do, and one beside the card has nowhere
 *     to go the moment the grid drops to a single column.  Inside the
 *     preview the answer is the same, for a measured reason: a card on the
 *     projects page is 334px wide, and a 200px thumbnail beside the facts
 *     leaves them 96px, at which every label wraps.  Measured on that card,
 *     the two-column preview came out 436px tall against the 337px the one
 *     column gives it.  One column at every width is one shape to keep
 *     right.
 *
 * 2.  **It is flush with the card, not floating near it.**  The two borders
 *     overlap into a single line, so the pointer crosses no gap on its way
 *     in.  A gap would un-hover the card mid-journey and take the preview
 *     away before it could be read, which is the failure WCAG 1.4.13's
 *     "hoverable" names.
 *
 * 3.  **The pointer opens it only where there is a pointer.**  A touch
 *     browser reports `hover: none` and synthesises a sticky `:hover` on
 *     tap; without the media query the first tap on a card would open a
 *     preview across the next card instead of following the link.  Keyboard
 *     focus opens it everywhere, since that gesture means the same thing on
 *     every device.
 *
 * Escape closes an open preview, which is the third WCAG requirement and the
 * one no selector can express: docs/javascripts/preview.js sets
 * `data-preview="dismissed"` on the card, the `:not()` below reads it, and
 * the mark is lifted when the pointer or the focus leaves.
 */

.md-typeset .card-preview {
  display: none;
  position: absolute;
  /* `top: 100%` is the card's *padding* box, one border width above its
     outer edge, so the two 1px borders land on each other as a single line
     and the pointer crosses no gap. */
  top: 100%;
  left: 0;
  right: 0;
  /* The cap sits just under the thumbnail's own 480px, so the image is
     never shown larger than it was captured: on a wide card the preview
     stops here rather than becoming a billboard, and on a narrow one it is
     simply the card's width. */
  max-width: 22rem;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
  padding: var(--space-3);
  background-color: var(--c-bg-raised);
  border: var(--border-width) solid var(--c-line-strong);
  /* Square where it meets the card, rounded where it leaves it: the pair
     reads as one object that grew, rather than two that collided. */
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}

.md-typeset .project-card:not([data-preview="dismissed"]):focus-within > .card-preview,
.md-typeset .project-card-pinned > .card-preview {
  display: grid;
}

@media (hover: hover) {
  .md-typeset .project-card:not([data-preview="dismissed"]):hover > .card-preview {
    display: grid;
  }
}

.md-typeset .card-preview-shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  /* The element carries the file's own width and height, so the box is the
     right size before the bytes arrive; this is what fills it until they do.
     The image is `loading="lazy"` and a preview has no box until it opens,
     so none of the six is fetched on a page load -- measured in Chromium,
     not assumed. */
  background-color: var(--c-bg-sunken);
}

.md-typeset .card-preview-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.md-typeset .card-preview-facts {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  margin: 0;
  /* A rem token, not an em one: the card is already at --type-small, and an
     em-relative size here would compound with it to something smaller than
     the site sets anywhere. */
  font-size: var(--type-ui);
}

.md-typeset .card-preview-facts dt {
  margin: 0;
  font-weight: 400;
  color: var(--c-fg-faint);
}

.md-typeset .card-preview-facts dd {
  margin: 0;
  color: var(--c-fg);
  /* A DOI is one unbreakable token as far as the line breaker is concerned,
     and a card column is narrow.  Breaking it is better than widening the
     grid track around it. */
  overflow-wrap: anywhere;
}

.md-typeset .card-preview-src {
  /* Last line of the preview, pushed to its floor, so a preview with three
     facts and one with none end the same way. */
  margin: auto 0 0;
  font-size: var(--type-ui-small);
  color: var(--c-fg-faint);
}

/* On the style page one card is pinned open, in flow rather than floating.
 * A component that is `display: none` everywhere it appears is one the
 * contrast and font audits never measure -- `offsetParent === null` is the
 * first thing both skip -- so the open state would ship unmeasured.  This is
 * the one place it is pinned, and measuring it is the reason. */
.md-typeset .project-card-pinned > .card-preview {
  position: static;
  margin-top: var(--space-2);
  border-radius: var(--radius-md);
}

@media (prefers-reduced-motion: no-preference) {
  .md-typeset .card-preview {
    animation: card-preview-in var(--motion-preview) var(--motion-ease) both;
  }
}

@keyframes card-preview-in {
  from {
    opacity: 0;
    transform: translateY(calc(-1 * var(--space-2)));
  }
}

/* ── tag ──────────────────────────────────────────────────────────────────
 *
 *   `Agda`{.tag}   `invited`{.tag}   `dormant`{.tag}
 *
 * A code span, because `attr_list` attaches to elements Markdown produces and
 * a bare `[text]{.tag}` is not one of them -- it renders literally.  `em` and
 * `strong` were the alternatives and both already carry meaning here (venue,
 * emphasised author), so the neutral one is the right host.  It is restyled
 * out of the code face: these are labels, not code.
 */

.md-typeset .tag {
  display: inline-block;
  /* An inline-block is a block container, so it inherits the hanging indent
     the publication and talk entries set and applies it to its own first
     line -- which slides the label out of its own border box.  Caught by
     looking at a rendered talk entry with a tag on it, not by reading this. */
  text-indent: 0;
  font-family: var(--font-body), sans-serif;
  font-size: var(--type-small);
  font-weight: 400;
  line-height: var(--leading-heading);
  color: var(--c-fg-muted);
  background-color: transparent;
  border: var(--border-width) solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  white-space: nowrap;
}

/* ── publication entry, talk entry, and secondary-project entry ───────────
 *
 *   <div class="publications" markdown>
 *   - **Title**
 *     Authors, with **William DeMeo** emphasised.  *Venue*, **volume**:pages, year.
 *     [DOI](url) · [arXiv:id](url) · [PDF](url)
 *   </div>
 *
 * (each entry line ends with two spaces, so the lines stay one paragraph)
 *
 * A talk entry is the same grammar with a different second line -- venue,
 * location, year -- and the slides link on the title rather than in a row of
 * their own.  They share these rules rather than being copies of each other,
 * because they are the same shape: a title, a line of attribution, and an
 * optional line of links.  Where they differ, the rules are separated below.
 *
 * A secondary-project entry (#28) is the third consumer, and it is one line:
 *
 *   <div class="secondary" markdown>
 *   - **[name](url)**: one sentence.  `dormant`{.tag}
 *   </div>
 *
 * It joins every rule above the two that promote the title, and takes neither
 * of those.  That is the whole of the difference and it is deliberate.  The
 * list sits under the project grid, so its names have to read as subordinate
 * to a card title; at --type-h3 in the display face they would be the same
 * size as one, and the section would compete with the grid instead of closing
 * it.  Kept at the entry's own size the name is bold, accented and inline
 * with its sentence, and it keeps the faint underline every body link has,
 * which a display-size title drops as noise but a body-size name needs as its
 * only affordance.  A name with nothing to link to is bold text, which is the
 * same floor a talk with no slides degrades to.
 *
 * The publication list is written by `gen_publications.py` from
 * `bibliography.json` (M5-1, #29): the generator emits exactly this Markdown
 * and knows nothing about these classes, so the page wraps its output and adds
 * nothing.  Every optional part is optional in the generator too -- an entry
 * with no DOI, no arXiv identifier and no venue simply has fewer lines.
 */

.md-typeset .publications ul,
.md-typeset .talks ul,
.md-typeset .secondary ul {
  list-style: none;
  margin-left: 0;
  padding-left: 0;
  max-width: var(--measure);
}

.md-typeset .publications li,
.md-typeset .talks li,
.md-typeset .secondary li {
  margin: 0 0 var(--space-6);
  /* A hanging indent, the way a reference list has always been set.
     `text-indent` applies to the first line of the block only, and it is
     inherited, so it reaches the <p> that a loose list wraps each item in --
     which is why this works whether or not the source has blank lines between
     entries.  The lines after each <br> keep the padding. */
  padding-left: var(--space-6);
  text-indent: calc(-1 * var(--space-6));
  font-size: var(--type-small);
  color: var(--c-fg-muted);
}

.md-typeset .publications li:last-child,
.md-typeset .talks li:last-child,
.md-typeset .secondary li:last-child {
  margin-bottom: 0;
}

/* Material gives a list item inside a loose list its own paragraph margins. */
.md-typeset .publications li > p,
.md-typeset .talks li > p,
.md-typeset .secondary li > p {
  margin: 0;
}

/* The emphasised author -- and the volume number, which is marked up the same
   way -- steps forward out of the muted metadata. */
.md-typeset .publications li strong,
.md-typeset .talks li strong,
.md-typeset .secondary li strong {
  color: var(--c-fg);
}

.md-typeset .publications li > strong:first-child,
.md-typeset .publications li > p:first-child > strong:first-child,
.md-typeset .talks li > strong:first-child,
.md-typeset .talks li > p:first-child > strong:first-child {
  font-family: var(--font-display), var(--font-body), sans-serif;
  font-size: var(--type-h3);
  font-weight: var(--display-weight);
  letter-spacing: var(--tracking-display);
}

/* A linked title is the artifact itself -- the slides, for a talk -- so it
   carries the accent.  A talk with no slides is a title in plain text, and
   that is the whole of the degradation. */
.md-typeset .publications li > strong:first-child a,
.md-typeset .publications li > p:first-child > strong:first-child a,
.md-typeset .talks li > strong:first-child a,
.md-typeset .talks li > p:first-child > strong:first-child a {
  border-bottom: none;
}

/* A publication or a talk is two or three lines and is spaced to be read one
   at a time; a secondary entry is one line, and eight of them spaced that way
   read as eight separate things rather than as a list.  Everything else about
   the block is shared above.  The `:last-child` rule wins over this one on
   specificity whatever the order, so the last entry still ends flush. */
.md-typeset .secondary li {
  margin-bottom: var(--space-3);
}

/* ── timeline entry ───────────────────────────────────────────────────────
 *
 *   <div class="timeline" markdown>
 *   2023–
 *   :   **Formal Verification Engineer**, Formal Methods — [IO](url)
 *       What the position actually was.
 *   </div>
 *
 * A definition list, because that is what a timeline is: a date, and what
 * happened then.  `def_list` is already enabled, the source reads as the thing
 * it describes, and the CV's education and grants sections are written this
 * way already -- wrapping them is the whole migration.
 *
 * An open-ended range is `2023–` with nothing after the dash, which needs no
 * support here beyond not assuming two dates.
 */

.md-typeset .timeline dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--space-6);
  /* No row gap: the rule down the left of the second column is continuous
     only if the rows touch, so the spacing between entries is padding on the
     rows themselves. */
  row-gap: 0;
  max-width: var(--measure-heading);
  margin: var(--space-8) 0;
}

/* No bottom padding here, deliberately.  A grid row is as tall as its tallest
   item, so padding on the date would set the row height whenever it exceeded
   the entry's -- which is exactly the last row, where the entry's padding is
   removed.  The stretched entry's rule would then hang below its own last line
   by the height of that padding.  The spacing between rows is the entry's job
   alone; the date is one line and never the taller of the two. */
.md-typeset .timeline dt {
  grid-column: 1;
  margin: 0;
  /* Flush with the margin, not ragged against the entry: an open range such
     as `2023–` otherwise floats right of the closed ones above and below it
     (William, on review of #42).  The PDF's date gutter is set the same way. */
  text-align: left;
  font-size: var(--type-small);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--c-fg-muted);
}

.md-typeset .timeline dd {
  position: relative;
  grid-column: 2;
  margin: 0;
  padding-left: var(--space-4);
  padding-bottom: var(--space-6);
  border-left: var(--border-width) solid var(--c-line);
  font-size: var(--type-small);
  color: var(--c-fg-muted);
}

.md-typeset .timeline dd:last-of-type {
  padding-bottom: 0;
}

/* The node on the line.  Centred on the rule, and set against the first line
   of the entry rather than its middle, so entries of different heights line
   their dots up with their dates. */
.md-typeset .timeline dd::before {
  content: "";
  position: absolute;
  top: var(--space-2);
  left: calc(-0.5 * (var(--space-1) + var(--border-width)));
  width: var(--space-1);
  height: var(--space-1);
  border-radius: 50%;
  background-color: var(--c-accent);
}

.md-typeset .timeline dd strong {
  color: var(--c-fg);
}

/* Material's mobile breakpoint.  Below it the date gutter costs more width
   than it earns, so the entry stacks under its date and keeps the rule. */
@media screen and (max-width: 44.9375em) {
  .md-typeset .timeline dl {
    display: block;
  }

  .md-typeset .timeline dt {
    text-align: left;
    padding-bottom: var(--space-1);
  }
}

/* ── playground ───────────────────────────────────────────────────────────
 *
 * The live Agda checker (M4-9b, #144).  Everything below styles elements
 * that `playground.js` creates, with two exceptions: `.agda-exercise`, which
 * is a `<div markdown>` in playground.md, and `.agda-exercise__gate`, which
 * `playground_hook.py` renders from the assets' manifest.  Those two are the
 * page with JavaScript off, and they are styled to stand on their own rather
 * than to look like a widget waiting for its script.
 *
 * No motion.  The only thing that moves is the download's `<progress>`,
 * which is state, so there is no reduced-motion branch here (ADR-009).
 */

.md-typeset .agda-exercise {
  margin: var(--space-8) 0;
}

/* The consent sentence.  Set apart from the prose, because it is a claim
 * about what pressing the button costs and a reader skimming for the button
 * must not skim past it. */
.md-typeset .agda-exercise__gate {
  max-width: var(--measure);
  color: var(--c-fg-muted);
  font-size: var(--type-small);
  border-left: 2px solid var(--c-line-strong);
  padding-left: var(--space-4);
  margin: var(--space-4) 0;
}

.md-typeset .agda-exercise__controls,
.md-typeset .agda-exercise__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

/* The same control the proof terminal's replay is: a real <button>, so Tab
 * reaches it, Enter and Space fire it, and the global :focus-visible ring
 * applies without anything here. */
.md-typeset .agda-button {
  appearance: none;
  background: none;
  font: inherit;
  font-size: var(--type-small);
  color: var(--c-fg-muted);
  border: var(--border-width) solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
  transition: color 120ms, border-color 120ms;
}

.md-typeset .agda-button:hover,
.md-typeset .agda-button:focus-visible {
  color: var(--c-accent-hover);
  border-color: var(--c-accent);
}

.md-typeset .agda-button--primary {
  color: var(--c-fg);
  border-color: var(--c-accent);
}

.md-typeset .agda-button[disabled] {
  cursor: default;
  opacity: 0.6;
}

.md-typeset .agda-button[disabled]:hover {
  color: var(--c-fg-muted);
  border-color: var(--c-line-strong);
}

.md-typeset .agda-exercise__status,
.md-typeset .agda-exercise__hint {
  font-size: var(--type-small);
  color: var(--c-fg-muted);
  font-variant-numeric: tabular-nums;
}

.md-typeset .agda-exercise__controls progress {
  inline-size: 12rem;
  block-size: 0.4rem;
}

.md-typeset .agda-exercise__controls--failed .agda-exercise__status {
  color: var(--c-error);
}

/* The editor wears the same hairline box and the same mono face as every
 * code block on the site (`.highlight` above), because it replaces one: the
 * page should not visibly change register when the checker arrives. */
.md-typeset .agda-editor {
  display: block;
  inline-size: 100%;
  font-family: var(--font-mono), monospace;
  font-size: var(--type-code);
  line-height: 1.6;
  color: var(--c-fg);
  background: var(--c-bg-raised);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  resize: vertical;
  tab-size: 2;
}

.md-typeset .agda-editor:focus-visible {
  border-color: var(--c-accent);
}

/* Visible, not a screen-reader-only label: a text box holding Agda with no
 * caption is a box whose purpose a reader has to infer from context. */
.md-typeset .agda-editor__label {
  display: block;
  font-size: var(--type-ui);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--c-fg-faint);
  margin-bottom: var(--space-2);
}

/* The verdict is one line and it is the answer, so it gets the weight the
 * rest of the controls do not. */
.md-typeset .agda-verdict {
  margin: var(--space-4) 0 0;
  font-family: var(--font-mono), monospace;
  font-size: var(--type-small);
  font-variant-numeric: tabular-nums;
  color: var(--c-fg-muted);
}

.md-typeset .agda-verdict--ok {
  color: var(--proof-ok);
}

.md-typeset .agda-verdict--error {
  color: var(--c-error);
}

/* Agda's own words, verbatim, including its layout: `white-space: pre` with
 * a scroller rather than wrapping, because a wrapped error message puts the
 * caret under the wrong column and a caret under the wrong column is worse
 * than a horizontal scrollbar. */
.md-typeset .agda-output {
  margin: var(--space-3) 0 0;
  font-size: var(--type-code);
  line-height: 1.6;
  white-space: pre;
  overflow-x: auto;
  background: var(--c-bg-sunken);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  color: var(--c-fg);
}

/* The input-method palette.  Small, quiet, and directly under the box it
 * types into: it is a keyboard the page is lending the reader, not a toolbar.
 * Each key is a real <button>, so Tab reaches it and the global focus ring
 * applies; the sequence that types the same character is in its title and in
 * its aria-label, because the palette's job is to become unnecessary. */
.md-typeset .agda-palette {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
}

.md-typeset .agda-palette__lead {
  font-size: var(--type-small);
  color: var(--c-fg-muted);
  margin-right: var(--space-2);
}

.md-typeset .agda-palette__key {
  appearance: none;
  background: var(--c-bg-raised);
  font-family: var(--font-mono), monospace;
  font-size: var(--type-code);
  line-height: 1;
  color: var(--c-fg);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-sm);
  min-inline-size: 1.9em;
  padding: var(--space-2);
  cursor: pointer;
  transition: border-color 120ms, color 120ms;
}

.md-typeset .agda-palette__key:hover,
.md-typeset .agda-palette__key:focus-visible {
  color: var(--c-accent-hover);
  border-color: var(--c-accent);
}

/* The position Agda complained about, as a control rather than as prose: it
 * puts the caret on the characters instead of leaving a reader to count to
 * them.  Set in the verdict's own mono face so the line reads as one thing. */
.md-typeset .agda-verdict__where {
  appearance: none;
  background: none;
  font: inherit;
  color: inherit;
  border: 0;
  border-bottom: var(--border-width) dotted currentcolor;
  padding: 0;
  cursor: pointer;
}

.md-typeset .agda-verdict__where:hover,
.md-typeset .agda-verdict__where:focus-visible {
  border-bottom-style: solid;
}

@media print {
  .md-typeset > p,
  .md-typeset > ul,
  .md-typeset > ol {
    max-width: none;
  }

  /* A card grid prints as a list; two narrow columns on paper are worse than
     one full-width one. */
  .md-typeset .project-grid {
    display: block;
  }

  /* A preview opens on hover or on focus, and paper has neither.  Same
     reasoning as the proof terminal's controls below: what cannot be
     operated in this medium should not appear in it. */
  .md-typeset .card-preview {
    display: none !important;
  }

  .md-typeset .project-card {
    margin-bottom: var(--space-4);
  }

  /* The proof terminal's controls, but not its content (M3-6, #22).  The
     finished proof lines and the goal pill at zero are the evidence and
     belong on paper; the replay button and the tab bar are controls nobody
     can operate there.  That is the same reasoning that ships both `hidden`
     without JavaScript -- a control that does nothing should not exist --
     applied to the medium where they can never do anything.
     Measured in print emulation: Material already gives the header, the
     search form, the theme toggle and the footer navigation zero area, so
     those need nothing here; these two were what still printed. */
  .md-typeset .proof-replay,
  .md-typeset .proof-tabs {
    display: none !important;
  }

  /* ...and then every panel, because hiding the tab bar removed the only way
     to reach four of the five.  proof_hook.py ships panels 2 to 5 `hidden`
     for the screen, where a tab switches them; on paper that left one proof
     printed and four unreachable, which is worse than what it replaced.
     Measured before and after: 5 panels, 4 carrying `hidden`, 1 visible in
     print; now 5.  `[hidden]` is an attribute, so this needs the override. */
  .md-typeset .proof-panel[hidden] {
    display: block !important;
  }
}
