.katex-display, .box {
  /* background-color: var(--quote-bg); */
  /* border-radius: 4px; */
  /* padding: 0.75em 1em; */
  /* margin: 1em 0;
  overflow-x: auto; */
}

/* KaTeX's second vlist row only measures depth, but it otherwise sits above
   links in array cells and intercepts pointer events. */
.katex .vlist-t2 > .vlist-r:nth-child(2) {
  pointer-events: none;
}

/* Blackboard bold (\mathbb, \R, \N, ...) in STIX Two's outlined double-struck
   rather than KaTeX_AMS; see "SITP Double Struck" in theme/fonts/fonts.css.
   katex.min.css loads after this file, hence the `span` to outrank its
   `.katex .mathbb`. KaTeX_AMS stays as the fallback for anything unmapped. */
.katex span.mathbb {
  font-family: "SITP Double Struck", KaTeX_AMS;
}

/* .katex-display>.katex {
  text-align: left !important;
} */

#first-edition {
  margin-top: 0;
}

#of-tensor-programs {
  margin-top: -24px;
  margin-bottom: 0;
}

#of-tensor-programs+p {
  margin-top: 0;
}

#recommended-books,
#recommended-books-1,
#recommended-books-2,
#recommended-books-3,
#recommended-lectures,
#recommended-lectures-1,
#recommended-lectures-2 {
  margin-bottom: -16px;
}

#recommended-lectures,
#recommended-lectures-1,
#recommended-lectures-2 {
  margin-top: 0;
}

p:has(> em)+h5[id^="recommended-"] {
  margin-top: 0;
}

/* Standalone fully-italic paragraphs (e.g. "*Forward Pass*") sit tight.
   Keyed off the .em-only class set in custom.js, not `p:has(> em:only-child)`:
   `:only-child` ignores text nodes, so the latter also matched paragraphs with
   an inline *phrase* and surrounding text, wrongly collapsing their margins. */
p.em-only {
  margin: 0;
}

#mathematics-for-machine-learning,
#tensor-programming,
#tensor-interpretation-and-compilation {
  margin-bottom: -16px;
}

.full-bleed {
  width: 90vw;
  position: relative;
  left: 50%;
  margin-left: -45vw;
}

.full-bleed iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  display: block;
}

.full-bleed-side {
  width: 90vw;
  position: relative;
  left: 50%;
  margin-left: -45vw;
  display: flex;
  gap: 1rem;
}

.full-bleed-side iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  display: block;
}

/* A full-bleed video gallery: one player on stage down the left, the rest
   stacked in a rail down the right. Clicking a rail video swaps it onto the
   stage and the outgoing one drops into the rail. The swap is done purely by
   re-assigning grid tracks — the iframes never move in the DOM, because
   re-parenting an <iframe> reloads it and would restart whatever the reader had
   playing. `.is-active` comes from custom.js, as does `--rail-rows` (one row per
   video that is not on stage; CSS cannot count children).

   The row owns a track per player AND a track per caption, and the cells opt
   into them with `subgrid`. That shared track list is what lets the stage line
   up with the rail exactly: the stage player spans every track except the last
   caption's, so its top edge is the first thumbnail's top edge and its bottom
   edge is the last thumbnail's bottom edge, with the two captions left sitting
   side by side underneath. Cells with private row tracks cannot do this — their
   rows resolve independently and the stage can only be centered in the leftover
   space, which is what leaves a gap at each end.

   `minmax(0, …)` on the columns overrides the automatic minimum of an `fr`
   track. Without it a track refuses to shrink below its content's min-content
   width, and an aspect-ratio player in a definite-height area reports that as
   (height x 16/9) — enough to push the rail off the right edge of the screen. */
.video-row {
  width: 90vw;
  position: relative;
  left: 50%;
  margin-left: -45vw;
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
  grid-template-rows: repeat(var(--rail-rows, 3), auto auto);
  column-gap: 1rem;
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

/* Cells default into the rail, two tracks each, and only the picked one is
   hoisted out — so the rail keeps source order and auto-places top-to-bottom,
   never reshuffling as the reader clicks between videos. */
.video-cell {
  grid-column: 2;
  grid-row: span 2;
  display: grid;
  grid-template-rows: subgrid;
  min-width: 0;
}

.video-cell.is-active {
  grid-column: 1;
  grid-row: 1 / -1;
}

/* Counting the player's end line from the back (`-2`) is what makes one pair of
   rules serve both roles: a rail cell spans two tracks, so `1 / -2` is just its
   player track, while the stage spans all of them and the same span reaches the
   last player track — stopping short of the caption. */
.video-cell > iframe,
.video-cell > .video-cell-toggle {
  grid-column: 1;
  grid-row: 1 / -2;
}

.video-cell > p {
  grid-column: 1;
  grid-row: -2 / -1;
  align-self: start;
  margin: 0;
  /* Row gaps are 0 so that the player-to-caption step can be tighter than the
     gap between rail videos; the captions carry both spacings themselves. */
  padding: 0.5rem 0 1rem;
  font-size: 1.3rem;
  line-height: 1.35;
  text-align: center;
}

/* Rail players are sized by their column, and 16/9 sets the height — they are
   what gives the shared tracks their heights. The bare-iframe selector is the
   pre-JS fallback: until custom.js wraps them in cells the players are direct
   children, and they still paint at 16/9. */
.video-cell > iframe,
.video-row > iframe {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  border: 0;
}

/* The stage fills its span instead of keeping 16/9, because its area is not
   16/9: it inherits the rail's height, and the rail carries three captions to
   the stage's one. Filling is what puts the edges where the request wants them;
   the small surplus becomes letterboxing inside the player, which sits on the
   player's own black background and is not visible in practice. */
.video-cell.is-active > iframe {
  height: 100%;
  aspect-ratio: auto;
}

/* The click target. It exists because a cross-origin <iframe> swallows every
   pointer event inside it — a click on the player itself never reaches the page
   — so "click a video" has to mean "click a transparent layer over it". Only
   rail videos carry one; on stage it is removed outright (below) so nothing
   sits between the reader and the controls of the video they came to watch. */
.video-cell-toggle {
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: #fff;
  font: inherit;
  cursor: pointer;
  transition: background 120ms ease-in;
}

.video-cell-toggle::after {
  content: "watch";
  font-size: 1.3rem;
  letter-spacing: 0.05em;
  padding: 0.35em 0.9em;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.72);
  opacity: 0;
  transition: opacity 120ms ease-in;
}

.video-cell-toggle:hover,
.video-cell-toggle:focus-visible {
  background: rgba(0, 0, 0, 0.18);
}

.video-cell-toggle:hover::after,
.video-cell-toggle:focus-visible::after {
  opacity: 1;
}

.video-cell.is-active > .video-cell-toggle {
  display: none;
}

/* One column has no room for a stage and a rail, and no margin to bleed into
   either, so the gallery collapses to a plain in-measure stack with the staged
   video on top. Cells take their two rows back from the parent — there is
   nothing left to line up with — and the `1 / -2` and `-2 / -1` placements above
   keep working unchanged against a two-track grid. Same breakpoint the sidenotes
   collapse at. */
@media (max-width: 1000px) {
  .video-row {
    width: 100%;
    left: auto;
    margin-left: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
  }

  .video-cell,
  .video-cell.is-active {
    grid-column: 1;
    grid-row: auto;
    grid-template-rows: auto auto;
  }

  .video-cell.is-active {
    order: -1;
  }

  .video-cell.is-active > iframe {
    height: auto;
    aspect-ratio: 16 / 9;
  }
}

/* How far a margin note hangs out of the text column, as a share of the measure.
   Both gutters use it — the left one through `left`, the right one through a
   negative `margin-right` — so the two are mirror images and the page keeps one
   symmetric margin rather than two unrelated ones. With notes 35% wide it also
   fixes the gap between note and text at 2% on each side. */
:root {
  --margin-note-offset: 37%;
}

.sidenote {
  float: right;
  clear: right;
  margin-right: calc(-1 * var(--margin-note-offset));
  width: 35%;
  margin-top: 0.3rem;
  margin-bottom: 0;
  font-size: 1.2rem;
  line-height: 1.3;
}

/* The title page's two notes hang off lines only a few rows apart, so they
   float into the gutter back to back and read as one run-on block. Give them a
   blank line between. Front matter only: in the chapters the prose between two
   notes already spaces their floats apart. */
.sidenote.front-note {
  margin-bottom: 1rem;
}

/* A float whose parent is an *inline* box gets hit-tested as part of that
   inline, so the inline's own fragment ends up over everything preceding the
   float on that line. On the title page the "Jeffrey Zhang" link sits in the
   same `<em>` as the sidenote that follows it, so the `<em>` swallowed every
   click on it — the link was styled and hrefed, just unreachable. Positioned
   descendants paint after floats, so giving those links a layer of their own
   hands the clicks back. Only links *before* the note in the same inline are
   affected; ones after it, or outside the emphasis, always worked. */
:is(em, strong, small, b, i):has(.sidenote, .defnote, .lecnote, .cppnote) a {
  position: relative;
}

/* A blockquote insets its own content, so its notes push out by that inset
   again to land back on the shared gutter edge. */
blockquote .sidenote {
  margin-right: calc(-1 * var(--margin-note-offset) - 20px);
}

/* Pin each defnote to the line of its term instead of float-stacking.
   Absolute + auto top keeps the note's vertical static position (the line
   where the span sits in flow), while left/width place it in the margin.
   Requires a positioned containing block: see `.content main` below. */
/* Pinned the same way, and deliberately the same shape: a `.lecnote` is the
   lecture behind a term, hung in the same left gutter as the term itself.
   Authored as the defnote's SIBLING, never nested inside it — a nested span
   would land the refine ladder inside the note's first inner tag (mdbook-refine
   closes a defnote at the next `</span>`) and would fold the lecture title into
   the slugged anchor id in custom.js. Sharing the line is what connects the two:
   both take their static position from the same line of text, so the restack
   sweep below drops the lecture just under its label with nothing else to say
   so. That is also why one class covers both cases — a lecnote with no defnote
   beside it (the M.L.E. lecture in §1.2.4) simply stands on its own line. */
.defnote,
.lecnote {
  position: absolute;
  left: calc(-1 * var(--margin-note-offset));
  width: 35%;
  /* `--margin-note-raise` lifts one note off the line it would otherwise take,
     for when a tall item hangs lower than the things it sits beside. Set it
     per-note in the markdown (`style="--margin-note-raise: 48px"`); the restack
     sweep in custom.js carries it through, which a plain inline `margin-top`
     would not survive — the sweep clears that property on every pass. */
  margin-top: calc(0.3rem - var(--margin-note-raise, 0px));
  margin-bottom: 0;
  font-size: 1.2rem;
  line-height: 1.3;
  text-align: right;
  /* Muted gray via opacity so it tracks --fg in both light and dark themes. */
  opacity: 0.55;
}

blockquote .defnote,
blockquote .lecnote {
  left: calc(-1 * var(--margin-note-offset) - 20px);
}

/* == C++ notes ============================================================
   A remark that holds for the reference implementation and not for borscht's
   own — what `std::vector<T>` does here, what the chapter's code does instead.
   Same gutter, same shape and same 0.55 muting as a `.defnote`/`.lecnote`, and
   swept by the same restack pass in custom.js, so a C++ note cannot land on top
   of a definition. Authored the same way too: an inline span on the line whose
   vertical position the note should take, and a SIBLING of any defnote sharing
   that line rather than nested inside it.

       <span class="cppnote">*`std::vector<T>` copies on assignment ...*</span>

   The icon is the one thing done differently from a `.lecnote`, which pastes
   its Font Awesome `<svg>` into the markdown. A lecture's icon is furniture the
   author picks per note; a C++ note's icon *is* the note's type, so CSS supplies
   it and authoring stays a bare span.

   The logo rides as a `mask-image` painted with `background-color: currentColor`
   rather than as an `<img>`. That is what lets one file serve both themes: the
   mark takes the note's own color and muting, exactly like the `.fa-svg` lecture
   icon does, so ./assets/cpp.svg needs none of the `_dark` twin that every drawn
   `.png` figure there carries. Dropping the brand blue is the point and not a
   shortcut — a saturated hexagon would be the one loud thing in a margin built
   entirely out of muted gray. */
.cppnote {
  position: absolute;
  left: calc(-1 * var(--margin-note-offset));
  width: 35%;
  margin-top: calc(0.3rem - var(--margin-note-raise, 0px));
  margin-bottom: 0;
  font-size: 1.2rem;
  line-height: 1.3;
  text-align: right;
  opacity: 0.55;
}

blockquote .cppnote {
  left: calc(-1 * var(--margin-note-offset) - 20px);
}

/* `.cppnote-item` is the mobile echo: under 1000px there is no gutter, and
   custom.js rebuilds these notes as list items at the foot of the chapter. The
   marker cannot ride along in the copied innerHTML the way a lecture's inline
   icon does, so the list item is given a class of its own and hangs the same
   mask here.

   `../../assets/` because a url() in a stylesheet resolves against the
   stylesheet, and an `additional-css` entry keeps its theme/css/ path in the
   built book while src/assets is copied to /assets at the root — the same two
   hops every `_dark` figure swap further down this file makes. */
.cppnote::before,
.cppnote-item::before {
  content: "";
  display: inline-block;
  width: 1.3em;
  height: 1.3em;
  /* Sits the mark on the cap height rather than the baseline, the same way
     `.defnote img, img.icon` seats an inline illustration. */
  vertical-align: -0.3em;
  margin-right: 0.4em;
  background-color: currentColor;
  -webkit-mask: url("../../assets/cpp.svg") center / contain no-repeat;
  mask: url("../../assets/cpp.svg") center / contain no-repeat;
}

/* A third-party embed hung in the left gutter — so far only the tweet behind
   the preface's research question. Written as a block `<div>` after the
   paragraph rather than as an inline span on a line: the widget replaces the
   authored `<blockquote>` with an iframe of its own, which cannot live inside
   an inline box. It takes `.defnote`'s absolute placement in the gutter but
   none of its prose treatment — a card is a picture, not prose, so it keeps
   full opacity instead of the 0.55 muting the note apparatus wears,
   and it hugs the gutter's right edge, against the measure, when the widget
   renders narrower than the 35% column. `!important` beats the inline styles
   the widget writes onto its own element. custom.js re-sweeps the stack once
   the iframe swaps in (see the ResizeObserver in the restack block); the card
   goes away with the rest of the gutter under 1000px, which is why the same
   quote also stands as an ordinary numbered sidenote in the prose. */
.defnote-embed {
  opacity: 1;
}

.defnote-embed .twitter-tweet,
.defnote-embed twitter-widget {
  margin-left: auto !important;
  margin-right: 0 !important;
}

/* An illustration set as an icon riding inline with the text rather than as a
   figure: images in a defnote (always icon-sized, the margin has no room for a
   figure) plus anything opting in with `class="icon"` in running prose. Height
   in `em` ties the icon to whatever font-size it lands in; `width: auto` plus a
   per-image aspect-ratio set alongside the src resolves the width before the
   file decodes, so the line never reflows around a zero-width box. Taller than
   the text so the illustration stays legible, with vertical-align pulling it
   back down to sit centered on the cap height rather than the baseline. In a
   defnote it inherits that note's 0.55 opacity, like the rest of the margin
   apparatus. */
.defnote img,
img.icon {
  display: inline-block;
  height: 1.6em;
  width: auto;
  vertical-align: -0.5em;
  /* No trailing margin: every icon so far leads an inline `code` span, and
     mdBook already pads those 0.3em on the left (`:not(pre) > .hljs`), which
     supplies the gap. Adding margin here doubled it. An icon placed before
     plain prose instead needs its own space in the markdown. */
  margin-right: 0;
}

/* Lecture-video markers. mdBook 0.5 bundles Font Awesome Free 6.2.0 and inlines
   it through the `{{fa}}` handlebars helper, but that helper only runs over the
   theme templates — chapter markdown is rendered to HTML before it ever reaches
   handlebars, so `{{fa}}` written in a `.md` comes out literally. The icon is
   therefore pasted as a raw `<svg>` at each video caption, wrapped in the same
   `.fa-svg` span mdBook's own chrome uses so its `width/height: 1em` and
   `fill: currentColor` (css/general.css) apply for free. currentColor is the
   point: unlike the `.png` icons this needs no `_dark` twin to survive ayu.
   `.video-icon` only adds what the caption context needs — a touch more size
   than the surrounding text and a real gap, since a caption is plain prose and
   not the inline `code` span that `img.icon` leans on for its spacing. */
.fa-svg.video-icon svg {
  width: 1.1em;
  height: 1.1em;
  vertical-align: -0.18em;
  margin-right: 0.4em;
  /* Matches the muted weight of the caption line itself rather than competing
     with the link text it introduces. */
  opacity: 0.75;
}

/* Clickable defnotes: behave like header anchors (click updates URL hash). */
.defnote-link {
  cursor: pointer;
}

.sidenote-number {
  counter-increment: sidenote-counter;
}

.sidenote-number::after,
.sidenote::before {
  font-size: 1.2rem;
  position: relative;
  vertical-align: baseline;
}

.sidenote-number::after {
  content: counter(sidenote-counter);
  top: -0.5rem;
  left: 0.1rem;
  color: rgb(0, 150, 207)
}

.sidenote::before {
  content: counter(sidenote-counter) " ";
  top: -0.5rem;
  color: rgb(0, 150, 207)
}

/* Block-level float sidenotes: use data-n attribute so DOM order doesn't matter */
.sidenote-float::before,
.defnote-float::before {
  content: attr(data-n) " ";
  font-size: 1.2rem;
  position: relative;
  vertical-align: baseline;
  top: -0.5rem;
  color: rgb(0, 150, 207);
}

.sidenote-ref::after {
  content: attr(data-n);
  font-size: 1.2rem;
  position: relative;
  vertical-align: baseline;
  top: -0.5rem;
  left: 0.1rem;
  color: rgb(0, 150, 207);
}

.content {
  counter-reset: sidenote-counter;
}

/* Containing block for absolutely-positioned .defnote margin notes.
   `%` in .defnote (left/width) resolves against this text column, matching
   the geometry the old float used relative to the paragraph. */
.content main {
  position: relative;
}

/* --------------------------------------------------------------------------
   Jupyter notebook cells, emitted by preprocessors/mdbook-nb from committed,
   already-executed .ipynb files. Static In[]/Out[] blocks in the style of ISL:
   steel-blue prompts hanging in the left gutter, a cream input box (syntax
   highlighting still comes from mdBook's highlighter), warm-brown text output,
   and a pink box for tracebacks.
   -------------------------------------------------------------------------- */
:root {
  --nb-prompt: #2e6d8e;        /* In[]/Out[] labels */
  --nb-in-bg: #fdf6ec;         /* input code box */
  --nb-in-border: #e7dcc0;
  --nb-out-color: #9c4a1c;     /* text/plain + execute_result output */
  --nb-error-bg: #f7d7d9;      /* traceback box */
  --nb-error-color: #7b241c;
  --nb-stderr-bg: #fdeaea;     /* stderr stream */
  --nb-out-max-height: 22em;   /* tall outputs scroll instead of running on */
  --nb-hl-bg: rgba(184, 127, 0, 0.15);  /* `hl=` line band, okabe orange */
  /* Line height of the In[] code, pinned rather than left at `normal` because
     the `hl=` bands are positioned in multiples of it. mdbook-nb emits the band
     offsets as calc(var(--nb-code-lh) * 1em * n); change this and they follow.
     1.15 is what Source Code Pro's `normal` already came to at the code size, so
     pinning it left every existing cell where it was; raise it for airier code.
     --nb-hl-bleed is how far a band is widened past the text column so it
     reaches the edges of the box: mdBook's `pre > code` padding.

     Both have to be declared here, not down on the code element that uses them.
     mdbook-nb's inline `--nb-hl-size`/`--nb-hl-pos` reference them, and a custom
     property has its var()s substituted on the element it is *declared* on — the
     .nb-in-code div. Anything they name has to be visible there or the whole
     value goes invalid and the band swells to fill the box. */
  --nb-code-lh: 1.15;
  --nb-hl-bleed: 1rem;
}
html.coal, html.navy, html.ayu {
  --nb-prompt: #6bb3d6;
  --nb-in-bg: #1f2733;
  --nb-in-border: #33404f;
  --nb-out-color: #e0975a;
  --nb-error-bg: #3a1c1f;
  --nb-error-color: #e0a0a0;
  --nb-stderr-bg: #2c1618;
  --nb-hl-bg: rgba(230, 159, 0, 0.17);
}

.nb-cell {
  position: relative;
  margin: 0.9rem 0;
}

/* Prompts hang just left of the box (nearer than the wider .defnote margin,
   so the two never collide). */
.nb-prompt {
  position: absolute;
  left: -7rem;
  width: 6rem;
  text-align: right;
  padding-right: 0.55rem;
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.82em;
  line-height: 1.5;
  color: var(--nb-prompt);
  white-space: nowrap;
  user-select: none;
}

/* Source notebook's filename, emitted once per `{{#nb}}` by mdbook-nb. Sits
   above the input box, aligned with it, quieter than the prompt beside it. */
.nb-file {
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.78em;
  line-height: 1.5;
  color: var(--nb-prompt);
  opacity: 0.7;
  padding-bottom: 0.2rem;
  overflow-wrap: anywhere;
  user-select: none;
}

/* When `src-url` is configured the label is a link to the notebook in the repo.
   Keep it looking like the label — the underline on hover is the only tell. */
.nb-file a {
  color: inherit;
  text-decoration: none;
}

.nb-file a:hover {
  color: inherit;
  text-decoration: underline;
}

/* Manual counterpart to .nb-file, for hand-written fenced blocks that aren't
   notebook cells: put `<div class="code-file">path/to/file.rs</div>` (blank line
   after it, so pulldown-cmark still parses the fence as Markdown) above the
   fence and the block gets the same quiet filename header. Wrap the label in an
   <a> for the linked variant, exactly as `src-url` does for notebooks. */
.code-file {
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.78em;
  line-height: 1.5;
  color: var(--nb-prompt);
  opacity: 0.7;
  padding-bottom: 0.2rem;
  overflow-wrap: anywhere;
  user-select: none;
}

.code-file a {
  color: inherit;
  text-decoration: none;
}

.code-file a:hover {
  color: inherit;
  text-decoration: underline;
}

/* Close the browser's default `pre` top margin so the label sits on the box the
   way .nb-file does. `+ pre` covers both the plain block and the `playground`
   one mdBook emits for runnable Rust. */
.code-file + pre {
  margin-top: 0;
}

.nb-in-code pre {
  background: var(--nb-in-bg);
  border: 1px solid var(--nb-in-border);
  border-radius: 4px;
}

.nb-in-code pre > code {
  line-height: var(--nb-code-lh);
}

/* `{{#nb file.ipynb:cell hl=3-5,8}}` — tint the named source lines so prose can
   point at them. mdbook-nb emits one --nb-hl-img layer per range, each sized to
   that range's line count and offset to its first line; here they just get
   plugged into the code element's background.

   Why a background and not <mark> around the lines: mdBook highlights code in
   the browser, and hljs.highlightBlock rebuilds each <code> from its
   textContent, so any markup baked in by a preprocessor is discarded on load. A
   background survives. The cost is that the bands are computed from
   --nb-code-lh above rather than measured, so the code's line-height has to
   stay pinned. Painted on <code>, not <pre>, because the active highlight.js
   stylesheet gives <code> its own opaque background — a band under that would
   be invisible — and because `1em` then means the code's font size. */
.nb-in-code[data-hl] pre > code {
  background-image: var(--nb-hl-img);
  background-size: var(--nb-hl-size);
  background-position: var(--nb-hl-pos);
  background-repeat: no-repeat;
  /* Offsets are measured from the top-left of the text; painting is allowed out
     to the padding box, which is what lets the bleed above show. */
  background-origin: content-box;
  background-clip: padding-box;
}

/* VS Code-style semantic colors for the In[] code: Light+ on the light themes,
   Dark+ on the dark ones. mdBook's highlight.js 10 grammar only knows keywords,
   strings, comments, numbers and one lumped `built_in`; the `.nb-*` token
   classes (function, type/module, variable, control keyword, bracket depth)
   are added after it runs by the Python pass in theme/js/custom.js. Colors go on
   spans only, never on <code>'s background, so the `hl=` bands above survive. */
.nb-in-code {
  --nb-c-fg: #1f1f1f;
  --nb-c-kw: #0000ff;       /* def, class, lambda, and/or/not, True/None */
  --nb-c-ctrl: #af00db;     /* import, for, in, if, return, ... */
  --nb-c-fn: #795e26;
  --nb-c-var: #001080;
  --nb-c-type: #267f99;     /* classes and modules */
  --nb-c-str: #a31515;
  --nb-c-num: #098658;
  --nb-c-com: #008000;
  --nb-c-br1: #0431fa;      /* bracket pair colorization, by nesting depth */
  --nb-c-br2: #319331;
  --nb-c-br3: #7b3814;
}
html.coal .nb-in-code, html.navy .nb-in-code, html.ayu .nb-in-code {
  --nb-c-fg: #cccccc;
  --nb-c-kw: #569cd6;
  --nb-c-ctrl: #c586c0;
  --nb-c-fn: #dcdcaa;
  --nb-c-var: #9cdcfe;
  --nb-c-type: #4ec9b0;
  --nb-c-str: #ce9178;
  --nb-c-num: #b5cea8;
  --nb-c-com: #6a9955;
  --nb-c-br1: #ffd700;
  --nb-c-br2: #da70d6;
  --nb-c-br3: #179fff;
}
.nb-in-code code.language-python { color: var(--nb-c-fg); }
.nb-in-code code.language-python span { font-style: normal; font-weight: normal; }
.nb-in-code .hljs-keyword, .nb-in-code .hljs-literal { color: var(--nb-c-kw); }
.nb-in-code .hljs-string { color: var(--nb-c-str); }
.nb-in-code .hljs-subst { color: var(--nb-c-fg); }
.nb-in-code .hljs-number { color: var(--nb-c-num); }
.nb-in-code .hljs-comment { color: var(--nb-c-com); }
.nb-in-code .hljs-title, .nb-in-code .hljs-meta,
.nb-in-code .hljs-built_in, .nb-in-code .nb-fn { color: var(--nb-c-fn); }
.nb-in-code .hljs-params { color: inherit; }
.nb-in-code .hljs-keyword.nb-ctrl { color: var(--nb-c-ctrl); }
.nb-in-code .hljs-built_in.nb-type, .nb-in-code .nb-type { color: var(--nb-c-type); }
.nb-in-code .nb-var { color: var(--nb-c-var); }
.nb-in-code .nb-fbrace { color: var(--nb-c-kw); }
.nb-in-code .nb-br1 { color: var(--nb-c-br1); }
.nb-in-code .nb-br2 { color: var(--nb-c-br2); }
.nb-in-code .nb-br3 { color: var(--nb-c-br3); }

/* Output: no box, warm-brown monospace, aligned to the code column. */
.nb-out {
  margin: 0.25rem 0 0.1rem;
}
.nb-out-text,
.nb-out-html {
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.9em;
  line-height: 1.4;
  color: var(--nb-out-color);
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  /* Sideways only. A vertical scroller here would be a wheel trap: the pointer
     rests over an output for most of the page, and the browser latches the
     gesture to the box under it, so the page stops moving with the reader's
     fingers. The height cap lives in .nb-clamped below, which hides the
     overflow instead of scrolling it. */
  overflow-x: auto;
  overflow-y: hidden;
}
/* Text output: don't wrap long lines (a one-line list/array), scroll sideways.
   HTML output (pandas tables etc.) keeps normal wrapping so cells lay out. */
.nb-out-text {
  white-space: pre;
}
.nb-out-stderr {
  background: var(--nb-stderr-bg);
  padding: 0.3em 0.5em;
  border-radius: 3px;
}
.nb-out-img img {
  max-width: 100%;
  height: auto;
}

/* Theme-swapped notebook images: a cell may carry a dark-styled variant in its
   output metadata (see preprocessors/mdbook-nb), emitted alongside the light
   one. Show light by default; swap to dark on the dark themes. Single-image
   outputs have no .nb-img-* class and are unaffected. */
.nb-out-img .nb-img-dark { display: none; }
html.coal .nb-out-img .nb-img-dark,
html.navy .nb-out-img .nb-img-dark,
html.ayu  .nb-out-img .nb-img-dark { display: block; }
html.coal .nb-out-img .nb-img-light,
html.navy .nb-out-img .nb-img-light,
html.ayu  .nb-out-img .nb-img-light { display: none; }
.nb-out-html { color: inherit; }

.nb-error {
  background: var(--nb-error-bg);
  color: var(--nb-error-color);
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.9em;
  padding: 0.3em 0.6em;
  border-radius: 3px;
  white-space: pre;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The height cap on a tall output: clamp and offer a button, never a nested
   scrollbar (see the overflow note on .nb-out-text above). theme/js/custom.js
   measures every output and adds .nb-clamped to the .nb-out wrapper only where
   the cap actually bites, so a two-line print() is left alone instead of being
   quietly made unscrollable-past. Cells tagged `full-output` are skipped there,
   which is what keeps .nb-full's opt-out working with no rule of its own.

   The fade is a mask rather than an overlaid gradient: the clamped element is
   the <pre> itself, and a ::after on it would be text at the end of the output
   rather than a layer over it. A mask also fades .nb-out-stderr's and
   .nb-error's tinted backgrounds along with their text, which an overlay in one
   fixed color could not do across six themes. */
.nb-out.nb-clamped > .nb-out-text,
.nb-out.nb-clamped > .nb-out-html,
.nb-out.nb-clamped > .nb-error {
  max-height: var(--nb-out-max-height);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5em), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5em), transparent);
}

/* Quiet and monospaced, matching the In[]/Out[] prompts and the .nb-file label
   rather than reading as page furniture. */
.nb-out-more {
  display: block;
  margin-top: 0.2rem;
  padding: 0;
  border: none;
  background: none;
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 0.78em;
  line-height: 1.5;
  color: var(--nb-prompt);
  opacity: 0.7;
  cursor: pointer;
  user-select: none;
}

.nb-out-more:hover {
  opacity: 1;
  text-decoration: underline;
}

/* `display: block` above outranks the UA sheet's `[hidden] { display: none }`,
   so the attribute needs saying again — custom.js hides the button rather than
   dropping it when a re-measure finds the output no longer overflows. */
.nb-out-more[hidden] {
  display: none;
}

/* Below the Tufte margin breakpoint the left gutter disappears: drop prompts
   inline above their box instead of letting them hang off-page. */
@media (max-width: 1000px) {
  .nb-prompt {
    position: static;
    display: block;
    width: auto;
    text-align: left;
    padding: 0 0 0.15rem 0;
  }
}

@media (max-width: 1000px) {
  .sidenote,
  .defnote,
  .lecnote,
  .cppnote {
    display: none;
  }

  .sidenote-number::after {
    content: counter(sidenote-counter);
    cursor: pointer;
    text-decoration: underline;
  }

  .mobile-only {
    display: block;
  }

  .mobile-sidenotes {
    margin-top: 2rem;
    border-top: 1px solid currentColor;
    opacity: 0.7;
  }

  .mobile-sidenotes h6 {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 0.5rem;
  }

  .mobile-sidenotes ol,
  .mobile-sidenotes ul {
    font-size: 1.2rem;
    line-height: 1.4;
    padding-left: 1.5rem;
    margin: 0;
  }

  /* The lecture list follows the sidenote list under its own heading rather
     than joining it: dropping unnumbered items into that <ol> would walk its
     numbering out of step with the .sidenote-number markers in the text. */
  .mobile-sidenotes h6 + ul {
    list-style: none;
    padding-left: 0;
  }

  /* Every list but the first is preceded by its own heading, whichever kind
     of list it is — sidenotes come as an <ol>, lectures and C++ notes as
     <ul>s. */
  .mobile-sidenotes :is(ol, ul) + h6 {
    margin-top: 1rem;
  }

  /* The margin's muting is the gutter's business: down here the note is body
     content in its own section, which already carries a 0.7 of its own. */
  .mobile-sidenotes .cppnote-item::before {
    opacity: 0.75;
  }

  .dual {
    grid-template-columns: 1fr;
  }
}

.toc ul,
.toc li {
  list-style: none;
}

.toc > ul {
  padding-left: 0;
}

.toc ul ul {
  padding-left: 2rem;
}

/* Hover-expanded contents. A third list level in the TOC is not part of the
   compact outline: it holds that section's subsections, revealed only while its
   entry is hovered (or focused into, for keyboard readers). The panel is taken
   out of flow and hung under the entry, so opening one never reflows the
   entries below it — the compact TOC keeps its shape and the expansion just
   floats over the top. The `li` is the hover target rather than the `<a>` so
   the pointer can travel into the panel without closing it (the panel is a DOM
   descendant, so :hover still holds), and the open delay keeps a mouse merely
   crossing the list from flickering panels open on the way past. Everything in
   this paragraph is the pointer half; the touch half is the `@media (hover:
   none)` block at the end of the section. */
.toc ul ul > li {
  position: relative;
}

/* The affordance: an entry that has an expansion says so. */
.toc ul ul > li:has(> ul) > a::after {
  content: " ▾";
  font-size: 0.8em;
  opacity: 0.4;
}

.toc ul ul > li:hover > a::after,
.toc ul ul > li:focus-within > a::after {
  opacity: 0.75;
}

.toc ul ul ul {
  position: absolute;
  top: 100%;
  /* Indented by the same 2rem step the level above it uses, so the panel reads
     as one rung deeper rather than as a floating box. */
  left: 2rem;
  z-index: 20;
  width: max-content;
  max-width: calc(100% - 2rem);
  margin: 0;
  padding: 1rem 1.6rem;
  background: var(--theme-popup-bg);
  border: 1px solid var(--theme-popup-border);
  border-radius: 4px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
  font-size: 0.92em;
  line-height: 1.6;
  /* `visibility` (not `display`) so the panel is inert to the pointer and to
     tabbing while closed, but still transitionable. */
  opacity: 0;
  visibility: hidden;
  transition: opacity 100ms ease-out, visibility 0s linear 100ms;
}

.toc ul ul > li:hover > ul,
.toc ul ul > li:focus-within > ul {
  opacity: 1;
  visibility: visible;
  transition: opacity 120ms ease-in 140ms, visibility 0s linear 140ms;
}

/* The section's "In which ..." blurb, hoisted into the panel by custom.js from
   the blockquote under its heading. `max-width` is doing two jobs: it keeps the
   blurb to a readable measure, and — since a max-content sized box is clamped
   by its children's max-widths — it is also what stops one long sentence from
   stretching the whole panel to the full text column. */
.toc ul ul ul > .toc-blurb {
  max-width: 58ch;
  margin-bottom: 0.9rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--theme-popup-border);
  line-height: 1.5;
  text-wrap: pretty;
  opacity: 0.72;
}

/* The blurb arrives as the blockquote's own markup — a <p>, sometimes several —
   so its paragraph margins are re-set to the panel's own spacing rather than
   the body's. */
.toc ul ul ul > .toc-blurb > p {
  margin: 0;
}

.toc ul ul ul > .toc-blurb > p + p {
  margin-top: 0.6rem;
}

@media (prefers-reduced-motion: reduce) {
  .toc ul ul ul,
  .toc ul ul > li:hover > ul,
  .toc ul ul > li:focus-within > ul,
  .toc-toggle::after {
    transition: none;
  }
}

/* The toggle (built in custom.js) is the touch counterpart of hovering an
   entry, and only that: with a pointer the panel is already one hover away, so
   the control would be a second way to do a thing the reader can do. */
.toc-toggle {
  display: none;
}

/* Nothing to hover with a finger, so the panels are opened by tap instead. Two
   changes, and the second is the point: they lay out in flow rather than
   floating (a card pinned to a line of text is a pointer idiom — it assumes
   something is hovering to keep it alive), and they start closed, so the
   outline stays the short list it is meant to be until the reader opens one.
   `display` rather than the visibility/opacity pair used above, because iOS
   leaves :hover stuck on the last-tapped element and those rules would hold a
   panel open under it; `display: none` outranks them. */
@media (hover: none) {
  .toc ul ul ul {
    position: static;
    width: auto;
    max-width: none;
    padding: 0 0 0 2rem;
    background: none;
    border: 0;
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    display: none;
  }

  /* Opened, the panel brings its blurb with it — which the old flattened
     fallback dropped, since it showed every panel at once and would have
     carried a paragraph for each. A panel opened by tap is one the reader
     asked for, and the blurb is most of the answer. */
  .toc ul ul > li[data-toc-open] > ul {
    display: block;
  }

  /* The ▾ moves off the link and onto the button, which is what a tap can
     actually land on without also following the link. */
  .toc ul ul > li:has(> ul) > a::after {
    content: none;
  }

  /* Sized to the glyph, not to the finger. A button box big enough to tap would
     be taller than the 2.3rem line the entries sit on, and would push apart only
     the rows that have one — an outline where the spacing tells you which
     sections happen to have subsections. The box stays inline-sized and the
     finger is served by the ::before below. */
  .toc-toggle {
    position: relative;
    display: inline-block;
    margin-left: 0.35em;
    padding: 0;
    vertical-align: baseline;
    background: none;
    border: 0;
    color: inherit;
    font-family: inherit;
    font-size: 0.85em;
    line-height: 1;
    opacity: 0.5;
    cursor: pointer;
  }

  .toc-toggle::after {
    content: "▾";
    transition: transform 120ms ease;
  }

  /* The actual tap target, out of flow so it costs the layout nothing. It grows
     mostly rightward, into the empty space past the end of the entry, because
     that is the one direction with nothing to steal a tap from: vertically it
     stops inside the line's own leading, and on the left it stops short of the
     link text. */
  .toc-toggle::before {
    content: "";
    position: absolute;
    inset: -0.7rem -3.2rem -0.7rem -0.3rem;
  }

  .toc ul ul > li[data-toc-open] > .toc-toggle {
    opacity: 0.85;
  }

  .toc ul ul > li[data-toc-open] > .toc-toggle::after {
    transform: rotate(180deg);
  }
}

p:has(> small > a[href="#table-of-contents"]) {
  margin-top: 0;
}

h2:has(+ p > small > a[href="#table-of-contents"]) {
  margin-bottom: 0;
}

/* Section nav under a heading: prev on the left, up (table of contents) centered,
   next on the right. Each link is a flex item; the outer two take equal space so
   the middle one sits on the true center whatever the label lengths. With no
   prev section, an empty <span> holds the left slot. */
small.secnav {
  display: flex;
  align-items: baseline;
}
small.secnav > * {
  flex: 1;
}
small.secnav > :nth-child(2) {
  flex: 0 0 auto;
}
small.secnav > :last-child {
  text-align: right;
}

p:has(+ ul) {
  margin-bottom: 0;
}

.mobile-only {
  display: none;
}

.dual {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  align-items: start;
  margin: 1em 0;
}

/* `min-width: 0` is what actually keeps the split down the middle: a grid item's
   automatic minimum size is its min-content width, so a cell holding a wide
   <pre> demands more than its 1fr share, stretches its track, and shoves the
   divider off-center (and the second column off-screen). Zeroing it lets the
   tracks stay equal and the code scroll inside its own box. */
.dual > * {
  margin: 0 !important;
  min-width: 0;
}

/* Variant of `.dual` for a primary/aside pair: the first cell keeps the full
   text measure and the second one sits out in the sidenote margin. The grid box
   itself stays the width of the text column (so `100%` is the measure), and the
   second track simply overhangs to the right — landing on the same edge as a
   `.sidenote`. The 1.5rem the grid gap eats is given back to the track, so the
   pair spans 100% + `--margin-note-offset` and ends on the gutter edge. */
.dual-margin {
  grid-template-columns: 100% calc(var(--margin-note-offset) - 1.5rem);
}

/* The margin cell's prompt would hang 7rem left of its box, i.e. on top of the
   first cell's code. Drop it inline above the box instead. Inline-block (rather
   than the breakpoint's `display: block`) puts it on the same line as the
   filename label, so the header stays one line tall and the two cells' code
   boxes still start level. */
.dual-margin > *:nth-child(2) .nb-file,
.dual-margin > *:nth-child(2) .nb-prompt {
  position: static;
  display: inline-block;
  width: auto;
  text-align: left;
  padding: 0 0 0.2rem 0;
}

.dual-margin > *:nth-child(2) .nb-file {
  padding-right: 0.6em;
}

/* Below the sidenote breakpoint there is no margin to sit in — stack instead.
   Declared here rather than in the breakpoint block above so it wins on order
   against the `.dual-margin` rule. */
@media (max-width: 1000px) {
  .dual-margin {
    grid-template-columns: 1fr;
  }
}

/* Prose/code columns want their tops flush (hence `align-items: start` above),
   but paired figures rarely have matching aspect ratios — center them so the
   shorter one doesn't hang off the top of the row. */
.dual > img {
  align-self: center;
  justify-self: center;
}

/* Code in the right-hand cell of a pair gets more room to breathe by
   overhanging the whole right gutter, ending on the same edge as a
   `.sidenote`. Only the code moves: the grid keeps its tracks, so the math or
   prose on the left stays inside the measure and the divider stays put. A
   margin percentage resolves against the grid area, (measure − 1.5rem gap) / 2,
   so twice `--margin-note-offset` of the cell falls short of the gutter by
   0.37 × 1.5rem; the 0.555rem makes that up.
   A `{{#nb}}` embed (`.nb-cell`) counts as code here just like a bare <pre>.
   Where both cells are code, the whole grid widens by the gutter instead, so
   the two boxes stay the same width. */
.dual:not(.dual-margin) > *:not(pre, .nb-cell) + :is(pre, .nb-cell):last-child {
  margin-right: calc(-2 * var(--margin-note-offset) - 0.555rem) !important;
}

.dual:not(.dual-margin):has(> :is(pre, .nb-cell) + :is(pre, .nb-cell):last-child) {
  margin-right: calc(-1 * var(--margin-note-offset));
}

/* Stacked, there is no gutter to overhang into. */
@media (max-width: 1000px) {
  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + :is(pre, .nb-cell):last-child {
    margin-right: 0 !important;
  }

  .dual:not(.dual-margin):has(> :is(pre, .nb-cell) + :is(pre, .nb-cell):last-child) {
    margin-right: 0;
  }
}

/* A plotting cell's outputs hang in the left gutter, mirroring the code's
   overhang into the right one, so the row reads plot | math | code. Same pair
   as above: prose or math, then a notebook cell. The whole `.nb-outputs` stack
   moves, so an Out[] printed beside the plot travels with it, in order. There
   the grid is exactly the measure wide, so with the `.dual` as containing block
   the stack reuses `.defnote`'s gutter geometry as-is, and `top: 0` sits it
   level with the row instead of down at its static position under the code.

   For the `.dual` to be the containing block the cell has to stop being one.
   The gutter leaves no room left of an output for its Out[] prompt, so the
   prompt sits above it, as below the breakpoint. Each output shrinks to its
   content and hugs the measure like `.defnote-embed`, keeping its own lines
   left-aligned, and wraps rather than scrolling sideways in the narrow column.
   The 0.3rem top margin is every gutter note's; the restack sweep in custom.js
   adds its pushes on top of it, which keeps the stack and a nearby note off
   each other. Stacked, the outputs stay under their code. */
@media (width > 1000px) {
  .dual:not(.dual-margin):has(> *:not(pre, .nb-cell) + .nb-cell:last-child .nb-out-img) {
    position: relative;
  }

  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + .nb-cell:last-child:has(.nb-out-img) {
    position: static;
  }

  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + .nb-cell:last-child .nb-outputs:has(.nb-out-img) {
    position: absolute;
    top: 0;
    left: calc(-1 * var(--margin-note-offset));
    width: 35%;
    margin-top: 0.3rem;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
  }

  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + .nb-cell:last-child .nb-outputs:has(.nb-out-img) > .nb-out {
    max-width: 100%;
  }

  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + .nb-cell:last-child .nb-outputs:has(.nb-out-img) .nb-prompt {
    position: static;
    display: block;
    width: auto;
    text-align: left;
    padding: 0 0 0.15rem 0;
  }

  .dual:not(.dual-margin) > *:not(pre, .nb-cell) + .nb-cell:last-child .nb-outputs:has(.nb-out-img) .nb-out-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }
}

.doc-preview-xref .xref-body .dual > :is(pre, .nb-cell):last-child {
  margin-right: 0 !important;
}

.doc-preview-xref .xref-body .dual:has(> :is(pre, .nb-cell) + :is(pre, .nb-cell):last-child) {
  margin-right: 0;
}

/* A verse with its worked example pinned inside it — the broadcasting limerick
   and the shape stack it describes. The blockquote itself is the flex
   container, so the figure sits within the quote's own rule and background;
   `:has` is what picks it out, since Markdown gives no way to put a class on a
   `>` block.
   Flex rather than `float: right` because the verse is a block box: a float
   only shortens its line boxes, and the quote's background would still run
   underneath the figure.

       > line one<br>
       > line two
       >
       > <div class="verse-aside-figure">
       > <img ...>
       > <div class="verse-aside-shapes">...</div>
       > </div>
*/
blockquote:has(> .verse-aside-figure) {
  display: flex;
  align-items: center;
  gap: 2.5rem;
  flex-wrap: wrap;
}

/* Both children size to their own content and pack to the left, so the figure
   sits against the longest line of the verse rather than out on the quote's
   right edge. `flex-wrap` then does the narrow-screen job on its own: when the
   pair no longer fits the measure, the figure drops below the verse. */
blockquote:has(> .verse-aside-figure) > p {
  margin: 0;
}

blockquote:has(> .verse-aside-figure) > .verse-aside-figure {
  flex: none;
}

.verse-aside-figure {
  text-align: right;
}

/* `aspect-ratio` is the asset's own 736 x 1104, so the box is reserved before
   the image decodes and the shape stack under it does not jump. */
.verse-aside-figure img {
  display: block;
  width: 4.5rem;
  aspect-ratio: 736 / 1104;
  margin: 0 auto 0.3rem auto;
}

/* The shape stack is set in the body face rather than as math: it is the
   figure's caption, and KaTeX's bold roman digits read as a display equation
   next to the verse. `max-content` shrinks the box to the widest row so the
   rule under the operands spans exactly them, and `tabular-nums` keeps the
   three rows' digits in columns even in a proportional face. */
.verse-aside-shapes {
  width: max-content;
  margin: 0 auto;
  text-align: right;
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

.verse-aside-shapes > span {
  display: block;
}

/* The leading `1 x` that broadcasting *supplies* rather than the operand
   carrying it. Opacity rather than a color so it recedes by the same amount in
   every theme. */
.verse-aside-supplied {
  opacity: 0.4;
}

.verse-aside-sum {
  border-top: 1px solid currentColor;
  margin-top: 0.15em;
  padding-top: 0.15em;
}

/* Opt-in Goudy Initialen drop cap (gwern-style, with a Renaissance woodcut
   initial in place of gwern's yinit). Wrap a paragraph in a `.dropcap` div in
   the Markdown — keep blank lines inside so mdBook renders the contents:

       <div class="dropcap">

       Opening paragraph text...

       </div>

   Only the FIRST paragraph's FIRST letter is styled; the letter must be an
   uppercase A-Z (the only glyphs the font provides). `::first-letter` honors
   just font/float/margin/color/line-height, which is all we need here.

   Sized as a plain float rather than with `initial-letter`: the two engines
   that implement initial-letter disagree badly on how big an N-line cap is
   (Blink and WebKit measured ~40% apart for the same declaration), while an em
   font-size is the one sizing every engine agrees on.

   Unlike a free-form cap, the boxed initial shows any misalignment, so the
   numbers are derived, not tuned. Every glyph is an 800x800 square sitting on
   the baseline. The square should run from line 1's cap height down to line
   4's baseline.

   The catch: line metrics come from the "first available font", the first
   family whose unicode-range covers U+0020. None of the per-letter Goudy files
   does, so the cap's ascent/descent are the FALLBACK's, not Goudy's. With a
   bare `serif` that meant each platform's Times, and the square sat ~9px high
   in Chrome on macOS. Naming New Computer Modern as the fallback pins the
   metrics to a font that is already loaded and has identical hhea/win metrics
   everywhere (ascent 1.127, descent 0.290).

   Body text (NCM, `.content p` line-height 1.45em), in body ems:

       half-leading                 = (1.45 - 1.127 - 0.290) / 2 = 0.0165
       line-1 cap height            = 0.0165 + 1.127 - 0.683     = 0.4605  below p top
       line-1 cap -> line-4 baseline = 3 x 1.45 + 0.683          = 5.033

   so font-size = 5.033 / 0.8 = 6.29em. The cap, in its own ems (margins on the
   pseudo-element resolve against ITS font-size), with line-height 0.8:

       baseline below float top = 0.8/2 + (1.127 - 0.290)/2 = 0.8185
       square top below float top = 0.8185 - 0.8           = 0.0185
       margin-top = 0.4605 / 6.29 - 0.0185                 = 0.0547em

   The float's margin box ends at 0.0547 + 0.8 = 0.855 (5.38em of body text),
   between line 4's top (4.35) and line 5's (5.8), so exactly four lines wrap.
   Measured in Chrome: square top 7.5px (7.37 derived), bottom on line 4's
   baseline to the pixel. For three lines use 2 x 1.45 + 0.683 in place of
   5.033 and rederive font-size and margin-top.

   No left margin: the glyph's left side bearing is 0, so the rule sits flush
   with the text column. The right side bearing is 40/1000 (0.25em of body
   text); margin-right adds another 0.25em for a half-em gutter. */
.dropcap > p:first-of-type::first-letter,
.xref-dropcap::first-letter {
  font-family: "Goudy Initialen", "New Computer Modern", serif;
  font-weight: normal;
  font-style: normal;
  color: var(--fg);
  float: left;
  line-height: 0.8;
}
.dropcap > p:first-of-type::first-letter {
  font-size: 6.29em;
  margin: 0.0547em 0.04em 0 0;
}

/* The same cap on a hover preview card (`.xref-dropcap`, set in
   theme/js/custom.js on a quoted section opening). A card quotes the book, so
   it should open the way the page does — but it is a peek, ~10 lines tall at
   most, and a four-line initial in it would be the card rather than an opening
   to it. Three lines, and the numbers rederived by the method above, because
   the card sets its prose at a different leading: `.doc-preview` is
   line-height 1.4 (unitless, so it applies to the quoted paragraph) where the
   page is 1.45em, and the geometry is built out of that number.

       half-leading                  = (1.4 - 1.127 - 0.290) / 2 = -0.0085
       line-1 cap height             = -0.0085 + 1.127 - 0.683   =  0.4355  below p top
       line-1 cap -> line-3 baseline = 2 x 1.4 + 0.683           =  3.483

   so font-size = 3.483 / 0.8 = 4.354em, and in the cap's own ems

       margin-top = 0.4355 / 4.354 - 0.0185 = 0.0815em

   The float's margin box ends at 0.0815 + 0.8 = 0.8815 (3.838em of card text),
   between line 3's top (2.8) and line 4's (4.2), so exactly three lines wrap.
   margin-right is the same half-em gutter as on the page — 0.25em of body text,
   which at this size is 0.25 / 4.354 = 0.057 cap ems. For two lines use
   1 x 1.4 + 0.683 in place of 3.483 and rederive both numbers.

   The cap is drawn in the card and nowhere else in the preview: the ghosted
   run-up above a quote is the tail of the paragraph before it, clipped to a
   line or two, and an initial has no business there. */
.xref-dropcap::first-letter {
  font-size: 4.354em;
  margin: 0.0815em 0.057em 0 0;
}

/* gwern's signature lead-in: the first rendered line set in small caps. Because
   it's ::first-line (not a fixed word count), it reflows with the column and
   breaks at the wrap, exactly like gwern's. The ::first-letter above wins
   for the initial glyph, so only the trailing text on line 1 is affected. */
.dropcap > p:first-of-type::first-line,
.xref-dropcap::first-line {
  font-variant-caps: small-caps;
  font-variant: small-caps;
  letter-spacing: 0.02em;
}

/* == Section ornament ======================================================
   A Markdown `---` sets a Vectorian ornament in place of a hairline, the way
   Bernstein & Newhouse's "Old Optimizer, New Norm: An Anthology"
   (arXiv:2409.20325) closes each story with `\pgfornament[width=0.3\columnwidth]{82}`.
   Same ornament, same 30% of the measure, centered.

   The shape is ./assets/ornament.svg, the pgfornament path data converted to
   SVG (provenance and license in that file's header). It rides as a mask
   painted in currentColor, like the C++ note's logo, so it takes the text color
   in every theme with no `_dark` twin. `aspect-ratio` gives the empty `hr` its
   height from its width, and the minimum keeps the eye legible on a phone,
   where 30% of the column is a sliver.

   `../../assets/` for the same reason as `.cppnote`: url() resolves against
   this stylesheet, which keeps its theme/css/ path in the built book. Print
   drops backgrounds by default, and the ornament *is* a background. */
main hr {
  width: 30%;
  min-width: 12rem;
  height: auto;
  aspect-ratio: 454 / 24;
  margin: 2em auto;
  border: 0;
  color: var(--fg);
  background-color: currentColor;
  -webkit-mask: url("../../assets/ornament.svg") center / contain no-repeat;
  mask: url("../../assets/ornament.svg") center / contain no-repeat;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

/* Headings h1-h4 in small caps, echoing the dropcap lead-in above. Scoped to
   `main` so the sidebar/menu chrome is untouched, and it rides on the header
   anchor mdBook wraps every heading title in (font-variant inherits).
   New Computer Modern carries a real `smcp` feature (CMU did not, so these used
   to be synthesized from scaled capitals) — the 0.02em tracking is the usual
   small-caps courtesy. Inline code and KaTeX opt back out: identifiers are
   case-bearing, and `borscht.forward()` in small caps reads as a typo. */
main h1,
main h2,
main h3,
main h4 {
  font-variant-caps: small-caps;
  font-variant: small-caps;
  letter-spacing: 0.02em;
}

main h1 code, main h2 code, main h3 code, main h4 code,
main h1 .katex, main h2 .katex, main h3 .katex, main h4 .katex {
  font-variant-caps: normal;
  font-variant: normal;
  letter-spacing: normal;
}

/* Acronyms set like the headings above. The heading rule only lowers *lowercase*
   letters into small caps, so an already-uppercase acronym would come through
   unchanged; `all-small-caps` adds `c2sc` on top of `smcp` (NCM ships both) so
   capitals shrink too, and the same 0.02em tracking keeps the color even.
   Usage: <span class="smallcaps">HTDP</span>. */
.smallcaps {
  font-variant-caps: all-small-caps;
  letter-spacing: 0.02em;
}

/* A chapter epigraph, set the way LaTeX's `epigraph` package does: a narrow
   ragged-right block pushed against the right edge of the text column, with
   the attribution on its own line underneath. A <div> rather than a `>` quote
   so it picks up none of mdBook's blockquote rule and background. Narrow
   screens get the whole measure, since 60% of a phone is a few words a line.

       <div class="epigraph">

       Philosophy is written in that great book ...

       <span class="epigraph-source">Galileo Galilei [8]</span>

       </div>
*/
.epigraph {
  width: 60%;
  margin: 2rem 0 2.5rem auto;
}

.epigraph > p {
  margin: 0 0 0.5em 0;
}

.epigraph-source {
  display: block;
}

@media (max-width: 1000px) {
  .epigraph {
    width: auto;
    margin-left: 2rem;
  }
}

/* Theme-dependent owl illustration. The default <img src> points at owl.png
   (used by the light/rust themes); these override the rendered image per theme
   via `content`. Paths are relative to this CSS file, which mdBook emits at
   theme/css/custom-*.css, so assets/ is two levels up. */
html.coal img.owl {
  content: url("../../assets/owl2.png");
}

html.navy img.owl,
html.ayu img.owl {
  content: url("../../assets/owl3.png");
}

html.navy img.owl-forest,
html.ayu img.owl-forest,
html.coal img.owl-forest {
  content: url("../../assets/owl_forest2.png");
}

html.navy img.hipowl,
html.ayu img.hipowl,
html.coal img.hipowl {
  content: url("../../assets/hipowl_dark.png");
}

/* Theme-dependent probability-space diagram, mirroring the owl swap above. */
html.coal img.probspace,
html.navy img.probspace,
html.ayu img.probspace {
  content: url("../../assets/probspace_dark.png");
}

html.coal img.vector,
html.navy img.vector,
html.ayu img.vector {
  content: url("../../assets/vector_dark.png");
}

html.coal img.tensors,
html.navy img.tensors,
html.ayu img.tensors {
  content: url("../../assets/tensors_dark.png");
}

html.coal img.randvar,
html.navy img.randvar,
html.ayu img.randvar {
  content: url("../../assets/randvar_dark.png");
}

html.coal img.randproc,
html.navy img.randproc,
html.ayu img.randproc {
  content: url("../../assets/randproc_dark.png");
}

html.coal img.chain,
html.navy img.chain,
html.ayu img.chain {
  content: url("../../assets/chain_dark.png");
}

html.coal img.conditional,
html.navy img.conditional,
html.ayu img.conditional {
  content: url("../../assets/conditional_dark.png");
}

html.coal img.jointbars,
html.navy img.jointbars,
html.ayu img.jointbars {
  content: url("../../assets/jointbars_dark.png");
}

html.coal img.jointmatrix,
html.navy img.jointmatrix,
html.ayu img.jointmatrix {
  content: url("../../assets/jointmatrix_dark.png");
}

html.coal img.stochastic_matrix,
html.navy img.stochastic_matrix,
html.ayu img.stochastic_matrix {
  content: url("../../assets/stochastic_matrix_dark.png");
}

html.coal img.jointmatrixmarginal,
html.navy img.jointmatrixmarginal,
html.ayu img.jointmatrixmarginal {
  content: url("../../assets/jointmatrixmarginal_dark.png");
}

html.coal img.broadcast,
html.navy img.broadcast,
html.ayu img.broadcast {
  content: url("../../assets/broadcast_dark.png");
}

html.coal img.hand-flame,
html.navy img.hand-flame,
html.ayu img.hand-flame {
  content: url("../../assets/hand-flame_dark.png");
}

html.coal img.berouilli_logreg,
html.navy img.berouilli_logreg,
html.ayu img.berouilli_logreg {
  content: url("../../assets/berouilli_logreg_dark.png");
}

html.coal img.losscurve,
html.navy img.losscurve,
html.ayu img.losscurve {
  content: url("../../assets/losscurve_dark.png");
}

html.coal img.neuron,
html.navy img.neuron,
html.ayu img.neuron {
  content: url("../../assets/neuron_dark.png");
}

html.coal img.multiclass_logisticregression,
html.navy img.multiclass_logisticregression,
html.ayu img.multiclass_logisticregression {
  content: url("../../assets/multiclass_logisticregression_dark.png");
}

html.coal img.multiclass_logisticregression_circuit,
html.navy img.multiclass_logisticregression_circuit,
html.ayu img.multiclass_logisticregression_circuit {
  content: url("../../assets/multiclass_logisticregression_circuit_dark.png");
}

html.coal img.ffn,
html.navy img.ffn,
html.ayu img.ffn {
  content: url("../../assets/ffn_dark.png");
}

html.coal img.transformer,
html.navy img.transformer,
html.ayu img.transformer {
  content: url("../../assets/transformer_dark.png");
}

html.coal img.cpugpu,
html.navy img.cpugpu,
html.ayu img.cpugpu {
  content: url("../../assets/cpugpu_dark.svg");
}

html.coal img.ffn_circuit,
html.navy img.ffn_circuit,
html.ayu img.ffn_circuit {
  content: url("../../assets/ffn_circuit_dark.png");
}

html.coal img.ffn_matmul,
html.navy img.ffn_matmul,
html.ayu img.ffn_matmul {
  content: url("../../assets/ffn_matmul_dark.png");
}

html.coal img.neuron_linreg,
html.navy img.neuron_linreg,
html.ayu img.neuron_linreg {
  content: url("../../assets/neuron_linreg_dark.png");
}

html.coal img.vocabindexmap,
html.navy img.vocabindexmap,
html.ayu img.vocabindexmap {
  content: url("../../assets/vocabindexmap_dark.png");
}

html.coal img.onehot,
html.navy img.onehot,
html.ayu img.onehot {
  content: url("../../assets/onehot_dark.png");
}

html.coal img.discriminant_decisionbad,
html.navy img.discriminant_decisionbad,
html.ayu img.discriminant_decisionbad {
  content: url("../../assets/discriminant_decisionbad_dark.png");
}

html.coal img.discriminant_decision,
html.navy img.discriminant_decision,
html.ayu img.discriminant_decision {
  content: url("../../assets/discriminant_decision_dark.png");
}

html.coal img.discriminant_weight_bad,
html.navy img.discriminant_weight_bad,
html.ayu img.discriminant_weight_bad {
  content: url("../../assets/discriminant_weight_bad_dark.png");
}

html.coal img.discriminant_weight,
html.navy img.discriminant_weight,
html.ayu img.discriminant_weight {
  content: url("../../assets/discriminant_weight_dark.png");
}

html.coal img.semanticgap,
html.navy img.semanticgap,
html.ayu img.semanticgap {
  content: url("../../assets/semanticgap_dark.png");
}

html.coal img.venn,
html.navy img.venn,
html.ayu img.venn {
  content: url("../../assets/venn_dark.png");
}

html.coal img.venn_disjoint,
html.navy img.venn_disjoint,
html.ayu img.venn_disjoint {
  content: url("../../assets/venn_disjoint_dark.png");
}

/* Prose color-keys that name a part of a figure ("the orange output ..."), in
   the same Okabe-Ito hues the figures are drawn with. The dark values are the
   palette verbatim, matching the strokes in the *_dark.png variants. On white,
   though, only the blue is legible as text — the palette is tuned for fills,
   and orange/green/purple land at 2.3:1, 3.4:1 and 3.1:1 — so the light values
   are those three darkened (0.8x, 0.85x, 0.8x) to a uniform ~4.3:1. KaTeX
   inherits `color`, so inline math is keyed along with the prose around it. */
:root {
  --okabe-orange: #b87f00;
  --okabe-blue: #0072b2;
  --okabe-green: #008662;
  --okabe-purple: #a36186;
}
html.coal, html.navy, html.ayu {
  --okabe-orange: #e69f00;
  --okabe-blue: #56b4e9;
  --okabe-green: #35c79a;
  --okabe-purple: #e4a0c8;
}

/* Color key off (the menu-bar droplet, wired in custom.js): remapping the
   variables to `inherit` is enough to neutralize every keyed span at once —
   prose and KaTeX alike — because nothing sets a literal hex. Figures keep
   their colors; they're PNGs, and the prose still names the colors, so the
   toggle is about de-noising the running text, not hiding the key entirely. */
html[data-colorkey="off"] {
  --okabe-orange: inherit;
  --okabe-blue: inherit;
  --okabe-green: inherit;
  --okabe-purple: inherit;
}

/* Definition-link toggle: `a.defref` is tagged in custom.js (a link whose only
   content is the italic term, i.e. `[*term*](#anchor)`). Off, it reads as the
   plain italic term: no link color or underline, and pointer-events: none so
   neither the click nor the xref hover card fires. */
html[data-defrefs="off"] .content a.defref,
html[data-defrefs="off"] .content a.defref:hover {
  color: inherit;
  text-decoration: none;
  pointer-events: none;
  cursor: text;
}

.okabe-orange {
  color: var(--okabe-orange);
}

.okabe-blue {
  color: var(--okabe-blue);
}

.okabe-green {
  color: var(--okabe-green);
}

.okabe-purple {
  color: var(--okabe-purple);
}

/* Custom [!QUESTION] admonition, populated by custom.js (mdBook's built-in
   alert renderer only recognizes NOTE/TIP/IMPORTANT/WARNING/CAUTION). Reuses
   mdBook's own `.blockquote-tag`/`.blockquote-tag-title` layout, with its own
   accent color so "pause and think" prompts are visually distinguishable from
   [!WARNING]'s. Burnt-orange, matching .lemma's accent (#c9791f) above. */
:root {
  --blockquote-question-color: #c9791f;
}
html.coal, html.navy, html.ayu {
  --blockquote-question-color: #e0a458;
}

.blockquote-tag-question {
  border-inline-start-color: var(--blockquote-question-color);
}

.blockquote-tag-question .blockquote-tag-title {
  color: var(--blockquote-question-color);
}

/* URL-as-text links (wikipedia / SEP / nlab references, where the visible link
   text IS the full URL) render in New Computer Modern Mono — the Latin Modern
   Mono / Computer Modern typewriter face that arxiv.org/pdf/2311.04329 uses for
   URLs. Code blocks stay on --mono-font (Source Code Pro), so URLs remain
   distinct. Tagged by custom.js; scoped so it never touches code blocks or
   code-identifier links like `nanochat`/`numpy`. `.url-mono code` wins over
   `code` on specificity. */
a.url-mono,
a.url-mono code {
  font-family: "New Computer Modern Mono", monospace;
  font-weight: 400;
  font-style: normal; /* -> NewCMMono10-Regular, upright like arxiv's Latin
                          Modern Mono; `normal` overrides the italic inherited
                          from the sidenotes' *…* wrapper. */
}

/* Module/atom names — Linear, Embed, Conv2D — in New Computer Modern Sans, the
   sans companion of the body roman. This is the `\textsf{}` those names are set
   in in the papers they come from (Large et al. 2024): a family swap inside one
   superfamily, so the name reads as an identifier without --mono-font's
   typewriter color, and without spending `code`, which the book reserves for
   things a reader could actually type. The four NCM Sans cuts are already
   loaded by theme/fonts/fonts.css — this is the first rule to use them.

   Usage: <span class="mod">Linear</span>. */
.mod {
  font-family: "New Computer Modern Sans", sans-serif;
  font-size: 0.97em;  /* NCM Sans runs optically larger than the roman at an
                         equal em (x-height 444/1000 vs 431, caps 694 vs 683),
                         so 0.97 sits its x-height on the body's. */
  font-style: normal; /* NCM Sans is slanted, not truly italic; keep the name
                         upright inside emphasized runs and sidenotes. */
}

/* Definition/theorem "snackbars" in the style of arxiv.org/pdf/2311.04329:
   a titled header bar, a lightly tinted body, and a thick accent rule on the
   left. Usage — leave a blank line after the opening tag so mdBook renders the
   markdown inside:

     <div class="definition" data-title="Definition 2.2.2: Probability measure">

     A **probability measure** $\mathbb{P}$ over ... such that
     1. $\mathbb{P}(\Omega) = 1$,
     2. ...

     </div>

   Use class "theorem", "lemma", or "example" for the accent-color variants. */
.definition,
.theorem,
.lemma,
.example {
  --snack-accent: #1c4a54;   /* left rule (and title, unless --snack-title-color) */
  --snack-title-bg: #cbdde3; /* header bar */
  --snack-body-bg: #ebf3f6;  /* body */
  border-left: 5px solid var(--snack-accent);
  border-radius: 3px;
  margin: 1.5em 0;
  background: var(--snack-body-bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  overflow: hidden;
}

/* Red, matching the [!CAUTION] admonition's brick-red (--blockquote-caution-color
   in the light theme): dusty-rose header, pale-pink body, near-black title. */
.theorem { --snack-accent: #b52731; --snack-title-color: #64151b; --snack-title-bg: #e1a9ad; --snack-body-bg: #f4dfe0; }
/* Orange, sampled from circle A in the venn-diagram assets: burnt-orange
   rule, tan header, pale peach body, near-black title. */
.lemma   { --snack-accent: #c9791f; --snack-title-color: #4a2c0c; --snack-title-bg: #e9c9a5; --snack-body-bg: #f7ebdd; }
/* Amber/yellow, matching the original Lemma screenshot: gold rule,
   warm-yellow header, cream body, near-black title. */
.example { --snack-accent: #c8912f; --snack-title-color: #3d2f14; --snack-title-bg: #f4dfa6; --snack-body-bg: #fdf8ec; }

.definition[data-title]::before,
.theorem[data-title]::before,
.lemma[data-title]::before,
.example[data-title]::before {
  content: attr(data-title);
  display: block;
  background: var(--snack-title-bg);
  color: var(--snack-title-color, var(--snack-accent));
  font-weight: 700;
  padding: 0.5em 1em;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}

.definition > :not(pre),
.theorem > :not(pre),
.lemma > :not(pre),
.example > :not(pre) {
  padding-left: 1em;
  padding-right: 1em;
}

/* ol/ul need more left padding than prose: the marker gutter (numerals,
   bullets) lives inside that padding-box, and 1em is too narrow — wide
   markers like "III." get clipped against the border, reading as "I.".
   Keep the hanging-indent look but give the gutter enough room. */
.definition > ol,
.theorem > ol,
.lemma > ol,
.example > ol,
.definition > ul,
.theorem > ul,
.lemma > ul,
.example > ul {
  padding-left: 2.5em;
}

.definition > :first-child:not([data-title]),
.definition > *:nth-child(1) { margin-top: 0.75em; }
.definition > :last-child,
.theorem > :last-child,
.lemma > :last-child,
.example > :last-child { margin-bottom: 0.75em; }

/* Dark themes: dim the tints so they read as panels, not glare. */
html.coal .definition, html.navy .definition, html.ayu .definition {
  --snack-accent: #7fb3bf; --snack-title-bg: #22333a; --snack-body-bg: #1a262c;
}
html.coal .theorem, html.navy .theorem, html.ayu .theorem {
  --snack-accent: #e2707e; --snack-title-color: #f6d3d7; --snack-title-bg: #3a171b; --snack-body-bg: #2a1014;
}
html.coal .lemma, html.navy .lemma, html.ayu .lemma {
  --snack-accent: #e0975a; --snack-title-color: #f0d3b4; --snack-title-bg: #3a2814; --snack-body-bg: #2c1d10;
}
html.coal .example, html.navy .example, html.ayu .example {
  --snack-accent: #d0a94e; --snack-title-color: #e8d9b0; --snack-title-bg: #322c1c; --snack-body-bg: #262117;
}
html.coal :is(.definition,.theorem,.lemma,.example)[data-title]::before,
html.navy :is(.definition,.theorem,.lemma,.example)[data-title]::before,
html.ayu  :is(.definition,.theorem,.lemma,.example)[data-title]::before {
  border-bottom-color: rgba(255, 255, 255, 0.08);
}

/* Sidenotes inside a snackbar are relocated by custom.js into this footer and
   rendered as footnotes at the BOTTOM of the box, not in the page margin (the
   box's overflow:hidden would clip a margin float anyway). The inline
   superscript marker stays put; only the note text moves here. */
.definition .box-notes,
.theorem .box-notes,
.lemma .box-notes,
.example .box-notes {
  margin-top: 1em;
  padding-top: 0.5em;
  border-top: 1px solid rgba(0, 0, 0, 0.18);
  font-size: 0.82em;
  line-height: 1.4;
}
.box-notes .sidenote {
  float: none;
  clear: none;
  display: block; /* also overrides the <=1000px `display:none` for margin notes */
  position: relative; /* containing block for the absolutely-placed number */
  width: auto;
  margin: 0 0 0.35em 0;
  padding-left: 1.5em; /* gutter holding the number; content hangs past it */
  font-size: inherit;
}
.box-notes .sidenote:last-child { margin-bottom: 0; }
/* Footnote number: use the frozen data-n (the live CSS counter is wrong once the
   note is moved out of document order). Positioned absolutely at the top-left so
   it doesn't occupy its own line — the reference list flows up beside it. */
.box-notes .sidenote::before {
  content: attr(data-n);
  position: absolute;
  top: 0;
  left: 0;
  font-size: 0.75em;
  color: inherit;
}
html.coal .box-notes, html.navy .box-notes, html.ayu .box-notes {
  border-top-color: rgba(255, 255, 255, 0.18);
}


/* == Refinement ladders =====================================================
   A concept the book defines more than once — vaguely first, then precisely —
   carries a ladder of dots: one per definition, the current one filled, the
   others linking to their rung. Injected by preprocessors/mdbook-refine from
   `data-refine="<key>"`; see that file for the authoring syntax.

   Two forms. `.refine-ladder-box` is a strip inside a theorem/lemma/example box,
   reading as a second header bar under the title, with worded steps. Inside a
   margin `.defnote` the label has no room for words, so `.refine-ladder-note`
   is dots alone, hung under the term. A definition box takes the note form: it
   is given a margin label of its own, and its ladder hangs there rather than
   across the box. */
.refine-ladder {
  display: flex;
  align-items: center;
  gap: 0.6em;
  font-size: 0.78em;
  letter-spacing: 0.04em;
  text-transform: lowercase;
}

.refine-dots {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
}

/* Dots are drawn, never written: custom.js slugs a defnote's textContent into
   its anchor id, so any text here would rename existing links. */
.refine-dot {
  display: inline-block;
  width: 0.62em;
  height: 0.62em;
  border: 1px solid currentColor;
  border-radius: 50%;
  opacity: 0.55;
  transition: opacity 0.12s ease, transform 0.12s ease;
}
a.refine-dot:hover { opacity: 0.9; transform: scale(1.25); }
.refine-dot-current { background: currentColor; opacity: 1; }

/* Steps read as directions, not links: no underline, arrows on the outside. */
.refine-step { text-decoration: none; opacity: 0.8; }
.refine-step:hover { opacity: 1; text-decoration: underline; }
.refine-prev::before { content: "\2190\00a0"; }  /* <- */
.refine-next::after  { content: "\00a0\2192"; }  /* -> */
/* Ends of the ladder: the direction still shows, greyed, so the reader can see
   they are standing on the vaguest (or the most precise) statement. */
.refine-step-off { opacity: 0.25; }

/* Box form. The `> ` selectors match the specificity of the .definition rules
   above (padding, first-child margin) so these win by source order. */
.definition > .refine-ladder-box,
.theorem > .refine-ladder-box,
.lemma > .refine-ladder-box,
.example > .refine-ladder-box {
  justify-content: flex-end;
  margin: 0;
  padding: 0.3em 1em;
  background: var(--snack-title-bg);
  color: var(--snack-title-color, var(--snack-accent));
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  /* Sits under the ::before title bar; a hairline separates the two. */
  box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.06);
}
/* Restore the gap the title bar normally leaves before the body: the ladder
   displaced the first child that `*:nth-child(1)` was targeting. */
.definition > .refine-ladder-box + *,
.theorem > .refine-ladder-box + *,
.lemma > .refine-ladder-box + *,
.example > .refine-ladder-box + * { margin-top: 0.75em; }
html.coal :is(.definition,.theorem,.lemma,.example) > .refine-ladder-box,
html.navy :is(.definition,.theorem,.lemma,.example) > .refine-ladder-box,
html.ayu  :is(.definition,.theorem,.lemma,.example) > .refine-ladder-box {
  border-bottom-color: rgba(255, 255, 255, 0.08);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

/* Margin-note form: its own line under the term, right-aligned like the label. */
.refine-ladder-note {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.25em;
}

/* Landing marker: stepping to another rung scrolls to a ladder that is a few
   pixels tall, so tint the definition it belongs to instead. */
@keyframes refine-arrive {
  from { background-color: rgba(28, 74, 84, 0.22); }
  to   { background-color: transparent; }
}
:is(.definition, .theorem, .lemma, .example):has(.refine-ladder:target),
.defnote:has(.refine-ladder:target) {
  animation: refine-arrive 1.6s ease-out;
}
/* A margin note is dimmed to 0.55 in the gutter; bring the one just stepped to
   up to full strength so it is findable. */
.defnote:has(.refine-ladder:target) { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  :is(.definition, .theorem, .lemma, .example):has(.refine-ladder:target),
  .defnote:has(.refine-ladder:target) { animation: none; }
}


/* == Source logos as reference-list bullets =================================
   Each logo is defined once as a data URI (self-contained). A .ref-list renders
   references as a list whose BULLET is the source logo, picked from each link's
   destination and hung into the indent (wrapped URLs align cleanly). PCM has no
   domain/favicon, so it's matched by its book DOI (9781400830398) and drawn as a
   masked nautilus shell that tints with the text color. */
:root {
  --logo-mathworld:  url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAKSElEQVRoge2Ye3SV1ZnGf8/+vnNObifQhLsXUEAuRlAQULxSrVWpraVaLg5YdDnVDg5xwJnWUU/jtFW0rZVI27S6ABGF4HJaqjhddNphqrNgIVC5VcDBCwEkICQhyck5Od9+54+D7VqdtZJwaf/K8++3vr2fZ7/vu/f7PtCNbnSjG904Deivs6yJ1Lf//9pVVQbYmdzpzAlIpUKSw/uT0EiwCeAGI4pPfPXAYYydEG3gSOY9qmY3nIltT19AqraEXlyEMQVnNwODMRxSK2YZZIWgYsDyh6/jwAYi/0uCaA1bWw7ws6+3/+0F1NTEyJWNw9vdyK4HBiA5zNJgn2BsAw5hjEKqQMqAlQBtQBysDdNOzF7FZ1ZQOWsf6KTT69QEPPVCMYUFszA/F2kgqBlsN9AT2VIi2wbmwF2F03TQe+CHgAYgWjEa8tHRoBOC3sSiR5kzfRM6ORHBSZN/traEwM0HHsKpJ95Wgd2P3Ot4q8CxDC+Hs6FIYxBDwI6DCzBiSKUnUqoYqRGUxGwIuIvZsG0X4yvqWLeuyyJOTsBTLxQTuPlI9yKLYbxN2s+hoL0BC64BTQUNRtYbXBOwAaceoAymvUgR0AAW4lQKVgzWgtSOOA9pLAVl7zOhYm9XRYRdJl9TE6O9YBZmc4DNQD3iYwo1mihxF6IAqR7YQktTNekLWiitj5FgKDA3Xxf+O8hdj7d6As0Ey2FKIwaAHcR0Li54jL4j6oDtXaHV9QjcdOdlYN/D0YZy/4wPypG7FafRwBvkoicwDuM0hUTRemL1CeLhfDxXg5ZgVorcLUCId/+OiwaB2wQWgvYjZcE2gS7HcFz7d2/xmxXZzmi5LpFP1Zbg7W7Mzsb7H2OBIcZh1gN8NVsbaqiccYjWaA3YBrxVEyv6IaaQ0N3Lf+tp2tvuBfskf2x+GriBGCOROw8YgrQBp3LgKNJXiLdO6gq1rgnorQvBX4+cR3wAegBsM7ADcwPofyB/m7XtPo73byMbAroQYyPf2LGHVV+NKCo6CyRyeoSg/VE8yzB2Yf4+zN7Csw1vyxEFQD9CdycLfpHsjFrnKZRKhRT1mgO6FofHNAyxEd/+BN4dwGkmxX3eYdzII5QP/xzOPYTZK4g/AHdwU69yrvvKYULNxutjCo8t5b676rlhSgan80gXvEisvQ7xBZRbggVxxERgIPHcb1nzyocd0es8Asnh/YHJiF3APrAMGRYxd2YT7dl1eHsb7GHKh03D6R6MF8nqcTJ8n9D9K9JAEu5pjFuR305bUSHgkB1FKibMxHDBFsRxfHALogw4jijENJWamtjpCUgwGjEIyOGtCLkQ8/n/HpzVQmA14NtxwQKwd/HZZcybmmbe1DT33b6OKHoM6SCQQdyFiz1H9coHCWNXIEsS11lYNCD/drhKjL4YPweaEeNJJ8tOQ4AJGINZDLM+GIvAZ0i4yX86mUw8By7IF6guxbvLSS0u+HQBAgLMJ8H+HmMOso1gY8AewHQDsiXI/RKny0AQ+WdQtBR0CNGbwJ3VEcOOayCFo6jPbGA00jtY9gm8+yPSdNrD/Xz5a4dR5h8wyomoRGrBuVkUBaOYPLWewVceo7z0S0AZzdHLzJvxPuMq1tPa89eUuP8B9cPnniTyPyXLSwQ6G2elZIPfEdg1oMEYr/PGK3tPMQKAKEYCaTsZ6sm534PfSBA+RLZtBmgizp6lctp2/MeLwR4BVwL2NMP6fhPZbLA32RxrAqCqylM1s4koewTzjVj4DpV37GH+9H1YtAk0nFjmHKT+QIgn3hG9TgRcGGIWIoTZefhsD+ZNTePdz/Dsx+xJjJ1YsBWAuXMz3D9tM22ZeZhewHQbqALcBVytS3h8+WdI1cbzqRkkwJVQkPtzy2DhbtBQXOJboN5InfYKnQjYkUPK4i2H2SCKkl+ktjagoKEB+V04jgAjMH87zywr/dNvD85qwUW/AeowViIVg/8XSoJFlNu3qa79OgSTgUIybgQLl49n0aopBP4LiEGYNWN+LYbhrcOeqCu90GFEGvgvsNupD3biSuqQuxHju8hyoPsIYmP5/ssLmT99Dxj4leOR4jS0fo+EayAqKKcgGkYQjEGMRYzC6I9zj2MujY+OAvVgh5BbgtkMoA3zracuoKrKWLhiByIAdmF8hHIponALWCPZ479ifY9GLs/8kXjsYQqChVSvWIJb8RZenwe/hoe/dpD8SNkCfASs5Zk1CdS0AFkTuXAZufY2grbj+JwjXro6v5+GYdZE6Oo7othZERs51iNlkSaCW4VYg/gy0k6a6ppY9dWIf5q5GRfdjeclpC+B+ynSWCJ2k1oc5y8Hp+yREKezMdtB5W35An5gdgO+MAkIbzdjNgr4kFxu/+kIgMb0XmAjMAlxObloNWbHMCZTPvJ2frCyEIBv3HGMT3q/RLr9mxhpIIn0LXoV/YiFtTfz1IsDSaXyEVdwDtAPac+fN0o5CoKLcBqEYzaiJ+JXHH2vuSN6XRspF66cg2MBsDf/mNnVmG3B6TZMq2nJLiX93gGqqjw/qR1Jzr+I8RhmrThNBncZWAtYHcZ2HKWYbiUX3Y1I49z5GOMIdCNGT2RJTE1EuZuovGNPR9S6Ng9MvvUQFlyNbBTSpcC7NGefJB6+CVxBIphCcZ8kN0xrBn8PzmVo0bPMn7aTG6/6HdbzNZTbhtGEdC5iKuJDnIbidAnQD0eENAp4H6kv0mK2N61m02u+I2pdm8i2thygoserSCNOiB5Dj8RE3mn8Dy7q/Rg+cw1Os3HRnYiziaJ/Y9Or+dDPnZsB6oA6zNaxaMVnwU3ElCJru3Bxj2v2KLgWc9NBF4N9RDaq7Yrd0rUIbHrN8/lb9uHC0UAFUhIYS59EIVFuO0d3bSZXuJbC+JWgC5Drz8ARPbluSoZJUzJ88RpjwgRjazoOmWmINNsanufxWa2Ubmunv5UQC+aCrkCAZwHH+q1l3dIOTx9OylYxUf3yWAieRYwA+0+MZnBZ8L8G9y74R5GtxqwRufEY54PagcNIBzFrQ0zH7HUi/zZh2APvz0eagHQdWAzP86itivvv/KQrrE7OFzIT1SuuxLlq4ByM3wN/AMbkbw1roT36R47t/l9KRyZQtidBbCBOw4AhwAwgA3YQ1AQ6CnYpsgvzzoUtJ/KPUDnjUFcpnZytUlUF4yvqKCh7H7mL828DvXCsxewSTGU4N46C8h7gPc4dIwz2E9duPEOBwUS6n0T4PK3ReuJ2LjAJ5PEsxvuqkyEPp+rMpVKOviNGEnEPTlMwPoNZDum3ePsIEcdRiFGYtwvNg64CvYvYjrc+SMOBgWAf4O05lHmlq2lz+gI+xYJfJIm3TiIezMTrc8jHT5i3jRgfA4YYCRRh1o5cEZxozox9SMvJ5mpp7L+Lqkm5U6FwZuz1Bc8lKS4djdl0xHhEb4wkECKBYeQ90EbwH4JeI5d7g53NH5yOM33mBHyKmpoY6WQZgTsLowxPnBDwZphvRYlDpMP9tG5poaqq0yuyG93oRje68VfH/wGdlFfhAo71zAAAAABJRU5ErkJggg==");
  --logo-wikipedia:  url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAZlBMVEX///8AAAAzMzMwMDAtLS0eHh4oKCj6+voYGBgaGhrR0dFbW1s9PT3z8/NPT09WVlYQEBDo6OhjY2Pf3995eXnExMRtbW1FRUWXl5doaGi+vr6zs7OdnZ3Z2dkjIyOQkJCEhISmpqYcB05TAAABn0lEQVRYhe2UyWKEIAyG2XFDUdxm3Mb3f8mCgJ0e6mBPPfBd/AUTEpIIQCQSiUT+Kz1kQ5WXhYRqhHjQisJlg6zMhxRmoxbVIGH9uwOuNkkIpqMAvO4pIWwy6sHY+jyWGMGduA5ioog0VjYEl4dQCbGn1klycTzwZt7BItGqjOjgblcU3D7agyxBVFn5IKw1z3xwe3P6If6DArOX85UiqUNeYGffedEG2JtbkNzKhhhfee52OqhCHAiEV5dzl2IiMpi5nbwMsQdgY3hwIeh0xsoHoHwqn6glou5TnQ5OFu8Y8TAHppKVVbzApHerYp0D7XXqSLrr2tcznQmG1NBSnpUUuivcjYbV8DyX2OP2BGF8qAUuVyY/4bqJj3MFeyFEJyP7/NrmJy3DyDxnaaQpv5DPOw4UPSrJ4WSKmmo50zv2uukIeejKF8BPNQ2uoeVJUaqENP20UJRknQyvoWVFbBsLH03/uFFDy7wiLO0U6b4iYXP4Tr2evzY9Ur6179Ay3856pGjgHL6TQT9FeqSK0Dl8p/n+AU/7H+wjkUjkJl+4BBLrIwrZsAAAAABJRU5ErkJggg==");
  --logo-grokipedia: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAYFBMVEUNDQ0AAAD8/Pz///8HBwfp6enLy8v4+PgYGBgvLy+8vLzT09PHx8fOzs6EhISrq6vg4OBBQUGcnJw5OTlXV1eNjY0mJiajo6N7e3va2tqWlpYfHx+2trZISEh1dXVsbGwzpoV0AAACBUlEQVRYhe2W2ZaDIAxAGxapWnG32vX//3LYbEcLwfacmafmTZJcTCAJu91XvoILLOQD76ssk3yqpjwp+1v6FoQBjAe6FFL09VYIQEOUh/LiRVaWWS7Ul/6klZBxAoORGPtJPhLQHoVa0otxAECubSnvANhzEUpL6GMASM1e9LKKlsHdrJ8iAGhpyM4QaIMDYG/3L31mOgq/5pdNZeP3W4HQoaH+jc1UHQD0lB4wAAzWPwkYMaC0QAGZBXQhIzjQCQHMPxDIgLaQiFKpjxYQTjQbTiMG4BZwD9swrJggNf6EPm/we6ICNADxdu+YARcbQf4xILcA9KqgAB47hBhA2BxG6g0BuEOIVfzfA45rADNtkTkJD4k5B+uKZ8O1TtNhb+WctnXdegm2mZCXglWNaC2VH1BYwFoL48QptfGZzk4m/0m7xq1qYa1Qkkrh/MshmIObA1w9eoBubhbhYsfPEfB2Z0xyrJqAG2WGAUb3C2fvVDClggJ2QAJXaTOgd4fl60lbAI+K9o2fbYDWZcEzGTYBsDw6AHaMxuxkgxDDy33cBpgTSdV0WJo6QLzlQkfcnaufl16XA8cm/4Kg+ruxpdWpc4+sWmZu7aXUPMJgyOj8sBO84uLRCAq5bWypPWVClk1EFI3+n61jT7e+dmwuSZEnyaWR3e6D9/Lirf3hwP3KV/5JfgDkFRJivFlZxgAAAABJRU5ErkJggg==");
  --logo-pcm:        url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAilBMVEX////yjTDzkB3yiyzzmk3++fPzjRD97NzzigD2sWzxhhf74tPxhx/5z67xhRLzjhf2tYD1oEP4v4n0lif859b5zKLzlUD+8+ryiyb4w5n4xJLykTT4yaTyhgD1qGj1rXL617v3u4373cX1pE/0n1b606z0nDj1qVr0o173un785M761rTxfwD2rWJzOlF8AAACBklEQVRYhe2XXXejIBCGESJalmC6GoMxnyabtun2//+9ZYYkp3vOINr20veKo8PDzDAwytikH9SvHlWrAYDaqLB0spvHIDOR9EgIrXfVNwAgq/Z9XiBAB2Q93dpDBLCZk2r2rbEYiWn6ACJJg6+zo0aEWnwRwFi6UOCkCvkQBTB2wFSo7ssAVglHEC1t9QBksiz5EzxaFiWIl6d1diMo2KpjBJAXhfQAN/LKt09+2TkQDFkOnwCc3wH8oZx7J84uCE3mkQTcHABCwXHdq4IsDAT8uTyjLrl0BPlyt0sMdSoIwEPZCwST4/5ttIthMxLA2Ie8P4SNsPvRgJUDFCW8TqEUzqMB7OQymWMaa2dYjwdADN8CXJwHEkNoneFsNKBzD4sLjFbuVIvXsYCMQwrWMIRKstSlQADSzKtaSyhGn4IG6mA+DLDc5jfhaViiXQ2VmA0EfDpMPPdRdXAWqE2IAOT2zZu1IlDJdAgS5WI4Xb3V0WVAWPJKIgDd22/U+v1+gxzwRiIdiBWSn6+hu1BVNAzQwL0uNLUFQwDVDNZPzIGeHzvO72eD/dFQNRQCdIubXmutcbpQofVJwMZYL+Gbs9CzQPwhgP7vG0OLcGuOAYTVqm36Gx8F+Gu8VLtr+r9vaED6UGxyADBOE2ACTAAv/OVBgOuI24/xgKxygkF6dYOeq2vSj+sfqlooEnLksIYAAAAASUVORK5CYII=");
}
.ref-list { display: block; margin: 0; padding-left: 0; } /* indent comes from .sidenote's gutter */
.ref-item { display: block; padding-left: 1.5em; margin: 0.2em 0; line-height: 1.35; }
/* Whole reference list in New Computer Modern Mono Regular, upright — matching
   arxiv's Latin Modern Mono URL style (deterministic; also covers the non-URL
   PCM citation, keeping the list uniform). */
.ref-item a { font-family: "New Computer Modern Mono", monospace; font-weight: 400; font-style: normal; }
.ref-item a[href*="mathworld.wolfram.com"]::before,
.ref-item a[href*="wikipedia.org"]::before,
.ref-item a[href*="grokipedia.com"]::before,
.ref-item a[href*="9781400830398"]::before {
  content: "";
  display: inline-block;
  width: 1em; height: 1em;
  margin-left: -1.5em;   /* hang into the .ref-item indent, acting as the bullet */
  margin-right: 0.5em;
  vertical-align: -0.15em;
  background: center/contain no-repeat;
}
.ref-item a[href*="mathworld.wolfram.com"]::before { background-image: var(--logo-mathworld); }
.ref-item a[href*="wikipedia.org"]::before        { background-image: var(--logo-wikipedia); }
.ref-item a[href*="grokipedia.com"]::before       { background-image: var(--logo-grokipedia); }
.ref-item a[href*="9781400830398"]::before        { background-image: var(--logo-pcm); }
/* Dark themes: the black Wikipedia W would vanish — flip it to white. */
html.coal .ref-item a[href*="wikipedia.org"]::before,
html.navy .ref-item a[href*="wikipedia.org"]::before,
html.ayu  .ref-item a[href*="wikipedia.org"]::before { filter: invert(1) brightness(1.7); }

/* --------------------------------------------------------------------------
   Hover previews (behavior in theme/js/custom.js). One small floating card,
   shared by both kinds of peek: `.doc-preview-docs` for a library documentation
   link, showing the fetched function signature + summary, and
   `.doc-preview-xref` for a cross-reference into the book, showing the target
   block quoted out of its own page. Reuses the notebook palette (cream in
   light, dark in the dark themes) so it reads as part of the same surface
   family; the signature line borrows the In[]/Out[] prompt color and the book's
   --mono-font.
   -------------------------------------------------------------------------- */
.doc-preview {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1000;
  max-width: min(420px, calc(100vw - 24px));
  padding: 0.7rem 0.8rem;
  background: var(--nb-in-bg);
  color: var(--fg);
  border: 1px solid var(--nb-in-border);
  border-radius: 6px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  font-size: 1.35rem;
  line-height: 1.4;
  opacity: 0;
  transform: translateY(-2px);
  pointer-events: none;
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.doc-preview.visible {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.doc-preview-sig {
  font-family: var(--mono-font, "Source Code Pro", monospace);
  font-size: 1.2rem;
  color: var(--nb-prompt);
  white-space: pre-wrap;
  word-break: break-word;
  margin-bottom: 0.4rem;
}
.doc-preview-summary {
  margin: 0;
}
.doc-preview-loading {
  color: var(--nb-prompt);
  font-style: italic;
}
.doc-preview-src {
  margin-top: 0.5rem;
  font-size: 1.1rem;
  font-family: var(--mono-font, "Source Code Pro", monospace);
  opacity: 0.65;
}

/* A cross-reference card quotes the book's own markup — math, emphasis, inline
   icons — rather than text pulled out of a foreign page, so it is set in the
   body face at the card's size and mostly just needs its page-level margins
   taken off. It runs a little wider than a signature card because it holds
   whole sentences. */
.doc-preview-xref {
  max-width: min(460px, calc(100vw - 24px));
}
/* Where the quote comes from: the name of the section it sits in, or a chapter's
   own title for a bare chapter link. A quote that carries its own heading — a
   section, a numbered box — gets none, since that heading rides in the run
   itself. It names a heading, so it is set like one — the same real small caps,
   0.02em tracking and ink as `main h1`-`h4` above, in the body face — and is
   held apart from the prose under it by small caps and size. (`text-transform:
   uppercase` would defeat this: `smcp` needs real lowercase letters to lower,
   and pre-uppercased text comes through as plain capitals.) */
.doc-preview-xref .xref-title {
  font-variant-caps: small-caps;
  font-variant: small-caps;
  letter-spacing: 0.02em;
  font-size: 1.25rem;
  margin-bottom: 0.45rem;
}
/* A run too long for the card is capped rather than allowed to fill the screen
   — this is a peek, and the link is right there for the rest of it — but capped
   is not cut off: the chapter either side of the quote scrolls in place, and the
   card opens scrolled to the quote rather than to the top of the run (`fill` in
   theme/js/custom.js). `overscroll-behavior` keeps that wheel off the page
   underneath, which matters twice over here, since a page scroll is what
   dismisses the card.

   Both ends fade while there is more past them, and both fades are cut into the
   element's own box by one mask. A mask rather than the absolutely-positioned
   overlay this was before it scrolled: an overlay inside a scroll container is
   painted in the *content* box and slides out of view with the first wheel
   click. One mask rather than two, because two would be two declarations of one
   property and the second would win — so the ends are the custom properties
   below, and at 0 a gradient's two stops meet, which leaves that end simply
   opaque and costs an unclipped card nothing.

   The head fade doubles as the run-up the card used to draw as its own ghosted
   strip above the scroller: `fill` leaves the last 2.4em of the block before the
   quote showing over this 1.2em fade, so the page still thins out into the
   card's edge, and that block's last line sits below the fade, legible, with the
   quote itself under it at full ink. */
.doc-preview-xref .xref-body {
  --fade-top: 0px;
  --fade-bottom: 0px;
  max-height: min(28em, 60vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),
    transparent 100%
  );
}
/* Drawn only where there really is more, by measurement in theme/js/custom.js
   and re-measured as the card is scrolled: a short quote gets no phantom fade, a
   reader who has scrolled to the foot of a long one sees its last line plainly,
   and one who has scrolled to the head of the run sees its first the same way. */
.doc-preview-xref .xref-body.xref-clipped { --fade-bottom: 3em; }
.doc-preview-xref .xref-body.xref-clipped-top { --fade-top: 1.2em; }
.doc-preview-xref .xref-body p { margin: 0 0 0.5em; }
.doc-preview-xref .xref-body > :first-child { margin-top: 0; }
.doc-preview-xref .xref-body > :last-child { margin-bottom: 0; }
.doc-preview-xref .xref-body img { max-width: 100%; }

/* A heading the reader has scrolled down onto. The card's quote runs on past
   the section it was opened for (see `blocks` in theme/js/custom.js), so these
   are the boundaries it crosses on the way, and they are set like the card's
   own title one size down — same real small caps, same tracking, same ink —
   so that a section met halfway down the card reads as the same kind of thing
   as the one named at the top of it. The rule above each is what a heading's
   whitespace does on the page: `h2, h3` carry a 2.5em block start in
   general.css, which inside a 16em card would be a screenful of nothing. */
.doc-preview-xref .xref-body :is(h1, h2, h3, h4, h5, h6) {
  font-variant-caps: small-caps;
  font-variant: small-caps;
  letter-spacing: 0.02em;
  font-size: 1.2rem;
  margin: 0 0 0.45em;
  padding-top: 0.9em;
  border-top: 1px solid var(--nb-in-border);
}
/* Nothing to rule off at the very top of the card: the first heading in a run
   is either the card's opening or right under its title. */
.doc-preview-xref .xref-body > :is(h1, h2, h3, h4, h5, h6):first-child {
  padding-top: 0;
  border-top: 0;
}
/* Identifiers are case-bearing, so they opt back out — `borscht.forward()` in
   small caps reads as a typo. (The same courtesy `main h1`-`h4` extends above.) */
.doc-preview-xref .xref-body :is(h1, h2, h3, h4, h5, h6) :is(code, .katex) {
  font-variant-caps: normal;
  font-variant: normal;
  letter-spacing: normal;
}
/* mdBook wraps every heading's title in its own anchor. On the page that anchor
   is invisible — a heading is not blue — and in the card it stays that way; it
   is still live, so a reader who has scrolled to a section can click into it. */
.doc-preview-xref .xref-body :is(h1, h2, h3, h4, h5, h6) a {
  color: inherit;
  text-decoration: none;
}
/* A code block quoted mid-run, set a notch down so a card's worth of it is a
   useful amount. It keeps its own horizontal scroll — a card is narrower than
   the text column, and wrapping code to fit would misrepresent it. */
.doc-preview-xref .xref-body pre {
  font-size: 0.88em;
  margin: 0 0 0.5em;
  padding: 0.5em 0.6em;
  overflow-x: auto;
}
/* A side-by-side pair, met in a card that is narrower than one of the two
   columns it wants. It stacks, which is what it already does on a phone (the
   `.dual` rule in the mobile block above). */
.doc-preview-xref .xref-body :is(.dual, .dual-margin) {
  grid-template-columns: 1fr;
  gap: 0.6rem;
}
/* A figure quoted mid-run. `.full-bleed` widens itself to 90vw and pulls left
   by half of that, which in a 460px card would hang most of the image off the
   side of the screen; in here it is simply as wide as the card. */
.doc-preview-xref .xref-body .full-bleed {
  width: auto;
  left: auto;
  margin-left: 0;
}
/* A quoted definition box keeps its own border and numbered title bar — it
   should look like the thing the reader is about to jump to — and loses the
   page margins it would otherwise press against the card's padding. It keeps a
   little below it: a box is no longer the last thing in a card, and the prose
   that now follows it needs to be off it. */
.doc-preview-xref .xref-body :is(.definition, .theorem, .lemma, .example) {
  margin: 0 0 0.6em;
}
/* The word the link was pointing at, banded in the same wash the notebooks use
   to point at a line of code. */
.doc-preview-xref .xref-term {
  background: var(--nb-hl-bg);
  border-radius: 2px;
  padding: 0 0.15em;
  margin: 0 -0.15em;
}


/* Blinking caret for the title-page typewriter (see `.retype` in custom.js). */
.retype-active::after {
  content: "▌";
  animation: retype-blink 1s steps(1) infinite;
}

@keyframes retype-blink {
  50% { opacity: 0; }
}
