/* Public site styles. Design goals (DESIGN.md, "Public UI"): one fixed
 * typographic voice — Karla (sans) for headings and chrome, Petrona
 * (serif) for prose, JetBrains Mono for dates and code — rem-based type
 * off a 100% root so zoom and reader font preferences are respected,
 * WCAG AA contrast in both themes. theme.js stamps html[data-theme]
 * pre-paint. Font files are materialized from @fontsource packages
 * (frontend/copy-fonts.mjs) and embedded; CSP forbids CDN fonts. */

:root {
  --bg: #f9faf9;
  --fg: #1a1e1b;
  /* 4.73:1 on --bg. The previous #6f7772 measured 4.40:1 — under the 4.5
   * AA floor this file's header promises, and muted carries real prose
   * (blockquotes, excerpts, dates), not just decoration. Darkened by the
   * smallest step that clears the line. */
  --muted: #6a726d;
  --border: #e1e6e2;
  --link: #1a1e1b;
  --surface: #eef1ee;
  /* The accent: the notebook green. One hue, two lightnesses — every
   * dot, pin mark, and underline draws from it. */
  --accent: oklch(0.42 0.09 155);
  --code-bg: #0d1117; /* code stays dark in both themes (chroma github-dark) */
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: "Karla", system-ui, -apple-system, "Segoe UI", sans-serif;
  --serif: "Petrona", Georgia, "Times New Roman", serif;
}
:root[data-theme="dark"] {
  --bg: #151816;
  --fg: #e5e9e6;
  --muted: #8b938d;
  --border: #2b302c;
  --link: #e5e9e6;
  --surface: #1d211e;
  --accent: oklch(0.72 0.09 155);
}

* { box-sizing: border-box; }
/* Reading sits one browser-zoom step (110%) over the browser default,
 * set at the root so every rem — prose, waypoints, the folio —
 * scales together. A percentage multiplies the reader's own font-size
 * preference rather than replacing it. */
/* The page itself never scrolls sideways. Anything too wide for the
 * viewport — a wide table, a long code line — scrolls inside its own
 * box; a document that slides under the thumb on every vertical swipe
 * is the worst reading experience a phone can have. This is the
 * backstop, not the fix: give the wide thing its own overflow-x so it
 * stays reachable rather than clipped. */
html { height: 100%; font-size: 110%; overflow-x: hidden; }
/* Prose is the default voice (serif); headings and chrome opt into sans. */
body {
  margin: 0;
  min-height: 100%;
  max-width: 100%;
  overflow-x: hidden;
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.65;
  background: var(--bg);
  color: var(--fg);
}
h1, h2, h3, h4, h5, h6 {
  font-family: var(--sans);
}

a { color: var(--link); text-underline-offset: 0.2em; }
a:hover { text-decoration-thickness: 2px; }
:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.mono { font-family: var(--mono); }
.muted { color: var(--muted); }

.skip-link {
  position: absolute;
  left: -999px;
  top: 0.5rem;
  background: var(--bg);
  padding: 0.4rem 0.8rem;
  z-index: 10;
}
.skip-link:focus { left: 0.5rem; }

/* Site mark: understated monogram box, top left. */
.site-header { padding: 1.25rem 1.5rem; }
.site-header .mark {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  text-decoration: none;
  font-weight: 600;
  letter-spacing: 0.05em;
}
.site-header .mark:hover { border-color: var(--fg); }

/* The publication: no masthead, no header — the page opens straight
 * onto the writing, and ALL chrome is the folio line at the foot. One
 * centered 60rem column. The explicit width (not auto) matters: on the
 * archive page main is a flex item, and with an auto cross-size the
 * auto margins would shrink it to fit-content instead of stretching
 * (Safari follows the spec on this; Chrome stretches anyway). */
main { width: 100%; max-width: 60rem; margin: 1.5rem auto; padding: 0 1.25rem; }

/* An article page is the narrow reading column, not the 60rem listing
 * one, and the folio rule must span the text it closes — at 60rem the
 * line ran wider than the prose above it, reading as a second rule
 * beside .article-nav's. Scoped on .article-header because only
 * web/article's Body emits it: the listings and the not-found page keep
 * the wide column. */
main:has(> article > .article-header) { max-width: 42rem; }

/* The folio: ALL of the page's chrome, one line at the foot — icons
 * only, each with a title. In the flow, never fixed. */
.folio {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin-top: 2.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
}
.folio .gap { flex: 1; }
.folio a { color: var(--muted); text-decoration: none; display: block; }
.folio svg { width: 1rem; height: 1rem; display: block; }
.folio a:hover, .folio a:focus-visible { color: var(--fg); }
.folio .theme-toggle {
  border: none;
  background: transparent;
  color: var(--muted);
  padding: 0;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
}
.folio .theme-toggle:hover { color: var(--fg); }

/* Month/year listing: semantically headings + lists; laid out as a
 * wrapping grid of month columns, newest first, flowing left→right then
 * top→down — a calendar page, not a scroller. One column on narrow
 * screens falls out of auto-fill naturally. */
.months {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 2.25rem 3rem;
  padding: 1rem 0.25rem 1.5rem;
}
/* Month markers: small, muted, mono — waypoints, not headlines. No rule
 * under them: the quiet mono voice already separates a marker from the
 * titles below it, and a full-width line over a one-post month reads as
 * underlining the empty space rather than heading the column. The
 * pinned shelf keeps the page's one horizontal rule. */
.month h2 {
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: lowercase;
  color: var(--muted);
  margin: 0 0 0.65rem;
}
.month ul { list-style: none; margin: 0; padding: 0; }
.month li { margin: 0.55rem 0; }
.month a { text-decoration: none; }
.month a:hover, .month a:focus-visible { text-decoration: underline; }
/* Titles stay on one line; the full title is in the title attribute. */
.listing-title {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.attribution { display: block; font-size: 0.75rem; font-family: var(--sans); }
.attribution a { color: inherit; text-decoration: none; }
.attribution a:hover, .attribution a:focus-visible { color: var(--fg); text-decoration: underline; }

.listing-header h1 { font-size: 1.3rem; margin: 0.5rem 0 0.25rem; }
.listing-header p { margin: 0 0 1rem; }

/* Pinned posts: a wordless shelf across the top of the grid — titles,
 * then a dividing rule; the month columns always start below it. */
.pinned {
  margin: 0.5rem 0 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
.pinned ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem 2.25rem;
}
.pinned li {
  margin: 0;
  min-width: 0;
  display: flex;
  gap: 0.55rem;
  align-items: baseline;
}
/* The pin mark: ↖, pointing back up to the top of the page it leads. */
.pin-mark {
  color: var(--accent);
  font-size: 0.7rem;
  font-family: var(--sans);
}
/* Pinned titles wrap — never ellipsize. */
.pinned .listing-title {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
}
.pinned a { text-decoration: none; }
.pinned a:hover, .pinned a:focus-visible { text-decoration: underline; }

/* ── The archive matrix ────────────────────────────────────────────────
 * The whole archive as one grid: twelve month names across the top, one
 * year per row down the side, one accent tick per post. Every label is
 * said exactly once and alignment does the rest — no rules, no boxes, no
 * badges: position IS the date.
 *
 * The calendar is chrome. It never moves and never scrolls; only the
 * post list below it does. That means the page owns the viewport (a
 * bounded flex column from html down to .arch-detail) rather than the
 * document scrolling as a whole. Pages without .archive are untouched. */
.archive-page,
.archive-page body {
  height: 100dvh;
  overflow: hidden;
}
.archive-page body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.archive-page main {
  flex: 1;
  min-height: 0;
  padding-bottom: 0;
  display: flex;
  flex-direction: column;
}
/* The pinned shelf and the folio hold their places while only the
 * archive's detail list scrolls between them. Vertical space is scarce
 * when the calendar is fixed chrome, so the shelf and the folio sit
 * closer here than on a scrolling page. */
.archive-page .pinned,
.archive-page .folio,
.archive-page .listing-header { flex: none; }
.archive-page .pinned { margin-bottom: 0.6rem; padding-bottom: 0.6rem; }
.archive-page .folio { margin-top: 0.75rem; }
.archive-page .arch-detail { padding-bottom: 1rem; }
.archive {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.arch-cal { flex: none; position: relative; }

/* The axes. One shared grid template keeps the month heads and every
 * year row in the same twelve columns — that alignment is the whole
 * mechanism, so the head and the rows must never drift apart. */
.arch-head,
.arch-row {
  display: grid;
  grid-template-columns: 2.9rem repeat(12, minmax(0, 1fr));
  gap: 0 0.3rem;
  align-items: start;
  padding: 0 0.4rem;
}
.arch-head {
  height: 1.9rem;
  align-items: center;
  margin-bottom: 0.35rem;
}
/* Month labels: the same quiet mono waypoint voice as the month grid. */
.arch-mon {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  color: var(--muted);
  text-transform: lowercase;
  padding-left: 0.2rem;
}
.mon-short { display: none; }

/* A year is the unit of selection: the whole row is the link. */
.arch-row {
  padding-top: 0.2rem;
  padding-bottom: 0.45rem;
  border-radius: 5px;
  text-decoration: none;
  color: inherit;
}
.arch-row:hover { background: color-mix(in oklab, var(--fg) 4%, transparent); }
.arch-row[aria-current="true"] { background: color-mix(in oklab, var(--accent) 13%, transparent); }
.arch-yr {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-top: 0.3rem;
  text-align: center;
}
/* The open year goes to full contrast — the row tint and the year mark
 * say the same thing twice, quietly. */
.arch-row[aria-current="true"] .arch-yr { color: var(--fg); font-weight: 600; }
/* A cell is a tally that wraps. Its height is capped so one burst month
 * cannot turn its row into a tall stripe and push the other years off
 * the screen: past the cap the marks clip, and the row keeps reading as
 * a row. The cap is generous enough that ordinary months never reach it,
 * and the exact count is never the tick's job anyway — the year's
 * accessible name carries it, and the list below names every post. */
.arch-cell {
  min-height: 1.7rem;
  max-height: 4.2rem;
  padding: 0.28rem 0.2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-content: flex-start;
  overflow: hidden;
}
/* One mark per post. Square, not round: a tally, not a status dot — the
 * dot grammar belongs to the studio and means something else there. */
.arch-tick {
  width: 6px;
  height: 6px;
  border-radius: 1px;
  background: var(--accent);
}

/* The one region that scrolls. */
.arch-detail {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-top: 1.25rem;
  padding-bottom: 2.5rem;
}
.arch-detail .months { padding-top: 0; }

/* The fold widget sits in the axis origin — the one cell the grid never
 * uses, where the year column meets the month row — and on the same left
 * edge as the month labels below, so the page has one left margin in
 * both states.
 *
 * The glyph is start-aligned, not centred: the hit area is wider when the
 * grid is open (it spans the year column) than when it is shut, and a
 * centred glyph would slide as that width changes — the icon must not
 * appear to move when the only thing that changed is the target size. */
.arch-fold {
  position: absolute;
  left: 0.25rem;
  top: 0;
  width: 2.9rem;
  height: 1.9rem;
  z-index: 2;
  border: none;
  background: transparent;
  padding: 0;
  display: grid;
  place-items: center start;
  cursor: pointer;
  color: var(--muted);
}
.arch-fold:hover { color: var(--fg); }
/* The calendar glyph: a desk calendar whose pages flip. The block and
 * binding hold still; .arch-leaf is the one page that turns, hinged on
 * the binding at the top. Perspective makes the turn read as a page
 * lifting toward the reader rather than a flat squash. */
.arch-valve {
  width: 1.25rem;
  height: 1.25rem;
  overflow: visible;
  perspective: 40px;
}
/* The leaf hinges on the binding at the top and always RESTS flat on the
 * block — the glyph is one fixed picture of a calendar, never a picture
 * of a half-turned page. Motion is a loop, not a state: a page lifts
 * over the rings, carries past, and settles back where it started, so
 * the icon at the end of an animation is identical to the icon at the
 * start and can play any number of times without drifting. Whether the
 * grid is open is said by the grid, not by the icon's resting pose. */
/* The axis is the BINDING — the line through the rings at the top of the
 * block (viewBox y≈5), not the leaf's own edge. Deliberately NOT
 * transform-box: fill-box: that would measure the origin from the leaf's
 * own bounding box, putting the hinge partway down the page and flipping
 * it from the bottom. view-box keeps the origin in the glyph's own
 * coordinates, so the page swings on the rings the way it physically
 * would. The leaf starts below the hinge, so the turn carries it up and
 * over the binding. */
.arch-leaf {
  transform-box: view-box;
  transform-origin: 12px 5px;
}
/* The click: one complete page-turn. The page swings up off the block,
 * over the binding, and down the BACK of the calendar — a real turn, not
 * a lift-and-drop. It runs the whole 180° rather than reversing, because
 * reversing is exactly what reads as "flipped up and came back".
 *
 * What sells it is occlusion, not the arc: the leaf is drawn behind the
 * block and clipped to the strip above the binding, so it is only ever
 * visible while it is genuinely above the hinge. At 0° and at 180° it
 * lies flat below the hinge, fully clipped — invisible — which is why
 * the glyph is identical before and after and the turn can play any
 * number of times without leaving a trace. */
/* Opening turns a page BACK — into the past, the direction the grid is
 * for. Closing runs the very same turn FORWARD, bringing the page down
 * onto the block again, so the two gestures mirror each other and the
 * pair reads as one page going away and coming back. Reverse also
 * re-times the easing, which is what keeps the returning turn from
 * feeling like a rewind: it decelerates into the block the way the
 * opening turn decelerates away from it. */
.arch-leaf.flipping { animation: arch-page-turn 0.66s cubic-bezier(0.42, 0, 0.35, 1); }
.arch-leaf.flipping.forward { animation-direction: reverse; }
@keyframes arch-page-turn {
  0%   { transform: rotateX(0deg) scaleY(1); }
  /* Edge-on at the hinge — the handoff from the front copy to the behind
   * copy, and where the shortening begins. */
  50%  { transform: rotateX(-90deg) scaleY(0.85); }
  /* Past the hinge the page is BEHIND the block, so it must not stick
   * out the top of the glyph — there is no calendar body up there to
   * hide it (the block stops at the binding). Shortening it as it goes
   * over keeps the whole turn inside the icon and reads as the page
   * tucking away behind the binding rather than flying off the top.
   * Measured across the arc, not just at the keyframes: the worst case
   * is around -145°, so the ramp has to start at the hinge. */
  75%  { transform: rotateX(-135deg) scaleY(0.42); }
  100% { transform: rotateX(-180deg) scaleY(0.3); }
}
/* The peek: a dog-ear on the page's bottom-right corner. At rest the
 * corner is flat and the ear is invisible; on hover it unfolds — the
 * corner lifts and turns back on the fold's diagonal, the way a thumbed
 * page does. It says "this page turns" without moving the page, so it
 * never competes with the click's full turn. Scaling from the corner is
 * what makes it grow out of the page rather than appear on top of it. */
.arch-ear {
  /* view-box, like the leaf: the corner it grows from is a point in the
   * glyph (bottom-right of the page), not an offset into the ear's own
   * little bounding box. */
  transform-box: view-box;
  transform-origin: 20px 20.5px;
  opacity: 0;
  transform: scale(0.2);
  transition: opacity 0.18s ease, transform 0.28s cubic-bezier(0.3, 0.9, 0.3, 1.2);
}
/* The flap sits at a low opacity so the turned-back corner reads as the
 * page's underside rather than a solid block of accent. */
.arch-ear-flap { opacity: 0.45; }
@media (hover: hover) {
  .arch-fold:hover .arch-ear,
  .arch-fold:focus-visible .arch-ear { opacity: 1; transform: scale(1); }
}
/* Mid-turn the corner has nothing to cling to — hide it so the page
 * reads as one clean leaf while it flips. */
.arch-leaf.flipping ~ .arch-ear { opacity: 0; transform: scale(0.2); }
/* The roll: the grid rolls up under its own top edge, a wall chart on a
 * spring. grid-template-rows 1fr→0fr animates the height without ever
 * capping it, so a fifth year needs no measurement. */
.arch-roll {
  display: grid;
  grid-template-rows: 1fr;
  overflow: hidden;
  transition: grid-template-rows 0.6s cubic-bezier(0.36, 0, 0.2, 1);
}
.arch-cal[data-open="shut"] .arch-roll { grid-template-rows: 0fr; }
.arch-rollinner {
  min-height: 0;
  overflow: hidden;
  transition: transform 0.6s cubic-bezier(0.36, 0, 0.2, 1), opacity 0.4s ease 0.1s;
}
.arch-cal[data-open="shut"] .arch-rollinner {
  transform: translateY(-1.6rem);
  opacity: 0;
  transition-delay: 0s, 0s;
}
/* The year strip unrolls into the row the months just vacated, so
 * navigation survives the fold. */
/* Folded, the strip IS the page's top line, so the icon that leads it
 * sits on the same left edge as the month labels below (.months' 0.25rem
 * padding), and the years follow just past the icon. The wide gutter
 * belongs to the OPEN grid, where the year column needs it; there is no
 * column to clear here. The fold stays absolutely positioned (it is a
 * sibling of the strip, not a child), so the strip's padding is what
 * reserves room for it — keep the two in step. */
.arch-strip {
  height: 0;
  overflow: hidden;
  opacity: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0 1rem;
  align-items: center;
  padding-left: 2rem;
  transition: height 0.6s cubic-bezier(0.36, 0, 0.2, 1), opacity 0.3s ease;
}
/* Folded there is no year column to span, so the hit area shrinks to the
 * glyph. Left edge and glyph position are unchanged — only the target
 * narrows, which the reader never sees. */
.arch-cal[data-open="shut"] .arch-fold { width: 1.5rem; }
.arch-cal[data-open="shut"] .arch-strip {
  height: 1.9rem;
  opacity: 1;
  transition-delay: 0s, 0.25s;
}
.arch-ystrip {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  text-decoration: none;
  padding: 0.1rem 0;
}
.arch-ystrip:hover { color: var(--fg); }
.arch-ystrip[aria-current="true"] { color: var(--fg); font-weight: 600; }

/* The fold is the one control that cannot work without JS, so it stays
 * hidden until timeline.js claims it (the script sets data-fold="ready").
 * Everything else on this page — every year, the whole grid — is server
 * rendered and works with the script blocked. */
.arch-fold { display: none; }
.arch-cal[data-fold="ready"] .arch-fold { display: grid; }

/* Reduced motion: nothing is lost by dropping the page-turn and the
 * peek — both are decoration that begins and ends on the same picture,
 * and the grid itself says whether it is open. */
@media (prefers-reduced-motion: reduce) {
  .arch-roll, .arch-rollinner, .arch-strip { transition: none; }
  .arch-leaf, .arch-leaf.flipping { animation: none; transform: none; }
  /* The dog-ear still appears on hover — it is an affordance, not just
   * decoration — it simply does not unfold. */
  .arch-ear { transition: opacity 0.18s ease; transform: none; }
  .arch-fold:hover .arch-ear,
  .arch-fold:focus-visible .arch-ear { transform: none; }
}

/* Phones: twelve columns still have to fit, so month names become their
 * initials and the gutter narrows. Ticks shrink and centre; a burst
 * month is allowed to make its row taller — that IS the information —
 * but the cell wraps rather than running as one tall stripe. */
@media (max-width: 48rem) {
  .arch-head, .arch-row {
    grid-template-columns: 2.2rem repeat(12, minmax(0, 1fr));
    gap: 0 1px;
    padding: 0 0.15rem;
  }
  .mon-long { display: none; }
  .mon-short { display: inline; }
  .arch-mon { padding-left: 0; text-align: center; font-size: 0.58rem; }
  .arch-yr { font-size: 0.62rem; }
  .arch-cell { padding: 0.28rem 1px; gap: 3px; justify-content: center; }
  .arch-tick { width: 5px; height: 5px; }
  /* A year row is a tap target, so a thin year clears 44px even when its
   * ticks do not fill it. Cells stay top-aligned (align-items: start from
   * the base rule) so a burst month still reads as a block growing down
   * from the top of its row. */
  .arch-row { min-height: 44px; }
  /* Touch targets: 44px of height for the fold and the year strip, at
   * the same glyph size. The fold's left edge tracks .months' padding
   * here too, so the glyph, the years, and the month labels share one
   * margin on phones as well. */
  .arch-head { height: 44px; margin-bottom: 0.2rem; }
  .arch-fold { left: 0.25rem; width: 2.2rem; height: 44px; }
  .arch-strip { gap: 0 0.2rem; padding-left: 1.9rem; }
  .arch-cal[data-open="shut"] .arch-strip { height: 44px; }
  .arch-ystrip { min-height: 44px; display: flex; align-items: center; padding: 0 0.5rem; }
}

/* Article: centered column, no chrome, reads like a page. The back
 * chevron and the title are one header block, so the space between them
 * is small — this padding only has to clear the chevron, not restate the
 * page's top margin. */
article {
  max-width: 42rem;
  margin: 0 auto;
  padding-top: 0.8rem;
}

/* The title line: the back chevron sits BESIDE the title, not above it.
 * The title is what the page is, so it starts the page; the way out is
 * an exit hanging in the margin beside it, not a heading competing for
 * the first line. */
.article-titleline {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}
/* Quiet to the point of nearly disappearing, and only darkens when
 * reached for — see .article-back:hover below. The negative margin hangs
 * the glyph in the margin so the title itself stays on the column's left
 * edge, aligned with the prose beneath it. */
.article-back {
  flex: none;
  margin-left: -1.4rem;
  font-family: var(--sans);
  font-size: 0.95rem;
  line-height: 1;
  color: color-mix(in oklab, var(--muted) 55%, transparent);
  text-decoration: none;
  transition: color 0.12s;
  /* The hit area is larger than the glyph: padding extends the target in
   * every direction, and the negative margins keep that extra area from
   * pushing the title sideways. A one-character link is otherwise a
   * needle to click. */
  padding: 0.6rem 0.75rem;
  margin-top: -0.6rem;
  margin-bottom: -0.6rem;
  margin-right: -0.65rem;
}
.article-back:hover,
.article-back:focus-visible { color: var(--fg); }

/* Next/previous: newer on the left, older on the right, walking the
 * context the URL implies (blog, or /_ for the whole timeline). No rule
 * of its own — the folio below is the page's one horizontal line, and a
 * second one here only boxed the nav in between them. The margin does
 * the separating. */
.article-nav {
  max-width: 42rem;
  margin: 3rem auto 0;
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  font-family: var(--sans);
  font-size: 0.85rem;
}
/* Same treatment as the back chevron: quiet until reached for, with a
 * target bigger than the text. The negative margins let the padding grow
 * the hit area without moving the labels off the column's edges. */
.article-nav a {
  color: color-mix(in oklab, var(--muted) 55%, transparent);
  text-decoration: none;
  max-width: 45%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.12s;
  padding: 0.5rem 0.7rem;
  margin: -0.5rem -0.7rem;
}
.article-nav a:hover, .article-nav a:focus-visible { color: var(--fg); }
.article-nav .nav-older { margin-left: auto; text-align: right; }
.article-header h1 { font-size: 1.55rem; line-height: 1.3; margin: 0 0 0.4rem; min-width: 0; }
.article-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0 0 2rem;
}
.article-meta a { color: var(--muted); }
.article-body { font-size: 1rem; }
.article-body h1, .article-body h2, .article-body h3 { line-height: 1.35; }
.article-body img { max-width: 100%; height: auto; }
/* Prose links underline in the accent — the notebook's ink shows
 * through without recoloring the text itself. */
.article-body a {
  text-decoration-color: var(--accent);
}
.article-body blockquote {
  margin: 1.5rem 0;
  padding: 0 0 0 1rem;
  border-left: 2px solid color-mix(in oklab, var(--accent) 40%, transparent);
  color: var(--muted);
}
.article-body hr { border: 0; border-top: 1px solid var(--border); margin: 2.5rem 0; }

/* Tables: markdown's own, styled like the csv preview's data idiom so
 * the two read as one thing. A wide table scrolls INSIDE itself —
 * `display: block` is what makes the element a scroll container at all
 * (a table box ignores overflow), and `width: max-content` keeps the
 * columns at their natural width instead of collapsing to the box. */
.article-body table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  margin: 1.5rem 0;
  border-collapse: collapse;
  font-family: var(--sans);
  font-size: 0.85rem;
  line-height: 1.5;
}
.article-body th, .article-body td {
  padding: 0.4rem 0.7rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}
/* The head is the axis, so it carries the quiet mono voice the month
 * headers and waypoints use — a marker, not a headline. */
.article-body thead th {
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: var(--muted);
  white-space: nowrap;
}
.article-body tbody tr:last-child td { border-bottom: none; }

/* Code: always a dark surface, in both themes (single chroma style). */
.article-body pre, .chroma {
  background: var(--code-bg);
  color: #e6edf3;
  border-radius: 6px;
  padding: 1rem;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: 1.55;
}
.article-body code { font-family: var(--mono); font-size: 0.85em; }
.article-body :not(pre) > code {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1em 0.35em;
}

/* Journal entries: dated breakpoints inside one article. The label sits
 * close under its rule — the two are one unit marking where a section
 * starts, so the space belongs BETWEEN entries (margin-top), not between
 * a rule and the label it introduces. Labels are free text ("the
 * wedding", "5-ish years ago"), not just dates, so nothing here may
 * assume a length or a format. */
.journal-entry { margin-top: 2.75rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.journal-entry .entry-date {
  font-size: 0.8rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--accent);
  margin: 0 0 1rem;
}

/* Section permalinks: every heading (id from its text, rendered by
 * internal/markdown) and journal entry (id from its date/label) carries
 * a quiet # link, revealed on hover — always faintly there without one. */
.article-body :is(h1, h2, h3, h4, h5, h6),
.journal-entry { scroll-margin-top: 1.25rem; }
.article-body .heading-anchor {
  font-family: var(--mono);
  font-size: 0.85em;
  margin-left: 0.45em;
  color: var(--muted);
  text-decoration: none;
  opacity: 0;
  transition: opacity 0.12s;
}
.article-body :is(h1, h2, h3, h4, h5, h6):hover .heading-anchor,
.article-body .heading-anchor:focus-visible { opacity: 1; }
.article-body .heading-anchor:hover { color: var(--accent); }
@media (hover: none) {
  .article-body .heading-anchor { opacity: 0.6; }
}

.article-tags { margin-top: 3rem; }
.article-tags ul { list-style: none; display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0; padding: 0; }
.article-tags a { font-size: 0.8rem; color: var(--muted); text-decoration: none; }
.article-tags a:hover { color: var(--accent); }

/* Media embeds ([](media:slug), resolved at read time). */
.media-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  grid-auto-rows: 9rem;
  gap: 3px;
  border-radius: 8px;
  overflow: hidden;
  margin: 1.5rem 0;
}
.media-gallery img, .media-gallery video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  cursor: zoom-in;
}
.media-gallery video { cursor: default; }
/* The gallery lightbox (lightbox.js). */
dialog.lightbox {
  border: none;
  background: transparent;
  padding: 0;
  max-width: 92vw;
  max-height: 92vh;
}
dialog.lightbox::backdrop { background: color-mix(in srgb, #000 82%, transparent); }
dialog.lightbox img {
  max-width: 92vw;
  max-height: 92vh;
  display: block;
  border-radius: 6px;
}
/* The attachment box: quiet, downloadable rows. */
.attachment-box {
  display: grid;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  margin: 1.5rem 0;
  background: var(--bg);
}
.attachment-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 0.85rem;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  font-family: var(--mono);
  font-size: 0.72rem;
  text-decoration: none;
  color: var(--fg);
  min-width: 0;
}
.attachment-box .attachment-row:last-child { border-bottom: none; }
.attachment-row:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.attachment-row .ext { color: var(--accent); flex: none; }
.attachment-row .fname {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.attachment-row .fsize { margin-left: auto; color: var(--muted); flex: none; }
/* A single file link inline in prose. */
.attachment-link { text-decoration-color: var(--accent); }
.attachment-size {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  margin-left: 0.45rem;
}
/* The data idiom: a csv/tsv preview head plus its download row. */
.csv-embed {
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow-x: auto;
  background: var(--bg);
}
.csv-embed table {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  margin: 0.6rem 1rem 0.4rem;
}
.csv-embed th {
  text-align: left;
  font-weight: 400;
  color: var(--fg);
  padding: 0.1rem 1.1rem 0.25rem 0;
  border-bottom: 1px solid var(--border);
}
.csv-embed td { padding: 0.18rem 1.1rem 0.18rem 0; }
.csv-embed figcaption {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  font-family: var(--mono);
  font-size: 0.72rem;
}
.csv-embed figcaption a { color: var(--fg); text-decoration: none; }
.csv-embed figcaption a:hover { text-decoration: underline; }
.csv-embed figcaption .ext { color: var(--accent); }
.csv-embed figcaption .fsize { margin-left: auto; color: var(--muted); }
/* Sandboxed html embeds ([](embed:slug), resolved at read time). */
.html-embed {
  position: relative;
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.html-embed iframe {
  display: block;
  width: 100%;
  height: 24rem;
  border: none;
}
/* The fullscreen affordance (lightbox.js, progressive): the PARENT page
 * fullscreens its own figure — the iframe's sandbox is unchanged. The
 * .maximized class is the fixed-overlay fallback for browsers without
 * element fullscreen (iOS Safari). */
.html-embed .embed-expand {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.15rem 0.5rem;
  cursor: pointer;
}
.html-embed .embed-expand:hover,
.html-embed .embed-expand:focus-visible { color: var(--accent); }
.html-embed:fullscreen,
.html-embed.maximized {
  margin: 0;
  border: none;
  border-radius: 0;
  background: var(--bg);
}
.html-embed.maximized {
  position: fixed;
  inset: 0;
  z-index: 50;
}
.html-embed:fullscreen iframe,
.html-embed.maximized iframe { height: 100%; }
/* Strudel players (```strudel fences and [](strudel:slug), resolved at
 * read time). The bar is the whole widget: a title at the left, the
 * transport at the right. The audio engine is a 0x0 sandboxed iframe —
 * it makes sound, never a picture, so it is laid out away entirely. */
.strudel {
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.strudel-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 0.9rem;
  font-family: var(--mono);
  font-size: 0.72rem;
}
.strudel-title {
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The transport leads: play is the object's main affordance, so it is the
 * biggest mark in the widget and sits first, where the eye lands. */
.strudel-transport {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
}
/* Shared button reset. Borders belong to the individual buttons: this
 * selector (class + element) outranks the single-class .strudel-play below
 * regardless of source order, so a border set here would win and erase the
 * play ring. */
.strudel-transport button {
  display: grid;
  place-items: center;
  padding: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
/* The big circled play button. The ring is the button — a real target,
 * 2.6rem across, not an icon you have to aim at. position:relative is
 * load-bearing: the engine frame absolutely positions itself over this
 * button (see .strudel-engine below). */
.strudel-play {
  position: relative;
  width: 2.6rem;
  height: 2.6rem;
  border: 1.5px solid currentColor;
  border-radius: 999px;
  color: var(--fg);
  transition: color 120ms ease, transform 120ms ease;
}
.strudel-play:hover,
.strudel-play:focus-visible { color: var(--accent); }
.strudel-play:active { transform: scale(0.94); }
/* Playing: the button wears the accent, so a glance down a long post
 * finds what is sounding. */
.strudel-play[aria-pressed="true"] { color: var(--accent); }
/* Loop is secondary, so it sits a step smaller than play — the missing ring
 * already reads as "lesser", so the size gap stays small.
 *
 * The glyph is sized on the span: the shared reset gives these buttons
 * `font: inherit`, so a font-size here would be what the ⟳ inherits from
 * .strudel-head's 0.72rem mono rather than its own size. */
.strudel-loop {
  width: 2.1rem;
  height: 2.1rem;
  border: none;
  border-radius: 999px;
}
.strudel-loop > span {
  font-size: 1.15rem;
  line-height: 1;
}
.strudel-loop:hover,
.strudel-loop:focus-visible { color: var(--accent); }
.strudel-loop[aria-pressed="true"] { color: var(--accent); }
.strudel-loop[aria-pressed="false"] { opacity: 0.5; }
/* The play/pause glyph is drawn in CSS so the two states are one element
 * that morphs, rather than swapped text that reflows the row. It is
 * nudged right by an eighth of its width: a triangle's optical center
 * sits left of its bounding box, so a mathematically centered one reads
 * as leaning back against the ring. */
.strudel-glyph {
  width: 0.8rem;
  height: 0.92rem;
  margin-left: 0.1rem;
  background: currentColor;
  /* a right-pointing triangle */
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.strudel-play[aria-pressed="true"] .strudel-glyph {
  /* two bars: pause. Centered, so the nudge is dropped. */
  margin-left: 0;
  clip-path: polygon(0 0, 34% 0, 34% 100%, 0 100%, 0 0, 66% 0, 100% 0, 100% 100%, 66% 100%, 66% 0);
}
.strudel-play[data-busy="true"] .strudel-glyph {
  /* loading the engine: a quiet pulse, no spinner chrome */
  animation: strudel-pulse 1s ease-in-out infinite;
}
@keyframes strudel-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .strudel-play[data-busy="true"] .strudel-glyph { animation: none; opacity: 0.55; }
}
/* An engine failure speaks in the same quiet voice as a missing embed. */
.strudel-error {
  padding: 0 0.9rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
}
/* The audio engine, laid transparently over the play button. It has to
 * receive the pointer press itself: user activation does not cross into a
 * sandboxed (cross-origin) frame, and Safari will not start an
 * AudioContext without it — see internal/web/strudel.go. It draws nothing;
 * the button underneath is what the reader sees. */
.strudel-engine {
  position: absolute;
  /* An <iframe> is a replaced element with a 300x150 intrinsic size, so it
   * needs explicit dimensions — `inset` alone leaves it at its default and
   * it spills across the widget. Sized from the button and pulled out over
   * its 1.5px ring, so the whole visible circle activates the frame. */
  top: -2px;
  left: -2px;
  width: calc(100% + 4px);
  height: calc(100% + 4px);
  border: none;
  background: transparent;
  border-radius: 999px;
}
/* The accordion: a fence's source, folded away by default. The player is
 * the object in the prose; the code is there when the reader wants it.
 * <details> does the folding, so it works with JS off. */
.strudel-code {
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.strudel-code > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.9rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  cursor: pointer;
  list-style: none; /* the caret is drawn below, on the label */
}
.strudel-code > summary::-webkit-details-marker { display: none; }
.strudel-code > summary:hover { color: var(--fg); }
/* A caret that turns with the fold — the one place a state mark is right,
 * because the thing it describes (the code) is hidden when closed. */
.strudel-code-label::before {
  content: "›";
  display: inline-block;
  margin-right: 0.4rem;
  transition: transform 120ms ease;
}
.strudel-code[open] .strudel-code-label::before { transform: rotate(90deg); }
.strudel-copy {
  margin-left: auto;
  padding: 0.1rem 0.4rem;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}
.strudel-copy:hover,
.strudel-copy:focus-visible { color: var(--accent); }
.strudel-copy[data-copied="true"] { color: var(--accent); }
/* The folded code sits flush inside the widget: it is part of the object,
 * not a block that happens to follow it. */
.strudel-code > pre {
  margin: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
@media (prefers-reduced-motion: reduce) {
  .strudel-code-label::before { transition: none; }
}
/* Footnotes (goldmark's .footnotes, sectioned at read time by
 * markdown.SectionFootnotes): a quiet rule, smaller muted text, mono
 * numbers. Multi-block pages get .footnotes-sectioned — the <ol>
 * markers give way to dotted "2.1" labels carried in data-label. */
.article-body sup .footnote-ref {
  font-family: var(--mono);
  font-size: 0.95em;
  color: var(--accent);
  text-decoration: none;
}
.article-body sup .footnote-ref:hover { text-decoration: underline; }
.footnotes {
  margin-top: 2.5rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.footnotes hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 0 0 1rem;
}
.footnotes ol { margin: 0; padding-left: 1.5rem; }
.footnotes li { margin: 0.4rem 0; }
.footnotes li::marker { font-family: var(--mono); font-size: 0.85em; }
.footnotes-sectioned ol { list-style: none; padding-left: 0; }
/* The label sits on the note's first line: the first paragraph flows
 * inline after it (later paragraphs keep their block shape). */
.footnotes-sectioned li > p:first-of-type { display: inline; }
.footnotes-sectioned li[data-label]::before {
  content: attr(data-label);
  font-family: var(--mono);
  font-size: 0.85em;
  color: var(--accent);
  margin-right: 0.6em;
}
.footnotes .footnote-backref {
  font-family: var(--mono);
  color: var(--muted);
  text-decoration: none;
}
.footnotes .footnote-backref:hover { color: var(--accent); }

.missing-media,
.missing-post {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--muted);
  border: 1px dashed var(--border);
  border-radius: 6px;
  padding: 0.15rem 0.5rem;
}

.not-found { text-align: center; padding-top: 4rem; }

/* Small screens: tighter margins, same single column. */
@media (max-width: 48rem) {
  main { padding: 0 1rem; }
  article { padding-top: 1.1rem; }
  /* No margin to hang the chevron in on a narrow screen: let it sit in
   * the column with the title rather than off the edge. Both targets
   * grow to the 44px touch minimum here (see the frontend-guidelines
   * skill); the negative margins keep that from moving the text. */
  .article-back {
    margin-left: -0.6rem;
    padding: 0.8rem 0.85rem;
    margin-top: -0.8rem;
    margin-bottom: -0.8rem;
    margin-right: -0.75rem;
  }
  .article-nav a {
    padding: 0.7rem 0.8rem;
    margin: -0.7rem -0.8rem;
  }
}

/* Theme toggle: just the glyph, no box, fixed bottom-left. Only the
 * studio's preview shell (previewPage) uses this fixed id form — the
 * reading surfaces render `.theme-toggle` inside the folio instead.
 * A11y comes from the aria-label, the :focus-visible outline, and
 * glyph contrast — not a border.
 *
 * Scoped to the preview page's own header rather than left bare on the
 * id, because the editor shell (layout.Focus) loads app.css AND this
 * sheet, in that order, and renders the same id. A bare `#theme-toggle`
 * here ties app.css on specificity and wins on order — which silently
 * undid app.css's phone rule that docks the toggle into the editor's top
 * bar, dropping it back onto the bottom-left corner where it covered the
 * margin sheet's controls. The preview page is the one document with a
 * .site-header, so this claims exactly the surface it was written for
 * and leaves the editor's copy to app.css. */
body:has(> .site-header) #theme-toggle {
  position: fixed;
  left: 1rem;
  bottom: 1rem;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 1rem;
  cursor: pointer;
  padding: 0.5rem;
}
body:has(> .site-header) #theme-toggle:hover { color: var(--fg); }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* KaTeX sizing relative to the smaller body font. */
.katex { font-size: 1.05em; }

/* Chess boards: the ![](fen:) snapshot and the ```pgn game (both rendered
 * at save time by internal/markdown, hydrated by the chess island).
 *
 * The square colors are custom properties because the SVG reads them —
 * the board is drawn with presentation attributes only, since the public
 * host's CSP forbids inline styles and a <style> inside the SVG would be
 * dropped, leaving every shape black. Keep them here, not in the markup. */
:root {
  --chess-light: #e8e2d0;
  --chess-dark: #9aa88f;
  --chess-white: #fbfbf9;
  --chess-black: #23282a;
  --chess-highlight: oklch(0.78 0.11 120);
}
:root[data-theme="dark"] {
  --chess-light: #b9b3a4;
  --chess-dark: #6a7864;
  --chess-white: #f2f2ee;
  --chess-black: #1a1e20;
  --chess-highlight: oklch(0.62 0.11 120);
}
.chess-board,
.chess-game {
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  padding: 0.9rem;
  max-width: 26rem;
}
.chess-game:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chess-boardwrap { line-height: 0; }
.chess-svg {
  width: 100%;
  height: auto;
  border-radius: 4px;
  /* The glyphs are text nodes inside the SVG; the serif face has no
   * chess coverage, so name the stack that does. */
  font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", sans-serif;
}
/* The title names the game; a snapshot has none — the prose around it is
 * doing that work, and a caption would repeat the sentence above it. */
.chess-title {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  margin-bottom: 0.6rem;
}
.chess-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.7rem;
}
.chess-btn {
  appearance: none;
  background: none;
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--fg);
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1;
  padding: 0.3rem 0.55rem;
  cursor: pointer;
}
.chess-btn:hover,
.chess-btn:focus-visible { color: var(--accent); border-color: var(--accent); }
.chess-btn:disabled { opacity: 0.35; cursor: default; }
.chess-btn:disabled:hover { color: var(--fg); border-color: var(--border); }
/* The copy button and the analysis link sit apart from the transport:
 * they act on the position, not on the playback. */
.chess-reset { margin-left: auto; }
.chess-analyse,
.chess-analyse:visited {
  text-decoration: none;
  display: inline-block;
  color: var(--fg);
}
/* Squares are clickable, so say so with the cursor. The rest of the SVG
 * (the board's frame, the empty rims) keeps the default. */
.chess-svg [data-square] { cursor: pointer; }
/* Explored moves are spoken, not printed — the board already shows the
 * position, and a second visible move list would compete with the game's
 * own. Kept in the flow (not display:none) so screen readers announce it. */
.chess-status {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  margin: 0.5rem 0 0;
  min-height: 1em;
}
/* The move list is the game's index — no move numbers printed beside
 * every ply, since the list order already carries them. */
.chess-moves {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.3rem;
  list-style: none;
  margin: 0.8rem 0 0;
  padding: 0.7rem 0 0;
  border-top: 1px solid var(--border);
  counter-reset: ply;
}
.chess-move { margin: 0; }
.chess-move-btn {
  appearance: none;
  background: none;
  border: 0;
  border-radius: 3px;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.72rem;
  padding: 0.15rem 0.3rem;
  cursor: pointer;
}
.chess-move-btn:hover,
.chess-move-btn:focus-visible { color: var(--accent); }
.chess-move.current .chess-move-btn {
  color: var(--fg);
  background: var(--surface);
}
.chess-source { margin-top: 0.8rem; }
.chess-source summary {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  cursor: pointer;
}
.chess-source pre {
  font-size: 0.72rem;
  white-space: pre-wrap;
  margin: 0.5rem 0 0;
}
/* Before hydration (and forever without JavaScript) the figure holds the
 * notation the server rendered. That is the useful fallback for a chess
 * reader, so it is styled rather than hidden. */
.chess-fen,
.chess-pgn {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  white-space: pre-wrap;
  word-break: break-all;
}
