
    /* Layout design tokens. Single source of truth for the widths that
       get re-used across bleed mechanisms + the TOC sidebar. Every
       wide-content selector below computes its margins/caps from these
       so "how wide does our content go?" and "how much room does the
       TOC claim?" are one-liner edits, not a scattergun. */
    :root {
      /* Cap for `.figure.bleed-wide`'s middle grid track. Body sizes
         to its widget's max-content; this is just the upper ceiling
         for "as wide as the viewport will give us". */
      --content-max-width: 1800px;

      /* TOC sidebar geometry. --toc-safe-zone is the horizontal strip
         (from viewport-left) that bled content must stay clear of —
         TOC width + its right-side gap. Default 0 (symmetric bleed,
         no avoidance); the gated rule further down lifts it to the
         real value only when #toc-sidebar is actually in the DOM
         AND we're on a wide-enough viewport to render it as a
         sidebar rather than inline-collapsed. */
      --toc-width: 264px;
      --toc-gap: 24px;
      --toc-safe-zone: 0px;

      /* Minimum horizontal clearance between bled content and the
         viewport edge. Lives on the bleed-figure grid's gutter tracks
         (as their `min` value), so bled content can never sit closer
         than this to the viewport edge — both at the right edge and
         at the left edge when the TOC isn't there. The bleed wrapper
         itself goes flush to the viewport; only the inner content
         track is constrained by this token. */
      --bleed-edge-inset: 1rem;

      /* Bleed-wrapper margins. Anchor a wrapper's edges to the actual
         viewport edges by negating `.posts-rt`'s viewport offset. The
         `--posts-rt-{left,right}` props are written by syncBleedAnchors()
         in js/init.js from a live `.posts-rt.getBoundingClientRect()`.
         The fallback `calc(50vw - 50%)` is the math equivalent of the
         old `calc(50% - 50vw)` rule and resolves correctly when the
         parent is centered (dev.html, Vercel) or when JS hasn't run
         yet — so first paint matches the centered case, then the
         measurement snaps it to the actual edges in offset cases (the
         Webflow 768–991px band).
         All bleed surfaces use these — `.figure.bleed-wide` (this
         file) and `.vpd-hero` (MULTI_ARTICLE_CSS_INNER in export.py).
         Don't reimplement the calc; reference these tokens. */
      --bleed-margin-left:  calc(-1 * var(--posts-rt-left,  calc(50vw - 50%)));
      --bleed-margin-right: calc(-1 * var(--posts-rt-right, calc(50vw - 50%)));

      /* Site-wide beige — used for chips, panels, callouts, etc. */
      --beige: #f2eee7;
    }

    /* Reserve the avoidance strip exactly when both conditions hold:
       (a) #toc-sidebar exists in the document (build_html.py emits
       it when front-matter `toc: true` AND there are headings), and
       (b) the viewport is wide enough that d-article's natural left
       edge clears the TOC's right edge (plus the visual gap).

       Threshold derivation: d-article inherits its left edge from
       `.posts-rt { max-width: 740px; padding: 0 1rem; margin: auto }`,
       which gives d-article-left = (vw − 740) / 2 + 16. For
       d-article-left ≥ TOC-width + TOC-gap (= 288), we need
       vw ≥ 740 + 2·(288 − 16) = 1284. Below that, the prose column's
       natural left edge would sit underneath the TOC sidebar — and
       since the TOC's float-wrap region is bounded by `height: 100vh`
       in the rule below, text past the first viewport stops wrapping
       around the float and starts visibly overlapping the (still
       sticky-positioned) TOC.

       The 1284px floor avoids that band entirely: above 1284, the
       prose has clearance; below, the TOC switches to inline-block
       (rule at the bottom of this stylesheet), and the question
       becomes moot.

       Value when active: distance from viewport-left to the right
       edge of the TOC's gap zone — the minimum left-side clearance
       any bled content needs. Bleed wrapper anchors to the viewport
       edge, so this token lives in viewport coordinates with no
       correction term. */
    @media (min-width: 1284px) {
      :root:has(#toc-sidebar) {
        --toc-safe-zone: calc(var(--toc-width) + var(--toc-gap));
      }
    }

    d-article {
      max-width: none !important;
    }

    /* Don't touch .katex font-size here — KaTeX ships with a
       calibrated `.katex { font-size: 1.21em }` (and its internal
       layout is tuned around that ratio in ems). Overriding to a
       different magnitude in `rem` decoupled inline math from its
       surrounding text-size context AND introduced subpixel drift
       between `.katex-display`'s measured width and the rendered
       `.katex` content's `scrollWidth`, which `overflow-x: auto`
       on `.katex-display` then surfaced as a 1–3px ghost
       scrollbar. Color is fine to override; size isn't. */
    d-article .katex {
      color: rgba(0, 0, 0, 0.65) !important;
    }

    d-article h3 {
      text-transform: none !important;
      font-size: 1.35rem !important;
    }

    d-article h4 {
      text-transform: none !important;
      font-size: 1.0rem !important;
    }

    /* Hide DOI section */
    d-byline .byline.grid > div:nth-child(3) {
      display: none !important;
    }

    /* Inline code pill — scoped via .prose-section because all prose
       (main body, vpd-appendix, wrapped d-appendix contents) lives
       inside a .prose-section div, and the class scope keeps
       specificity predictable against Webflow's bare-tag rules.
       `figcaption code` is included as a sibling selector because
       figure wrappers (.figure) sit *outside* .prose-section, so
       their captions wouldn't otherwise match. */
    .prose-section code,
    figcaption code {
      font-family: "IBM Plex Mono", monospace !important;
      background-color: #f0f1f3 !important;
      padding: 1px 3px !important;
      border-radius: 3px !important;
      font-size: 0.88em !important;
      border: 1px solid #dcdee1 !important;
      color: #1d272a !important;
    }

    /* Table styling — same class scoping. Browser UA defaults (local) and
       Webflow's .posts-rt table rules (staging) both produce divergent
       results; owning this here keeps the two environments in sync. */
    .prose-section table {
      width: 100%;
      border-collapse: collapse;
      margin: 1rem 0;
    }
    .prose-section th,
    .prose-section td {
      padding: 0.2rem 0.75rem;
      text-align: left;
      vertical-align: top;
    }
    .prose-section th {
      font-weight: 600;
      border-bottom: 1px solid #ccc;
    }
    /* Inter-row separators. Distill's template adds these via
       `d-article table td { border-bottom }`, which only reaches
       main-body tables — appendix tables live in `.vpd-appendix`,
       outside `d-article`, so they'd otherwise render flat. Reassert
       at `.prose-section` scope so body and appendix match. */
    .prose-section td {
      border-bottom: 1px solid rgba(0, 0, 0, 0.05);
    }
    .prose-section tr:last-of-type td {
      border-bottom: none;
    }

    /* Inner row inside .eq-card that holds the equation plus an
       optional eq-anchor + eq-number pair (for labeled equations).
       Flex-row so the eq-number sits as a sibling to the right of
       the equation rather than overlapping it — the previous
       `position: absolute; right: 20px` shape painted the number
       on top of any equation wide enough to reach the row's right
       edge. The equation child (`.katex-display` for raw $$..$$
       blocks; `.eq-interactive` for ```equation``` blocks) takes
       `flex: 1; min-width: 0` so it absorbs available width, with
       its own `text-align: center` keeping the math visually
       centred inside its allotted box.

       Both equation paths emit the same .figure.bleed-wide >
       .figure-body > .eq-card > .eq-row shape, so this is the
       single source of truth for eq-row layout. */
    .eq-row {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 0.5em;
    }
    /* Don't let the equation flex-grow into all available width — that
       pushed the eq-number to the row's right edge, which then visibly
       drifted further right whenever .eq-tip-panel below grew the
       enclosing .eq-card wider (long-form tips, etc.). With no flex
       grow, the equation is content-width, the number sits next to it,
       and `justify-content: center` keeps the (equation + number)
       cluster centred regardless of the card's outer width.

       `min-width: 0` is still required so the equation can shrink
       below its intrinsic content width when the figure-body is
       narrower than the equation — `.katex-display`'s own
       `overflow-x: auto` then provides horizontal scroll inside the
       equation's box.

       Applied to every direct child rather than just the equation,
       because raw `$$..$$` blocks are rendered client-side by KaTeX
       auto-render, which wraps the produced `.katex-display` in a
       classless `<span>` (text-node replacement boundary) — that
       span ends up as the actual flex item, not `.katex-display`.
       Targeting `*` covers it without us having to reach into KaTeX's
       output shape. `.eq-number` (white-space:nowrap + flex-shrink:0)
       and `.eq-anchor` (zero-size) are unaffected by these declarations
       in practice. */
    .eq-row > * {
      min-width: 0;
      max-width: 100%;
    }
    /* Long equations overflow their container at narrow viewports.
       Apply `overflow-x: auto` at the outer .eq-card (the immediate
       child of .figure-body, mirroring the image figure pattern where
       <img> is the immediate child) so the whole card becomes a
       horizontal scroll container — equations pan inside the card
       rather than clipping symmetrically off both sides. The card's
       block-level layout means it fills the figure-body width; if its
       contents (the equation) are wider, they scroll.
       Defense-in-depth: also apply on .katex-display so raw inline
       $$..$$ blocks (rendered inside prose paragraphs without an
       enclosing .eq-card) still scroll. overflow-y stays visible —
       sub/superscripts, accent overlays etc. legitimately exceed the
       baseline box and shouldn't be clipped. */
    .eq-card {
      overflow-x: auto;
    }
    .katex-display {
      overflow-x: auto;
    }
    .eq-row > .eq-number {
      flex-shrink: 0;
      font-size: 1rem;
      color: #333;
      white-space: nowrap;
    }
    /* eq-anchor is a zero-size scroll target that sits inside .eq-row
       as a flex sibling. `align-self: stretch` is the default and
       harmless; the span has no intrinsic width or height so it
       contributes nothing visually. */
    .eq-anchor {
      display: block;
      width: 0;
      height: 0;
      overflow: hidden;
    }

    /* Shared "window too narrow" fallback for interactive widgets that
       can't render usefully on mobile (graph, attention equation, QK
       cards). Two classes, each saying exactly what it does:
         .vpd-mobile-hide    — hidden below 900px (applied to the mount)
         .vpd-mobile-warning — shown below 900px (inserted as a sibling)
       The figcaption sits beside both and is untouched, so the mobile
       user still gets the figure number + description telling them
       what they're being denied. See window.vpdAppendMobileWarning in
       init.js. */
    .vpd-mobile-warning {
      display: none;
      padding: 2rem 1.5rem;
      text-align: center;
      font-family: 'Suisse Intl Book', sans-serif;
      color: #5a6266;
      font-size: 0.95rem;
      line-height: 1.6;
    }
    .vpd-mobile-warning h4 {
      margin: 0 0 0.5rem;
      font-size: 1rem;
      font-weight: 500;
      color: #1d272a;
    }
    .vpd-mobile-warning p {
      margin: 0;
      color: #8c8473;
    }
    @media (max-width: 900px) {
      .vpd-mobile-hide { display: none; }
      .vpd-mobile-warning { display: block; }
    }

    figure {
      margin: 2rem 0;
      text-align: center;
    }

    /* Narrow-figure overrides are scoped under `d-article` and use
       !important to outweigh the hand-pasted Webflow Embed
       (`<div class="distill-css">`) which sets
       `d-article figure { width: 100% !important; max-width: 100% !important }`
       on every figure. We can't edit that Embed from this side, so
       narrow figures have to fight back at the same specificity +
       importance level. Mirrors the equivalent block in
       dev-shared.css, which simulates the same Webflow cascade
       locally. */
    d-article figure.fig-simplicity {
      max-width: 55% !important;
      margin-left: auto !important;
      margin-right: auto !important;
    }

    d-article figure.fig-simplicity img {
      width: 100% !important;
      height: auto !important;
    }

    d-article figure.fig-emojis {
      max-width: 38% !important;
      margin-left: auto !important;
      margin-right: auto !important;
    }

    d-article figure.fig-emojis img {
      width: 100% !important;
      height: auto !important;
    }

    figure img {
      max-width: 100%;
      height: auto;
    }

    /* Kill the rounded corners pasted into Webflow's `distill-css`
       Embed (`d-article figure img { border-radius: 1rem !important }`).
       Same arms-race pattern as the narrow-figure overrides above —
       we can't edit the Embed, so we counter-set at equal specificity
       + importance. Not mirrored in dev-shared.css because the Embed's
       border-radius rule isn't simulated there, so dev is already
       square-cornered; this rule is a no-op in dev and a fix in prod. */
    d-article figure img {
      border-radius: 0 !important;
    }

    /* (figure.wide / .case-study-graph / .case-study-attention all
       used to be separate selectors with their own bleed math. Now
       unified under `.figure` + `.figure.bleed-*` modifiers — see
       the `.figure` rule block below for the consolidated layout
       machinery.) */

    figcaption {
      font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      margin-top: 1rem;
      font-style: normal;
      color: #666;
      font-size: 0.9em;
      text-align: center;
    }
    figcaption .katex {
      font-style: normal;
    }
    figcaption .figure-ref {
      font-style: normal;
      font-weight: 600;
      color: inherit;
    }
    figcaption code {
      /* Pill chrome (bg, border, color, font-family) comes from the
         shared rule above. This just keeps inline code upright when
         the surrounding figcaption is italicised. */
      font-style: normal;
    }

    .tooltip {
      position: relative;
      display: inline;
      cursor: pointer;
      color: #0066cc;
      text-decoration: none;
      border-bottom: 1px dotted #0066cc;
    }

    .tooltip .tooltip-text {
      visibility: hidden;
      width: 400px;
      background-color: #333;
      color: #fff;
      text-align: left;
      border-radius: 6px;
      padding: 8px;
      font-size: 0.85em;
      position: absolute;
      z-index: 1000;
      bottom: 125%;
      left: 50%;
      margin-left: -200px;
      line-height: 1.4;
    }

    .tooltip .tooltip-text::after {
      content: "";
      position: absolute;
      top: 100%;
      left: 50%;
      margin-left: -5px;
      border-width: 5px;
      border-style: solid;
      border-color: #333 transparent transparent transparent;
    }

    .tooltip:hover .tooltip-text {
      visibility: visible;
    }

    sup {
      font-size: 0.75em;
      line-height: 0;
      position: relative;
      vertical-align: baseline;
      top: -0.5em;
    }

    .cite-group {
      color: #aaa !important;
      font-size: 0.85em;
      position: relative;
      cursor: default;
    }

    sup a {
      text-decoration: none !important;
      border-bottom: none !important;
    }

    .fn-ref a {
      color: #B17039 !important;
    }
    .fn-ref {
      position: relative;
      cursor: default;
    }
    .fn-popover {
      position: absolute;
      z-index: 1000;
      background: #fafafa;
      color: #333;
      font-size: 0.8rem;
      line-height: 1.5;
      padding: 10px 14px;
      border-radius: 4px;
      border: 1px solid #e0e0e0;
      width: max-content;
      max-width: 500px;
      /* border-box so max-width covers padding + border. Without
         this the popover.js positioner sets max-width: vw - 16 and
         then padding pushes the rendered box past the viewport edge
         on narrow screens. */
      box-sizing: border-box;
      display: none;
    }
    .fn-popover.fn-popover-visible {
      display: block;
      pointer-events: auto;
    }
    .fn-popover a {
      color: #4a7ab5;
      text-decoration: none;
    }
    .fn-popover a:hover {
      text-decoration: underline;
    }

    a.cross-ref,
    a.figure-ref,
    a.table-ref {
      color: #888 !important;
      text-decoration: none !important;
      border-bottom: none !important;
    }

    a.cross-ref:hover,
    a.figure-ref:hover,
    a.table-ref:hover {
      color: #666 !important;
    }

    a.eq-ref {
      cursor: default;
    }

    .eq-ref-popover {
      position: absolute;
      z-index: 1000;
      background: #fff;
      padding: 12px 18px;
      border-radius: 6px;
      border: 1px solid #e0e0e0;
      display: none;
      max-width: 90vw;
      overflow-x: auto;
      /* border-box so popover.js's max-width includes padding + border. */
      box-sizing: border-box;
    }
    .eq-ref-popover .katex-display {
      margin: 0 !important;
    }
    .eq-ref-popover.eq-ref-popover-visible {
      display: block;
      pointer-events: auto;
    }

    .figure-ref-popover {
      position: absolute;
      z-index: 1000;
      background: #fff;
      padding: 12px;
      border-radius: 6px;
      border: 1px solid #e0e0e0;
      display: none;
      box-sizing: border-box;
    }
    .figure-ref-popover.figure-ref-popover-visible {
      display: block;
      pointer-events: auto;
    }
    /* The cloned figure brings its own layout — inside the popover we
       want it to forget bleed grids and cap to popover width. */
    .figure-ref-popover figure,
    .figure-ref-popover .figure-body {
      margin: 0;
      width: 100%;
      padding: 0;
    }
    .figure-ref-popover img,
    .figure-ref-popover svg {
      max-width: 100%;
      height: auto;
      display: block;
    }
    .figure-ref-popover figcaption {
      font-size: 0.8rem;
      line-height: 1.4;
      margin-top: 8px;
      color: #555;
    }
    .figure-ref-popover figcaption .figure-ref {
      color: inherit;
      font-weight: 600;
    }
    /* Tables in figures (tab: anchors) — keep them readable but
       bounded by the popover width. */
    .figure-ref-popover table {
      font-size: 0.8rem;
      max-width: 100%;
    }

    .cite-popover {
      position: absolute;
      z-index: 1000;
      background: #fafafa;
      color: #333;
      font-size: 0.8rem;
      line-height: 1.5;
      padding: 10px 14px;
      border-radius: 4px;
      border: 1px solid #e0e0e0;
      width: max-content;
      max-width: 500px;
      /* border-box so popover.js's max-width includes padding + border. */
      box-sizing: border-box;
      display: none;
    }
    .cite-popover.cite-popover-visible {
      display: block;
      pointer-events: auto;
    }
    .cite-popover ol {
      margin: 0;
      padding-left: 1.4em;
      list-style: decimal;
    }
    .cite-popover li {
      margin-bottom: 4px;
      padding-bottom: 4px;
      border-bottom: 1px solid #eee;
    }
    .cite-popover li:last-child {
      margin-bottom: 0;
      padding-bottom: 0;
      border-bottom: none;
    }
    .cite-popover a {
      color: #4a7ab5;
      text-decoration: none;
    }
    .cite-popover a:hover {
      text-decoration: underline;
    }

    @media (max-width: 990px) {
      .tooltip .tooltip-text {
        width: 300px;
        margin-left: -150px;
      }
    }

    .tooltip::after {
      content: "\2060";
    }

    .table-of-contents {
      font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      margin: 0;
      padding: 0;
    }

    .table-of-contents a {
      display: block !important;
      color: #666;
      text-decoration: none !important;
      border-bottom: none !important;
      padding: 0.25rem 0;
      line-height: 1.4;
      transition: color 0.2s ease;
    }

    .table-of-contents a:hover {
      color: #333;
    }

    .table-of-contents .toc-indent-1 {
      padding-left: 1.5rem;
    }

    .callout {
      border-left: 3px solid #1d272a;
      padding: 1rem 1.25rem;
      margin: 1.5rem 0;
      background: #fafafa;
    }
    .callout p { margin-bottom: 0.5rem; }
    .callout p:last-child { margin-bottom: 0; }

    .prose-section {
      /* Prose carrier — no width constraint; fills whatever column the
         Webflow page chrome provides. No horizontal padding either
         (ambient `.padding-global ...` handles viewport-edge gutter).
         No vertical margin — child elements (headings, paragraphs)
         own the spacing via their own margins. */
      box-sizing: border-box;
      /* NOTE: previously this carried `content-visibility: auto` for an
         off-screen render skip (drag-resize was ~10× cheaper with it).
         Removed because `content-visibility: auto` implies `contain:
         paint` when the section is on-screen, which clips bled figures
         (.figure.bleed-wide) and equations to the prose-section's
         narrow box. The original architecture had figures sitting
         *outside* prose-section, where this wasn't a problem; but the
         actual DOM nests them inside, and the paint-clip showed up as
         "equations cropping at prose-column width" / "figures not
         actually bleeding".
         If we want the resize-perf back, the right fix is to move
         figure blocks out of prose-section at build time (split the
         prose section around them) rather than re-introducing the
         containment. */
    }

    /* Inside the appendix, h3 plays the role of subsection heading and
       gets the same divider-line chrome that h2s get elsewhere. Matches
       the border-top + padding-top values from the d-article h2 rule in
       dev-shared.css / Webflow's shared styles. */
    .vpd-appendix h3 {
      padding-top: 0.5rem !important;
      border-top: 1px solid #b4b4b4 !important;
    }

    .vpd-appendix {
      margin-top: 2rem;
      padding: 2rem 0 3rem;
      background: #f2eee7;
      /* Full-bleed background trick: huge horizontal box-shadow paints
         the same color way past the element's edges, and clip-path
         clips that paint to just the vertical extent. No layout
         change — the element's own box stays the same size and
         position; only the painted area extends to the viewport
         edges. Works in any centered-column context, no ancestor
         assumptions needed. */
      box-shadow: 0 0 0 100vmax #f2eee7;
      clip-path: inset(0 -100vmax);
    }

    /* === Figure layout: one ontology for every figure-bearing
       block ============================================================
       Two classes, one modifier:

         `.figure`        — outer wrapper. Owns rhythm (margin-bottom).
                            Without `.bleed-wide` it's a plain block
                            in the prose column.
         `.figure-body`   — content wrapper. Sized to its widget's
                            max-content; shrinks with the grid track
                            on narrow viewports.
         `.bleed-wide`    — modifier on `.figure`. Switches the wrapper
                            to a 3-track full-bleed grid:

           left-gutter | content | right-gutter
           min: max(--toc-safe-zone, --bleed-edge-inset) | fit-content(CAP) | --bleed-edge-inset
           max: 1fr                                      | fit-content(CAP) | 1fr

       Wide vp: gutters split slack equally → content centered.
       Squeeze: left gutter clamps to TOC-safe-zone first, right
       gutter eats the rest. Below 1284 the TOC goes inline,
       --toc-safe-zone falls to 0, and the layout re-centers.

       Two non-obvious choices that have bitten us before:

       (1) `fit-content(CAP)` not `minmax(0, CAP)`. The latter grows
       the middle track into free space *before* honoring fr on the
       gutters — gutters collapse, centering dies.

       (2) `min-width: 0` on .figure-body releases the grid item's
       default `min-width: auto` (= min-content). Without it, the
       middle track refuses to shrink below the widget's intrinsic
       min-content. Inner widgets (.ag-root, .ah-root) absorb the
       squeeze via their own overflow:auto. */

    .figure {
      margin-bottom: 2em;
      /* Same off-screen layout/paint skip as `.prose-section` — figures
         contain the most expensive subtrees (canvas widgets, KaTeX
         displays, large tables), so skipping them when scrolled out of
         view is where most of the resize-frame win comes from.
         1200px intrinsic-size is a generous figure default; bleed-wide
         widgets (graphs, heatmaps) are usually taller. */
      content-visibility: auto;
      contain-intrinsic-size: auto 1200px;
    }

    /* Bleed margins reference the `--bleed-margin-{left,right}` tokens
       defined on :root. See those tokens for the rationale (anchored
       to measured viewport offset; fallback resolves to the old
       centered-parent math). Same tokens are reused by `.vpd-hero` in
       MULTI_ARTICLE_CSS_INNER. */
    .figure.bleed-wide {
      margin-left:  var(--bleed-margin-left);
      margin-right: var(--bleed-margin-right);
      display: grid;
      grid-template-columns:
        minmax(max(var(--toc-safe-zone), var(--bleed-edge-inset)), 1fr)
        fit-content(var(--content-max-width))
        minmax(var(--bleed-edge-inset), 1fr);
    }

    .figure-body {
      grid-column: 2;
      width: fit-content;
      max-width: 100%;
      min-width: 0;
      box-sizing: border-box;
      margin: 0 auto;
    }

    /* Opt-in: container-sized body. Default `width: fit-content`
       sizes the body to its widget's max-content (good for graphs,
       images, attention — content has its own intrinsic width).
       Some widgets (generations carousel) want to fill the available
       container width regardless of intrinsic content size; they
       declare it via `.figure-body.fill`.

       Note: `.fill` does NOT need `contain: inline-size` on its
       caption — the body's width is already set externally
       (100% of its container), so caption max-content can't
       overgrow it. The figcaption rule above is harmless when the
       body is .fill (no-op), so we don't need to scope it. */
    .figure-body.fill {
      width: 100%;
    }

    /* === The figure-body sizing rule. Do not remove. ===
       Without this, .figure-body { width: fit-content } grows to the
       max of every child's max-content — and a figcaption with long
       unwrapped text has a huge max-content, so the body overgrows
       the widget and figures visually drift left inside their bleed
       track.

       `contain: inline-size` declares the caption's intrinsic
       inline-size as 0 (its width comes from its container, not
       its contents), so fit-content on the body consults only the
       widget. `width: 100%` then gives the caption back its size
       from the body — without it, the caption collapses to 0.

       Alternatives that don't work (we tried; see LAYOUT.md):
         - display: table body / table-caption figcaption — disables
           max-width:100% shrinking, right edge stops moving in.
         - width:0; min-width:100% on caption — caption's min-content
           still propagates up to parent's fit-content.
         - per-figure --figure-content-width — works but requires
           per-renderer + per-markdown-block boilerplate.
       See webflow_integration/LAYOUT.md → "How the body sizes
       itself" for the full table of failed approaches. */
    .figure-body > figcaption {
      contain: inline-size;
      width: 100%;
    }

    /* Image figures: cap the <img> instead of the body, so the same
       .bleed-wide grid covers both interactive widgets and images.
       width:100% makes the image fill the body; max-width caps the
       body's resolved size at 1400px on wide viewports (image's
       own max-content is the cap, fit-content() does the rest). */
    .figure-body > img {
      width: 100%;
      max-width: 1400px;
      height: auto;
      display: block;
      margin: 0 auto;
    }

    /* Float-sticky TOC, tucked all the way to the viewport's left edge
       via margin-left: calc(50% - 50vw). This is the classic "expand
       from parent to viewport" offset: 50% is half the parent's width
       (.vpd-body-flex, ≈ .content-wrapper), 50vw is half the viewport.
       Since .content-wrapper is viewport-centered, the calc evaluates
       to exactly the negative offset needed to land the TOC's left
       edge at x=0. No magic numbers; scales with viewport changes.
       float: left + position: sticky coexist — the float takes the TOC
       out of block flow (so it doesn't push body down), and sticky keys
       its scroll behavior to .vpd-body-flex (position: relative). */
    .toc-sidebar {
      float: left;
      position: sticky;
      top: 0;
      z-index: 1;  /* lift above .vpd-appendix's full-bleed box-shadow paint */
      width: var(--toc-width);
      min-width: var(--toc-width);
      margin-left: calc(50% - 50vw);  /* pins TOC's left edge to viewport x=0 */
      margin-right: var(--toc-gap);
      height: 100vh;
      overflow-y: auto;
      padding: 1.5rem 1rem;
      box-sizing: border-box;
      background: #fff;
      font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      font-size: 0.65rem;
    }

    .toc-sidebar-title {
      font-weight: 600;
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: #999;
      margin-bottom: 0.75rem;
    }

    .toc-sidebar-list,
    .toc-sidebar-list .toc-h3-list {
      list-style: none;
      margin: 0;
      padding: 0;
    }

    .toc-sidebar-list li {
      margin: 0;
      padding: 0;
    }

    .toc-sidebar-list li.toc-h3 {
      padding-left: 0.85rem;
      font-size: 0.60rem;
    }


    .toc-sidebar-list .toc-h2-link {
        font-weight: 600;
    }

    .toc-sidebar-list a {
      display: block;
      color: #888;
      text-decoration: none;
      border-bottom: none;
      padding: 0.15rem 0.4rem;
      line-height: 1.2;
      transition: color 0.15s ease, background 0.15s ease;
      border-radius: 3px;
    }

    .toc-sidebar-list a:hover {
      color: #333;
      background: #f5f5f5;
    }

    .toc-sidebar-list a.toc-active {
      color: #111;
      font-weight: 600;
      background: #f0f0f0;
    }

    .toc-sidebar-list li.toc-appendix {
      font-size: 0.55rem;
    }

    /* Divider between the main sections and the appendices: applies to
       the first .toc-appendix at the top level of the TOC (i.e. the one
       directly preceded by a non-appendix sibling). */
    .toc-sidebar-list > li:not(.toc-appendix) + li.toc-appendix {
      border-top: 1px solid #ccc;
      margin-top: 0.5rem;
      padding-top: 0.5rem;
    }

    /* Disclosure toggle button on H2 rows with children. Hidden in the
       desktop sticky-sidebar layout (everything is expanded there);
       the inline-mode @media block flips it back to visible. */
    .toc-sidebar-list .toc-h2-toggle {
      display: none;
    }

    /* Mobile: render the TOC inline (full-width block sitting at the top
       of .vpd-body-flex, i.e. immediately after the hero). Each H2 row
       becomes a tap target whose H3 children are collapsed by default;
       js/toc.js wires the toggle behaviour. The disclosure caret is
       CSS-only, driven by the .is-open class on .toc-group.

       Selectors are anchored on `#toc-sidebar` to outweigh the
       `#toc-sidebar.toc-sidebar …` font-size overrides in
       webflow_integration/export.py's MULTI_ARTICLE_CSS_INNER, which
       is appended *after* POST_STYLES in the bundled stylesheet. */
    @media (max-width: 1283px) {
      #toc-sidebar.toc-sidebar {
        float: none;
        position: static;
        width: auto;
        min-width: 0;
        max-width: 100%;
        /* Top margin separates the TOC from the hero figure above;
           bottom margin separates it from the prose below. No outer
           border or padding — the TOC reads as part of the article
           flow rather than as a boxed-off card, with its links
           aligned to the same column as the body text. */
        margin: 1.5rem 0;
        padding: 0;
        height: auto;
        max-height: none;
        overflow: visible;
        border: none;
      }
      #toc-sidebar.toc-sidebar .toc-sidebar-title {
        margin-bottom: 0.5rem;
      }
      #toc-sidebar.toc-sidebar .toc-sidebar-list li,
      #toc-sidebar.toc-sidebar .toc-sidebar-list a {
        font-size: 0.95rem;
        line-height: 1.3;
      }
      #toc-sidebar.toc-sidebar .toc-sidebar-list li.toc-h3 {
        padding-left: 1rem;
        font-size: 0.85rem;
      }
      #toc-sidebar.toc-sidebar .toc-sidebar-list li.toc-appendix {
        font-size: 0.85rem;
      }
      #toc-sidebar.toc-sidebar .toc-sidebar-list a {
        /* Vertical padding for tap-target height; horizontal kept at
           zero so links flush-align with the prose left edge. */
        padding: 0.4rem 0;
      }
      /* Collapse H3 lists by default; .is-open (set by js/toc.js on tap)
         reveals them. */
      #toc-sidebar.toc-sidebar .toc-h3-list {
        display: none;
      }
      #toc-sidebar.toc-sidebar .toc-group.is-open > .toc-h3-list {
        display: block;
      }
      /* Split-target row: the link grows to fill, the toggle button
         hugs the right edge. The link itself is a plain nav target —
         the disclosure caret lives on the sibling button. */
      #toc-sidebar.toc-sidebar .toc-h2-row {
        display: flex;
        align-items: stretch;
        justify-content: space-between;
        gap: 0.25rem;
      }
      #toc-sidebar.toc-sidebar .toc-h2-row > .toc-h2-link {
        flex: 1 1 auto;
        min-width: 0;
      }
      #toc-sidebar.toc-sidebar .toc-h2-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        width: 2.25rem;
        min-height: 2.25rem;
        padding: 0;
        margin: 0;
        background: transparent;
        border: none;
        cursor: pointer;
        color: inherit;
        -webkit-tap-highlight-color: transparent;
      }
      #toc-sidebar.toc-sidebar .toc-h2-toggle::after {
        content: "";
        display: block;
        width: 0.45rem;
        height: 0.45rem;
        border-right: 1.5px solid #999;
        border-bottom: 1.5px solid #999;
        transform: translateY(-25%) rotate(-45deg);
        transition: transform 0.15s ease;
      }
      #toc-sidebar.toc-sidebar .toc-group.is-open > .toc-h2-row > .toc-h2-toggle::after {
        transform: translateY(0) rotate(45deg);
      }
    }

    .comp-tag, .graph-comp-tag {
      font-size: 0.78em;
      font-style: italic;
      background: #f2eee7;
      color: #5a6266;
      padding: 0.2em 0.35em;
      border-radius: 3px;
      cursor: default;
      border-bottom: 1px dashed #c5bfb5;
    }

    /* Popover is a plain fixed-height block — the inner .cc-card
       (emitted by buildComponentCard) fills it via height: 100% and
       runs its own pinned-header / scrolling-examples flex layout.
       Height is explicit (not max-height) so the percentage child
       resolves. min() clamps to viewport so the popover never extends
       off-screen on short windows. The loading state overrides
       height/width to auto so the spinner sits in a small natural-
       sized box. */
    .comp-popover {
      position: absolute;
      z-index: 2000;
      width: 600px;
      height: min(600px, calc(100vh - 32px));
      overflow: hidden;
      background: #fff;
      border: 1px solid #e5dfd8;
      border-radius: 8px;
      pointer-events: auto;
      display: none;
    }

    .comp-popover.comp-popover-visible {
      display: block;
    }

    .comp-popover-loading {
      width: auto;
      height: auto;
      padding: 10px 14px;
    }

    .comp-popover-spinner {
      width: 16px;
      height: 16px;
      border: 2px solid #e5dfd8;
      border-top-color: #8c9196;
      border-radius: 50%;
      animation: comp-popover-spin 0.7s linear infinite;
    }

    @keyframes comp-popover-spin {
      to { transform: rotate(360deg); }
    }

    @media (max-width: 500px) {
      .comp-popover {
        width: 300px;
      }
    }

    d-appendix {
      margin-top: 2rem;
      padding-top: 2rem;
      padding-bottom: 3rem;
      font-size: 0.875rem;
    }

    d-appendix h3 {
      font-family: "IBM Plex Mono", "Suisse Intl Mono", monospace;
      font-size: 0.875rem;
      line-height: 1.5;
      color: #1d272a;
      font-weight: 400;
      text-transform: uppercase;
    }

    d-appendix h3:first-child {
      margin-top: 0;
    }

    d-appendix ol {
      padding-left: 1.4em;
      list-style: decimal;
    }

    d-appendix li {
      font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      font-weight: 400;
      color: #646464;
      line-height: 1.4;
      font-size: 0.875rem;
      margin-bottom: 0.5em;
    }

    d-appendix li a {
      color: #4a7ab5;
      text-decoration: none;
    }

    d-appendix li a[title="Back"] {
      color: #B17039;
      text-decoration: none;
    }

    d-appendix li a:hover {
      text-decoration: underline;
    }

    /* ── Interactive equation ── */

    /* Equation-specific inner: lives inside .figure-body alongside the
       <figcaption>, owns only equation-shaped concerns:
         - position: relative — anchor for absolutely-positioned
           .eq-number (rendered against the card's right edge).
         - flex column + gap: 0.6em — stacks <label>, .eq-interactive,
           .eq-tip-panel with consistent breathing between them.
       Trailing rhythm to the next prose block is owned by
       `.figure { margin-bottom: 2em }` (single source of truth across
       every figure type). Inner elements (.eq-interactive,
       .katex-display, .eq-tip-panel) carry no vertical margin/padding
       of their own. */
    .eq-card {
      position: relative;
      display: flex;
      flex-direction: column;
      gap: 0.6em;
    }
    /* Anchor target only — empty, never visible. Drop it out of the
       flex flow so it doesn't add a `gap` above the equation. Hash
       links to its id still scroll correctly (browsers honour id
       targets on display:none elements, scrolling to the parent's
       position in flow). */
    .eq-card > label {
      display: none;
    }

    .eq-interactive {
      position: relative;
      isolation: isolate;        /* force stacking context */
      padding: 0;
      text-align: center;
      overflow: visible;
    }
    /* KaTeX defaults .katex-display to margin: 1em 0; we don't want
       that vertical space inside the eq-card — separation from
       surrounding prose is owned by `.figure { margin-bottom: 2em }`,
       and internal stacking is owned by .eq-card's flex `gap`. */
    .eq-interactive .katex-display { margin: 0 !important; }

    .eq-interactive .eq-chunk {
      display: inline;
      cursor: pointer;
      border-radius: 3px;
      padding: 2px 1px;
    }

    /* Single overlay on the container dims everything.
       isolation:isolate on .eq-interactive guarantees a stacking
       context, so KaTeX's internal position:relative elements
       (z-index:auto → paints at level 0) stay below this overlay. */
    .eq-interactive.has-focus::after {
      content: '';
      position: absolute;
      inset: 0;
      background: rgba(255, 255, 255, 0.85);
      pointer-events: none;
      z-index: 100;
      border-radius: 4px;
    }
    /* Focused chunk (+ its children) punches above the overlay */
    .eq-interactive.has-focus .eq-chunk.chunk-focus {
      position: relative;
      z-index: 200;
    }

    /* Inline explanation panel — emitted as a sibling of .eq-interactive
       inside .eq-card. Empty state shows a hover prompt; on chunk focus
       JS swaps the textContent for the chunk's tip text. Block flow ⇒
       naturally bounded, no positioning math, no clipping. Styling is
       deliberately minimal here; iterate after the structure lands. */
    .eq-tip-panel {
      /* Inset slightly from the eq-card edges so the panel reads as
         a contained block within the equation rather than a full-width
         band. .eq-card is flex-column (align-items: stretch by default)
         so horizontal margin is the cleanest way to shrink it. */
      margin: 0 1.5rem;
      padding: 0.5em 1em;
      background: var(--beige);
      border-radius: 4px;
      text-align: center;
      font-size: 0.85rem;
      color: rgba(26, 26, 26, 0.55);
      font-style: italic;
      min-height: 1.4em;
    }
    .eq-tip-panel.is-active {
      color: #1a1a1a;
      font-style: normal;
    }

    /* Prose ↔ equation linking */
    [data-hc] {
      border-bottom: 1.5px dashed rgba(26, 26, 26, 0.35);
      cursor: pointer;
      transition: border-color 0.1s;
    }
    [data-hc]:hover,
    [data-hc].prose-active {
      border-bottom-color: #1a1a1a;
      border-bottom-style: solid;
    }

    /* Hide KaTeX MathML dupes inside htmlClass spans */
    .katex [class^="hc-"] .katex-mathml { display: none; }
  

  /* ==== LAYER 1: no viewport breakout ====
     Previous versions of this file punched .vpd-body-layout out to 100vw
     via the `.vpd-bleed` class. That fought Webflow's natural centering
     (`.resource-content-main` is right-aligned inside its grid, so its
     center ≠ viewport center). Result: our content drifted 200+ px from
     the page chrome.

     Current model: content lives in `.content-wrapper` via Webflow's
     natural flow. Nothing is truly full-bleed. `.figure.bleed-wide`
     blocks bleed via a CSS Grid mechanism (see POST_STYLES `.figure`
     rules). `.vpd-bleed` is kept
     as a no-op class so the reparent JS (inlined into embed-1,
     requires re-paste to change) doesn't need updating. */
  .vpd-bleed {
    /* intentionally empty */
  }

  /* (Previously: `#toc-sidebar.toc-sidebar { display: none }` inside a
     `@media (max-width: 1200px)` block to hide the sidebar on narrow
     viewports. Removed when the TOC switched to inline-mobile rendering
     — the new mobile rules live in POST_STYLES alongside the desktop
     ones; see build_html.py. Adding it back here would defeat them.) */


  /* ==== LAYER 2: sections inside the breakout ==== */

  /* Title block — d-title + d-byline, left to fill the Webflow
     content column. No max-width or padding: the Webflow page-chrome
     already provides the correct column width and gutters. */
  .vpd-title-block {
    box-sizing: border-box;
  }

  /* Hero — full viewport bleed. Reuses --bleed-margin-{left,right} from
     POST_STYLES :root (see those tokens for rationale). Same anchoring
     as .figure.bleed-wide.
     Background paints the full bleed strip behind the centred figure. */
  .vpd-hero {
    margin-left:  var(--bleed-margin-left);
    margin-right: var(--bleed-margin-right);
    background: #ffffff;
    /* Hairline rules carve the hero out from the title block above and
       the body section below. #ccc matches the TOC sidebar's border-right
       (see POST_STYLES `.toc-sidebar`) for a consistent rule weight. */
    border-top: 1px solid #ccc;
    border-bottom: 1px solid #ccc;
  }

  /* Body — TOC is float+sticky positioned into the left gutter (see
     POST_STYLES `.toc-sidebar`); content flows normally beside it.
     `.vpd-body-flex` is no longer flex (name is historical — kept to
     avoid embed-1 re-paste). Its only job is to be the containing
     block for the sticky TOC: `position: relative` gives sticky a
     scroll range keyed to the article body's vertical extent, and
     `overflow: visible` (default) lets the TOC float stick out into
     the gutter via negative margin without being clipped. */
  .vpd-body-flex {
    position: relative;
  }
  /* Top spacing for the prose lives *inside* .vpd-body-content as padding
     rather than above it as margin. A margin-top here would collapse up
     through .vpd-body-flex (which has no border/padding/flow-root to block
     collapse) and push the whole flex container — including the float+sticky
     TOC sibling — 2rem below the hero, leaving a visible gap. Padding
     doesn't collapse, so .vpd-body-flex snaps flush to the hero and only
     the prose column gets pushed down. */
  .vpd-body-content {
    padding-top: 2rem;
  }

  /* (Previously: a narrow-viewport override that trimmed prose
     section horizontal padding. Obsoleted — ambient `.posts-rt`
     padding already handles the narrow-viewport gutter, and at
     wide viewports the 740px max-width provides enough whitespace.
     See build_html.py POST_STYLES `.prose-section`.) */

  /* ==== Webflow `.posts-rt <tag>` override patches ====
   *
   * Webflow's published site-wide CSS (shared.min.css) carries rules like
   * `.posts-rt h2 { font-family: "Suisse Intl" }`, `.posts-rt li {
   * font-family: "Suisse Works Book" }`, `.posts-rt figcaption { color:
   * var(--colors--grey-light) }`, etc. — set on the rich-text container's
   * descendants. Our POST_STYLES sets typography on `d-article` / nav
   * wrappers and relies on inheritance, which always loses to an explicit
   * rule on the descendant. Bump our selectors' specificity past
   * Webflow's (they're at 0-1-1, `.posts-rt d-article <tag>` lands us at
   * 0-1-2 which wins on tag-count). No `!important` needed. */

  /* TOC list items + links — reassert our sidebar font over
     `.posts-rt li { font-family: "Suisse Works Book"; font-size: 1.0625rem }`.
     ID + class selectors land us at 1-3-1. */
  #toc-sidebar.toc-sidebar .toc-sidebar-list li,
  #toc-sidebar.toc-sidebar .toc-sidebar-list a {
    font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont,
                 "Segoe UI", Roboto, sans-serif;
    font-size: 0.65rem;
    line-height: 1.2;
  }
  #toc-sidebar.toc-sidebar .toc-sidebar-list li.toc-h3 {
    font-size: 0.60rem;
  }
  #toc-sidebar.toc-sidebar .toc-sidebar-list li.toc-appendix {
    font-size: 0.55rem;
  }

  /* Body headings — reassert the Suisse Works serif family that our
     d-article typography expects. Webflow has two override layers:
     (1) `.posts-rt h2` via `shared.min.css` (0-1-1, no !important), and
     (2) a page-template inline `<style>` declaring
     `d-article h2 { font-family: "Suisse Intl" !important }` at
     specificity 0-0-2. Layer (2) is why we need `!important` ourselves:
     specificity alone loses to a lower-specificity rule flagged
     `!important`. Same shape applies to h3. */
  .posts-rt d-article h2,
  .posts-rt d-article h3,
  .posts-rt d-article h4,
  .posts-rt d-article h5,
  .posts-rt d-article h6,
  .posts-rt .vpd-appendix h2,
  .posts-rt .vpd-appendix h3,
  .posts-rt .vpd-appendix h4,
  .posts-rt .vpd-appendix h5,
  .posts-rt .vpd-appendix h6 {
    font-family: "Suisse Works Book", Georgia, serif !important;
  }

  /* Appendix body text — same shape of fight as the headings above,
     but for the prose / list / table / caption tags. The appendix
     container itself has no font-family rule from us, so it inherits
     Webflow's `body { font-family: "Suisse Intl Book" }` (and its
     22.42px size from `html { font-size: 19px }`). On top of that,
     `.posts-rt p` / `.posts-rt li` (0-1-1) apply "Suisse Works Book"
     (serif) to bare paragraphs and list items inside the rich-text
     container — visible as a serif TOC + references list at the
     bottom of the appendix. Reassert the d-article body family at
     0-2-2 so inheritance lands on Suisse Intl Book everywhere; no
     `!important` needed. */
  .posts-rt .vpd-appendix,
  .posts-rt .vpd-appendix p,
  .posts-rt .vpd-appendix li,
  .posts-rt .vpd-appendix td,
  .posts-rt .vpd-appendix th,
  .posts-rt .vpd-appendix figcaption {
    font-family: "Suisse Intl Book", -apple-system, BlinkMacSystemFont,
                 "Segoe UI", Roboto, sans-serif;
  }

  /* Appendix base font-size — match `d-article { font-size: 0.95rem }` from
     the page template. The appendix is a sibling of <d-article>, not a
     descendant, so it inherits body's larger root size instead. Without
     this, the prose-percent rule below resolves to 80% of a *larger*
     parent in the appendix than in the body, making appendix prose visibly
     bigger than body prose. Headings use absolute rem at higher specificity
     so they override this base. */
  .posts-rt .vpd-appendix {
    font-size: 0.95rem !important;
  }

  /* Figure caption — Webflow's rule picks `--colors--grey-light` (≈#B4B4B4)
     which is too faint. Match the body-text tone for readability. */
  .posts-rt d-article figcaption,
  .posts-rt .vpd-appendix figcaption {
    color: #666;
  }

  /* Article font scale — render at ~80% of the page-template's per-tag
     sizes for paper-density text. Beats Family B (`d-article <tag> {
     font-size: ... !important }`, spec 0-0-2 + !important) by reasserting
     at spec 0-1-2 + !important. Same selectors covered in the appendix.

     Two-rule split: prose (p/li/td/th) uses `font-size: 80%` because the
     page template assigns one body size, so a single percent works; the
     scope is `.prose-section` (and the appendix container) so the rule
     doesn't reach into figure internals — specifically the attribution
     graph's `.ag-panel`, which uses `.ag-panel * { font-size: inherit
     !important }` (graph.js) to make descendants inherit the panel's
     own size. Without scoping, the percent rule would beat that for
     <p>/<li> tags inside the panel while sibling spans/divs kept
     inheriting, producing visibly mixed sizes within one panel.

     Headings and figcaption use explicit rem because the page template
     assigns a different size per heading level (h2: 1.625rem, h3:
     1.35rem, h4: 1rem, figcaption: 0.75rem). A single `80%` would
     resolve against the parent (d-article body, ~1rem) and collapse all
     levels to the same value — destroying the visual hierarchy. The
     rems below are 0.8 × each level's original page-template size,
     preserving the same ratios as the original Webflow design at the
     reduced scale. */
  .posts-rt d-article .prose-section p,
  .posts-rt d-article .prose-section li,
  .posts-rt d-article .prose-section td,
  .posts-rt d-article .prose-section th,
  .posts-rt .vpd-appendix p,
  .posts-rt .vpd-appendix li,
  .posts-rt .vpd-appendix td,
  .posts-rt .vpd-appendix th {
    font-size: 80% !important;
    line-height: 1.5 !important;
  }
  .posts-rt d-article h2,
  .posts-rt .vpd-appendix h2 { font-size: 1.3rem !important; }
  .posts-rt d-article h3,
  .posts-rt .vpd-appendix h3 { font-size: 1.08rem !important; }
  .posts-rt d-article h4,
  .posts-rt .vpd-appendix h4 { font-size: 0.8rem !important; }
  .posts-rt d-article h5,
  .posts-rt .vpd-appendix h5 { font-size: 0.75rem !important; }
  .posts-rt d-article h6,
  .posts-rt .vpd-appendix h6 { font-size: 0.7rem !important; }
  /* figcaption stays at the page-template original (0.75rem) — it's
     already smaller than prose, so the 80% scale would crush it
     below comfortable reading size. */
  .posts-rt d-article figcaption,
  .posts-rt .vpd-appendix figcaption { font-size: 0.75rem !important; }

  /* `figure.fig-attr-graph-expl` (the "Explaining attribution graphs"
     diagram) is a raw <figure> element, so the page-template rule
     `d-article figure { width: 100% !important }` blows it up to the
     full column. Shrink + center to match the figure's natural aspect
     and the new prose density. dev-shared.css already does this at
     85% for the local dev page, but that file isn't shipped to
     production. */
  .posts-rt d-article figure.fig-attr-graph-expl {
    max-width: 70% !important;
    margin-left: auto !important;
    margin-right: auto !important;
  }

  /* Webflow's site-wide rich-text stylesheet ships
     `.w-richtext figure { max-width: 60% }` (spec 0-1-1). For body figures
     this is overridden by the page-template's
     `d-article figure { max-width: 100% !important }`, but the appendix
     lives outside `<d-article>` (it's a sibling injected into
     `.vpd-body-content`), so bare `<figure>` blocks in the appendix shrink
     to 60% of the prose column. Beat the Webflow rule at 0-2-1 — no
     `!important` needed since the conflicting rule isn't flagged. */
  .posts-rt .vpd-appendix figure {
    max-width: 100%;
  }

  /* Display equations — match the prose 80% scale. The `.eq-card` container
     sits in `.figure.bleed-wide > .figure-body`, *outside* `.prose-section`,
     so the prose rule above doesn't reach it; without this, display math
     would render at full d-article size next to 80%-scaled prose and look
     disproportionately large. Scaling the container (rather than `.katex-
     display` directly) lets KaTeX's natural 1.21em emphasis ratio carry
     through against the new smaller baseline. */
  .posts-rt d-article .eq-card,
  .posts-rt .vpd-appendix .eq-card {
    font-size: 80% !important;
  }

  /* Mobile overflow prevention — on narrow viewports, two content types
     legitimately render wider than the column and, without clipping,
     push the whole document past the viewport (mobile browsers then
     zoom the page out to fit, shrinking everything). Both fixes are
     "wrap in a horizontally scrollable block".

     1. Complex display-math blocks: KaTeX's `.katex-display` outer
        wrapper reports the right size, but its inner stacked elements
        (`.vlist-r`, `.mtable`, col-align spans for aligned equations
        with frac + sum stacks) render wider than the wrapper and leak
        out because every ancestor is `overflow-x: visible`.
     2. Markdown tables: intrinsic width from non-wrapping cell content.

     These apply at all viewport widths — on desktop the columns are
     wide enough that nothing actually needs to scroll and the rules
     are effectively no-ops. */
  .posts-rt d-article .katex-display,
  .posts-rt .vpd-appendix .katex-display {
    overflow-x: auto;
    overflow-y: hidden;
    max-width: 100%;
    /* KaTeX's default is `margin: 0.5em 0 0.5em 1em` — an asymmetric
       1em left indent that leaves display math slumped ~17px to the
       right and eats into limited mobile width. Reset to centered. */
    margin: 0.5em 0 !important;
    /* Slack inside the scroll container to absorb KaTeX's 2px-wide
       `.vlist-s` baseline shim (one per sum/integral/sub-or-super-
       scripted big operator), which otherwise sits flush against
       the right edge of the centered `.katex` content and pushes
       `scrollWidth` 2px past `clientWidth` — surfacing as a ghost
       horizontal scrollbar even when the equation visually fits with
       room to spare. 4px gives a hair of headroom in case future
       KaTeX versions add another shim. The shim is itself an upstream
       fix for browser baseline-quirks on zero-width inline-blocks
       (see KaTeX issue tracker), so we work around it at the
       container, not by neutralising the shim. */
    padding: 0 4px;
  }
  /* `display: block` decouples the <table> element from the inner
     table layout: the outer block fills its column (e.g. 708px in the
     figure) while the tbody/rows shrink to intrinsic content width.
     Distill's d-article default styles put a `border-bottom` and
     `margin-bottom` on the <table> element itself, which then stretch
     across the full block — past the visible cell area — rendering
     as a runaway full-width line under narrow tables. Neutralize them
     here so the cell-level borders (also from Distill) can do the
     visual work alone. */
  .posts-rt d-article table,
  .posts-rt .vpd-appendix table {
    display: block;
    overflow-x: auto;
    max-width: 100%;
    border-bottom: 0;
    margin-bottom: 1.5em;
  }

  /* (Previously: a `figure.wide` override re-asserting bleed margins
     with !important to beat the page-template rule
     `d-article figure { width: 100% !important }`. No longer needed:
     image figures are now `<div class="figure bleed-wide">` (a div, not
     a figure element), so the page-template selector doesn't match.
     The unified bleed math in POST_STYLES handles them without
     specificity gymnastics.) */
