/* Wikipedia-ish look for the treatise browser. */

* { box-sizing: border-box; }

:root {
  /* Height of the floating bar. Every in-page jump offsets by this, so it is
     declared once and re-declared in the narrow-screen block where the bar
     wraps to two rows. base.html also writes the measured value here. */
  --header-h: 44px;
  /* Anything scrolled to stops this far below the bar: fragment links from
     the chapter contents box, cross-references, search results, and
     find-in-page. Set on the scroll container, which is what makes it apply
     to all of them rather than only to clicked links. */
  scroll-padding-top: calc(var(--header-h) + 0.75rem);
}

body {
  margin: 0;
  font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  font-size: 15px;
  line-height: 1.6;
  color: #202122;
  background: #f8f9fa;
}

/* Belt and braces: some engines ignore root scroll-padding for certain
   scroll paths. :where() keeps specificity at 0 so nothing has to fight it. */
:where(h1, h2, h3, h4, h5, h6)[id],
:where(.display-math, .env)[id] {
  scroll-margin-top: calc(var(--header-h) + 0.75rem);
}

/* Floating bar. sticky, not fixed: it keeps its place in normal flow, so the
   first line of the article is never hidden underneath and no hand-maintained
   body padding has to be kept in sync with a bar whose height changes. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.25rem 1.25rem;
  min-height: var(--header-h);
  background: rgba(255, 255, 255, 0.92);
  -webkit-backdrop-filter: saturate(180%) blur(8px);
  backdrop-filter: saturate(180%) blur(8px);
  border-bottom: 1px solid #a2a9b1;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}

/* Opaque where the blur is unsupported, so text never shows through. */
@supports not (backdrop-filter: blur(8px)) {
  .site-header { background: #fff; }
}

.site-title {
  font-family: "Linux Libertine", Georgia, Times, serif;
  font-size: 1.1rem;
  line-height: 1.2;
  color: #202122;
  text-decoration: none;
  white-space: nowrap;
}

.site-header nav {
  display: flex;
  gap: 1rem;
  align-items: center;
}

.site-header nav a {
  color: #3366cc;
  text-decoration: none;
  font-size: 0.88rem;
  white-space: nowrap;
}

.site-header nav a:hover { text-decoration: underline; }

/* ---- header search ------------------------------------------------ */

.site-search {
  position: relative;      /* containing block for the suggestion list */
  margin-left: auto;
  min-width: 0;            /* or the input refuses to shrink and shoves
                              the nav links off the bar */
  flex: 0 1 22rem;
}

.site-search input[type="search"],
.search-page-form input[type="search"] {
  width: 100%;
  padding: 0.28rem 0.6rem;
  font: inherit;
  font-size: 0.88rem;
  color: #202122;
  background: #fff;
  border: 1px solid #a2a9b1;
  border-radius: 2px;
}

.site-search input[type="search"]:focus,
.search-page-form input[type="search"]:focus {
  outline: none;
  border-color: #36c;
  box-shadow: 0 0 0 2px rgba(51, 102, 204, 0.2);
}

.search-suggest {
  position: absolute;      /* absolute, not fixed: backdrop-filter makes the
                              header a containing block for fixed descendants */
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  background: #fff;
  border: 1px solid #a2a9b1;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
  max-height: 60vh;
  overflow-y: auto;
  z-index: 30;
}

.search-suggest[hidden] { display: none; }

.search-suggest li {
  padding: 0.35rem 0.6rem;
  cursor: pointer;
  font-size: 0.88rem;
  border-bottom: 1px solid #eaecf0;
}

.search-suggest li:last-child { border-bottom: 0; }
.search-suggest li.active,
.search-suggest li:hover { background: #eaf3ff; }

.suggest-title { color: #3366cc; }
.suggest-crumb { display: block; color: #72777d; font-size: 0.78rem; }

/* Visible only to screen readers / when focused. */
.skip-link {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.skip-link:focus {
  position: static;
  width: auto;
  height: auto;
  padding: 0.3rem 0.6rem;
}

/* The side padding is fluid between the two breakpoints. 3rem is right beside
   a 960px card with room to spare either side, and it is 12% of the screen on
   an 800px tablet — where the prose column can least afford to give it away
   and where no media query used to touch it, because the only one in the file
   starts at 720px. That band (721–1099px: every tablet in portrait, and a
   split-screen desktop window) was the worst-served width in the book.
   `4vw` tracks the viewport through it and the clamp pins the two ends, so
   the layout no longer changes only at one hard step. */
.article {
  max-width: 960px;
  margin: 1.5rem auto;
  padding: 2rem clamp(1rem, 4vw, 3rem);
  background: #fff;
  border: 1px solid #a2a9b1;
}

h1 {
  font-family: "Linux Libertine", Georgia, Times, serif;
  font-weight: normal;
  border-bottom: 1px solid #a2a9b1;
  padding-bottom: 0.2rem;
}

h2, h3 {
  font-family: "Linux Libertine", Georgia, Times, serif;
  font-weight: normal;
  border-bottom: 1px solid #eaecf0;
  padding-bottom: 0.15rem;
  margin-top: 1.6em;
}

/* \paragraph{} — a run-in heading, no rule, tighter than a subsection */
h4 {
  font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  font-size: 1rem;
  font-weight: bold;
  margin: 1.4em 0 0.2em;
}

a { color: #3366cc; text-decoration: none; }
a:hover { text-decoration: underline; }

.subtitle { color: #54595d; font-style: italic; }
.breadcrumb { font-size: 0.85rem; color: #54595d; }
.muted { color: #72777d; font-size: 0.85rem; }

/* TOC boxes */
.toc-box {
  display: inline-block;
  background: #f8f9fa;
  border: 1px solid #a2a9b1;
  padding: 0.8rem 1.4rem;
  font-size: 0.93rem;
  margin: 1rem 0;
}

.toc-right {
  float: right;
  max-width: 300px;
  margin: 0 0 1rem 1.5rem;
}

.toc-heading {
  text-align: center;
  font-weight: bold;
  margin-bottom: 0.4rem;
}

.toc-parts {
  list-style: none;
  padding-left: 0;
  margin-left: 0;
}
.toc-parts > li { margin: 0.25rem 0; }
.toc-parts summary,
.toc-chapters summary,
.toc-sections summary { cursor: pointer; }
.toc-chapters { margin: 0.2rem 0 0.4rem 0; }
.toc-chapters li { margin: 0.1rem 0; }

/* nested section lists inside the contents tree */
.toc-sections {
  list-style: none;
  padding-left: 1.3rem;
  margin: 0.15rem 0 0.3rem 0;
}
.toc-sections li { margin: 0.1rem 0; }

.toc-parts > li > details > summary > .rag {
  margin-left: 0.45em;
  margin-right: 0;
}

/* RAG completeness dots */
.rag {
  display: inline-block;
  width: 0.58em;
  height: 0.58em;
  border-radius: 50%;
  margin-right: 0.45em;
  vertical-align: 0.06em;
}
.rag-green { background: #2e7d32; }
.rag-amber { background: #e8a013; }
.rag-red   { background: #c62828; }

.toc-legend {
  display: grid;
  gap: 0.18rem;
  justify-content: center;
  font-size: 0.85em;
  color: #555;
  text-align: left;
  margin-bottom: 0.55rem;
}
.toc-legend > span {
  display: grid;
  grid-template-columns: 0.9rem auto;
  align-items: center;
  column-gap: 0.35rem;
}
.toc-legend .rag {
  justify-self: center;
  margin: 0;
}

.toc-count {
  color: #888;
  font-size: 0.8em;
  white-space: nowrap;
}

.tag-exp {
  background: #eaf3ff;
  border: 1px solid #a2a9b1;
  border-radius: 3px;
  font-size: 0.7rem;
  padding: 0 0.3rem;
  color: #36c;
  vertical-align: middle;
}

/* Chapter body environments.

   JUSTIFIED, to match the PDF. This was ragged-right before, and the reason is
   worth keeping because it is the failure mode to watch for rather than an
   argument against the setting: CSS justification is greedy first-fit with no
   paragraph-wide penalty model, and MathJax v3 CHTML sets `white-space: nowrap`
   on mjx-math, so every inline formula is an unbreakable atom. When one will
   not fit in the space left, the line ends early and its word spaces stretch
   to fill the measure. The observed instance was a line reading "Applying tanh
   to the sum reproduces Equation (30.29), since" stretched across the full
   measure because the formula after it was one 60-character atom.

   Three things keep that in hand, in order of how much they carry:

   1. `hyphens: auto` with a permissive `hyphenate-limit-chars`. Hyphenation
      cannot break a formula, but it multiplies the break points between words,
      so the greedy algorithm reaches the formula with far less slack left. It
      is load-bearing here in a way it was not under ragged right, and must not
      be removed without reverting the alignment with it.
   2. `text-wrap: pretty`, which lets an engine that supports it reconsider the
      last lines of a paragraph instead of committing greedily. Engines that do
      not support it ignore the declaration and lose only that improvement.
   3. Promoting long inline formulas to display math where they occur. This is
      the only fix for the atom problem itself, it is good typography
      independently of alignment, and under justification a missed one now
      costs a stretched line rather than a short one. */
.chapter-body {
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
  /* min word length, min chars before the break, min chars after. More
     permissive than the UA default, which is the point: more break points
     means less slack left over when an unbreakable formula ends the line. */
  hyphenate-limit-chars: 6 3 3;
  -webkit-hyphenate-limit-chars: 6 3 3;
  text-wrap: pretty;
  min-width: 0;
}

/* The exceptions, and they are few: a heading and an environment label are one
   line or less, so justification has nothing to distribute and a hyphen inside
   a numbered label ("Proposi-tion 14.3") reads as a defect. Captions are NOT
   here — in this book a caption is running prose, four to nine lines, and it
   is justified in the PDF for the same reason. Table cells carry their own
   inline text-align from the renderer's colspec and are unaffected either
   way. */
.chapter-body h1, .chapter-body h2, .chapter-body h3, .chapter-body h4,
.chapter-body .env-label, .chapter-body .env-title {
  text-align: left;
  hyphens: manual;
  -webkit-hyphens: manual;
}

.env {
  margin: 1rem 0;
  padding: 0.6rem 1rem;
  border-left: 3px solid #a2a9b1;
  background: #f8f9fa;
}

.env-label { font-weight: bold; }

.env-theorem, .env-lemma, .env-proposition, .env-corollary {
  border-left-color: #36c;
}

.env-phenomenon {
  border-left-color: #14866d;
  background: #f0f7f4;
}

.env-experiment {
  border: 1px solid #54595d;
  border-left: 4px solid #202122;
  background: #fdfdfd;
  padding: 0;
}

.env-experiment .env-title {
  background: #54595d;
  color: #fff;
  font-weight: bold;
  padding: 0.35rem 1rem;
}

.env-experiment > p, .env-experiment > div:not(.env-title) { padding: 0 1rem; }
.exp-field { margin: 0.7rem 1rem 0.2rem; }

.env-pending {
  border: 1px solid #edab00;
  border-left: 4px solid #edab00;
  background: #fef6e7;
}

.env-proof { border-left-color: #c8ccd1; background: #fff; }

.prooflink { font-style: italic; }

/* Knowledge-graph chain notes (\dependson, \derivationof, \tests,
   \assumes, \bystep) -- the PDF prints them as footnotesize italic
   paragraphs; a span (display:block) is used rather than a div so the
   paragraph pass never treats the opener as block-level HTML. */
.graph-note {
  display: block;
  font-size: 0.85rem;
  font-style: italic;
  color: #54595d;
  margin: 0.25rem 0 0.15rem;
}

/* ---- responsive display math -------------------------------------- *
 *
 * Why wide equations used to escape the column despite overflow-x: auto:
 * MathJax writes `style="min-width: 35.758em"` as an ELEMENT INLINE STYLE on
 * mjx-container for every tagged equation, and used width is
 * max(min-width, min(max-width, width)) — so `max-width: 100%` could never
 * win, and no stylesheet rule beats an inline style without !important.
 *
 * Exactly ONE element may scroll. Putting overflow-x on both .display-math
 * and the container inside it meant the inner one clipped first and the outer
 * never scrolled. mjx-container is the one that scrolls, because it exists
 * whether or not the renderer wrapped the block.
 *
 * Specificity: MathJax injects its own <style> into <head> at RUNTIME, i.e.
 * after this file, and its selectors are (0,2,1). Scoping under .chapter-body
 * reaches (0,3,1) so these rules actually apply. */

.display-math {          /* pure spacing wrapper — no overflow here */
  margin: 0.4rem 0;
  min-width: 0;
  max-width: 100%;
}

.chapter-body { overflow-wrap: break-word; }

/* `body ...` rather than a bare `mjx-container ...`: MathJax's own rule has
   the same shape and is injected later, so an equal-specificity selector
   loses the tie. This one is (0,2,2) and wins. */
.chapter-body mjx-container[jax="CHTML"][display="true"],
body mjx-container[jax="CHTML"][display="true"] {
  min-width: 0 !important;   /* load-bearing: beats MathJax's inline style */
  max-width: 100%;
  width: auto;

  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: #c8ccd1 transparent;

  /* classic Windows scrollbars are laid inside the box and would otherwise
     sit on the descenders of the bottom row */
  padding-bottom: 0.3rem;

  /* `safe center` centres while it fits and switches to start-alignment the
     moment it overflows. Plain centering overflows on BOTH sides, and
     scrollLeft cannot go below 0, so the left of a wide equation becomes
     permanently unreachable — worse than the original bug. Browsers without
     `safe` drop the declaration and fall back to flex-start, which is also
     safe; do not add a plain `center` fallback before it. */
  display: flex;
  justify-content: safe center;
  align-items: baseline;
  margin: 0.7em 0;
}

.chapter-body mjx-container[jax="CHTML"][display="true"][justify="left"]  { justify-content: safe flex-start; }
.chapter-body mjx-container[jax="CHTML"][display="true"][justify="right"] { justify-content: safe flex-end; }

/* Do NOT zero min-width on mjx-math/mjx-mtable as well: their inline
   min-width is what preserves the equation's natural width so there is
   something to scroll to. Only the outer box may shrink. */
.chapter-body mjx-container[jax="CHTML"][display="true"] > mjx-math { flex: 0 0 auto; }

.env, .env-experiment, .exp-field { min-width: 0; }
.env { overflow-wrap: break-word; }

/* \verb and \texttt */
code {
  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
  font-size: 0.92em;
  background: #f5f5f5;
  padding: 0 0.2em;
  border-radius: 2px;
}

.latex-table {
  overflow-x: auto;
  background: #f8f9fa;
  border: 1px dashed #a2a9b1;
  padding: 0.6rem;
  font-size: 0.8rem;
}

.table-figure { margin: 1.4rem 0; }

/* Tables may use the space the prose column does not need — as much of it as
   they need, and no more.

   `.article` is capped at 960px because that is a readable measure for
   running text. A table has no measure to protect: on a 1900px window it was
   being squeezed into the 864px content box while ~470px sat empty on either
   side. This widens the table block only — prose, headings and captions keep
   their measure.

   `width: max-content` is the whole correction, and it is load-bearing. This
   rule used to set the block to the full break-out width unconditionally, so
   EVERY table got a 1200px box: the three-column SI base-units table is 440px
   wide, and it was being centred in a white slab that overhung the article
   card by ~170px on each side and interrupted the card's border for its
   height. That reads as a broken table, not a wide one — and 4 of the 13
   content-sized tables in the book are narrower than the prose column, so it
   was the common case rather than the exception. Sized to `max-content` the
   block is only as wide as the table (or the 42rem caption, whichever is
   wider), so a narrow table now paints its white background entirely inside
   the white card, where it is invisible, and only a table that genuinely
   overflows the column reaches past it.

   `.table-figure-fixed` is excluded rather than clamped. It is the wrapper of
   a table whose colspec declared p{}/m{}/b{} widths, which the renderer lays
   out with `table-layout: fixed; width: 100%` — such a table fills whatever
   box it is given, so `max-content` has nothing to measure and, worse, the
   break-out actively misreports the source: p{0.13\linewidth} against a
   1200px block is more than twice the column width the same table has in
   print. Those stay at the prose measure, which is already ~1.8x the
   printed \linewidth.

   How the centring works: `margin-left: 50%` puts the block's left edge at the
   centre of the article's content box, and `translateX(-50%)` pulls it back by
   half its own width, so it ends up centred on the article's centre. The
   article is auto-margined, so that is also the viewport's centre, and the
   break-out is symmetric. `min(100vw - 4rem, …)` is what guarantees it can
   never be wider than the window and so can never put a horizontal scrollbar
   on the page; `.figure`'s own `overflow-x: auto` still handles a table too
   wide even for this. */
@media (min-width: 1100px) {
  .chapter-body .table-figure:not(.table-figure-fixed) {
    width: max-content;
    max-width: min(calc(100vw - 4rem), 1200px);
    margin-left: 50%;
    transform: translateX(-50%);
    /* `.article` is a white card with a 1px border on a grey page, and this
       block may overhang it. Two things go wrong without an opaque background
       of its own. An ancestor's border is painted in the ancestor's
       box-decoration step, BEFORE any descendant's background, and
       `.data-table` cells are transparent — so the card's left and right
       edges were drawn straight through the table as two grey hairlines,
       which reads as a broken table rather than as a wide one. And the parts
       sticking out past the card sat on the grey page background while the
       middle sat on white, so one table had two background colours.
       Painting the block white fixes both at once. The card's frame is then
       interrupted for the height of the block, which is the intended
       full-bleed reading: the table is wider than the card. The vertical
       padding is what makes that interruption land clear of the table's own
       top and bottom rules instead of touching them.

       Under `max-content` sizing this now costs nothing when the table does
       NOT overhang: white on white inside the card is invisible, which is
       why the declaration stays unconditional instead of being gated on a
       width nothing in CSS can test. */
    background: #fff;
    padding: 0.35rem 0;
  }
}

.data-table {
  border-collapse: collapse;
  margin: 0 auto;
  font-size: 0.92rem;
  max-width: 100%;
}

/* nowrap is the default because most tables in this book are narrow columns of
   numbers with units, where a break inside a value is worse than a wide table.
   It is NOT right for a LaTeX p{}/m{}/b{} column, which is a paragraph column
   that LaTeX itself wraps; the renderer emits `white-space:normal` inline on
   those cells, which wins over this rule. Before that, a prose table was laid
   out with every cell on one line: 5 columns of running text became roughly
   2500px of min-content, the table overflowed its scroll container, and the
   reader met it already scrolled with the first column cut off. */
.data-table th, .data-table td {
  padding: 0.35rem 0.9rem;
  border-bottom: 1px solid #eaecf0;
  white-space: nowrap;
}

/* Tables whose colspec declared proportional widths are laid out on them, as
   in the PDF. Automatic layout would ignore the declaration and re-fit the
   columns to their content, which is exactly what the author overrode by
   writing p{0.17\linewidth} in the first place. Tables of l/c/r columns get no
   class here and keep the browser's automatic algorithm.

   `width: 100%` is what makes such a table unreadable on a phone if nothing
   stops it: five declared columns dividing a 330px viewport give each prose
   column ~60px, i.e. six characters a line and forty lines a cell. A nowrap
   table cannot get into that state — its min-content width is its floor and
   it simply scrolls inside `.figure` — so the floor has to be supplied here.
   8rem a column is about fourteen characters at the narrow-screen font size,
   the point where a prose column stops reading as a stack of fragments;
   `--table-cols` comes from the renderer, which is the only place the count
   is known. The used width is max(min-width, min(max-width, width)), so this
   also beats the `max-width: 100%` in the narrow-screen block below — and
   above ~64rem of viewport the 100% is the larger of the two and the floor
   does nothing at all. */
.data-table.table-fixed {
  table-layout: fixed;
  width: 100%;
  min-width: calc(var(--table-cols, 3) * 8rem);
}

/* Hand-written PROSE tables (the check registries on /validation).
   The renderer emits `white-space: normal` per cell for a LaTeX p{}
   column; a table authored in a template has no colspec to carry that,
   and inheriting the nowrap default put a whole sentence of "what this
   check tests" on one line -- roughly 1500px of min-content, so the
   column that is the entire point of the table sat outside the card.
   Same layout contract as .table-fixed, plus wrapping. */
.data-table.prose-table {
  table-layout: fixed;
  width: 100%;
  min-width: calc(var(--table-cols, 3) * 8rem);
}
.data-table.prose-table th, .data-table.prose-table td {
  white-space: normal;
  vertical-align: top;
}

.data-table thead th {
  border-top: 2px solid #202122;
  border-bottom: 1px solid #202122;
  font-weight: bold;
  background: #f8f9fa;
}

.data-table tbody tr:last-child td { border-bottom: 2px solid #202122; }

/* Justified like the prose, not centred: the captions in this book are
   paragraphs (585 characters for tab:lie-kinematical, 446 for
   tab:lag-universality — eight or nine lines), and centred prose is readable
   at one or two lines only. LaTeX justifies them for the same reason. The
   max-width and auto margins still centre the caption BLOCK under the table;
   only the text inside it is justified. */
.table-caption {
  text-align: justify;
  font-size: 0.88rem;
  color: #54595d;
  margin-top: 0.5rem;
  max-width: 42rem;
  margin-left: auto;
  margin-right: auto;
}

/* Justified with the captions above. Most footnotes are a single line, where
   justification changes nothing (the last line of a paragraph is never
   stretched); declaring it anyway keeps one rule for all caption-class prose
   instead of an exception nobody can explain later. */
.table-footnote {
  text-align: justify;
  font-size: 0.8rem;
  color: #54595d;
  max-width: 42rem;
  margin: 0.4rem auto 0;
}

/* .center wraps four \begin{center} tables inside experiment boxes that have
   no overflow ancestor at all; .data-table's nowrap cells give them a hard
   min-content floor, so without a scroll container here they widen the page.
   max-width alone cannot help against nowrap. */
.center { text-align: center; overflow-x: auto; max-width: 100%; }

.figure { text-align: center; margin: 1rem 0; overflow-x: auto; }
.figure-ref { color: #72777d; font-style: italic; }

.figure-img {
  max-width: min(80%, 480px);
  height: auto;
  display: block;
  margin: 0.5rem auto;
}
/* Figure captions: same reasoning as .table-caption. Needed explicitly because
   `.figure` sets `text-align: center` to centre the image, and the caption
   would otherwise inherit it. The image itself stays centred through
   `.figure-img`'s auto margins, which do not depend on text-align. */
.caption { font-size: 0.88rem; color: #54595d; text-align: justify; }
/* The caption body is left in the text stream (so its math, dashes and
   cross-references render), which means the paragraph pass wraps it in a
   <p>. Strip that <p>'s own margins so the caption still sits tight under
   its figure. */
.caption p { margin: 0.35rem 0 0; }

/* how recently a DOI was proven to resolve, shown next to the link the
   reader is about to follow */
.doi-age {
  font-size: 0.8rem;
  color: #54595d;
  white-space: nowrap;
  margin-left: 0.15rem;
}

.cite { font-size: 0.8rem; vertical-align: super; }
/* Citation-role wrappers (\citeevidence etc.) -- a dotted underline marks
   a role-tagged citation; the role itself is in the title tooltip. */
.cite-evidence, .cite-data, .cite-method, .cite-history, .cite-context {
  border-bottom: 1px dotted #72777d;
}
.xref { color: #36c; }
.footnote { font-size: 0.85rem; color: #54595d; }

/* Validation dashboard (RAG) */
.verdict {
  padding: 0.7rem 1rem;
  border-left: 5px solid;
  margin: 1rem 0 1.5rem;
  font-size: 1.05rem;
}
.verdict-green { border-color: #14866d; background: #eef8f4; }
.verdict-amber { border-color: #edab00; background: #fef6e7; }
.verdict-red   { border-color: #d33; background: #fdeeee; }

.dot {
  display: inline-block;
  width: 0.7rem; height: 0.7rem;
  border-radius: 50%;
  margin-right: 0.4rem;
  vertical-align: baseline;
}
.dot-green { background: #14866d; }
.dot-amber { background: #edab00; }
.dot-red   { background: #d33; }
.dot-grey  { background: #a2a9b1; }

/* The graph explorer is a full-viewport canvas, not a reading card: it
   opts out of .article via base.html's main_class block and fills what
   is left below the header. */
.graph-shell {
  height: calc(100vh - var(--header-h, 44px));
  width: 100%;
  overflow: hidden;
  position: relative;
}

/* The explorer is a docked side panel plus a canvas, not a canvas with
   things floating on top of it: hiding the panel has to GIVE the graph
   the width back, which an overlay cannot do. */
.graph-layout {
  display: flex;
  height: 100%;
  width: 100%;
  align-items: stretch;
}
.graph-side {
  flex: 0 0 19rem;
  width: 19rem;
  border-right: 1px solid #eaecf0;
  background: #fff;
  padding: 0.75rem 0.85rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  transition: flex-basis 0.18s ease, width 0.18s ease,
              padding 0.18s ease;
}
.graph-side.is-hidden {
  flex-basis: 0;
  width: 0;
  padding-left: 0;
  padding-right: 0;
  border-right-width: 0;
  overflow: hidden;
}
.graph-side-show {
  position: absolute;
  top: 0.6rem;
  left: 0.6rem;
  z-index: 15;
  border: 1px solid #c8ccd1;
  border-radius: 4px;
  background: #fff;
  color: #54595d;
  padding: 0.2rem 0.45rem;
  line-height: 1;
  cursor: pointer;
}
.graph-side-show:hover { background: #f8f9fa; }
.graph-canvas { position: relative; flex: 1 1 auto; min-width: 0; }

/* ---- the graph's loading indicator -------------------------------
   Two modes on one component. `data-mode="determinate"` fills the ring
   from the bytes actually received; `data-mode="indeterminate"` spins a
   fixed quarter-arc, which is what the reader gets whenever the payload
   size cannot be trusted -- notably when the host serves the JSON
   gzipped, where Content-Length is the COMPRESSED size while the reader
   yields decompressed bytes, so a naive percentage sails past 100.

   ONLY `transform` and `opacity` are animated, and only on a plain
   div: those run on the compositor and keep moving while the main
   thread is blocked parsing 29 MB of JSON. Animating an SVG attribute,
   a conic-gradient or a rAF loop would freeze at exactly that moment. */
/* The overlay FADES IN ON A DELAY, and the delay is the point: a redraw
   that finishes inside it is never seen at all. Part IV -- the opening
   view -- draws in about a tenth of a second, and an indicator that
   flashes up and vanishes on every fast interaction reads as a glitch,
   not as feedback. Showing it costs nothing when the work is quick and
   everything when it is not, which is the whole trick. */
#graph-busy {
  opacity: 0;
  animation: graph-loader-appear 0.18s ease-out 0.25s forwards;
}
@keyframes graph-loader-appear { to { opacity: 1; } }

.graph-loader {
  display: flex; flex-direction: column; align-items: center;
  gap: 0.55rem; text-align: center; padding: 0.5rem 1rem;
  max-width: 22rem;
}
.graph-loader-ring { position: relative; width: 54px; height: 54px; }
.graph-loader-spin { position: absolute; inset: 0; }
.graph-loader-ring svg {
  width: 100%; height: 100%; display: block;
  transform: rotate(-90deg);   /* start the arc at twelve o'clock */
}
.graph-loader-track { fill: none; stroke: #e4e7eb; stroke-width: 4; }
.graph-loader-arc {
  fill: none; stroke: #3366cc; stroke-width: 4; stroke-linecap: round;
  /* circumference = 2*pi*20 = 125.664 */
  stroke-dasharray: 125.664;
  stroke-dashoffset: 94.248;   /* a quarter shown, for the spin */
  transition: stroke-dashoffset 0.18s linear;
}
.graph-loader-pct {
  position: absolute; inset: 0; display: flex;
  align-items: center; justify-content: center;
  font-size: 0.72rem; font-variant-numeric: tabular-nums; color: #54595d;
}
.graph-loader-title {
  margin: 0; font-size: 0.9rem; color: #202122; line-height: 1.35;
}
.graph-loader-detail,
.graph-loader-note {
  margin: 0; font-size: 0.78rem; color: #72777d; line-height: 1.35;
  font-variant-numeric: tabular-nums;
}
.graph-loader-note { color: #a2792c; }
.graph-loader-note:empty, .graph-loader-detail:empty { display: none; }

.graph-loader-ring[data-mode="indeterminate"] .graph-loader-spin {
  animation: graph-loader-turn 0.9s linear infinite;
}
/* No transition while spinning: the arc length is constant there, and a
   transition on a value that never changes only delays the switch back
   to determinate. */
.graph-loader-ring[data-mode="indeterminate"] .graph-loader-arc {
  transition: none;
}
@keyframes graph-loader-turn { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  /* The reader still gets the ring, the percentage and the phase --
     everything the spin was decorating. The appear DELAY is kept and
     only its fade is dropped: that delay suppresses flicker, which is
     not a motion effect and matters more, not less, to a reader who
     asked for less movement. */
  .graph-loader-ring[data-mode="indeterminate"] .graph-loader-spin {
    animation: none;
  }
  .graph-loader-arc { transition: none; }
  #graph-busy { animation-duration: 0.01s; }
}

@media (max-width: 720px) {
  /* on a phone the panel takes the screen when open, and the graph
     takes it when closed */
  .graph-side { flex-basis: 100%; width: 100%; }
}
.graph-shell .glabel {
  /* SCREEN pixels: graph-explorer.js counter-scales every label by 1/k,
     so this size is what the reader gets at any zoom. Before that it
     was 10px of LAYOUT, which rendered as a 3-pixel smear at the fitted
     0.86x and as a 106-pixel banner at 8x. */
  font-size: 11px;
  fill: #3c3c3c;
  paint-order: stroke;
  stroke: #fff;
  stroke-width: 2.5px;
  pointer-events: none;
}
.graph-shell .gnode { cursor: pointer; }
/* STROKES DO NOT ZOOM. A stroke-width is in user units, so the zoom
   transform multiplies it: at 8x the 1.4px declared for a declared edge
   renders ELEVEN pixels wide and the graph becomes a mush of ribbons --
   fatter lines rather than finer detail, which is the opposite of what
   zooming in is for, and the reason a zoomed pan frame was spending its
   time alpha-blending tens of thousands of overlapping fat strokes.
   non-scaling-stroke pins every stroke to the width it was declared at.
   The circles' white separating ring is in the same trap (a 0.6px ring
   became 5px and swallowed the node it was outlining), and the
   invisible hit line is better for it too -- 10 screen pixels of target
   at every zoom rather than 10 layout pixels. */
.graph-shell .gnode,
.graph-shell .links path,
.graph-shell .link,
.graph-shell .link-hit,
.graph-shell .focus-edge { vector-effect: non-scaling-stroke; }
.graph-shell .link-hit { cursor: crosshair; }

/* Knowledge-graph pages (/graph, /graph/node/...) */
.graph-columns { display: grid; gap: 0 2rem; }
@media (min-width: 1100px) {
  .graph-columns { grid-template-columns: 1fr 1fr; }
}
ul.chain { list-style: none; padding-left: 0; margin: 0.4rem 0; }
ul.chain ul.chain { padding-left: 1.4rem; border-left: 1px solid #eaecf0; }
ul.chain > li { margin: 0.3rem 0; }
.chain-edge {
  font-size: 0.72rem;
  color: #54595d;
  background: #f8f9fa;
  border: 1px solid #eaecf0;
  border-radius: 3px;
  padding: 0 0.3rem;
  margin-right: 0.2rem;
}
.chain-type { font-size: 0.85rem; color: #54595d; margin-right: 0.2rem; }
.chain-jump { text-decoration: none; margin-left: 0.25rem; }
.ego-graph, .graph-map {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 auto;
}
.ego-graph { max-width: 640px; }
.ego-graph text, .graph-map text { font-size: 11px; fill: #202122; }
.ego-graph .map-note, .graph-map .map-note {
  fill: #54595d;
  font-style: italic;
}
.graph-map .map-col {
  font-size: 12px;
  font-weight: bold;
  fill: #54595d;
  letter-spacing: 0.04em;
}
.graph-map a:hover circle, .ego-graph a:hover circle { stroke: #202122; }
.graph-map a:hover text, .ego-graph a:hover text { text-decoration: underline; }

/* the map is the page on /graph: let it use the full card, and a bit
   more on a wide window, since a node-link diagram has no measure to
   protect the way prose does */
.graph-figure { margin: 1rem 0 0.5rem; }
.graph-figure figcaption {
  max-width: 42rem;
  margin: 0.6rem auto 0;
  font-size: 0.9rem;
  color: #54595d;
}
.map-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.1rem;
  padding: 0;
  margin: 0.2rem 0 0.8rem;
  font-size: 0.85rem;
  color: #54595d;
}
.map-legend li { display: flex; align-items: center; gap: 0.35rem; }
.legend-line { width: 34px; height: 8px; }
.graph-foot { font-size: 0.9rem; }

/* a check whose nature is in its title= attribute; the dotted underline
   is the affordance that says "hover me" */
.check {
  border-bottom: 1px dotted #72777d;
  cursor: help;
  font-weight: 600;
}
ul.plain { list-style: none; margin: 0; padding: 0; }
.hidden { display: none; }

/* the relational grid — embedded in /validation section 2 and served
   standalone at /validation/table/<name>, from ONE partial */
.table-browser td, .table-browser th { white-space: nowrap; }

/* A huge grid defers its tail past the first paint. `grid-defer` is
   stamped by the server when the row count passes GRID_DEFER_ROWS
   (app.py); `grid-js` is set by an inline script in the partial, so a
   scripting-off reader never loses a row — for them this rule never
   arms and the whole table lays out as it always did. The 500 here is
   the client half of the trade (how many rows paint eagerly, ~20
   screenfuls of the 72vh scroller); filter-sync.js removes
   `grid-defer` once MathJax has settled, which is ONE full-table
   layout instead of one per typeset. */
html.grid-js .table-browser.grid-defer tbody tr:nth-child(n+501) {
  display: none;
}
.table-browser .filter-row input {
  width: 8rem;
  font-size: 0.85rem;
  padding: 0.1rem 0.3rem;
  border: 1px solid #c8ccd1;
  border-radius: 3px;
}
.table-browser .filter-row td { padding: 0.2rem 0.4rem; }

/* The filter chips. NOTE: no `display` here on purpose -- `.hidden`
   above is a single-class rule too, and giving .chip a display would
   win the tie by source order and show every chip that is supposed to
   be hidden. */
.chip {
  border: 1px solid #c8ccd1;
  border-radius: 999px;
  background: #f8f9fa;
  padding: 0.35rem 0.9rem;
  margin: 0.4rem 0;
  font-size: 0.88rem;
  line-height: 1.5;
}
.chip-default { border-color: #a7c7e7; background: #eef4fb; }
/* Two rules went with their markup on 2026-08-25. `.chip-saved` styled
   the saved-filter chip, and there is no saved filter any more -- there
   is ONE filter, shared by the three tabs. `.chip-x` styled the button
   that dismissed the Default-view chip, and a chip that can be
   dismissed is wrong now that the filter OUTLIVES the page: dismissing
   it once would leave a filter running invisibly for as long as the
   cookie lasts, which is the opposite of the never-filter-silently
   contract the chip exists to keep. */
.button-link {
  border: 1px solid #c8ccd1;
  border-radius: 3px;
  background: #fff;
  color: #36c;
  cursor: pointer;
  font: inherit;
  font-size: 0.88rem;
  padding: 0.15rem 0.6rem;
  text-decoration: none;
}
.button-link:hover { background: #f8f9fa; }
.filter-actions { display: flex; flex-wrap: wrap; gap: 0.5rem;
                  align-items: center; margin: 0.4rem 0; }
/* in the graph's side panel the same host holds several stacked
   paragraphs, not one row of controls */
.graph-side .filter-actions { display: block; }
.graph-side .chip { border-radius: 6px; padding: 0.3rem 0.5rem; }

/* /analytics: a magnitude bar beside its printed value. One hue only --
   the bar encodes size, never identity or state; identity is the row
   label and state keeps the reserved RAG dots. The value is always
   printed as text, so the bar can be read at a glance and checked
   exactly. */
.bar-cell { vertical-align: middle; }
.mini-bar {
  display: inline-block;
  height: 0.55rem;
  background: #6e8fbe;
  border-radius: 2px;
  vertical-align: middle;
}
/* a non-zero count must be visible even when the share rounds to 0% */
.mini-bar[style] { min-width: 2px; }

/* The relational store as one MULTITABLE (owner decision, Aug 2026):
   a tab per table with its row count under the active filter, the
   active tab's rows below, no pagination. The tabs are plain links --
   the styling is the only thing that makes them tabs, so scripting-off
   and the static export keep working unchanged. */
.table-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem;
  align-items: flex-end;
  margin: 0.9rem 0 0;
  border-bottom: 1px solid #a2a9b1;
  font-size: 0.82rem;
}
.table-tab {
  display: inline-block;
  padding: 0.22rem 0.55rem;
  border: 1px solid #c8ccd1;
  border-bottom: none;
  border-radius: 4px 4px 0 0;
  background: #f8f9fa;
  color: #3366cc;
  text-decoration: none;
  white-space: nowrap;
  position: relative;
  top: 1px;
}
.table-tab:hover { background: #eaecf0; text-decoration: none; }
.table-tab.is-current {
  background: #fff;
  border-color: #a2a9b1;
  border-bottom: 1px solid #fff;   /* joins the tab to its panel */
  color: #202122;
  font-weight: 700;
}
.table-tab .muted { font-size: 0.72rem; color: #72777d; }

/* The grid scrolls IN PLACE, both axes: with pagination gone the
   whole-book objects table is ~23k rows, and without a viewport cap it
   would make the page itself hundreds of screens tall. The column
   headers and the filter row stay pinned while the rows scroll. */
.grid-scroll {
  max-height: 72vh;
  overflow-y: auto;
  border: 1px solid #c8ccd1;
  border-top: none;
}
/* Sticky is scoped to the TWO pinned rows only: the third thead row
   (the apply button) must scroll away with the body, and a shared
   `thead td { position: sticky }` rule would leave it an unpinned but
   z-raised, opaque stray. The filter row's inset is the header row's
   real height, built from the quantities it comes from: .findings th
   is 0.9rem type at the body's 1.6 line-height plus 0.35rem padding
   each side and the border -- header cells are nowrap, so the row
   cannot wrap taller. */
.grid-scroll thead tr:first-child th,
.grid-scroll thead tr.filter-row:nth-child(2) td {
  position: sticky;
  background: #eaecf0;
  z-index: 2;
}
.grid-scroll thead tr:first-child th { top: 0; }
.grid-scroll thead tr.filter-row:nth-child(2) td {
  top: calc(0.9rem * 1.6 + 0.7rem + 1px);
}

/* a finding outside the active filter, listed after the ones inside it */
.findings tr.row-outside td { opacity: 0.72; }
.duality { font-size: 0.82rem; white-space: normal; }

/* The isomorphism certificate's count column. The number alone is
   ambiguous and was read as a defect: some duals enumerate the
   subjects that SATISFY the rule and some the subjects that VIOLATE
   it, so a clean edge-hygiene rule printed a bare "0" under a column
   headed "Subjects" and looked like a rule checking nothing. The unit
   line is therefore part of the value, not decoration -- never style
   it away. */
.subjects { white-space: normal; text-align: right; }
.subjects-count {
  display: block;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.subjects-unit {
  display: block;
  font-size: 0.75rem;
  line-height: 1.35;
  color: #5b6470;
  margin-top: 0.1rem;
}
.subjects-polarity {
  font-variant: small-caps;
  letter-spacing: 0.03em;
  font-weight: 600;
}
.subjects-satisfy .subjects-polarity { color: #2f7a3f; }
.subjects-violate .subjects-polarity { color: #9a6b00; }

.rag-summary { border-collapse: collapse; margin: 0.6rem 0 1.4rem; width: 100%; }
.rag-summary td { padding: 0.3rem 0.8rem 0.3rem 0; border-bottom: 1px solid #eaecf0; }
/* `width` on a table cell is a suggestion, not a bound, so a compound value
   ("5491 / 1645") wrapped inside the 5rem column and its second half dropped
   onto a line of its own, reading as if it belonged to the row below. A
   figure must never break across lines; nowrap lets the column take the width
   the widest value actually needs. */
.rag-summary .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  width: 5rem;
  white-space: nowrap;
}

/* /experiments: one table per part, listing chapters rather than data. It is
   a register, not a numeric table, so it overrides two .data-table defaults —
   the centring (a list should start at the measure's left edge) and the
   nowrap (titles and dataset keys are prose and must wrap). */
.exp-summary { max-width: 34rem; }
.exp-part {
  font-size: 1.05rem;
  margin: 1.6rem 0 0.4rem;
  padding-bottom: 0.2rem;
  border-bottom: 1px solid #eaecf0;
}
.exp-table { width: 100%; margin: 0 0 1.2rem; }
.exp-table th { text-align: left; white-space: nowrap; }
.exp-table th:not(:first-child),
.exp-table td:not(:first-child) { text-align: center; width: 8.5rem; }
.exp-table td:first-child { width: auto; }
.exp-table code { font-size: 0.85em; }
.exp-table .rag { margin-right: 0.45em; }

/* The missing-write-ups table's last column is a list of heading names, not a
   status: it is prose and must be given room and left alignment, or five
   names centred in 8.5rem wrap to five lines. Only the "Has" count keeps the
   narrow status geometry. */
.exp-missing td:last-child,
.exp-missing th:last-child { text-align: left; width: 22rem; }
.exp-missing td:nth-child(2),
.exp-missing th:nth-child(2) { width: 4.5rem; }

/* The two full lists sit below thirteen part tables, which is a long scroll
   from the summary -- say they are there and offer the jump. */
.exp-jump { margin: 0.6rem 0 0.2rem; }

/* A pending block is a link target from the table above it; without the
   offset the browser puts the heading flush against the viewport top and it
   reads as though the list started there. */
.pending-block { scroll-margin-top: 1.5rem; }

/* Pending derivations: the note is running prose with math in it, so it is
   set at the reading measure rather than in a table. The jump arrow is a
   marker, not a word — keep it out of the text flow's colour and weight. */
.pending-block { margin: 1.4rem 0; }
.pending-block h3 {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
}
.pending-list {
  margin: 0;
  padding-left: 1.6rem;
  max-width: 46rem;
}
.pending-list li {
  margin: 0.3rem 0;
  line-height: 1.5;
}
.pending-jump {
  color: #72777d;
  text-decoration: none;
  font-size: 0.85em;
  margin-right: 0.25em;
}
.pending-jump:hover { color: #3366cc; }

@media (max-width: 720px) {
  /* three narrow status columns cannot hold their width on a phone; let them
     collapse toward the text and keep the title column readable */
  .exp-table th:not(:first-child),
  .exp-table td:not(:first-child) { width: 4.5rem; }
  .exp-missing td:last-child,
  .exp-missing th:last-child { width: auto; }

  .exp-table-wrap {
    overflow-x: visible;
  }
  .data-table.exp-table {
    display: block;
    font-size: 0.92rem;
    min-width: 0;
  }
  .data-table.exp-table thead {
    display: none;
  }
  .data-table.exp-table tbody,
  .data-table.exp-table tr,
  .data-table.exp-table td {
    display: block;
    width: 100%;
  }
  .data-table.exp-table tr {
    padding: 0.65rem 0;
    border-top: 1px solid #a2a9b1;
  }
  .data-table.exp-table tr:last-child {
    border-bottom: 1px solid #a2a9b1;
  }
  .data-table.exp-table td {
    display: grid;
    grid-template-columns: 6.5rem minmax(0, 1fr);
    gap: 0.75rem;
    padding: 0.18rem 0;
    border-bottom: 0;
    text-align: left !important;
    white-space: normal;
  }
  .data-table.exp-table td::before {
    content: attr(data-label);
    color: #72777d;
    font-size: 0.84em;
  }
  .data-table.exp-table td:first-child {
    font-size: 1rem;
  }
  .data-table.exp-table td:first-child::before {
    padding-top: 0.08rem;
  }
}

/* The reference register is six columns of prose and DOIs. At 100% of a phone
   width that is ~55px a column, and `overflow-wrap: anywhere` below then
   breaks a DOI one character to a line — the same failure the chapter tables
   get their floor for. Give it a floor and let the wrapper scroll; on any
   screen wider than ~48rem the 100% wins and nothing changes. */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
}

.findings {
  border-collapse: collapse;
  width: 100%;
  min-width: 44rem;
  font-size: 0.9rem;
  margin: 0.6rem 0 1.6rem;
}
.findings th {
  text-align: left;
  border-bottom: 2px solid #202122;
  padding: 0.35rem 0.6rem 0.35rem 0;
}
.findings td {
  padding: 0.4rem 0.6rem 0.4rem 0;
  border-bottom: 1px solid #eaecf0;
  vertical-align: top;
}
.findings .num { text-align: right; font-variant-numeric: tabular-nums; }
.row-red   { background: #fdf4f4; }
.row-amber { background: #fffbf0; }

.asset-tag {
  display: inline-block;
  font-size: 0.75rem;
  background: #eaf3ff;
  border: 1px solid #a2a9b1;
  border-radius: 3px;
  padding: 0 0.3rem;
  margin-right: 0.25rem;
}

/* Where a source declared open can be read. The label is the bare host, so
   the cell stays narrow; the full URL is the link's tooltip. */
.access-tag { background: #eafaf0; }

/* An availability note can carry a DOI, an archive id or a shelf mark with
   no spaces in it. Let those break rather than forcing the column wider than
   the viewport. */
.findings td { overflow-wrap: anywhere; }

.bib-status {
  font-size: 0.75rem;
  vertical-align: super;
}

/* Bibliography */
.bib-list dt { margin-top: 0.8rem; }
.bib-list dd { margin-left: 1.5rem; }

/* Pager */
.pager {
  display: flex;
  justify-content: space-between;
  border-top: 1px solid #eaecf0;
  margin-top: 2rem;
  padding-top: 0.8rem;
}

.pager .next { margin-left: auto; }

/* Same clamp as `.article`: the footer line is meant to sit flush with the
   card's text, so its padding has to track the card's. */
.site-footer {
  max-width: 960px;
  margin: 0 auto 2rem;
  padding: 0 clamp(1rem, 4vw, 3rem);
  color: #72777d;
  font-size: 0.85rem;
}

/* Landing-page meta line (nbow.io + last-update stamp); subordinate to
   the mission sentence above it. */
.site-footer .footer-meta {
  margin-top: 0.5rem;
  font-size: 0.8rem;
}
.site-footer .footer-meta a { color: inherit; text-decoration: underline; }

.search-page-form { display: flex; gap: 0.5rem; margin: 1rem 0; }
.search-page-form input[type="search"] { flex: 1 1 auto; max-width: 32rem; }
.search-page-form button {
  padding: 0.28rem 0.9rem;
  font: inherit;
  font-size: 0.88rem;
  color: #202122;
  background: #f8f9fa;
  border: 1px solid #a2a9b1;
  border-radius: 2px;
  cursor: pointer;
}
.search-page-form button:hover { background: #eaecf0; }

/* Search results page */
.search-results { list-style: none; padding: 0; margin: 1rem 0; }

.search-result {
  padding: 0.6rem 0;
  border-bottom: 1px solid #eaecf0;
}

.search-crumb { font-size: 0.8rem; color: #72777d; }
.search-title { font-size: 1.05rem; }
.search-snippet { margin: 0.25rem 0 0; color: #202122; }
.search-meta { color: #54595d; font-size: 0.9rem; margin-bottom: 0.4rem; }

.search-flag {
  background: #fef6e7;
  border: 1px solid #edab00;
  border-radius: 3px;
  font-size: 0.7rem;
  padding: 0 0.3rem;
  color: #8a6100;
  vertical-align: middle;
}

mark { background: #fef6e7; color: inherit; }

.search-hint {
  background: #f8f9fa;
  border: 1px solid #a2a9b1;
  padding: 0.8rem 1.2rem;
  margin: 1rem 0;
  font-size: 0.92rem;
}
.search-hint code { background: #eaecf0; }

@media (max-width: 720px) {
  /* two rows: [title | search] then [nav]; the offset for in-page jumps
     follows automatically because everything reads --header-h */
  :root { --header-h: 104px; }

  body { font-size: 14px; }

  .site-header {
    flex-wrap: wrap;
    gap: 0.45rem 0.65rem;
    padding: 0.45rem 0.75rem 0.55rem;
  }
  .site-title {
    flex: 1 0 auto;
    font-size: 1rem;
  }
  .site-search {
    order: 3;
    flex: 1 0 100%;
  }
  .site-search input[type="search"],
  .search-page-form input[type="search"] {
    min-height: 2.3rem;
    font-size: 1rem;
  }
  .site-header nav {
    order: 2;
    flex: 1 0 100%;
    flex-wrap: wrap;
    gap: 0.2rem 0.7rem;
    overflow: visible;
    margin-left: 0;
  }
  .site-header nav a { font-size: 0.86rem; }

  .article {
    padding: 0.85rem;
    margin: 0.75rem 0;
    max-width: 100%;
    border-left: 0;
    border-right: 0;
  }
  .site-footer { padding: 0 1rem; max-width: 100%; }
  .toc-right { float: none; max-width: none; margin: 1rem 0; }
  .toc-box {
    display: block;
    padding: 0.75rem 0.7rem;
    width: 100%;
    overflow-wrap: anywhere;
  }
  .toc-box ol {
    padding-left: 1.25rem;
  }
  .toc-box ul {
    padding-left: 0;
  }
  .toc-parts,
  .toc-chapters,
  .toc-sections {
    margin-left: 0;
  }
  .toc-box summary {
    list-style: none;
    display: block;
    position: relative;
    padding-left: 0.85rem;
  }
  .toc-box summary::-webkit-details-marker { display: none; }
  .toc-box details > summary::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.65em;
    border-style: solid;
    border-width: 0.28em 0 0.28em 0.42em;
    border-color: transparent transparent transparent #202122;
    transform: translateY(-50%);
  }
  .toc-box details[open] > summary::before {
    transform: translateY(-50%) rotate(90deg);
  }
  .toc-chapters { padding-left: 1rem; }
  .toc-sections { padding-left: 0.95rem; }
  .toc-count {
    display: block;
    margin-left: 1.35rem;
    line-height: 1.35;
  }
  .toc-legend {
    display: grid;
    gap: 0.2rem 0.75rem;
    line-height: 1.35;
  }
  .toc-legend .rag {
    margin-left: 0;
    margin-right: 0.25em;
  }

  h1 { font-size: 1.45rem; }
  h2 { font-size: 1.2rem; }
  h3 { font-size: 1.05rem; }

  .chapter-body {
    text-align: left;
    text-wrap: pretty;
  }

  .chapter-body mjx-container[jax="CHTML"][display="true"] {
    font-size: 92%;
    margin: 0.6em 0;
  }

  /* Inline math is unbreakable, so on a narrow column one long \(...\) is
     the main cause of the whole PAGE scrolling sideways. Clamping it costs a
     baseline shift (an inline-block with non-visible overflow sits on its
     bottom margin edge), hence vertical-align and hence only here. */
  .chapter-body mjx-container:not([display="true"]) {
    display: inline-block;
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    vertical-align: middle;
  }

  .figure, .table-figure, .latex-table, .data-table { max-width: 100%; min-width: 0; }
  .data-table { font-size: 0.85rem; }
  .data-table th, .data-table td { padding: 0.3rem 0.5rem; }
  .figure-img { max-width: 100%; }

  .pager { flex-direction: column; gap: 0.5rem; }
  .pager .next { margin-left: 0; }
}

/* /bibliography: where each source is cited. A reader checking a claim wants
   to get from the entry back into the book, so every entry carries links to
   the chapters citing it, collapsed one link per chapter with a count. */
.bib-cited {
  margin-top: 0.25rem;
  font-size: 0.9rem;
  line-height: 1.5;
}
.bib-cited a { text-decoration: none; }
.bib-cited a:hover { text-decoration: underline; }
.bib-count { font-size: 0.85em; margin-left: 0.15em; }
