/* Self-hosted reader fonts, latin subset, ~91KB total. font-display: swap renders the
   system fallback immediately and swaps in the webfont when ready, so text is never
   invisible and the request never blocks layout. Files live in src/theme/assets/fonts/
   and ship to /fonts/; url() is relative to this stylesheet at /styles/. */
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/atkinson-hyperlegible-400.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/atkinson-hyperlegible-700.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/atkinson-hyperlegible-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Lexend";
  font-style: normal;
  font-weight: 100 900; /* variable weight axis: one file covers every weight */
  font-display: swap;
  src: url("../fonts/lexend-variable.woff2") format("woff2");
}
/* Bundled so ASCII diagrams render identically on every device. Android ships no SF Mono/
   Menlo, so it would fall to Roboto Mono and substitute box-drawing/block glyphs from a
   non-monospace fallback font, shearing the columns. JetBrains Mono covers every glyph our
   diagrams use (box drawing, arcs, █/░ blocks, arrows, ▼▶, ✓) at a uniform cell width.
   Its code ligatures are switched off below so `===`, `!=`, `->` stay literal. */
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/JetBrainsMono-Bold.woff2") format("woff2");
}

:root {
  /* System is the default reader font and the UI-chrome font: always installed, no load. */
  --font-system: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
  --font-atkinson: "Atkinson Hyperlegible", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-lexend: "Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --reader-font: var(--font-system);
  --reader-size: 17px;
  --measure: 72ch;
  --outline-width: 14rem;
  --radius: 6px;
}

:root[data-font="atkinson"] { --reader-font: var(--font-atkinson); }
:root[data-font="lexend"] { --reader-font: var(--font-lexend); }
:root[data-size="small"] { --reader-size: 15px; }
:root[data-size="large"] { --reader-size: 19px; }

:root[data-theme="light"][data-temp="warm"],
:root:not([data-theme]) {
  --paper: oklch(0.985 0.005 80);
  --paper-2: oklch(0.965 0.006 80);
  --paper-3: oklch(0.945 0.007 80);
  --ink: oklch(0.22 0.008 80);
  --ink-soft: oklch(0.50 0.008 80);
  --ink-mute: oklch(0.65 0.005 80);
  --rule: oklch(0.90 0.008 80);
  --rule-strong: oklch(0.80 0.010 80);
  --accent: oklch(0.55 0.15 45);
  --accent-soft: oklch(0.92 0.04 50);
  --mark: oklch(0.92 0.085 92);
  --exercise: oklch(0.95 0.028 52);
  --note: oklch(0.955 0.018 75);
  --tip: oklch(0.95 0.026 145);
  --warn: oklch(0.94 0.045 82);
  --danger: oklch(0.94 0.05 35);
  --shadow: 0 12px 32px rgb(44 34 16 / 16%);
}

:root[data-theme="dark"][data-temp="warm"] {
  --paper: oklch(0.18 0.008 80);
  --paper-2: oklch(0.215 0.009 80);
  --paper-3: oklch(0.255 0.010 80);
  --ink: oklch(0.94 0.005 80);
  --ink-soft: oklch(0.70 0.006 80);
  --ink-mute: oklch(0.55 0.005 80);
  --rule: oklch(0.32 0.012 80);
  --rule-strong: oklch(0.44 0.012 80);
  --accent: oklch(0.74 0.14 55);
  --accent-soft: oklch(0.30 0.06 50);
  --mark: oklch(0.31 0.07 88);
  --exercise: oklch(0.245 0.035 55);
  --note: oklch(0.235 0.018 75);
  --tip: oklch(0.24 0.025 145);
  --warn: oklch(0.26 0.04 82);
  --danger: oklch(0.25 0.045 35);
  --shadow: 0 14px 34px rgb(0 0 0 / 38%);
}

:root[data-theme="light"][data-temp="cool"] {
  --paper: oklch(0.97 0.006 250);
  --paper-2: oklch(0.94 0.009 250);
  --paper-3: oklch(0.90 0.014 250);
  --ink: oklch(0.25 0.035 255);
  --ink-soft: oklch(0.46 0.035 255);
  --ink-mute: oklch(0.62 0.025 255);
  --rule: oklch(0.86 0.018 250);
  --rule-strong: oklch(0.75 0.03 250);
  --accent: oklch(0.52 0.105 225);
  --accent-soft: oklch(0.91 0.04 225);
  --mark: oklch(0.92 0.08 90);
  --exercise: oklch(0.935 0.03 228);
  --note: oklch(0.93 0.025 225);
  --tip: oklch(0.93 0.035 180);
  --warn: oklch(0.94 0.04 80);
  --danger: oklch(0.93 0.04 20);
  --shadow: 0 12px 32px rgb(36 48 65 / 16%);
}

:root[data-theme="dark"][data-temp="cool"] {
  --paper: oklch(0.27 0.02 250);
  --paper-2: oklch(0.31 0.02 250);
  --paper-3: oklch(0.36 0.023 250);
  --ink: oklch(0.91 0.018 250);
  --ink-soft: oklch(0.78 0.02 250);
  --ink-mute: oklch(0.68 0.018 250);
  --rule: oklch(0.40 0.022 250);
  --rule-strong: oklch(0.50 0.026 250);
  --accent: oklch(0.80 0.09 220);
  --accent-soft: oklch(0.39 0.055 230);
  --mark: oklch(0.37 0.07 86);
  --exercise: oklch(0.345 0.04 230);
  --note: oklch(0.34 0.035 225);
  --tip: oklch(0.34 0.038 180);
  --warn: oklch(0.35 0.04 80);
  --danger: oklch(0.34 0.04 20);
  --shadow: 0 14px 34px rgb(0 0 0 / 38%);
}

/* Neutral: achromatic chrome (chroma 0) for readers who want zero tint. The accent stays
   a restrained slate-blue so links and focus remain obviously interactive, and the semantic
   callout tints keep their hues since they carry meaning by color, not decoration. */
:root[data-theme="light"][data-temp="neutral"] {
  --paper: oklch(0.985 0 0);
  --paper-2: oklch(0.965 0 0);
  --paper-3: oklch(0.945 0 0);
  --ink: oklch(0.22 0 0);
  --ink-soft: oklch(0.48 0 0);
  --ink-mute: oklch(0.62 0 0);
  --rule: oklch(0.90 0 0);
  --rule-strong: oklch(0.80 0 0);
  --accent: oklch(0.50 0.07 250);
  --accent-soft: oklch(0.93 0.025 250);
  --mark: oklch(0.92 0.085 92);
  --exercise: oklch(0.95 0.022 250);
  --note: oklch(0.955 0.018 250);
  --tip: oklch(0.95 0.026 145);
  --warn: oklch(0.94 0.045 82);
  --danger: oklch(0.94 0.05 35);
  --shadow: 0 12px 32px rgb(20 22 26 / 14%);
}

:root[data-theme="dark"][data-temp="neutral"] {
  --paper: oklch(0.18 0 0);
  --paper-2: oklch(0.215 0 0);
  --paper-3: oklch(0.255 0 0);
  --ink: oklch(0.94 0 0);
  --ink-soft: oklch(0.70 0 0);
  --ink-mute: oklch(0.55 0 0);
  --rule: oklch(0.32 0 0);
  --rule-strong: oklch(0.44 0 0);
  --accent: oklch(0.74 0.09 250);
  --accent-soft: oklch(0.32 0.05 250);
  --mark: oklch(0.31 0.07 88);
  --exercise: oklch(0.245 0.035 250);
  --note: oklch(0.235 0.02 250);
  --tip: oklch(0.24 0.025 145);
  --warn: oklch(0.26 0.04 82);
  --danger: oklch(0.25 0.045 35);
  --shadow: 0 14px 34px rgb(0 0 0 / 38%);
}

/* Readers who ask the OS for more contrast get it without a control: ink steps to
   near-black/near-white, the secondary inks climb well past AA, and the hairlines
   strengthen so structure stays visible. Ink chroma drops to 0 here — at these extremes
   the temperature tint is imperceptible, and one achromatic ramp per polarity beats
   maintaining six. The papers, callout tints, and temperature axis are untouched, so the
   reader's chosen room survives; only the text and lines push harder against it.
   (data-temp is always set by the boot script; the bare :not([data-theme]) selector
   covers the no-JS fallback.) */
@media (prefers-contrast: more) {
  :root[data-theme="light"][data-temp],
  :root:not([data-theme]) {
    --ink: oklch(0.12 0 0);
    --ink-soft: oklch(0.30 0 0);
    --ink-mute: oklch(0.40 0 0);
    --rule: oklch(0.72 0 0);
    --rule-strong: oklch(0.55 0 0);
  }
  :root[data-theme="dark"][data-temp] {
    --ink: oklch(0.98 0 0);
    --ink-soft: oklch(0.84 0 0);
    --ink-mute: oklch(0.76 0 0);
    --rule: oklch(0.46 0 0);
    --rule-strong: oklch(0.60 0 0);
  }
  /* The accent keeps each variant's hue (links must still read as that book's links)
     but moves further from its paper. */
  :root[data-theme="light"][data-temp="warm"],
  :root:not([data-theme]) { --accent: oklch(0.45 0.15 45); }
  :root[data-theme="light"][data-temp="cool"] { --accent: oklch(0.42 0.105 225); }
  :root[data-theme="light"][data-temp="neutral"] { --accent: oklch(0.40 0.07 250); }
  :root[data-theme="dark"][data-temp="warm"] { --accent: oklch(0.82 0.13 55); }
  :root[data-theme="dark"][data-temp="cool"] { --accent: oklch(0.86 0.08 220); }
  :root[data-theme="dark"][data-temp="neutral"] { --accent: oklch(0.82 0.08 250); }
}

/* Syntax highlight token colors (highlight.js classes), keyed on light/dark. */
:root[data-theme="light"], :root:not([data-theme]) {
  --hl-keyword: oklch(0.42 0.14 280);
  --hl-string: oklch(0.45 0.11 150);
  --hl-number: oklch(0.48 0.14 40);
  --scroll-shadow: rgb(0 0 0 / 14%);
}
:root[data-theme="dark"] {
  --hl-keyword: oklch(0.80 0.10 270);
  --hl-string: oklch(0.82 0.10 150);
  --hl-number: oklch(0.82 0.12 55);
  --scroll-shadow: rgb(0 0 0 / 42%);
}

*, *::before, *::after { box-sizing: border-box; }
html { font-size: var(--reader-size); -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: var(--sidebar-width, 18rem) minmax(0, 1fr);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--reader-font);
  line-height: 1.65;
  font-feature-settings: "kern", "liga", "ss01";
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}
body.sidebar-collapsed {
  --sidebar-width: 0rem;
}
a { color: inherit; }
/* One visible keyboard-focus ring for every interactive element. The accent clears 3:1
   against paper in all four variants, so the ring is always perceivable. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}
/* The backdrop is only used as a tap-to-close overlay on mobile. Keep it out of the
   body grid flow at all widths so it never consumes a grid cell (which would push the
   sidebar and main content onto the wrong columns). */
.sidebar-backdrop {
  position: fixed;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}
.skip-link {
  position: fixed;
  left: .75rem;
  top: .75rem;
  transform: translateY(-200%);
  padding: .45rem .7rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  z-index: 10;
}
.skip-link:focus { transform: translateY(0); }
/* Fixed control cluster pinned to the top-left, over the sidebar's top padding.
   Holds only the navigation toggle (hamburger): when the sidebar is collapsed this is
   the single piece of chrome left on screen, so reading is undistracted. Search and the
   reader-settings menu live inside the sidebar and collapse away with it. */
.sidebar-controls {
  position: fixed;
  top: 1rem;
  left: .85rem;
  z-index: 41;
  display: flex;
  align-items: center;
  gap: .4rem;
}
.sidebar-toggle {
  width: 2.4rem;
  height: 2.4rem;
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: .26rem;
  padding: 0 .58rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-mute);
  cursor: pointer;
}
.sidebar-toggle span {
  display: block;
  height: 2px;
  width: 100%;
  border-radius: 999px;
  background: currentColor;
}
.sidebar-toggle:hover,
.sidebar-toggle:focus-visible {
  color: var(--ink);
  background: var(--paper-2);
  border-color: var(--rule);
}
.sidebar {
  position: sticky;
  top: 0;
  height: 100dvh;
  overflow: auto;
  padding: 4.2rem 1.35rem 2rem;
  background: var(--paper-2);
  border-right: 1px solid var(--rule);
  font-family: var(--font-system);
  transition:
    transform 180ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
body.sidebar-collapsed .sidebar {
  transform: translateX(-100%);
  opacity: 0;
  pointer-events: none;
}
.brand { display: block; text-decoration: none; margin-bottom: 1.5rem; }
.brand__eyebrow {
  display: block;
  color: var(--ink-soft);
  font-size: .68rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  margin-bottom: .35rem;
}
.brand__title {
  display: block;
  color: var(--ink);
  font-family: var(--reader-font);
  font-size: 1.15rem;
  font-weight: 650;
  line-height: 1.2;
}
/* The sidebar's top toolbar: the search trigger takes the room, the reader-settings "A"
   button sits beside it. Both live inside the sidebar so they slide away when it collapses,
   leaving only the floating hamburger. */
.sidebar-bar {
  display: flex;
  align-items: stretch;
  gap: .5rem;
  margin-bottom: 1.35rem;
}
/* ---- Search ----
   Pagefind's UI is built for a wide container, so it lives in a command-palette dialog,
   not the narrow sidebar. The sidebar holds only a quiet trigger; "/" and Cmd/Ctrl-K open
   the same dialog. */
.search-trigger {
  display: flex;
  align-items: center;
  gap: .55rem;
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: .6rem .7rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-soft);
  font: 500 .9rem/1 var(--font-system);
  text-align: left;
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.search-trigger:hover,
.search-trigger:focus-visible {
  border-color: var(--rule-strong);
  color: var(--ink);
}
.search-trigger svg { flex: none; opacity: .7; }
.search-trigger__label { flex: 1 1 auto; }
.search-trigger kbd {
  flex: none;
  min-width: 1.2rem;
  padding: .12rem .32rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper-2);
  color: var(--ink-mute);
  font: 600 .72rem/1.1 var(--font-system);
  text-align: center;
}

.search-dialog {
  width: min(640px, 92vw);
  max-width: 92vw;
  max-height: 70vh;
  margin: 12vh auto auto;
  padding: 0;
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.search-dialog[open] { display: flex; flex-direction: column; }
.search-dialog::backdrop {
  background: rgb(0 0 0 / 45%);
  backdrop-filter: blur(2px);
}
.search-dialog #search,
.search-dialog .pagefind-ui {
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
}
.search-dialog .pagefind-ui {
  flex: 1 1 auto;
  --pagefind-ui-scale: .9;
  --pagefind-ui-primary: var(--accent);
  --pagefind-ui-text: var(--ink);
  --pagefind-ui-background: var(--paper);
  --pagefind-ui-border: var(--rule);
  --pagefind-ui-tag: var(--paper-3);
  --pagefind-ui-border-width: 1px;
  --pagefind-ui-border-radius: 6px;
  --pagefind-ui-font: var(--font-system);
}
/* Pagefind nests the results drawer INSIDE this form, as a sibling of the input rather
   than of .pagefind-ui — so the form, not .pagefind-ui, has to be the flex column that
   gives the drawer a bounded height to scroll within. Without min-height: 0 here, the
   drawer's content height wins and the form (and everything below it) just grows past
   the dialog instead of clipping and scrolling. */
.search-dialog .pagefind-ui__form {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.search-dialog .pagefind-ui__form::before { left: 1.1rem; opacity: .55; }
.search-dialog .pagefind-ui .pagefind-ui__search-input {
  flex: none;
  height: 3.4rem;
  padding-left: 3rem;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  background: var(--paper);
  font-size: 1.05rem;
}
.search-dialog .pagefind-ui .pagefind-ui__search-input:focus { outline: 0; }
.search-dialog .pagefind-ui .pagefind-ui__search-clear {
  position: absolute;
  right: .75rem;
  top: 50%;
  transform: translateY(-50%);
  padding: .32rem .6rem;
  border: 1px solid var(--rule) !important;
  border-radius: var(--radius);
  background: var(--paper-2) !important;
  color: var(--ink-soft) !important;
  font: 600 .75rem/1 var(--font-system);
  cursor: pointer;
}
.search-dialog .pagefind-ui .pagefind-ui__search-clear:hover {
  color: var(--ink) !important;
  border-color: var(--rule-strong) !important;
}
.search-dialog .pagefind-ui__drawer {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  /* Without this, a wheel/touch scroll that bottoms out here chains to the page behind
     the dialog instead of stopping. */
  overscroll-behavior: contain;
  padding: .25rem .65rem .65rem;
  background: var(--paper);
}
/* Belt-and-suspenders for the same scroll-chaining issue: showModal() does not lock
   background scroll on its own, so reader-settings.js toggles this class while the
   dialog is open. */
body.search-open { overflow: hidden; }
.search-dialog .pagefind-ui__message {
  padding: 1rem .4rem .5rem;
  color: var(--ink-soft);
  font-size: .85rem;
}
.search-dialog .pagefind-ui__result {
  padding: .85rem .4rem;
  border-top: 1px solid var(--rule);
}
.search-dialog .pagefind-ui__result:first-of-type { border-top: 0; }
.search-dialog .pagefind-ui__result-title {
  font-size: 1rem;
  font-weight: 700;
  margin: 0 0 .15rem;
}
/* Result titles are links, so they wear the accent like every other link in the reader.
   Pagefind injects its Svelte styles into <head> at runtime (after this sheet), so an
   equal-specificity rule loses the tie; `!important` is the clean way to win it back. */
.search-dialog .pagefind-ui .pagefind-ui__result-link {
  color: var(--accent) !important;
  text-decoration: none;
}
.search-dialog .pagefind-ui .pagefind-ui__result-link:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.search-dialog .pagefind-ui .pagefind-ui__result-excerpt {
  font-size: .9rem;
  line-height: 1.5;
  color: var(--ink-soft);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Replace Pagefind's loud yellow highlight with a calm accent wash. */
.search-dialog .pagefind-ui mark,
.search-dialog mark {
  background: var(--accent-soft) !important;
  color: var(--ink) !important;
  padding: 0 .12em;
  border-radius: 2px;
}
.toc ol { list-style: none; padding-left: 0; margin: 0; }
.toc ol ol { padding-left: 1rem; margin: .35rem 0 .55rem; }
.toc li { margin: .25rem 0; }
.toc a, .toc span {
  display: inline-block;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: .9rem;
  line-height: 1.35;
  border-radius: 3px;
  padding: .22rem .35rem;
  margin-left: -.35rem;
}
.toc span { color: var(--ink-soft); font-weight: 650; margin-top: .45rem; }
/* Author-placed `---` separators that group the table of contents. */
.toc li.toc-divider { height: 0; margin: 1.4rem 0; border-top: 1px solid var(--rule); list-style: none; }
/* Author-placed `## ` part titles labelling a group of entries. */
.toc li.toc-part { margin: 1.4rem 0 .5rem; list-style: none; }
.toc li.toc-part:first-child { margin-top: 0; }
.toc .toc-part__label {
  display: block;
  color: var(--ink-mute);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  margin: 0;
  padding: 0 .35rem;
}
.toc a:hover, .toc a[aria-current="page"] { color: var(--ink); background: var(--paper-3); }
.toc a[aria-current="page"] { font-weight: 700; }
/* Collapsible navigation groups. */
.toc-group__head { display: flex; align-items: center; gap: .25rem; }
.toc-group__head > a, .toc-group__head > .toc-label { flex: 1 1 auto; min-width: 0; }
.toc-group__head > .toc-label { cursor: pointer; }
.toc-fold {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.55rem;
  height: 1.55rem;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-mute);
  cursor: pointer;
}
.toc-fold:hover { color: var(--ink); background: var(--paper-3); }
.toc-fold svg { transition: transform 150ms ease; }
.toc-group[data-open="true"] > .toc-group__head .toc-fold svg { transform: rotate(90deg); }
.toc-group[data-open="false"] > ol { display: none; }
/* Reader settings now live inside the sidebar as a flat inline disclosure rather than a
   floating popover: an absolute popover would be clipped by the sidebar's overflow, and a
   panel that sits in the page plane belongs to the Flat-Paper rule, not the Float shadow. */
.reader-tools {
  font-family: var(--font-system);
}
.reader-tools__button {
  flex: none;
  align-self: stretch;
  width: 2.4rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-soft);
  font: 700 1rem/1 var(--reader-font);
  cursor: pointer;
  transition: border-color 120ms ease, color 120ms ease;
}
.reader-tools__button:hover,
.reader-tools__button:focus-visible,
.reader-tools[data-open="true"] .reader-tools__button {
  color: var(--ink);
  border-color: var(--rule-strong);
}
.reader-tools__panel {
  display: none;
  margin-bottom: 1.35rem;
  overflow: hidden;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.reader-tools[data-open="true"] .reader-tools__panel { display: block; }
.reader-tools__row {
  display: grid;
  /* Every row is a fixed three-column grid so the dividers line up across all rows,
     regardless of how many options a row has. */
  grid-template-columns: repeat(3, 1fr);
  border-bottom: 1px solid var(--rule);
}
.rt-caption {
  font: 650 .6rem/1.2 var(--font-system);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-mute);
  padding: .55rem .75rem .2rem;
}
.reader-tools__row:last-child { border-bottom: 0; }
.reader-tools__row button {
  min-height: 2.65rem;
  padding: .55rem .75rem;
  border: 0;
  border-right: 1px solid var(--rule);
  background: transparent;
  color: var(--ink-soft);
  font: 600 .9rem/1 var(--font-system);
  cursor: pointer;
}
.reader-tools__row button:last-child { border-right: 0; }
.reader-tools__row button:hover,
:root[data-size="small"] [data-cb-size="small"],
:root[data-size="base"] [data-cb-size="base"],
:root[data-size="large"] [data-cb-size="large"],
:root[data-font="system"] [data-cb-font="system"],
:root[data-font="atkinson"] [data-cb-font="atkinson"],
:root[data-font="lexend"] [data-cb-font="lexend"],
:root[data-theme-source="system"] [data-cb-theme="system"],
:root[data-theme-source="manual"][data-theme="light"] [data-cb-theme="light"],
:root[data-theme-source="manual"][data-theme="dark"] [data-cb-theme="dark"],
:root[data-temp="warm"] [data-cb-temp="warm"],
:root[data-temp="neutral"] [data-cb-temp="neutral"],
:root[data-temp="cool"] [data-cb-temp="cool"] {
  color: var(--accent);
  background: var(--paper-3);
}
.reader-tools__row [data-cb-size="small"] { font-size: .78rem; }
.reader-tools__row [data-cb-size="base"] { font-size: 1rem; }
.reader-tools__row [data-cb-size="large"] { font-size: 1.22rem; }
.main {
  width: min(100%, calc(var(--measure) + 8rem));
  padding: 4.5rem clamp(1.25rem, 6vw, 5rem) 6rem;
  /* Flex column so the pager can be pushed to the bottom on short pages. */
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}
/* When a page opts into the "On this page" rail, the reading column grows to seat the
   article and the outline side by side; the pager below it keeps the article's measure. */
.main:has(.outline) {
  width: min(100%, calc(var(--measure) + var(--outline-width) + 11.5rem));
}
.reader-body:has(.outline) {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) var(--outline-width);
  column-gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: start;
}
.home-hero { max-width: 48rem; padding: 6vh 0 2rem; }
.home-eyebrow {
  font-family: var(--font-system);
  color: var(--accent);
  font-size: .75rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  margin: 0 0 .8rem;
}
.home-hero h1 {
  font-size: clamp(2.4rem, 7vw, 5.6rem);
  line-height: .98;
  margin: 0;
  letter-spacing: 0;
}
.home-tagline {
  max-width: 38rem;
  color: var(--ink-soft);
  font-size: 1.25rem;
  line-height: 1.45;
  margin: 1.25rem 0 0;
}
.home-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; }
.button {
  display: inline-flex;
  align-items: center;
  min-height: 2.5rem;
  padding: .55rem .85rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--font-system);
  text-decoration: none;
}
.button--brand { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
  margin-top: 3rem;
}
.feature {
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
}
.feature h2 { font-size: 1rem; margin: 0 0 .35rem; }
.feature p { margin: 0; color: var(--ink-soft); }
/* --surface names the background an element actually sits on. Transparent scroll
   containers (.table-scroll, .ascii-diagram) paint their edge-fade covers with it so the
   fade blends into whatever surface they're nested in — the page, or a tinted callout that
   overrides --surface below. Default: the page background. */
.reader-article {
  max-width: var(--measure);
  --surface: var(--paper);
  /* Minimum breathing room above the pager; the pager's own margin-top: auto takes over
     and grows the gap on short pages. */
  margin-bottom: 4rem;
}
.reader-article h1 {
  font-size: clamp(2rem, 3vw, 3rem);
  line-height: 1.08;
  margin: 0 0 .75em;
}
.reader-article h2 { margin-top: 2.2em; font-size: 1.55rem; line-height: 1.2; }
.reader-article h3 { margin-top: 1.7em; font-size: 1.2rem; }
/* Anchored jumps (outline, in-page links) land the heading clear of the top edge instead
   of flush against it. */
.reader-article :is(h1, h2, h3, h4, h5, h6) { scroll-margin-top: 4rem; }
.reader-article p, .reader-article ul, .reader-article ol, .reader-article blockquote { margin: 0 0 1em; }
/* Lists. The 1em block margin above separates a whole list from the surrounding prose. Inside
   a list that margin is wrong: a nested sub-list's bottom margin would push the next sibling
   item down a full line (the gap after a deeply-nested item). So list items keep an even,
   modest rhythm, and a nested list hugs its parent — a small top margin to clear the parent
   item's text, no bottom margin. Uniform at any depth, ordered or unordered, mixed. */
.reader-article li { margin: .25em 0; }
.reader-article li > ul,
.reader-article li > ol { margin: .25em 0 0; }
/* Loose lists (a blank line between items wraps each item's text in <p>) must not inherit the
   full 1em paragraph gap between every item. */
.reader-article li > p { margin: .25em 0; }
/* text-wrap: pretty avoids body orphans/rivers; balance evens out short headings so a lone
   word never dangles. Both are progressive — no effect where unsupported. */
.reader-article p { text-wrap: pretty; }
.reader-article :is(h1, h2, h3, h4) { text-wrap: balance; }
/* Blockquotes set apart with a quiet rule and softened ink, not a heavy bar. */
.reader-article blockquote {
  padding-left: 1.2em;
  border-left: 2px solid var(--rule-strong);
  color: var(--ink-soft);
  font-style: italic;
  hanging-punctuation: first last;
}
.reader-article a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
/* A faint accent wash on hover. color-mix keeps it theme-aware across all four palettes —
   a hardcoded rgba would only read right on one background. */
.reader-article a:hover {
  background-color: color-mix(in oklch, var(--accent) 12%, transparent);
  text-decoration-thickness: 2px;
}
.reader-article code {
  font-family: var(--font-mono);
  font-size: .88em;
  padding: 1px 6px;
  background: var(--paper-3);
  border-radius: 3px;
}
/* Author highlight: a marker-pen wash on a phrase or sentence the author wants to stress.
   It uses its own --mark token (a calm honey in the warn hue family, a touch louder than the
   block tints as accent-soft is, since inline spans need more presence than washes), kept
   distinct from the accent wash so a highlight never reads as a link or a search hit. The L of
   --mark tracks the audited --warn band per variant, so --ink holds >=4.5:1 in all six themes.
   box-decoration-break keeps the wash whole when a highlight wraps across lines. */
.reader-article mark {
  background: var(--mark);
  color: var(--ink);
  padding: 0 .18em;
  border-radius: 3px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.reader-article img { max-width: 100%; height: auto; }
/* Pure-CSS horizontal scroll shadows (Lea Verou): the cover gradients ride with the
   content (background-attachment: local) while the radial edge shadows stay pinned to
   the box. A cover sits over its shadow until the content scrolls past that edge, so a
   shadow only appears on a side that still has hidden content. The stack is declared on
   the scroll containers themselves (not :root) because var() inside a custom property
   resolves where the property is declared: each container's --scroll-cover (the surface
   the covers fade into) and --scroll-edge (the shadow color) must be the values used. */
.reader-article .table-scroll, .code, .ascii-diagram {
  --scroll-shadow-bg:
    linear-gradient(to right, var(--scroll-cover) 30%, transparent) left center / 28px 100% no-repeat local,
    linear-gradient(to left, var(--scroll-cover) 30%, transparent) right center / 28px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, var(--scroll-edge), transparent) left center / 12px 100% no-repeat,
    radial-gradient(farthest-side at 100% 50%, var(--scroll-edge), transparent) right center / 12px 100% no-repeat;
}
/* Tables — booktabs translation: three rules, no verticals, no cell borders,
   restrained header, generous interior space, top-aligned cells. */
.reader-article .table-scroll {
  margin: 1.6em 0;
  overflow-x: auto;
  max-width: 100%;
  /* Covers fade into the surrounding surface; the edge shadow stays a quiet rule tint. */
  --scroll-cover: var(--surface);
  --scroll-edge: var(--rule);
  background: var(--scroll-shadow-bg);
}
.reader-article table {
  border-collapse: collapse;
  font-feature-settings: "kern", "liga", "onum", "tnum";
  font-size: 0.95rem;
  line-height: 1.45;
  /* The three booktabs rules */
  border-top: 1.5px solid var(--ink);
  border-bottom: 1.5px solid var(--ink);
  margin: 0;
}
.reader-article thead tr { border-bottom: 1px solid var(--ink-soft); }
.reader-article th, .reader-article td {
  padding: 0.75em 1.4em;
  vertical-align: top;
  text-align: left;
  border: none;
}
.reader-article th:first-child, .reader-article td:first-child { padding-left: 0.2em; }
.reader-article th:last-child,  .reader-article td:last-child  { padding-right: 0.2em; }

/* Header: label, not shout — restrained weight, small-caps, sized down */
.reader-article thead th {
  font-weight: 500;
  font-size: 0.78em;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding-top: 0.6em;
  padding-bottom: 0.7em;
}

/* First column = row anchor: regular weight, doesn't wrap unless forced */
.reader-article tbody th, .reader-article tbody td:first-child {
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
}
/* If the first column gets long, the author can allow wrap with class="wrap" */
.reader-article tbody th.wrap, .reader-article tbody td.wrap { white-space: normal; }

.reader-article tbody td { color: var(--ink-soft); }

/* Numeric columns: tabular figures (opt-in via class="num") */
.reader-article td.num, .reader-article th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* Switch off JetBrains Mono's programming ligatures so source stays literal: `===` renders
   as three equals signs (not one merged glyph), and likewise !=, ->, =>, <=, |>, ::, etc.
   calt (contextual alternates) carries most of them; liga/dlig cover the rest. Applies to
   every mono surface: inline code, code blocks, and ASCII diagrams. */
.reader-article code, .code, .ascii-diagram, .reader-article pre, .reader-article pre code {
  font-variant-ligatures: none;
  font-feature-settings: "liga" 0, "calt" 0, "dlig" 0;
}
.code, .ascii-diagram {
  overflow-x: auto;
  max-width: 100%;
  padding: .9rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: .88rem;
  line-height: 1.45;
  /* The shared scroll-shadow stack, fading into the code tint. */
  --scroll-cover: var(--paper-2);
  --scroll-edge: var(--scroll-shadow);
  background: var(--scroll-shadow-bg);
  background-color: var(--paper-2);
}
.ascii-diagram {
  border: none;
  /* Same scroll-shadow technique, but transparent (no code tint/border), so the covers fade
     into whatever surface the diagram sits on — page or callout — via --surface. */
  --scroll-cover: var(--surface);
  background-color: transparent;
}
/* Code inside a block must not inherit the inline-code chip background/padding. */
.reader-article pre code {
  background: none;
  padding: 0;
  font-size: inherit;
  border-radius: 0;
}
/* Copy-code button (injected by copy-code.js on hover-capable devices only). The wrapper is
   the positioning context — the code block itself scrolls, so the button rides this instead.
   The button is chrome: system sans, a quiet hairline control that fades in on hover. */
.code-wrap { position: relative; }
.code-copy {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.25rem 0.55rem;
  font-family: var(--font-system);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease,
    background-color 0.12s ease;
}
/* Revealed by hovering anywhere on the block, or when the button takes keyboard focus. */
.code-wrap:hover .code-copy,
.code-copy:focus-visible { opacity: 1; }
.code-copy:hover { color: var(--ink); background: var(--paper-2); border-color: var(--rule-strong); }
/* Copied: confirm with full ink, not the accent — the accent stays rationed to links/nav/focus. */
.code-copy[data-copied="true"] { color: var(--ink); border-color: var(--rule-strong); }
.hljs-comment, .hljs-quote { color: var(--ink-mute); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-name, .hljs-tag { color: var(--hl-keyword); }
.hljs-string, .hljs-title, .hljs-section, .hljs-attribute, .hljs-literal, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-addition { color: var(--hl-string); }
.hljs-number, .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title.class_ { color: var(--hl-number); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 600; }
.cb-block {
  display: block;
  margin: 1.25rem 0;
  padding: 1rem 1rem .1rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--paper-2);
}
.cb-block__label, .cb-block summary {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-family: var(--font-system);
  font-size: .82rem;
  font-weight: 750;
  color: var(--ink);
  margin-bottom: .6rem;
}
.cb-block summary { cursor: pointer; list-style: none; }
.cb-block summary::-webkit-details-marker { display: none; }
/* `display: flex` on summary drops the native disclosure triangle, so draw our own
   caret at the trailing edge: points right when closed, rotates down when open. */
.cb-block summary::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: .45rem;
  height: .45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 150ms ease;
  opacity: .55;
}
.cb-block[open] summary::after { transform: rotate(45deg); }
.cb-block__icon { flex: none; width: 1.05em; height: 1.05em; opacity: .85; }
/* Disclosure callouts (solution, aside, expandable) collapse to just their summary band.
   The shared block padding (1rem top / .1rem bottom) assumes a body sits below the label, so
   a closed disclosure rendered top-heavy. Carry the vertical rhythm on the summary itself so
   the label band stays centered whether the block is open or closed, and part the header from
   the body with a hairline rule once it opens. */
details.cb-block { padding-top: 0; padding-bottom: 0; }
details.cb-block > summary { margin-bottom: 0; padding-block: .82rem; }
details.cb-block[open] > summary { padding-bottom: .7rem; border-bottom: 1px solid var(--rule); }
details.cb-block[open] > .cb-block__body { padding-top: .7rem; padding-bottom: .85rem; }
/* Each callout sets --surface (not just background) so nested scroll containers fade into
   the callout's tint instead of the page's --paper. background reads from --surface so the
   two can never drift. */
.cb-block--note { --surface: var(--note); background: var(--surface); }
.cb-block--tip { --surface: var(--tip); background: var(--surface); }
.cb-block--important, .cb-block--warning { --surface: var(--warn); background: var(--surface); }
.cb-block--caution { --surface: var(--danger); background: var(--surface); }
.cb-block--exercise { --surface: var(--exercise); background: var(--surface); }
/* The three disclosure callouts carry no semantic tint. Previously they sat on the page's
   own --paper and dissolved into the reading column; lift them to --paper-2 (the first
   tonal layer) so they read as set apart. They share the fill on purpose: like
   important/warning sharing the amber wash, the icon and label (a speech bubble + "Aside",
   a check + "Solution", a plus + "More") carry the distinction, not the wash. --paper-3 is
   reserved for the inline-code chip, so a callout on --paper-3 would erase code backgrounds
   inside it. The generic `callout` joins them: it's an untinted box whose only distinction
   is the optional author-supplied label (and no icon at all). */
.cb-block--callout, .cb-block--solution, .cb-block--aside, .cb-block--expandable { --surface: var(--paper-2); background: var(--surface); }
/* ---- "On this page" outline ----
   A sticky reader-position guide in the right margin, opt-in per page via front matter.
   It follows the sidebar TOC's vocabulary (sans, Ink-Soft links, Paper-3 hover) so the two
   navigation surfaces read as one system; the active section wears the rationed accent
   because it is, like the current sidebar item, a live navigation marker. */
.outline {
  grid-column: 2;
  position: sticky;
  top: 4.5rem;
  align-self: start;
  max-height: calc(100dvh - 6rem);
  overflow: auto;
  overscroll-behavior: contain;
  font-family: var(--font-system);
}
.outline__label {
  margin: 0 0 .7rem;
  color: var(--ink-mute);
  font-size: .7rem;
  font-weight: 650;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.outline__list { list-style: none; margin: 0; padding: 0; }
.outline__item { margin: 0; }
.outline a {
  display: block;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: .82rem;
  line-height: 1.35;
  padding: .26rem .45rem;
  border-radius: 3px;
  transition: color 120ms ease, background-color 120ms ease;
}
.outline a:hover { color: var(--ink); background: var(--paper-3); }
.outline a.is-active { color: var(--accent); font-weight: 650; }
/* h3 entries nest a clear step under their h2 parent so the rail mirrors the page shape.
   This must follow `.outline a` so its `padding` shorthand doesn't reset the indent (equal
   specificity, so source order decides). */
.outline__item--h3 a { padding-left: 1.7rem; }
/* Below this width the sidebar + measure + rail no longer coexist without cramping the
   prose, so the rail steps aside and the article keeps its full reading measure. */
@media (max-width: 1200px) {
  .main:has(.outline) { width: min(100%, calc(var(--measure) + 8rem)); }
  .reader-body:has(.outline) { display: block; }
  .outline { display: none; }
}
.pager {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  width: 100%;
  max-width: var(--measure);
  /* auto top margin pushes the pager to the bottom of the (min 100dvh) main column
     when the article is short; collapses to the natural gap when content is tall. */
  margin-top: auto;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-system);
}
.pager a { text-decoration: none; color: var(--ink); }
.pager span { display: block; color: var(--ink-soft); font-size: .75rem; text-transform: uppercase; letter-spacing: .1em; }
.pager__next { text-align: right; }
/* On small screens the sidebar is an off-canvas drawer controlled by `body.nav-open`
   (transient, never persisted) — independent of the desktop `sidebar-collapsed` state. */
@media (max-width: 860px) {
  body {
    display: block;
    --sidebar-width: 0rem;
  }
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    width: min(19rem, 86vw);
    max-width: 100vw;
    height: 100dvh;
    z-index: 35;
    border-right: 1px solid var(--rule);
    box-shadow: var(--shadow);
    transform: translateX(-100%);
    opacity: 0;
    pointer-events: none;
  }
  body.nav-open .sidebar {
    transform: translateX(0);
    opacity: 1;
    pointer-events: auto;
  }
  .sidebar-backdrop {
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgb(0 0 0 / 28%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  body.nav-open .sidebar-backdrop {
    opacity: 1;
    pointer-events: auto;
  }
  /* On mobile the toggle is not pinned: switching from fixed to absolute lets it sit at the
     top of the page and scroll away with the content instead of hovering over the prose.
     While the drawer is open it re-pins to fixed so it stays anchored over the nav as the
     close affordance instead of scrolling off with the page. */
  .sidebar-controls { position: absolute; }
  body.nav-open .sidebar-controls { position: fixed; }
  .main { padding-top: 4rem; }
  /* The search palette sits higher and gets more height on small screens, where the
     on-screen keyboard eats the bottom of the viewport. */
  .search-dialog { margin-top: 7vh; max-height: 80vh; }
}

/* Readers who ask for reduced motion get the same layout with no movement: the sidebar
   slide, the TOC caret rotate, the backdrop fade, and the disclosure caret all collapse
   to an instant change. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* On screen the disclosure callouts (aside, solution, expandable) stay collapsed until the
   reader opens them. On paper there is nothing to click, so their bodies must always print:
   Chrome and Safari hide a closed <details> body when printing, which would silently drop
   all aside content from a printed textbook. Force every body open for print and drop the
   now-meaningless caret. (Firefox already auto-expands <details> in print; this makes the
   result consistent across browsers.) */
@media print {
  /* Two mechanisms hide a closed <details> body, and a printed textbook must defeat both.
     Older Chrome and Safari hide it via display on the child, so force the body to block.
     Chrome 124+ instead applies content-visibility:hidden to the ::details-content pseudo,
     which display cannot override, so make that visible too. Browsers that don't know the
     pseudo simply drop that one rule (Firefox already auto-expands <details> in print). */
  details.cb-block > .cb-block__body {
    display: block !important;
  }
  details.cb-block::details-content {
    content-visibility: visible !important;
  }
  .cb-block summary::after {
    display: none;
  }
}
