/* ─────────────────────────────────────────────────────────────────────────
   Native motion: MPA View Transitions + reduce-motion guard.

   The shadcn theme already names header / sidebar / page / toc / footer
   with `view-transition-name="..."`, but never opts the site in. Adding
   `@view-transition { navigation: auto }` flips on native cross-fades for
   every same-origin navigation in browsers that support the MPA flavor
   (Chromium, Safari TP) — zero JS, automatic fallback elsewhere.

   The reduce-motion block strips animation/transition durations to ~0ms
   for users who've asked the OS to limit motion. Not just a politeness:
   keeping animations on for vestibular-sensitive readers actively hurts.
   ───────────────────────────────────────────────────────────────────────── */
@view-transition { navigation: auto; }

/* When navigating TO a page whose named elements (header, toc) have no
   counterpart on the source page (e.g. switching versions where the old
   build predates the custom header/toc templates), the browser fades those
   elements in from invisible — making them appear to momentarily disappear.
   Setting duration to 0 makes them appear instantly instead. */
::view-transition-new(header):only-child,
::view-transition-old(header):only-child,
::view-transition-new(toc):only-child,
::view-transition-old(toc):only-child {
  animation-duration: 0s;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after, ::backdrop {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  @view-transition { navigation: none; }
}

/* ─────────────────────────────────────────────────────────────────────────
   Mewbo console color palette — ported here so the docs site shares the
   same visual identity. Source: apps/mewbo_console/src/index.css.
   Aesthetic: Anthropic cream (light) / warm carbon (dark) with clay primary.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --background: hsl(45 28.6% 97.3%);
  --foreground: hsl(0 0% 4%);

  --card: hsl(0 0% 100%);
  --card-foreground: hsl(0 0% 4%);

  --popover: hsl(0 0% 100%);
  --popover-foreground: hsl(0 0% 4%);

  --primary: hsl(14.8 63.1% 59.6%);
  --primary-foreground: hsl(0 0% 100%);

  /* The clay accent as TEXT, not as a fill.
     `--primary` is identical in both themes, and it was tuned against the
     dark carbon surface. Inherited unchanged by light it lands at 2.6-3.0:1
     on the cream page, so every link, active ToC entry, active sidebar row,
     focus ring and tab underline failed contrast in light while passing in
     dark. One token cannot serve both roles: a fill, and legible text on
     that fill, pull lightness in opposite directions.
     Measured at 42%: 5.24:1 on the page and 4.60:1 on the active sidebar
     chip, which is a 12% clay tint and therefore the lightest surface this
     token ever sits on. That chip is what sets the value: 44% cleared the
     page but measured 4.30:1 there. */
  --primary-text: hsl(14.8 63.1% 42%);

  --secondary: hsl(48 25% 92.2%);
  --secondary-foreground: hsl(60 3% 21%);

  --muted: hsl(48 25% 92.2%);
  /* 52% measured 3.45:1 on the cream page, under the 4.5:1 body bar — and
     this token carries inactive ToC entries and footer text, which are read
     rather than glanced at. 44% measures 4.56:1. */
  --muted-foreground: hsl(45 3% 44%);

  --accent: hsl(48 25% 92.2%);
  --accent-foreground: hsl(60 3% 21%);

  --destructive: hsl(0 55% 47%);
  --destructive-foreground: hsl(0 0% 100%);

  /* Faint warm hairline (~#e9e6dd): a quiet step off the cream page, replacing
     the old translucent tan that composited to a loud ~#cecCC1. Mirrors how
     Devin keeps light-mode borders near-invisible (#eff1f5). */
  --border: hsl(48 16% 89%);
  --input: hsl(48 25% 92.2%);
  /* The focus indicator is a non-text UI component: WCAG 1.4.11 wants 3:1
     against what surrounds it, and the shared clay measured 2.97:1 on the
     cream page — under the bar by a hair, and a focus ring nobody can see is
     the one indicator that has to be seen. Matches --primary-text. */
  --ring: hsl(14.8 63.1% 42%);

  /* Clean white panel behind transparent-PNG media: brighter than the cream
     page, so framed images read as a raised surface. */
  --image-backing: hsl(0 0% 100%);

  --sidebar: hsl(45 28.6% 97.3%);
  --sidebar-foreground: hsl(0 0% 4%);
  --sidebar-primary: hsl(14.8 63.1% 59.6%);
  --sidebar-primary-foreground: hsl(0 0% 100%);
  --sidebar-accent: hsl(48 25% 92.2%);
  --sidebar-accent-foreground: hsl(60 3% 21%);
  --sidebar-border: hsl(48 16% 90%);
  --sidebar-ring: hsl(14.8 63.1% 59.6%);
}

.dark {
  /* Deep warm carbon. Tuned to sit at ~#161513 — matching Devin/Mintlify's
     #141414 page surface, far deeper than the old washed #262624. One flat
     surface for page/header/sidebar; borders (not shade steps) do the
     separating, so contrast against the near-white foreground stays high. */
  --background: hsl(40 6% 8%);
  --foreground: hsl(60 14% 97%);

  /* Dark's surfaces are what `--primary` was tuned against, so text and fill
     are the same value here. The alias exists in both themes so a call site
     never branches on which is active. */
  --primary-text: var(--primary);

  /* Cards & code blocks lift a hair above the page so framed content reads
     as slightly raised — the only place we keep a shade step. */
  --card: hsl(40 5% 12%);
  --card-foreground: hsl(60 14% 97%);

  /* Popovers / the search-&-Ask-AI modal float just above the page. */
  --popover: hsl(40 5% 11%);
  --popover-foreground: hsl(60 14% 97%);

  --primary: hsl(14.8 63.1% 59.6%);
  --primary-foreground: hsl(0 0% 100%);

  --secondary: hsl(40 4% 14%);
  --secondary-foreground: hsl(55 9% 74%);

  --muted: hsl(40 4% 16%);
  --muted-foreground: hsl(48 6% 62%);

  --accent: hsl(40 4% 17%);
  --accent-foreground: hsl(60 14% 97%);

  --destructive: hsl(0 73% 59%);
  --destructive-foreground: hsl(0 0% 100%);

  /* Whisper-subtle hairline (~#2c2b29): only ~+18 rgb over the page, the same
     low-contrast step Devin uses (#26292d over #141414). Solid, not the old
     translucent light-gray that composited to a loud ~#5d5d54. */
  --border: hsl(40 5% 17%);
  --input: hsl(40 5% 15%);
  --ring: hsl(14.8 63.1% 59.6%);

  /* Deeper-than-page backing for transparent-PNG media so framed images gain
     depth instead of dissolving into the surface. */
  --image-backing: hsl(40 7% 5%);

  --sidebar: hsl(40 6% 8%);
  --sidebar-foreground: hsl(60 14% 97%);
  --sidebar-primary: hsl(14.8 63.1% 59.6%);
  --sidebar-primary-foreground: hsl(0 0% 100%);
  --sidebar-accent: hsl(40 4% 17%);
  --sidebar-accent-foreground: hsl(60 14% 97%);
  --sidebar-border: hsl(40 5% 15%);
  --sidebar-ring: hsl(14.8 63.1% 59.6%);
}

/* ─────────────────────────────────────────────────────────────────────────
   Fenced code blocks: subtle border, rounded corners, horizontal scroll
   when long so code doesn't bleed into the sidebar or the viewport.
   ───────────────────────────────────────────────────────────────────────── */

div.codehilite,
div.highlight {
  border: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  border-radius: 0.5rem;
  overflow: hidden;
  max-width: 100%;
}

div.codehilite pre,
div.highlight pre {
  margin: 0;
  overflow-x: auto;
  max-width: 100%;
  white-space: pre;
  word-wrap: normal;
  box-sizing: border-box;
}

div.codehilite code,
div.highlight code {
  white-space: pre;
  word-wrap: normal;
}

article pre:not(.mermaid),
.md-content pre:not(.mermaid) {
  overflow-x: auto;
  max-width: 100%;
}

/* Inline code: keep it flexible so long identifiers can wrap with surrounding text. */
article p code,
article li code,
article td code {
  overflow-wrap: break-word;
  word-break: break-word;
}

/* ── Hover cards on a phone ────────────────────────────────────────────────
   The card is anchor-positioned against its trigger (see
   shadcn/extensions/hover_card.py), which is right on a desktop and
   impossible on a phone: a 350px card anchored to a word near the right edge
   of a 320px screen has nowhere to go, and it pushed the page sideways —
   measured 467px of document in a 414px viewport.

   `position-try-fallbacks` is the purpose-built answer and is Chromium-only
   today, so this does not use it. Instead the card stops being a tooltip
   below 700px and becomes a sheet pinned to the bottom of the viewport, which
   is where a phone expects secondary content anyway and which cannot overflow
   in either axis by construction.

   Every offset is reset, not just the ones a given variant sets: the four
   positions each set a different pair, and `span.hover-card` additionally
   centres itself with Tailwind's `translate` property, which `transform:
   none` does NOT undo.

   This rule lives HERE rather than in tailwind/hover_card.css because the
   Tailwind build folds `translate` away as redundant next to `transform`,
   and the declaration never reached base.css — source looked right, build was
   broken, and only a test against the built site caught it. mewbo.css ships
   verbatim. */
@media (max-width: 700px) {
  article .hover-card {
    position: fixed;
    inset: auto 1rem 1rem 1rem;
    transform: none;
    translate: none;
    justify-self: initial;
    align-self: initial;
    width: auto;
    min-width: 0;
    max-width: none;
  }
}

/* Display math is the one construct that genuinely cannot wrap — KaTeX ships
   `white-space: nowrap` on `.katex-display > .katex` because an equation
   broken at an arbitrary point is not the same equation. So it gets the same
   deal a code block gets: scroll the equation, not the page. Without this
   the theme had no rule for it at all and a wide equation moved the whole
   document. */
article .katex-display,
.md-content .katex-display {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  /* `overflow` on a block establishes a scroll container whose top/bottom
     would otherwise clip the taller glyphs (integrals, big fractions). */
  padding-block: 0.25rem;
}

/* Mermaid diagrams get the framed treatment images get, plus an expanded
   viewer — see "Mermaid diagrams" near the end of this file. An unrendered
   fence (mermaid failed to load, or superfences was never configured) still
   needs the width guard so a wide source block cannot widen the column. */
pre.mermaid,
.mermaid,
div.mermaid {
  max-width: 100%;
  overflow-x: auto;
}

/* Content images sit directly on the page without a matte or outer frame. */
article img:not(.no-border),
.md-content img:not(.no-border) {
  box-sizing: border-box;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 0.5rem;
  max-width: 100%;
  height: auto;
}
/* Keep media-kit images frameless even when used outside article content. */
article .ms-shot__frame img,
article .ms-shots img,
article .ms-devices img,
.md-content .ms-shot__frame img,
.md-content .ms-shots img,
.md-content .ms-devices img {
  padding: 0;
  background: transparent;
  border: 0;
}

/* ── Media width ceiling in expanded (full-width) layout ─────────────────────
   `html.layout-full main article` drops to `3xl:max-w-none` (tailwind/
   article.css:5-7), which is right for prose — text at 1600px+ wants the room
   — but it leaves every media block with NO upper bound at all. A screenshot
   or a carousel then scales to whatever the monitor is, so the same image is
   a comfortable 672px in fixed layout and an unreadable 1900px-wide slab on an
   ultrawide, flush to both edges of the column with nothing beside it.

   A ceiling, not a percentage: media stops growing at `--ms-media-max-width`
   and centres, so the column keeps widening for text while images hold a size
   a person can actually take in at one glance. `--ms-media-gutter` is the
   breathing room on each side, which matters most here — at full width the
   article's own `3xl:px-6` is the only inset, and an image pinned to it reads
   as bleeding off the page rather than sitting on it.

   Scoped to `.layout-full` and the 3xl breakpoint that actually releases the
   `max-w-2xl`; below that the article cap already bounds these and this rule
   must not shrink them. Consumers retune via the two custom properties. */
html.layout-full {
  --ms-media-max-width: 68rem;
  --ms-media-gutter: 2rem;
}

@media (min-width: 1600px) {
  html.layout-full main article :is(
    p > img,
    figure,
    .ms-shot,
    .ms-shots,
    .ms-devices,
    video,
    .table-wrapper,
    .mermaid,
    div.mermaid
  ) {
    max-width: min(
      100%,
      calc(var(--ms-media-max-width) - 2 * var(--ms-media-gutter))
    );
    margin-inline: auto;
  }

  /* A bare <img> is a block in this theme (preflight sets `display:block`), so
     it takes the same ceiling directly — `p > img` above only catches the
     markdown-paragraph case. */
  html.layout-full main article .typography img {
    max-width: min(
      100%,
      calc(var(--ms-media-max-width) - 2 * var(--ms-media-gutter))
    );
    margin-inline: auto;
  }
}

/* Sticky top nav: one hairline marks where the header ends and the page
   begins, matching the Mewbo console top-bar treatment.

   Scoped away when a tab rail exists, because the rail then IS the bottom of
   the nav block and draws that edge itself. Unscoped, this rule and the rail's
   own border land on the same y and paint 2px where the design says 1. Two
   other rules in this file also target that edge, which is why the doubling
   survived a search for the obvious one. */
body:not(.ms-has-header-tabs) header.sticky {
  border-bottom: 1px solid var(--border, rgba(120, 120, 120, 0.2));
}

/* Horizontal rules (`---` in markdown): the theme ships them with 0 margin
   while leaving the following heading's large top-margin untouched, so an
   hr followed by an h2 has no gap above and ~80px below. Give the rule
   symmetric breathing room and collapse the next block's top margin so
   the separator provides one balanced gap. */
article hr,
.md-content hr {
  margin: 2.5rem 0;
  border: 0;
  border-top: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  background: none;
  height: 0;
}
article hr + *,
.md-content hr + * {
  margin-top: 0 !important;
}

/* ─────────────────────────────────────────────────────────────────────────
   Heading wrap fix — prevent page-level headings from breaking mid-word on
   narrow viewports.

   Root cause: the theme sets `word-break: break-all` on h1 in @layer base,
   which fragments at every character rather than at word boundaries.
   Overriding to `normal` means line breaks only happen at spaces; pairing
   with `overflow-wrap: break-word` still handles truly unbreakable tokens
   (long URLs, identifiers) gracefully without mid-character cuts.

   `clamp()` font-size scales h1 and h2 down smoothly on phones so a typical
   heading (10–20 chars) fits on one line at ~360 px viewport width while
   still reaching full size above the md breakpoint.

   UI chrome labels (sidebar group headers, TOC title) are short fixed
   strings we control, so `white-space: nowrap` + ellipsis is safe.
   ───────────────────────────────────────────────────────────────────────── */

article h1,
.md-content h1 {
  word-break: normal;
  overflow-wrap: break-word;
  font-size: clamp(1.6rem, 5.5vw, 2.25rem);
}

article h2,
.md-content h2 {
  word-break: normal;
  overflow-wrap: break-word;
  font-size: clamp(1.15rem, 4vw, 1.5rem);
}

article h3,
.md-content h3 {
  word-break: normal;
  overflow-wrap: break-word;
}

/* Page header: flex justify-between row on desktop (title left, nav buttons
   right). On mobile the button group (~110 px) steals too much horizontal
   space and the title wraps even at reduced font-size. Stack to column so
   the h1 always gets the full content width; push the button group to the
   right edge of its own row via align-self. */
@media (max-width: 639px) {
  article #page-header {
    flex-direction: column;
    gap: 0.35rem;
  }
  article #page-header > div:last-child {
    align-self: flex-end;
  }
}

/* Sidebar group label: short fixed label that should not wrap in the narrow
   sidebar column. */
.mewbo-sidebar-group-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* TOC section heading ("On this page"): already height-constrained to 1.5rem;
   nowrap keeps it on one line when the TOC panel is narrow. */
.mewbo-toc__heading {
  white-space: nowrap;
}

/* ─────────────────────────────────────────────────────────────────────────
   Mobile navigation dialog. The override at overrides/templates/bottom_sidebar.html
   simply includes the desktop sidebar (templates/sidebar.html) inside a
   full-viewport <dialog>, so the mobile menu reuses the exact same items,
   icons, typography, and nested-section rendering as the desktop sidebar.
   Only thing needed here: make the dialog fill the viewport and place a
   close button, since the theme's JS always calls showModal() and leaves
   no way to dismiss when there's no backdrop to tap.
   ───────────────────────────────────────────────────────────────────────── */
dialog.ms-mobile-nav {
  position: fixed;
  inset: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--foreground);
  z-index: 50;
  overflow: hidden;
}
dialog.ms-mobile-nav::backdrop { background: transparent; }
.ms-mobile-nav__panel {
  position: relative;
  width: 100%;
  height: 100%;
  padding-top: var(--header-height, 58px);
  background: var(--background);
  color: var(--foreground);
  overflow-y: auto;
  overflow-x: hidden;
  box-sizing: border-box;
}
.ms-mobile-nav__close {
  position: absolute;
  top: calc((var(--header-height, 58px) - 32px) / 2);
  right: 1rem;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 0.375rem;
  background: var(--background);
  color: var(--foreground);
  cursor: pointer;
  z-index: 1;
}
.ms-mobile-nav__close:hover { background: var(--accent); }

/* ─────────────────────────────────────────────────────────────────────────
   Admonitions — GitHub-flavored callouts authored as
   > [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION]
   and translated to <div class="admonition {type}"> by mkdocs-callouts.
   ───────────────────────────────────────────────────────────────────────── */

article .admonition,
.md-content .admonition {
  --adm-accent: var(--muted-foreground);
  --adm-tint: color-mix(in srgb, var(--adm-accent) 10%, transparent);
  margin: 1.25rem 0;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  border-left: 4px solid var(--adm-accent);
  border-radius: 0.5rem;
  background: var(--adm-tint);
  color: var(--foreground);
}

/* Title: shadcn forces display: inline !important on the first two paragraphs
   of any admonition, so we override with higher specificity + !important to
   turn the title into its own block with icon alignment. */
article .admonition > p.admonition-title,
.md-content .admonition > p.admonition-title {
  display: flex !important;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.4rem 0 !important;
  font-weight: 600;
  color: var(--adm-accent);
  letter-spacing: -0.01em;
}

article .admonition > p.admonition-title iconify-icon,
.md-content .admonition > p.admonition-title iconify-icon {
  flex: 0 0 1rem;
  width: 1rem;
  height: 1rem;
  color: var(--adm-accent);
}

/* Space-holder shown until icon-inject.js fires (before DOMContentLoaded).
   Uses :not(:has(iconify-icon)) so it disappears the moment the element
   is injected — no flash, no inline SVGs. */
article .admonition > p.admonition-title:not(:has(iconify-icon))::before,
.md-content .admonition > p.admonition-title:not(:has(iconify-icon))::before {
  content: "·";
  flex: 0 0 1rem;
  text-align: center;
  color: var(--adm-accent);
  font-size: 1.25rem;
  line-height: 1;
}


/* Body paragraphs: shadcn sets them inline too; force block and tighten spacing. */
article .admonition > p:not(.admonition-title),
.md-content .admonition > p:not(.admonition-title) {
  display: block !important;
  margin: 0.35rem 0 0 0 !important;
}

article .admonition > *:last-child,
.md-content .admonition > *:last-child {
  margin-bottom: 0;
}

/* Type-specific accents. Colors mirror GitHub's callout palette. */
.admonition.note {
  --adm-accent: #0969da;
}
.dark .admonition.note {
  --adm-accent: #58a6ff;
}

.admonition.tip {
  --adm-accent: #1a7f37;
}
.dark .admonition.tip {
  --adm-accent: #3fb950;
}

.admonition.important {
  --adm-accent: #8250df;
}
.dark .admonition.important {
  --adm-accent: #a371f7;
}

.admonition.warning {
  --adm-accent: #9a6700;
}
.dark .admonition.warning {
  --adm-accent: #d29922;
}

.admonition.caution,
.admonition.danger {
  --adm-accent: #cf222e;
}
.dark .admonition.caution,
.dark .admonition.danger {
  --adm-accent: #f85149;
}


/* ─────────────────────────────────────────────────────────────────────────
   Search trigger and dialog polish.

   The theme's trigger button ships without an icon and the theme's dialog
   caps the result list at 300px, which makes long queries feel truncated.
   Here we give the trigger a search icon + ⌘K hint, make the dialog wider
   on large screens, and let the result list grow to ~70vh before scrolling.
   ───────────────────────────────────────────────────────────────────────── */

/* Trigger: reveal the injected SVG icon and keep the shortcut hint right-aligned. */
.search-trigger {
  gap: 0.5rem;
}
.search-trigger > svg.lucide-search {
  opacity: 0.6;
  flex-shrink: 0;
}
/* The search hint is a real shortcut, so it uses the site's keycaps — two
   separate caps, as the keyboard has two keys. It shipped as one flat
   monospace box reading "Ctrl K": no caps, and a different typeface from
   every other shortcut on the site.

   The caps are scaled DOWN here, though. This sits inside a search field, not
   in a sentence, and keycaps at prose size overpower the placeholder beside
   them. One `font-size` on the group does it, because the caps are sized in
   `em` throughout. */
.search-trigger > .search-shortcut,
.search-trigger > .search-shortcut:has(kbd) {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.15em;
  flex-shrink: 0;
  font-size: 0.78rem;
  line-height: 1;
  opacity: 0.85;
}
@media (max-width: 639px) {
  .search-trigger > .search-shortcut {
    display: none;
  }
}

/* Dialog: allow a taller, wider result list on big screens. Leaves the small-
   screen behaviour intact (the theme's own sizing still applies as a floor). */
dialog#search-dialog {
  width: min(640px, calc(100vw - 2rem));
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 4rem);
}

dialog#search-dialog > div {
  width: 100%;
}

#mkdocs-search-results {
  max-height: min(70vh, 520px);
  overflow-y: auto;
  padding: 0.25rem 0.5rem;
}

#mkdocs-search-results article {
  padding: 0.6rem 0.65rem;
  border-radius: 0.375rem;
}
#mkdocs-search-results article:hover {
  background: var(--muted, rgba(120, 120, 120, 0.08));
}
#mkdocs-search-results article h3 {
  margin: 0 0 0.15rem 0;
  font-size: 0.95rem;
}
#mkdocs-search-results article h3 a {
  text-decoration: none;
  color: var(--primary, currentColor);
}
#mkdocs-search-results article p {
  margin: 0;
  font-size: 0.82rem;
  color: var(--muted-foreground, currentColor);
  line-height: var(--leading-ui);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
#mkdocs-search-results > p {
  padding: 1rem;
  margin: 0;
  color: var(--muted-foreground, currentColor);
  text-align: center;
}

/* Live feedback states rendered by search-enhance.js while the user types. */
#mkdocs-search-results > p.search-hint {
  padding: 1.25rem 1rem;
  font-size: 0.85rem;
  color: var(--muted-foreground, currentColor);
  text-align: center;
}
#mkdocs-search-results > p.search-hint.busy {
  opacity: 0.7;
}
#mkdocs-search-results > p.search-hint.too-short,
#mkdocs-search-results > p.search-hint.loading {
  color: var(--muted-foreground, currentColor);
}

/* ── Prose vertical rhythm ────────────────────────────────────────────────
   One scale, four steps, named for the job each one does. Change the rhythm
   here; do not reach for a Tailwind spacing utility in a page or a partial.

       flow          one block to the next inside a passage
       topic gap     before an H4
       subsection    before an H3
       section       before an H2

   The ladder must stay strictly increasing: section > subsection > topic >
   flow. That is what tells a reader how deep a break is without reading it.

   The old scale broke in two places. An H2 carried `mt-12 lg:mt-20`, so on a
   wide screen a section opened 80px down — over three times the paragraph
   step, which is what reads as "a massive gap". And an H4 carried exactly the
   paragraph step, so the shallowest heading announced itself no more than an
   ordinary line break.

   ONE OWNER PER GAP: the top margin of the block that follows. Bottom margins
   on top-level blocks are zeroed below. Margins collapse inside .typography,
   so a gap is max(prev.margin-bottom, next.margin-top) — not a sum — but two
   authors setting the two halves is how a list ends up 24px from the next
   paragraph while an admonition ends up 20px from it. Zeroing one half makes
   the surviving half the whole answer.

   The one seam where margins genuinely ADD is above .typography: `article` is
   a flex column with `gap: 2rem`, and a flex gap does not collapse with a
   margin. So a first block carrying its own top margin sits 2rem + its margin
   below the masthead. `> *:first-child` zeroes it, leaving the flex gap as the
   single source there too.

   Every selector matches the reach of the rule it replaces. The prose rules in
   base.css are unlayered and reach (0,1,2) — `article .typography h2`,
   `article p:not(:first-child)` — and mewbo.css is linked after base.css, so an
   equal-specificity selector wins on source order and a shorter one loses in
   silence. See the masthead note below, which says the same thing at length.

   This block sits BEFORE the masthead and hero sections on purpose. Several of
   their rules — `article .ms-hero > :is(h1, …, h6) { margin: 0 }` in
   particular — reach the same (0,1,2) as the rules here, so they need to come
   after to win. A page-level composition owns its own spacing; the ladder is
   the default it opts out of.
   ────────────────────────────────────────────────────────────────────────── */
article {
  --prose-flow: 1.5rem;
  --prose-topic-gap: 2rem;
  --prose-subsection-gap: 2.5rem;
  --prose-section-gap: 3rem;
  /* After a heading, before what it introduces. Deliberately much tighter than
     the space above the heading: a heading has to read as belonging to the
     content under it, not floating between two passages. */
  --prose-heading-lead: 1rem;
  /* Around a fenced code block. A step above the body flow because a code
     block is a framed object with its own border, radius and background
     padding, not another line of prose — at the flow step its frame very
     nearly touches the sentence introducing it, and a wall of prose and code
     runs together with nothing to breathe on. One rung up the same ladder, so
     the page still reads as one rhythm rather than a special case. */
  --prose-code-gap: 2rem;
}

/* ── Leading ─────────────────────────────────────────────────────────────
   Declared at :root rather than beside the gap tokens above, because the ToC
   rail, the footer and the search dialog all sit outside `article` and need
   the same ladder.

   The gaps above are the space BETWEEN blocks; these are the space between
   lines inside one. The page read as cramped because the two had drifted
   apart: paragraphs were already generous at 1.625 while everything read
   alongside them lagged — list items at 1.5, admonition and caption text at
   1.4-1.43, the ToC at 1.4. A reader does not experience those as separate
   components, so a page mixing them reads as unevenly tight rather than as
   deliberately varied.

   Four rungs, chosen by how the text is READ rather than by what element it
   happens to be:

     body     continuous prose you read line after line. The loosest,
              because that is where leading does the most work.
     ui       short runs in chrome — a ToC entry, a caption, a footer link.
              These wrap to two or three lines at most, so they want air
              without the paragraph's full openness, which would make a
              two-line entry look like two separate entries.
     compact  dense surfaces where a line is a unit rather than part of a
              flow: a line of code, a keycap.
     tight    display type. Headings are large enough that generous leading
              pulls a two-line heading apart into two headings.

   Unitless on purpose: these are inherited by elements at many different
   font sizes, and a `rem` value would be a fixed gap that suffocates small
   type and yawns on large. */
:root {
  --leading-body: 1.7;
  --leading-ui: 1.55;
  --leading-compact: 1.45;
  --leading-tight: 1.25;
}

/* Continuous prose. `article` itself carries the body rung so anything that
   does not opt out inherits it — including markup the theme has no rule for,
   which is where the old drift came from. Reach matches base.css's own prose
   rules, which are unlayered at (0,1,1)-(0,1,2); mewbo.css is linked after
   base.css, so an equal-specificity selector wins on source order. */
article,
article .typography :is(p, li, dd, dt, blockquote, figcaption, details, summary, .admonition, .tabbed-block) {
  line-height: var(--leading-body);
}

/* Chrome, and the smaller type inside prose. A caption or an admonition is
   read in a glance, not line after line, and is set smaller — at the body
   rung its lines drift apart instead of holding together as one note.

   Table cells are deliberately NOT here. They were the subject of their own
   fix (v1.22.1) precisely because a wrapped cell is a small paragraph, not a
   label: `tailwind/table.css` puts them on `leading-relaxed`, and routing
   them through this rung would quietly undo that. They inherit the body rung
   from `article` instead, which is a touch looser again and is what the note
   in table.css already argues for — a table here is prose set in columns,
   not a data grid. */
article .typography :is(figcaption, .admonition),
.mewbo-toc__link,
.mewbo-footer__col-links a,
.mewbo-footer__tagline {
  line-height: var(--leading-ui);
}

/* A line of code is addressed individually — you scan for one, you cite one
   by number — so it is set as a unit rather than as part of a flow. Loosening
   this to the body rung would also break the line-number gutter's alignment,
   which pairs row for row with the code beside it. */
article div.codehilite :is(pre, code),
article table.codehilitetable :is(pre, code),
article pre:not(.mermaid) {
  line-height: var(--leading-compact);
}

/* Display type sets its own. A heading at 30px with body leading puts half a
   line of air between its two lines and stops reading as one phrase. */
article .typography :is(h1, h2, h3, h4, h5, h6) {
  line-height: var(--leading-tight);
}

/* Descendant, not child, to match base.css's `article .typography h2` exactly —
   a heading nested in a tabbed block or an admonition is on the same ladder.
   The masthead title is `article .typography > h2:first-child` at (0,2,2) and
   is therefore untouched by these. */
article .typography h2 { margin-top: var(--prose-section-gap); }
article .typography h3 { margin-top: var(--prose-subsection-gap); }
article .typography h4,
article .typography h5,
article .typography h6 { margin-top: var(--prose-topic-gap); }

/* Every other top-level block sits on the flow step. `hr` is excluded on
   purpose: it owns a symmetric 2.5rem separator of its own, above. */
article .typography > :is(p, ul, ol, dl, pre, blockquote, figure, table, details, section, div, video):not(:first-child) {
  margin-top: var(--prose-flow);
}

/* Top margins own the rhythm, so bottom margins contribute nothing. */
article .typography > :is(p, ul, ol, dl, pre, blockquote, figure, table, details, section, div, h2, h3, h4, h5, h6) {
  margin-bottom: 0;
}

/* The article's flex gap already separates the masthead from the prose, and it
   does not collapse — a top margin here would add to it. */
article .typography > :first-child {
  margin-top: 0;
}

/* `!important` fights base.css's `article .typography h2+p{margin-top:…
   !important}`, the built form of `[&+p]:!mt-4` in tailwind/article.css. Two
   important declarations are settled by specificity, and that one reaches
   (0,1,3), so the shape of this selector is what makes it win. It generalises
   the idea the Tailwind rule was groping at for H2 and paragraphs alone: every
   heading, and whatever it introduces.

   Being important, this rule cannot be beaten by source order, so the two
   compositions with a hand-tuned internal rhythm are excluded by name. The hero
   spaces its own children with a flex gap, and mkdocstrings holds a symbol's
   docstring deliberately tight under its signature. */
article .typography :is(h2, h3, h4, h5, h6):not(.ms-hero *, .doc *)
  + :is(p, ul, ol, dl, pre, blockquote, figure, table, details, section, div, video, h2, h3, h4, h5, h6) {
  margin-top: var(--prose-heading-lead) !important;
}

/* ── Fenced code blocks sit one rung up the ladder ────────────────────────
   A code block is a framed object, and the frame is the problem: its border
   and its internal padding both read as part of the gap, so the same
   `--prose-flow` that separates two paragraphs comfortably leaves the frame
   very nearly touching the sentence that introduces it. The pattern is the
   most common one in these docs — a line of prose, then the command it
   describes, then the next line of prose — so it repeats down the whole page
   and the result reads as one undifferentiated block.

   Both edges are set here, not just the top, because the rhythm is symmetric:
   what follows a block needs the same clearance as what precedes it, and the
   file's convention is that top margins own every seam.

   Three exclusions, each for a composition that spaces itself on purpose:
   an admonition is a small padded box that would look hollow at this step,
   mkdocstrings holds a symbol's body tight under its signature, and the hero
   uses a flex gap.

   `div.codehilite` / `div.highlight` are the wrappers `pymdownx.highlight`
   and `codehilite` emit. A bare `> pre` is the raw-HTML case; it is matched as
   a direct child only, so the `<pre>` INSIDE a wrapper keeps the zero margins
   codehilite.css gives it and the frame does not gain a band of dead space. */
article .typography
  :is(div.codehilite, div.highlight):not(:first-child, .admonition *, .doc *, .ms-hero *),
article .typography > pre:not(.mermaid, :first-child) {
  margin-top: var(--prose-code-gap);
}

/* Headings keep their own, larger section gaps — `:is(h2, …)` would otherwise
   be pulled DOWN to this step, which is the opposite of the intent. */
article .typography
  :is(div.codehilite, div.highlight):not(.admonition *, .doc *, .ms-hero *)
  + :not(h2, h3, h4, h5, h6),
article .typography > pre:not(.mermaid) + :not(h2, h3, h4, h5, h6) {
  margin-top: var(--prose-code-gap);
}

/* ─────────────────────────────────────────────────────────────────────────
   Landing page — hero, card grids, lifecycle strip, compatibility chips.
   Kept scoped via .ms-* class names so they only affect the home page
   (and any future page that opts in by using the same classes).
   Uses the existing shadcn CSS variables defined above so light/dark
   themes inherit automatically.
   ───────────────────────────────────────────────────────────────────────── */

/* ── Page masthead ─────────────────────────────────────────────────────────
   Every page opens as a pair. The H1 is a small accent label naming where you
   are, and the first H2 is the large visual title saying what the page claims.
   They are set tight against each other so the pair reads as one block.

   Every selector in this section is prefixed with `article` on purpose. The
   prose rules in tailwind/article.css are unlayered and reach (0,1,2), as in
   `article p:not(:first-child)` and `article .typography h3`. A bare
   `.ms-hero__lede { margin: 0 }` reaches only (0,1,0), so it loses and the
   block silently inherits body copy rhythm and body copy heading sizes. That
   is a defect with no symptom in the markup, the build or the tests. Matching
   the prose rules' reach is the whole reason these rules apply at all.
   ────────────────────────────────────────────────────────────────────────── */
article #page-header h1 {
  font-size: 0.85rem;
  line-height: 1.4;
  font-weight: 600;
  letter-spacing: 0.005em;
  color: var(--primary-text, var(--primary, #c46b48));
  word-break: normal;
}

/* The copy and pager buttons share the label's row and are 38px tall against a
   19px label, so they set the masthead's height. Bottom-aligning the two puts
   the label directly above the title and stops the cluster hanging past it.

   An earlier revision lifted the cluster out of flow instead. That did close
   the gap, but it left the cluster overlapping whatever came next, so both the
   label and the title had to reserve 7rem of right padding to clear it. On a
   short title that padding wrapped the line early and read as a stray indent
   on a heading with nothing beside it. Aligning costs nothing and reserves
   nothing. */
article #page-header {
  align-items: flex-end;
}

/* The masthead is one unit, so the article's 2rem inter-block gap must not
   land inside it. Cancelled only when a subtitle H2 actually opens the page,
   which leaves every other page's spacing untouched. */
article .typography:has(> h2:first-child) {
  margin-top: -2.25rem;
}
article .typography > h2:first-child {
  margin-top: 0;
  font-size: clamp(1.9rem, 3.2vw, 2.4rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 700;
  /* The title gets the full column, so a short one never wraps. If a consumer
     writes one long enough to wrap anyway, balance splits it evenly instead of
     leaving a single orphan word on the last line. */
  text-wrap: balance;
}

/* Hero. The elaborate variant of the same masthead, opted into by the class. */
article .ms-hero {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 0 0 0.5rem;
}

/* A hero states the page's own title, so the generated one duplicates it. The
   button cluster beside it stays. */
article:has(.ms-hero) #page-header h1 {
  display: none;
}

/* One owner for the hero's vertical rhythm, and it is the flex gap above. */
article .ms-hero > :is(p, div, section, ul, ol, h1, h2, h3, h4, h5, h6) {
  margin: 0;
}

.ms-hero__eyebrow {
  /* Primary-tinted breadcrumb-style label, matching the active-state treatment
     used by the left sidebar and right TOC, tying the three rails together. */
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.005em;
  color: var(--primary-text, var(--primary, #c46b48));
}

/* Markdown cannot put a class on a heading, so any heading inside the hero is
   the hero title. `.ms-hero__title` stays valid for heroes written as HTML. */
article .ms-hero :is(h1, h2, h3, h4, h5, h6),
article .ms-hero .ms-hero__title {
  font-size: clamp(1.9rem, 3.2vw, 2.4rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 700;
}

article .ms-hero__lede {
  font-size: 1.05rem;
  color: var(--muted-foreground, currentColor);
  max-width: 60ch;
}

/* Section heading weight. Geist is a variable face across 100 to 900, so the
   intermediate steps below render as asked rather than snapping to 700. The
   base scale set H2 lighter than H3, which left a section heading reading as
   less structural than the subsection under it. Each level now sits one short
   step above body copy and above the level below it. */
article .typography h2 { font-weight: 600; }
article .typography h3 { font-weight: 650; }
article .typography h4 { font-weight: 650; }

.ms-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0.25rem 0 0.5rem;
}
.ms-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.95rem;
  border-radius: 0.55rem;
  font-size: 0.92rem;
  font-weight: 500;
  text-decoration: none !important;
  border: 1px solid var(--border, transparent);
  transition: transform 0.05s ease, background-color 0.15s ease,
    border-color 0.15s ease;
}
.ms-btn:hover { transform: translateY(-1px); }
.ms-btn--primary {
  background: var(--primary, #c46b48);
  color: var(--primary-foreground, #fff) !important;
  border-color: var(--primary, #c46b48);
}
.ms-btn--secondary {
  background: var(--secondary, transparent);
  color: var(--secondary-foreground, currentColor) !important;
}
.ms-btn--ghost {
  background: transparent;
  color: var(--foreground, currentColor) !important;
}

/* Landing-page H2 icons — injected by icon-inject.js as <iconify-icon> elements. */
h2.ms-h2-icon {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
h2.ms-h2-icon > iconify-icon {
  flex: 0 0 1.35rem;
  width: 1.35rem;
  height: 1.35rem;
  color: var(--primary, #c46b48);
}

/* Differentiator pills row below the CTAs — scanner bait. */
.ms-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0.75rem;
}
.ms-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.32rem 0.7rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
  border-radius: 999px;
  background: var(--secondary, transparent);
  color: var(--secondary-foreground, currentColor);
  font-size: 0.82rem;
  font-weight: 500;
  line-height: 1;
}
.ms-pill__icon {
  /* Muted glyph so all five pill icons read as one system, matching shadcn's
     `Badge` convention (icon recedes, label carries the meaning). */
  color: var(--muted-foreground, currentColor);
  flex: 0 0 auto;
}

/* Hero screenshot row (sits below CTAs so primary actions stay above the fold) */
.ms-hero__screens {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 0.5rem;
}
.ms-hero__screens img {
  width: 100%;
  max-width: 520px;
  height: auto;
  border-radius: 0.6rem;
  border: 1px solid var(--border, transparent);
}

/* Generic card grid */
.ms-grid {
  display: grid;
  gap: 0.85rem;
  margin: 1rem 0 1.5rem;
}
.ms-grid--3 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.ms-grid--4 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.ms-grid--5 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

.ms-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 1rem 1.05rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  border-radius: 0.7rem;
  background: var(--card, transparent);
  color: var(--card-foreground, currentColor);
  text-decoration: none !important;
  transition: border-color 0.15s ease, transform 0.05s ease,
    box-shadow 0.15s ease;
}
a.ms-card:hover {
  border-color: var(--primary, #c46b48);
  transform: translateY(-1px);
  box-shadow: 0 4px 16px -8px rgba(0, 0, 0, 0.12);
}
.ms-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  margin-bottom: 0.2rem;
  border-radius: 0.5rem;
  background: color-mix(in srgb, var(--primary, #c46b48) 10%, transparent);
  color: var(--primary, #c46b48);
  font-size: 1.25rem;
  line-height: 1;
}
.ms-card__icon svg,
.ms-card__icon iconify-icon {
  width: 1.25rem;
  height: 1.25rem;
}
/* Multi-icon variant: each brand glyph gets its own mini-tile matching
   the single-icon style, laid out on a single row that never wraps. */
.ms-card__icon--multi {
  width: auto;
  min-width: 0;
  height: auto;
  padding: 0;
  gap: 0.3rem;
  background: transparent;
  border-radius: 0;
  flex-wrap: nowrap;
}
.ms-card__icon--multi svg,
.ms-card__icon--multi iconify-icon {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  padding: 0.625rem;
  box-sizing: content-box;
  border-radius: 0.5rem;
  background: color-mix(in srgb, var(--primary, #c46b48) 10%, transparent);
  flex-shrink: 0;
}
.ms-card__title {
  font-weight: 600;
  font-size: 1rem;
  color: var(--foreground, currentColor);
}
.ms-card__body {
  font-size: 0.88rem;
  color: var(--muted-foreground, currentColor);
  line-height: 1.45;
}
.ms-card__list {
  margin: 0.25rem 0 0;
  padding-left: 1.05rem;
  font-size: 0.86rem;
  color: var(--muted-foreground, currentColor);
}
.ms-card__list li { margin: 0.15rem 0; }
.ms-card__list a { color: inherit; }

/* Site-wide prose link style: dotted underline for inline links in content.
   Chrome elements (.ms-card, .ms-btn, .ms-chip, .ms-cta-row a) already set
   `text-decoration: none !important` and keep their flat look. Sidebar and
   search live outside `article`/`.md-content`, so they're unaffected. */
article a,
.md-content a {
  text-decoration: underline dotted;
  /* Accent the rule itself, softened against the link's own colour. At full
     strength a paragraph carrying several links reads as a block of colour
     rather than as text, and the underline competes with the word it marks. */
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}
/* Hover is where the underline should assert itself, so it goes to full accent. */
article a:hover,
.md-content a:hover {
  text-decoration-color: currentColor;
}

/* Lifecycle strip — numbered horizontal journey */
.ms-lifecycle {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0.75rem;
  counter-reset: ms-step;
  margin: 1rem 0 1.5rem;
}
/* The number and the step's name are one heading, so they share a row and the
   links sit under both. Stacking the counter above the title cost a line of
   height and read as a lone digit floating over a word. */
.ms-step {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: 0.5rem;
  row-gap: 0.35rem;
  padding: 1rem 1rem 0.9rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  border-radius: 0.7rem;
  background: var(--card, transparent);
}
.ms-step::before {
  counter-increment: ms-step;
  content: counter(ms-step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
  background: var(--primary, #c46b48);
  color: var(--primary-foreground, #fff);
  font-size: 0.78rem;
  font-weight: 700;
}
.ms-step__title {
  font-weight: 600;
  margin: 0;
}
/* Spans both columns so the links start under the number, not beside it. */
.ms-step__links {
  grid-column: 1 / -1;
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.85rem;
}
.ms-step__links a { color: var(--foreground, currentColor); }

/* Compatibility chips */
.ms-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
}
.ms-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.65rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  border-radius: 999px;
  background: var(--secondary, transparent);
  color: var(--secondary-foreground, currentColor) !important;
  font-size: 0.82rem;
  font-weight: 500;
  text-decoration: none !important;
}
.ms-chip:hover { border-color: var(--primary, #c46b48); }
.ms-chip__check {
  color: var(--primary, #c46b48);
  font-weight: 700;
}

/* "How it works" three-panel */
.ms-flow {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0.75rem;
  margin: 1rem 0 1.5rem;
  align-items: stretch;
}
.ms-flow__panel {
  position: relative;
  padding: 1rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  border-radius: 0.7rem;
  background: var(--card, transparent);
}
.ms-flow__step {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--primary, #c46b48);
  font-weight: 700;
  margin: 0 0 0.3rem;
}
.ms-flow__title {
  font-weight: 600;
  margin: 0 0 0.25rem;
}
.ms-flow__body {
  font-size: 0.88rem;
  color: var(--muted-foreground, currentColor);
  line-height: 1.45;
  margin: 0;
}

/* ─────────────────────────────────────────────────────────────────────────
   Content media primitives — screenshot figure, device mockups, carousel.
   Consolidated from per-consumer stylesheets so docs sites get them without
   forking. All tokenised. */

/* Keep the existing screenshot markup, but share a frameless caption overlay
   with carousel figures. The caption itself owns the scrim, so an uncaptioned
   image has no dark wash. No extra wrappers or generated text are needed. */
.ms-shot {
  margin: 1.25rem 0;
}
.ms-shot,
.ms-shots figure {
  position: relative;
  isolation: isolate;
  border: 0;
  border-radius: 0.5rem;
  overflow: hidden;
  background: transparent;
}
.ms-shot__frame {
  padding: 0;
  background: transparent;
}
.ms-shot__frame img {
  display: block;
  width: 100%;
  height: auto;
}
/* The caption rides in its own frosted-glass pill.

   `--ms-caption-alpha` is the one number that decides whether the text is
   readable, so it is not a taste setting. The pill composites over whatever
   the image happens to be, and a white image is the worst case: the backdrop
   the text lands on is `alpha` of the tint plus `1 - alpha` of white. At
   0.58 that bottoms out near #6b6b6b, which is 4.9:1 against white text —
   clear of WCAG AA with room to spare, and the blur can only pull the real
   backdrop toward the average, never past that bound. Anything below ~0.54
   fails AA over a bright photo, so retune it downward only alongside
   `test_caption_text_stays_readable_over_any_image`.

   The blur is what lets that alpha stay this low: detail behind the pill is
   averaged into a wash, so the text sits on flat tone instead of on edges,
   and the image still reads through as context rather than being painted
   out. `saturate` keeps the wash from going grey and muddy, and `brightness`
   leans on the backdrop itself, darkening what shows through without making
   the pill itself any more opaque. */
:root {
  --ms-caption-tint: 0 0 0;
  --ms-caption-alpha: 0.58;
  --ms-caption-blur: 14px;
}
.ms-shot > figcaption,
.ms-shots figcaption {
  position: absolute;
  inset: auto 1rem 0.75rem;
  z-index: 1;
  width: fit-content;
  max-width: calc(100% - 2rem);
  margin: 0 auto;
  /* Wider than it is tall, so the end caps read as caps and not as a
     rectangle whose corners happen to be rounded. */
  padding: 0.3rem 0.75rem;
  /* Any radius past half the height renders as a semicircular cap, so one
     large value stays a pill at every line count instead of needing a height
     the CSS cannot know. */
  border-radius: 999px;
  color: #fff;
  font-size: 0.875rem;
  line-height: 1.4;
  text-align: center;
  text-wrap: pretty;
  overflow-wrap: anywhere;
  background: rgb(var(--ms-caption-tint) / var(--ms-caption-alpha));
  backdrop-filter: blur(var(--ms-caption-blur)) saturate(135%) brightness(0.72);
  -webkit-backdrop-filter:
    blur(var(--ms-caption-blur)) saturate(135%) brightness(0.72);
  /* A hairline of light along the rim is what makes the glass legible as an
     object: it catches the edge on a dark image, where the tint alone would
     dissolve into the photo and leave the text looking unhoused. */
  border: 1px solid rgb(255 255 255 / 0.18);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  /* One soft shadow for depth. The old four-way 1px outline traced every
     glyph to survive a hard-edged backdrop; the blur removes those edges, so
     the outline is no longer holding anything up and only made the text
     shout. */
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.55);
  pointer-events: none;
}
/* Without a composited backdrop there is no blur to average the image, so the
   tint has to carry the contrast by itself and goes near-opaque. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ms-shot > figcaption,
  .ms-shots figcaption {
    --ms-caption-alpha: 0.88;
  }
}
.ms-shot > figcaption a,
.ms-shots figcaption a {
  color: inherit;
  pointer-events: auto;
}
.ms-shot > figcaption p,
.ms-shots figcaption p {
  margin: 0;
  color: inherit;
}
.ms-shot > figcaption code,
.ms-shots figcaption code {
  color: inherit;
  background: rgb(0 0 0 / 0.25);
}
@media (max-width: 640px) {
  .ms-shot > figcaption,
  .ms-shots figcaption {
    inset: auto 0.75rem 0.6rem;
    max-width: calc(100% - 1.5rem);
    /* Tighter than the desktop pill but still wider than tall, so the caps
       survive the smaller type. */
    padding: 0.25rem 0.625rem;
    font-size: 0.8rem;
  }
}

/* Side-by-side device mockups (e.g. laptop + phone) at matched height that
   never wrap between each other. Widths are tokens so the ratio tunes per use
   without forking. Images should carry `.no-border` to skip the content-image
   frame. Direct children: the images, then an optional `.ms-devices__caption`
   (forced onto its own line below via flex-basis). */
.ms-devices {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: center;
  gap: 2%;
  row-gap: 0.5rem;
  margin: 1.25rem 0;
}
.ms-devices img { height: auto; }
.ms-devices img:nth-of-type(1) { width: var(--ms-devices-primary, 71%); }
.ms-devices img:nth-of-type(2) { width: var(--ms-devices-secondary, 20%); }
.ms-devices__caption {
  flex-basis: 100%;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted-foreground, currentColor);
}

/* ── One canvas, separated by hairlines ────────────────────────────────────
   Every large surface is `--background`. The top bar, the tab rail, both side
   rails, the article and the footer are one continuous sheet, and what marks
   the joins is a 1px `--border` hairline. Nothing is tinted and nothing is
   elevated.

   This replaced a four-level fill scale that gave each region its own colour.
   The scale was measurably off the palette in both modes rather than merely
   strong: light painted the top bar `#ffffff`, the only cold neutral on a warm
   cream page, and dark painted it `#2a2925`, LIGHTER than `--card` and so
   above the top of the theme's own range. The palette already answers this —
   `--sidebar` is declared identical to `--background` in both modes, which is
   the theme stating that the rails are the canvas.

   Two artefacts came from the fills rather than from any rule, which is why
   neither is visible in the source:

   The rails ran the full page height while the article stopped where its text
   stopped, so the rail colour filled the ~130px between the last paragraph and
   the footer as a stripe across the full width. A region can only terminate
   mid-page if it is painted; unpainted, there is nothing to terminate.

   Each rail met the article on a bare colour change with no line on it, which
   reads as a smudge rather than an edge. A hairline is an edge or there is
   nothing; a soft step between two large fills is neither.

   The footer already worked this way and was the only chrome nobody
   complained about: `background: var(--background)` and one `--border` rule.
   The rest of the chrome now matches it.

   Hierarchy is carried by the hairlines, by the content column's measure and
   by the type scale. If a recessed rail is ever wanted back, it is one token
   consumed in one place, not a scale of four. */

/* The template already puts `bg-background` on the header, the shell and the
   sidebar. There is deliberately no rule here painting them: overriding that
   class is what put the chrome off-palette, and the correct value is the one
   the template already asks for. */

/* The rail divider spans the reading area, not the sidebar's own box.

   It is absolutely positioned inside the sticky sidebar, which is
   `100svh - 10rem` tall, so `bottom: 0` ended the line 160px above the fold at
   EVERY scroll position — a hairline stopping in open space, which is the one
   way to make a boundary read as a smudge rather than an edge. Measured at
   x=287: `--border` from the header down to y=534, then nothing.

   Height is taken from the viewport instead, less the header and the sidebar's
   own 0.6rem offset, so the line reaches the bottom of the screen. It is
   `bottom: auto` because a top and a bottom together resolve against the
   parent again, which is the box being escaped. */
.ms-rail-divider {
  bottom: auto;
  height: calc(100svh - var(--header-height) - 0.6rem);
}


/* The scrollbar gutter is reserved permanently.

   Any overlay that locks scrolling sets `overflow: hidden` on the root, and on
   a browser with CLASSIC scrollbars that removes ~15px of width and reflows
   every element on the page, then reflows it back on close. It reads as the
   page zooming and rebuilding itself around the viewer. It does NOT reproduce
   in a headless browser, which uses overlay scrollbars and therefore has no
   gutter to lose, so a measurement of the overlay's own styles will report the
   fix working while a real desktop still jumps. Reserve the gutter and hiding
   overflow costs no layout at all. */
:root {
  scrollbar-gutter: stable;
}

/* Full-screen image viewer (Viewer.js, theme `lightbox: true`).

   The library does the viewing. These rules do three things it does not: put a
   VISIBLE affordance on the page, stop the library reserving a strip of the
   screen for its own chrome, and bring that chrome onto theme tokens. */

/* The affordance. A cursor change says nothing until you are already hovering
   and nothing about what will happen, so an explicit button sits on the image.
   js/lightbox.js positions ONE of these over whichever image is hovered. */
.ms-zoomable {
  cursor: zoom-in;
}
.ms-zoom-hint {
  position: fixed;
  z-index: 40;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--border, #d4d4d8) 70%, transparent);
  border-radius: 0.55rem;
  background: color-mix(in srgb, var(--background, #fff) 94%, transparent);
  color: var(--foreground, #0a0a0a);
  cursor: pointer;
  box-shadow: 0 1px 2px -1px rgb(0 0 0 / 0.2), 0 4px 12px -4px rgb(0 0 0 / 0.25);
  transition: background-color 0.15s ease, transform 0.1s ease, opacity 0.15s ease;
}
.ms-zoom-hint[hidden] { display: none; }
.ms-zoom-hint svg { width: 16px; height: 16px; }
.ms-zoom-hint:hover {
  background: var(--background, #fff);
  transform: translateY(-1px);
}
.ms-zoom-hint:focus-visible {
  outline: 2px solid var(--primary, #c15f3c);
  outline-offset: 2px;
}

/* The detached gallery list js/lightbox.js hands to the library. It is markup,
   not content, and must never take part in layout. */
.ms-viewer-source { display: none; }

/* THE RULE THAT MAKES FULL SCREEN ACTUALLY FULL SCREEN.

   Viewer.js sizes the picture against `container.height - footer.offsetHeight`
   and reads that `offsetHeight` off the element directly. So a footer that is
   merely `position: absolute` still measures ~65px, and the library still
   hands that strip away — on an 844x390 landscape phone the image was held at
   78% of the size it could be, and the reader sees a small picture with empty
   bands around it. `position` alone does not fix this; the footer has to
   measure ZERO and let its children float on their own.

   Everything inside it is then placed explicitly, and `pointer-events` is
   handed back per child so a zero-height, full-width footer cannot swallow
   clicks meant for the image. */
.ms-viewer .viewer-footer {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  margin: 0;
  overflow: visible;
  pointer-events: none;
}
.ms-viewer .viewer-title,
.ms-viewer .viewer-toolbar {
  position: absolute;
  left: 0;
  right: 0;
  margin: 0;
  pointer-events: auto;
}

/* Toolbar and caption ride above the bottom edge, clear of a home indicator. */
.ms-viewer .viewer-toolbar {
  bottom: max(0.75rem, env(safe-area-inset-bottom));
  display: flex;
  justify-content: center;
}
.ms-viewer .viewer-title {
  bottom: calc(max(0.75rem, env(safe-area-inset-bottom)) + 3.25rem);
  max-width: min(92vw, 60ch);
  margin-inline: auto;
  padding: 0.3rem 0.7rem;
  border-radius: 0.5rem;
  /* The caption sits on the picture, so it carries its own backing rather
     than relying on whatever happens to be behind it. */
  background: rgb(0 0 0 / 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #fff;
  font: 500 0.8125rem / 1.4 var(--font-sans);
  text-align: center;
  opacity: 1;
}
.ms-viewer .viewer-title:empty { display: none; }

/* A small, static blur separates the viewer from the page behind it.
   `viewer-backdrop` is a class the library puts ON the container, not a child
   element, so this is one selector rather than a descendant one. */
.ms-viewer.viewer-backdrop {
  background: rgb(0 0 0 / 0.86);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

/* Real buttons with a surface, so they read as controls over a light image as
   well as a dark one, and are big enough to hit on a phone. */
.ms-viewer .viewer-toolbar > ul > li {
  width: 40px;
  height: 40px;
  margin: 0 0.2rem;
  border: 1px solid rgb(255 255 255 / 0.18);
  border-radius: 0.7rem;
  background: rgb(38 38 38 / 0.94);
  box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
  transition: background-color 0.15s ease, transform 0.1s ease;
}
.ms-viewer .viewer-toolbar > ul > li::before {
  margin: 0.55rem;
}
.ms-viewer .viewer-toolbar > ul > li:hover {
  background: rgb(58 58 58 / 0.98);
  transform: translateY(-1px);
}
.ms-viewer .viewer-button {
  width: 44px;
  height: 44px;
  top: max(0.75rem, env(safe-area-inset-top));
  right: max(0.75rem, env(safe-area-inset-right));
  inset-inline-end: max(0.75rem, env(safe-area-inset-right));
  border-radius: 0.8rem;
  background: rgb(38 38 38 / 0.94);
  border: 1px solid rgb(255 255 255 / 0.18);
}
.ms-viewer .viewer-button::before {
  /* The library centres its glyph for a round button twice the size. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
}
.ms-viewer .viewer-button:hover {
  background: rgb(58 58 58 / 0.98);
}
.ms-viewer .viewer-prev,
.ms-viewer .viewer-next {
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background-color: rgb(38 38 38 / 0.94);
  border: 1px solid rgb(255 255 255 / 0.18);
}
.ms-viewer .viewer-toolbar > ul > li:focus-visible,
.ms-viewer .viewer-button:focus-visible,
.ms-viewer .viewer-prev:focus-visible,
.ms-viewer .viewer-next:focus-visible {
  outline: 2px solid var(--primary, #c15f3c);
  outline-offset: 2px;
}

@media (max-width: 640px) {
  /* A phone has no room for rotate/reset beside the essentials. */
  .ms-viewer .viewer-toolbar > ul > li.viewer-rotate-left,
  .ms-viewer .viewer-toolbar > ul > li.viewer-rotate-right {
    display: none;
  }
  .ms-viewer .viewer-title {
    max-width: calc(100vw - 1.5rem);
    font-size: 0.75rem;
  }
}

/* Video. A player is as heavy as a carousel and had no rule in the theme at
   all, so every consumer centred it and spaced it with an inline style. Those
   all read `margin: 2rem auto 0`, which is 2rem above and nothing below, and
   an inline style cannot be corrected from here. The theme owns the box now,
   so a consumer writes `<video controls>` and deletes its inline margins. */
article .typography video,
.md-content video {
  display: block;
  width: 100%;
  max-width: 960px;
  height: auto;
  margin-block: 1.5rem;
  margin-inline: auto;
  border-radius: 0.6rem;
}

/* Image carousel (Swiper). Enabled with theme `carousel: true`, which makes the
   theme emit the Swiper CDN + init (js/carousel.js); consumers only write the
   `.swiper.ms-shots` markup. Swiper is themed to shadcn tokens so light/dark
   track automatically; its bold default arrow glyph is swapped for a thin
   masked lucide chevron. Inert (just a static first slide) if Swiper is absent. */
.ms-shots {
  width: 100%;
  max-width: 100%;
  /* Top margin is the theme's own block step, not a smaller one. A carousel is
     a framed object with a border and its own controls, so the 0.75rem it used
     to carry left it crowding whatever introduced it — tighter than the
     `--prose-flow` every paragraph, list and table around it gets, which is
     the opposite of what a heavier object wants. It sits on the code-block
     rung for that reason: a step above the body flow, the same allowance a
     fenced block gets and for the same reason.

     Stated as separate properties rather than the `margin` shorthand it
     replaces: the shorthand also reset the inline margins to 0, which is
     harmless here but meant this rule silently owned axes it had no opinion
     about. */
  margin-top: var(--prose-code-gap, 2rem);
  margin-bottom: 0.5rem;
  padding: 0;
  overflow: hidden;
  --swiper-theme-color: var(--primary, #c15f3c);
  --swiper-navigation-size: 20px;
  /* Swiper hardcodes its inactive bullet to black at 0.2 alpha. That measures
     19.95:1 on the light page and 1.15:1 on the dark one, so the indicator
     simply vanished in dark mode. Both bullet colours now come from theme
     tokens and track the mode.

     The active bullet takes `--primary-text`, not `--primary`. It is a
     non-text indicator, so it answers to the 3:1 floor, and the fill value
     measures 2.97:1 on the light page. See the accent note above. */
  --swiper-pagination-color: var(--primary-text, var(--primary, #c15f3c));
  --swiper-pagination-bullet-inactive-color: var(--foreground, #0a0a0a);
  --swiper-pagination-bullet-inactive-opacity: 0.5;
}
.ms-shots .swiper-slide {
  display: flex;
  justify-content: center;
  box-sizing: border-box;
}
.ms-shots figure { margin: 0; width: 100%; aspect-ratio: 16 / 9; }
article .ms-shots img,
.md-content .ms-shots img,
.ms-shots img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  background: transparent;
  border: 0;
  border-radius: 0.5rem;
}
.ms-shots .swiper-pagination {
  position: static;
  margin-top: 0.35rem;
  line-height: 1;
}
.ms-shots .swiper-button-prev,
.ms-shots .swiper-button-next {
  width: 32px;
  height: 32px;
  margin-top: -16px;
  border-radius: 999px;
  background: var(--card, hsl(0 0% 100%));
  border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  color: var(--card-foreground, currentColor);
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.ms-shots .swiper-button-prev:hover,
.ms-shots .swiper-button-next:hover {
  background: var(--accent, hsl(48 25% 92.2%));
  border-color: var(--ring, var(--border, rgba(0, 0, 0, 0.18)));
}
.ms-shots .swiper-button-prev::after,
.ms-shots .swiper-button-next::after {
  content: "";
  width: 16px;
  height: 16px;
  background-color: currentColor;
  -webkit-mask: var(--ms-chev) center / 16px 16px no-repeat;
  mask: var(--ms-chev) center / 16px 16px no-repeat;
}
.ms-shots .swiper-button-prev {
  --ms-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
}
.ms-shots .swiper-button-next {
  --ms-chev: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
}
/* Swiper v12 injects its own bold <svg class="swiper-navigation-icon">; hide it
   so only our thin chevron (the ::after) shows. */
.ms-shots .swiper-button-prev .swiper-navigation-icon,
.ms-shots .swiper-button-next .swiper-navigation-icon { display: none; }

@media (max-width: 720px) {
  .ms-shots .swiper-button-prev,
  .ms-shots .swiper-button-next { display: none; }
}

/* ─── Navigation chrome: sidebar / TOC active states ──────────────────────────
   Targets the shadcn theme's data-[active=true] hooks. We override the theme's
   default heavy fill (`bg-accent` on a leaf) with a soft tinted pill that uses
   the primary clay for text + icon — the shadcn `SidebarMenuButton` convention
   (`data-[active=true]:bg-sidebar-accent text-sidebar-accent-foreground`,
   tilted toward primary so the active row reads as "selected", not "button").
   ───────────────────────────────────────────────────────────────────────── */

/* Sidebar density + row highlight (Devin / DeepWiki cadence). The shadcn template
   ships airy defaults (h-8 group labels, p-2 groups, gap-1 menus) and makes leaf
   rows `lg:w-fit`, so hover/active hugs the text like a highlighter instead of a
   row/button selector. We tighten to a comfortable-but-compact rhythm (looser
   than a bare list, tighter than the stock spacing), keep section headings as
   bold sentence-case dividers (no uppercase), and stretch rows full-width.
   mewbo.css is unlayered, so these win over the layered Tailwind utilities
   without !important — except the icon-collapsed `size-8!`/`p-2!` rules, which
   keep !important and so still win in that mode (intended). */
.mewbo-sidebar-group {
  margin-top: 0.55rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}
.mewbo-sidebar-group:first-of-type { margin-top: 0; }
.mewbo-sidebar-group-label {
  height: auto;
  margin-bottom: 0.2rem;
  padding-top: 0.2rem;
  padding-bottom: 0.2rem;
  /* A group label names the entries under it, so it cannot be smaller than
     they are. At 0.7rem it rendered 11.2px against a 12.8px menu entry, which
     read as a caption on the rail rather than as the heading of a section.
     It now sits one step above the entries and carries the weight. */
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--foreground);
}
/* Comfortable, full-width rows: padding around the entry text plus a small gap
   between entries; each row stretches to the group's bounds so the highlight
   reads as a selected row, not a text run. */
[data-sidebar="menu"],
[data-sidebar="menu-sub"] { gap: 0.2rem; }
[data-sidebar="menu-button"] {
  width: 100%;
  max-width: 100%;
  min-height: 2rem;
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}

/* Soft active leaf — overrides the theme's `data-[active=true]:bg-accent
   data-[active=true]:border-accent` (cream fill, reads as a button on cream
   background) with a primary-tinted soft pill: clay text + icon, light primary
   tint background, transparent border. Uses the same tokens shadcn ships. */
[data-sidebar="menu-button"][data-active="true"] {
  background-color: color-mix(in srgb, var(--primary) 12%, transparent) !important;
  border-color: transparent !important;
  /* Text and glyph on the tint use --primary-text: on light's cream the fill
     value measured 2.62:1 against this very chip. The BACKGROUND above stays
     --primary, because a fill and legible text on it want opposite
     lightness. */
  color: var(--primary-text) !important;
}
[data-sidebar="menu-button"][data-active="true"] svg {
  color: var(--primary-text);
}
/* Idle leaf: muted icon glyphs (Devin pattern — icons recede until hovered).
   Opacity dials them ~25% softer than the muted text color, so the eye
   reads them as glyphs/decoration rather than as part of the link weight,
   pulling focus toward the center column. Hover lifts to full foreground. */
[data-sidebar="menu-button"]:not([data-active="true"]) > svg:first-of-type {
  color: var(--muted-foreground);
  opacity: 0.75;
  transition: color 0.15s, opacity 0.15s;
}
[data-sidebar="menu-button"]:not([data-active="true"]):hover > svg:first-of-type {
  color: var(--foreground);
  opacity: 1;
}

/* Expandable section affordance: quiet chevron at the row's end that
   rotates right→down when the section opens. Sits in the same muted
   register as the leading icon so it reads as a hint, not a button. */
.mewbo-sidebar-chevron {
  color: var(--muted-foreground);
  opacity: 0.55;
  transition: opacity 0.15s, transform 0.2s ease;
}
[data-sidebar="menu-button"]:hover .mewbo-sidebar-chevron { opacity: 0.9; }

/* Level-2 indent rail: shadcn already wraps sub-items in
   `ul.ml-3.5.border-l.pl-2.5` — bump the rail's contrast slightly so it
   reads as a visible hierarchy cue instead of a near-invisible hairline.
   Targeting the shared sub-menu slot so the rule is scoped, not global. */
[data-sidebar="menu-sub"] {
  border-left-color: color-mix(in srgb, var(--sidebar-border) 100%, var(--foreground) 8%) !important;
}

/* Right TOC: continuous left rail with primary-tinted active segment.
   The rail is a 1px inset shadow on each link (joins seamlessly between
   siblings); the active variant swaps it to a 2px primary shadow with no
   layout shift. IntersectionObserver in callbacks.js toggles data-active by
   matching the link's href to the topmost visible heading id. */
.mewbo-toc {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem 1rem 0;
  font-size: 0.875rem;
}
.mewbo-toc__heading {
  position: sticky;
  top: 0;
  margin: 0;
  height: 1.5rem;
  background: var(--background);
  color: var(--foreground);
  font-size: 0.8rem;
  font-weight: 500;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.mewbo-toc__heading-glyph {
  display: inline-flex;
  align-items: center;
  color: var(--muted-foreground);
  opacity: 0.75;
  flex-shrink: 0;
}
.mewbo-toc__list {
  display: flex;
  flex-direction: column;
}
.mewbo-toc__link {
  display: block;
  padding: 0.3rem 0 0.3rem 0.85rem;
  /* Inset shadow forms the rail. Two adjacent links' shadows touch since
     each runs the full height; no layout shift between idle/active. */
  box-shadow: inset 1px 0 0 var(--border);
  color: var(--muted-foreground);
  font-size: 0.8rem;
  line-height: var(--leading-ui);
  text-decoration: none;
  transition: color 0.15s ease, box-shadow 0.15s ease;
}
.mewbo-toc__link:hover { color: var(--foreground); }
.mewbo-toc__link[data-active="true"] {
  color: var(--primary-text);
  font-weight: 500;
  box-shadow: inset 2px 0 0 var(--primary-text);
}
/* H3s nest visually below their H2 parent: text indents further while the
   rail stays flush, so the parent/child relationship reads structurally. */
.mewbo-toc__link[data-depth="3"] { padding-left: 1.85rem; }

/* ─── Site header: three-zone composition ──────────────────────────────────
   Every interactive control sits on a single h-9 baseline and uses the
   theme's hover/focus tokens (--accent, --ring), so the row reads as one
   composed strip. .mewbo-icon-btn is the shared shadcn ghost-icon-button
   shell — reused for mobile-search, GitHub link, and theme toggle.
   ───────────────────────────────────────────────────────────────────────── */

/* Elevate the whole sticky stack, not each row. The contact shadow marks
   its lower edge; the broad downward shadow separates scrolling content.
   Keep this on the header's foreground stacking context so it paints OVER
   the article, with no overlay element intercepting links below the bar. */
.mewbo-header {
  /* One height for the two labelled header controls (search pill, Ask AI).
     Declared here rather than at :root because it describes the header's own
     control scale, and both controls are descendants. */
  --mewbo-nav-control-h: 2rem;
  /* A quiet satin sheen, derived entirely from the site's surface palette. */
  --mewbo-chrome-finish: linear-gradient(180deg,
    color-mix(in srgb, var(--card) 32.5%, var(--background)) 0%,
    color-mix(in srgb, var(--muted) 9%, var(--background)) 48%,
    color-mix(in srgb, var(--muted) 19%, var(--background)) 100%);
  background-image: var(--mewbo-chrome-finish);
  z-index: 50;
  isolation: isolate;
  box-shadow: 0 3px 6px -3px rgb(0 0 0 / 0.16),
    0 16px 30px -10px rgb(0 0 0 / 0.18);
}
.dark .mewbo-header {
  box-shadow: 0 3px 6px -3px rgb(0 0 0 / 0.5),
    0 18px 36px -10px rgb(0 0 0 / 0.55);
}

/* Only the LAST element of the nav block draws the line that ends it. With a
   tab rail the header's own bottom border sits at the same y as the rail's,
   which paints 2px of hairline where the design calls for 1 — measurable as
   two identical border rows rather than one. `ms-has-header-tabs` is on
   `body` whenever tabs are configured, so each case names its own edge. */
body:not(.ms-has-header-tabs) .mewbo-header {
  border-bottom: 1px solid var(--border);
}

/* Brand cluster: its mark is deliberately the header's largest visual cue;
   the nearby Docs badge identifies this surface without competing for that
   role. Visibility remains owned by the template's `hidden lg:inline-flex`.
   The inner SVG, image, and Iconify custom element all inherit the same box. */
.mewbo-brand {
  align-items: center; gap: 0.55rem;
  min-width: 0; height: 2.75rem; padding: 0 0.25rem;
  /* The name remains quieter than page content so the larger scale reads as
     identity, rather than an extra navigation control. */
  color: var(--muted-foreground); text-decoration: none;
  border-radius: 0.5rem;
  transition: background 0.15s, color 0.15s;
}
.mewbo-brand:hover { background: var(--accent); color: var(--foreground); }
.mewbo-brand__glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; flex: 0 0 1.75rem;
}
.mewbo-brand__glyph svg,
.mewbo-brand__glyph img,
.mewbo-brand__glyph iconify-icon { width: 100%; height: 100%; }
/* Align the two text baselines as one cluster, then center it on the mark. */
.mewbo-brand__text {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  min-width: 0;
}
.mewbo-brand__name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 1.03125rem; font-weight: 600; letter-spacing: -0.025em;
  line-height: 1;
}
.mewbo-brand__docs {
  /* Center the smaller lettering optically, not its padded border box. */
  position: relative;
  top: -0.1875rem;
  flex: 0 0 auto;
  padding: 0.08rem 0.25rem;
  border: 1px solid var(--border); border-radius: 0.2rem;
  color: var(--muted-foreground);
  font-size: 0.525rem; font-weight: 500; line-height: 1;
}
.mewbo-brand:hover .mewbo-brand__docs { border-color: var(--foreground); }

/* Mobile menu trigger. Visibility owned by Tailwind `flex lg:hidden`. */
.mewbo-menu-btn {
  align-items: center; gap: 0.5rem;
  height: 2.25rem; padding: 0 0.5rem;
  background: transparent; border: 0; color: var(--foreground);
  cursor: pointer; border-radius: 0.5rem;
  transition: background 0.15s;
}
.mewbo-menu-btn:hover { background: var(--accent); }

/* Shared ghost-icon-button shell. Composed from the same tokens shadcn ships
   (--accent, --ring) so swapping themes / dark mode just works. Visibility +
   display owned by Tailwind utilities on each template usage. */
.mewbo-icon-btn {
  align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem;
  padding: 0; border: 0;
  background: transparent; color: var(--foreground);
  cursor: pointer; border-radius: 0.5rem;
  transition: background 0.15s, color 0.15s;
}
.mewbo-icon-btn:hover { background: var(--accent); color: var(--accent-foreground); }
.mewbo-icon-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
.mewbo-icon-btn svg { width: 1rem; height: 1rem; flex-shrink: 0; }
/* Wide variant: expands width to fit a label/count alongside the icon
   (e.g. GitHub link with stargazers count). */
.mewbo-icon-btn--wide { width: auto; padding: 0 0.6rem; gap: 0.4rem; }
.mewbo-stargazers {
  font-size: 0.75rem; color: var(--muted-foreground);
  font-variant-numeric: tabular-nums;
}

/* ── Header dropdowns (version / branch) ──────────────────────────────────
   Two separate switchers can appear in the header: the mike one from
   `version_select.html` (a bare `<select>`, previously unstyled) and the
   Mewbo one built by js/version-switcher.js (Tailwind classes, `h-8`, plus a
   "Version" text label). Two spellings of the same control, so they agreed on
   nothing — different heights, different fonts, one labelled and one not, and
   the unstyled one clipped its own text along the bottom because a native
   select sizes its box from font metrics rather than the line box.

   One class now owns the geometry and both use it. The rules:
     · 2.25rem tall, matching `.mewbo-icon-btn` — every control in the row is
       the same height, which is what makes the row read as a row;
     · an icon marks what the dropdown switches, instead of a text label —
       the value already says "latest", so a word beside it was redundant and
       only one of the two had it;
     · `appearance: none` so the box is ours, and the chevron is a background
       image, because a `<select>` cannot hold a child element to draw one in.
   `line-height` is stated equal to the height; that is what stops the
   clipping.
   ────────────────────────────────────────────────────────────────────────── */
.ms-header-select {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  color: var(--muted-foreground);
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.ms-header-select:hover {
  background: var(--accent);
  color: var(--accent-foreground);
}
.ms-header-select:focus-within {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
.ms-header-select__icon {
  width: 0.875rem;
  height: 0.875rem;
  flex-shrink: 0;
  opacity: 0.75;
}
/* The inner select is the whole control minus the icon: no chrome of its own,
   because the wrapper draws the box. */
.ms-header-select > select {
  appearance: none;
  -webkit-appearance: none;
  height: 100%;
  max-width: 9rem;
  margin: 0;
  padding: 0 1.1rem 0 0;
  border: 0;
  background-color: transparent;
  color: var(--foreground);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 2.25rem;
  text-overflow: ellipsis;
  cursor: pointer;
  outline: none;
}

/* The mike selector has no wrapper to put an icon in — it is a lone <select>
   emitted by a template this theme does not control the innards of. It gets
   the same box, with the icon as a second background image on the left. */
#version-selector {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  align-items: center;
  height: 2.25rem;
  width: fit-content;
  max-width: 11rem;
  margin: 0;
  padding: 0 1.75rem 0 1.85rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background-color: transparent;
  color: var(--foreground);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 2.25rem;
  text-overflow: ellipsis;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
#version-selector:hover {
  background-color: var(--accent);
  color: var(--accent-foreground);
}
#version-selector:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}

/* Chevron (right) and history icon (left), inlined so neither costs a
   request. `currentColor` is not available inside a data URI, so the stroke
   is a fixed mid-grey that reads acceptably against both themes — the same
   compromise every `appearance: none` select makes. */
.ms-header-select > select,
#version-selector {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.15rem center;
  background-size: 0.85rem;
}
/* Chevron plus a leading mark, same as the branch switcher's — but this one
   lists mike's published refs ("main (latest)"), so the mark is lucide's
   git-branch rather than the history clock. Two dropdowns side by side need
   to say which is which at a glance, and that is the whole job of the icon
   now that neither carries a text label. */
#version-selector {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='3' x2='6' y2='15'/%3E%3Ccircle cx='18' cy='6' r='3'/%3E%3Ccircle cx='6' cy='18' r='3'/%3E%3Cpath d='M18 9a9 9 0 0 1-9 9'/%3E%3C/svg%3E");
  background-position: right 0.55rem center, left 0.6rem center;
  background-size: 0.85rem, 0.875rem;
}

/* The dropdown list itself is painted by the OS, which does not inherit the
   page theme — an unset option list is white-on-white in dark mode. */
.ms-header-select > select option,
#version-selector option {
  background: var(--popover);
  color: var(--popover-foreground);
}

/* On a phone the right-hand cluster only has room for the controls a reader
   actually reaches for: search, the repo link, and the theme toggle. The
   version and branch pickers are desktop affordances — nobody switches
   documentation versions mid-scroll on a phone — so they return at `md`
   rather than competing for a 320px row. */
@media (max-width: 767px) {
  #version-selector,
  #version-switcher,
  .ms-header-select,
  .mewbo-nav-version {
    display: none;
  }
}

/* Sun/moon cross-dissolve: rotate-90 + scale-0 swap, ~200ms. The shadcn
   idiom for theme toggles. Light = sun visible / moon hidden; .dark flips. */
.mewbo-theme-toggle { position: relative; }
.mewbo-theme-toggle__sun,
.mewbo-theme-toggle__moon {
  position: absolute; inset: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.mewbo-theme-toggle__sun  { transform: rotate(0)        scale(1); opacity: 1; }
.mewbo-theme-toggle__moon { transform: rotate(-90deg)   scale(0); opacity: 0; }
.dark .mewbo-theme-toggle__sun  { transform: rotate(90deg) scale(0); opacity: 0; }
.dark .mewbo-theme-toggle__moon { transform: rotate(0)      scale(1); opacity: 1; }

/* Search pill (centered, lg+). Shares --mewbo-nav-control-h with the Ask AI
   button so the two read as one pair of controls; a touch shorter than the
   h-9 icon buttons beside them, which are square and carry no label. */
.mewbo-nav-search-pill {
  display: inline-flex; align-items: center; gap: 0.5rem;
  width: 100%; max-width: 22rem;
  height: var(--mewbo-nav-control-h); padding: 0 0.75rem;
  border: 1px solid var(--border); border-radius: 0.5rem;
  background: var(--background); color: var(--muted-foreground);
  font-size: 0.875rem; cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.mewbo-nav-search-pill:hover {
  /* Quiet hover: the hairline firms up a touch, no loud clay ring. */
  border-color: color-mix(in srgb, var(--foreground) 16%, var(--border));
  background: var(--muted); color: var(--foreground);
}
.mewbo-nav-search-pill svg { flex-shrink: 0; opacity: 0.6; }
.mewbo-nav-search-pill span { flex: 1; text-align: left; }

/* ── Keycaps ──────────────────────────────────────────────────────────────
   A `<kbd>` has to look like a thing you press. The browser default is a
   monospace run of text, which reads as code; a single hairline box reads as
   a badge. What sells a keycap is that it has a TOP FACE and WALLS, lit from
   above — and that is entirely a matter of which edges are lighter than the
   face and which are darker.

   This previously shipped as a bundled font (Libertinus Keyboard) whose
   glyphs drew their own outlines. That is gone: a 46KB webfont was a network
   and packaging cost for pure decoration, only some labels had glyphs (so
   `Cmd` and the arrows fell back to a hand-drawn box and never matched the
   others), and nothing about it was themeable.

   The construction, adapted from miragecraft/keymason's layered technique:
     · the element itself is the dark under-key shadow, one step deeper than
       everything above it;
     · `::before` is the base, with ASYMMETRIC border colours — lighter along
       the top, darker at the sides, darkest along the front — which is what
       makes the walls read as walls rather than as a uniform frame;
     · the label sits on the top face, inset from those walls.
   Keymason spends two pseudo-elements and a 3x3 grid on this because it
   renders whole keyboards at 60px per unit. At text size the middle layer
   is under a pixel, so it is collapsed into one `::before` plus an inset
   highlight — the same lighting, at a scale where more layers would only
   produce fringing.

   Sized in `em` throughout, unlike keymason's fixed `--u`, because these
   caps sit inline in a sentence and in table cells and must track whatever
   type size surrounds them.
   ────────────────────────────────────────────────────────────────────── */

kbd:not(:has(kbd)):not(.mewbo-kbd) {
  /* Face and walls derive from `--muted`, so the cap belongs to the palette
     and inverts with the theme on its own. `--kbd-face` is stated as an
     opaque mix rather than a translucent fill: these sit on table rows that
     carry their own hover tint, and a translucent cap would shift colour as
     the row highlighted. */
  --kbd-face: color-mix(in oklab, var(--muted) 82%, var(--background));
  --kbd-edge-top: color-mix(in oklab, var(--kbd-face) 88%, white);
  --kbd-edge-side: color-mix(in oklab, var(--kbd-face) 86%, black);
  --kbd-edge-front: color-mix(in oklab, var(--kbd-face) 70%, black);

  position: relative;
  display: inline-block;
  height: auto;
  min-height: 0;
  min-width: 1.9em;
  box-sizing: border-box;
  /* The front wall is the deep one — a keycap is seen slightly from above,
     so its bottom edge shows and its top edge barely does. The extra bottom
     padding is that wall; the label stays optically centred because of it,
     not in spite of it. */
  padding: 0.28em 0.55em 0.36em;
  margin-inline: 0.12em;
  border: 1px solid var(--kbd-edge-side);
  border-top-color: var(--kbd-edge-top);
  border-bottom-color: var(--kbd-edge-front);
  border-radius: 0.36em;
  background: var(--kbd-face);
  /* Three cues, cheapest first: a bright inset lip along the top of the face
     (the light source), a dark inset along the front (the wall in shadow),
     and one crisp offset shadow under the whole cap — offset rather than
     blurred, which is what keeps it reading as a solid object sitting on the
     page instead of a floating card. */
  box-shadow:
    inset 0 0.08em 0 color-mix(in oklab, var(--kbd-edge-top) 60%, white),
    inset 0 -0.18em 0 -0.08em color-mix(in oklab, var(--kbd-edge-front) 55%, transparent),
    0 0.11em 0 color-mix(in oklab, var(--kbd-edge-front) 70%, transparent);

  font-family: var(--font-sans);
  font-size: 0.8em;
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: 0.01em;
  text-align: center;
  color: var(--foreground);
  white-space: nowrap;
  vertical-align: 0.06em;
}

/* `min-width` above is what keeps `C` from rendering as a narrow sliver
   beside `Command` — every cap is at least squarish, and long labels grow
   the cap horizontally rather than being shrunk to fit. Keymason solves the
   same problem by making width an authored choice; inline text has no such
   author, so the floor does it. */

/* Dark: the same lighting, recomputed. A cap on a near-black page cannot be
   built by lightening toward white — the face would glow — so the mixes are
   weaker and the front wall leans on the page colour beneath it. */
.dark kbd:not(:has(kbd)):not(.mewbo-kbd) {
  --kbd-face: color-mix(in oklab, var(--muted) 86%, var(--foreground) 3%);
  --kbd-edge-top: color-mix(in oklab, var(--kbd-face) 82%, white);
  --kbd-edge-side: color-mix(in oklab, var(--kbd-face) 74%, black);
  --kbd-edge-front: color-mix(in oklab, var(--kbd-face) 45%, black);
}

/* A cap nested in a tooltip is on an inverted surface and would otherwise
   paint a light key on a dark chip. Left flat there on purpose. */
[data-slot="tooltip-content"] kbd:not(:has(kbd)) {
  box-shadow: none;
  border-color: color-mix(in oklab, currentColor 30%, transparent);
}

/* The `++ctrl+shift+a++` group. `align-items: center` matters: the caps are
   inline-block with a front wall, so their boxes are taller than the `+`
   between them and a baseline alignment leaves the separator sitting low. */
.keys {
  display: inline-flex;
  align-items: center;
  gap: 0.1em;
  white-space: nowrap;
  /* The caps carry a drop shadow, so a line of them needs a little more
     room than a line of text or the row below crowds it. */
  line-height: 1.9;
}
.keys > span,
.keys .key-separator {
  margin-inline: 0.12em;
  color: var(--muted-foreground);
  font-size: 0.85em;
}

/* The nav pill's hint, same treatment as the search field's: two real
   keycaps rather than one bordered box holding two letters, scaled down to
   sit quietly beside the "Search…" placeholder. The wrapper keeps no chrome
   of its own — the caps inside it are the visible thing. */
.mewbo-kbd {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  /* Two keys, two caps, and a gap wide enough to read as air between them.
     At 0.15em (under 2px at this size) the caps touched and the pair read as
     ONE wider key with a hairline down the middle — which is what a reader
     sees before they read the glyphs. A shortcut is two keystrokes, so the
     hint has to look like two keys. */
  gap: 0.3em;
  flex-shrink: 0;
  border: 0;
  background: transparent;
  font-size: 0.78rem;
  line-height: 1;
  opacity: 0.85;
  pointer-events: none;
}
@media (max-width: 639px) { .mewbo-kbd { display: none; } }

/* Ask AI button: same h-9 token. Quiet by default — the label rides at
   muted-foreground (not stark white) so it sits as a peer of the search pill
   rather than shouting; the sparkle inherits that tone. It warms to full
   foreground on hover. */
.mewbo-nav-ask-ai-btn {
  display: inline-flex; align-items: center; gap: 0.4rem;
  height: var(--mewbo-nav-control-h); padding: 0 0.85rem;
  border: 1px solid var(--border); border-radius: 0.5rem;
  background: var(--background); color: var(--muted-foreground);
  font-size: 0.875rem; font-weight: 500; white-space: nowrap; cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.mewbo-nav-ask-ai-btn svg { opacity: 0.85; }
.mewbo-nav-ask-ai-btn:hover {
  background: var(--muted); color: var(--foreground);
  border-color: color-mix(in srgb, var(--foreground) 16%, var(--border));
}

/* ─── Header tab rail (theme.header_tabs) ──────────────────────────────────
   Second row of the sticky header, fused under the utility row on the SAME
   surface: the rail lives inside .mewbo-header, so the header's hairline +
   shadow land under it and both rows stick/scroll as one unit. The divider
   between the rows is the rail's own border-top. Items are quiet by
   default; the active tab takes the accent (--primary) plus a thin 2px
   underline sitting on the rail's bottom edge, flagged aria-current="page".
   On narrow screens the rail scrolls horizontally with hidden scrollbars.
   ───────────────────────────────────────────────────────────────────────── */

.ms-header-tabs { border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
.ms-header-tabs__rail {
  display: flex; align-items: stretch; gap: 0.25rem;
  height: 2.5rem;
  overflow-x: auto;
  scrollbar-width: none;            /* Firefox */
}
.ms-header-tabs__rail::-webkit-scrollbar { display: none; }
.ms-header-tabs__item {
  position: relative;
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0 0.65rem;
  color: var(--muted-foreground);
  font-size: 0.875rem; font-weight: 500; white-space: nowrap;
  text-decoration: none;
  transition: color 0.15s ease;
}
.ms-header-tabs__item:hover { color: var(--foreground); }
.ms-header-tabs__icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.ms-header-tabs__icon svg { width: 0.9375rem; height: 0.9375rem; }
.ms-header-tabs__item[aria-current="page"] { color: var(--primary-text); }
/* The underline hugs the rail's bottom edge (items stretch full height),
   inset to the item's padding box so it underlines icon + label only. */
.ms-header-tabs__item[aria-current="page"]::after {
  content: "";
  position: absolute; left: 0.65rem; right: 0.65rem; bottom: 0;
  height: 2px; background: var(--primary);
}

/* Header-height bookkeeping: sidebar/ToC sticky offsets all derive from
   --header-height (set on <body> by a Tailwind arbitrary-property utility —
   these unlayered rules beat it). With the rail on, the total grows by the
   rail's 2.5rem while the top row is pinned back to its original height
   (it sizes itself with h-(--header-height), which now names the TOTAL). */
body.ms-has-header-tabs { --header-height: calc(var(--spacing) * 14 + 2.5rem); }
body.ms-has-header-tabs .mewbo-header__row { height: calc(var(--spacing) * 14); }

/* ─── One left rail: brand · tab bar · sidebar nav ─────────────────────────
   Three navigation surfaces stack down the left of the page and each drew its
   gutter from a different owner, so their labels began at three different x
   positions (28px / 34.4px / 64px at 1440w — the sidebar visibly outdented):

     · the header row and the tab rail share a `.container-wrapper` padded
       `px-6` (24px), then each widget adds its OWN inner padding — 0.25rem on
       the brand, 0.65rem on a tab item;
     · the page body sits in a SECOND `.container-wrapper` padded `px-2` (8px),
       so the sidebar column starts 16px left of the header's content edge;
     · the sidebar's content column is `mx-auto` around a fixed
       `--sidebar-menu-width`, centring the rail inside a wider column and
       adding ~32px that nothing else shares. That centring is what dominates
       the misalignment, and it is the only offset here that is decorative.

   Each widget cancels its OWN declared padding against the line, so the LABEL
   lands on it and only the hover/active background bleeds outward — the shadcn
   idiom. Every offset below is the negation of a padding declared elsewhere in
   this file or in the template, not a measured constant. */
.mewbo-brand { margin-inline-start: -0.25rem; }          /* .mewbo-brand padding */
.ms-header-tabs__rail { margin-inline-start: -0.65rem; } /* .ms-header-tabs__item padding */
/* Below lg the brand is hidden and the menu trigger is what the rail lines up
   against, so it takes the same treatment. */
.mewbo-menu-btn { margin-inline-start: -0.5rem; }        /* .mewbo-menu-btn padding */

/* WHERE THE LINE COMES FROM. It is not chosen. The sidebar rail is a fixed
   `--sidebar-menu-width` sitting in a wider `--sidebar-width` column, and
   centring it in that column is the only placement that does not leave the
   rail hugging one edge with all the slack on the other. That centring sets
   the number, and the header follows it:

       8px   the body container's own `px-2`, where the column starts
     + 32px  half the slack between the column and the rail
     + 16px  the group's `p-2` plus the menu button's `p-2`
     ------
       56px

   An earlier revision solved the same misalignment by DROPPING the centring
   and pulling everything onto the header's 24px gutter. That aligned the three
   surfaces, but it pushed all 64px of column slack to the right of the rail
   and left every navigation surface hard against the viewport edge.

   Below lg there is no rail beside the header to agree with, since the sidebar
   becomes a dialog, so the gutter has no partner and stays on the container's
   own 24px. At 390px a 56px inset is a seventh of the viewport. */
:root {
  --mewbo-nav-gutter: 1.5rem;
}
@media (min-width: 1024px) {
  :root {
    --mewbo-nav-gutter: 3.5rem;
  }
}

/* The header row and the tab rail share a `.container-wrapper` whose `px-6`
   was the old line. Only the inline-start moves: the right edge carries the
   theme toggle and the repo link, which already sit on the header's own
   24px and have no rail below them to line up with. */
.mewbo-header > .container-wrapper,
.ms-header-tabs > .container-wrapper {
  padding-inline-start: var(--mewbo-nav-gutter);
}

/* Desktop only: the mobile navigation dialog includes this same markup, owns
   its own gutter, and has no header rail beside it to agree with. */
@media (min-width: 1024px) {
  [data-sidebar="content"] {
    margin-inline: auto;
    padding-inline: 0 0.5rem;
  }
  /* The menu button draws a 1px border (transparent until active) that neither
     the brand nor a tab has; without this its label sits one pixel right. */
  [data-sidebar="menu-button"] { margin-inline-start: -1px; }
}

/* ─── Full-bleed app slot (app.html) ───────────────────────────────────────
   `template: app.html` pages render their content into this single <main>:
   it spans exactly the viewport below the sticky header and scrolls
   internally, so an embedded full-page app (e.g. Scalar) owns the space. */
.ms-app-main {
  height: calc(100svh - var(--header-height));
  overflow: auto;
}

/* ─── Mewbo Ask AI + Unified Search Modal ──────────────────────────────────── */

/* ── B. Modal container + backdrop ──────────────────────────────────────── */

#mewbo-modal {
  position: fixed;
  top: max(4rem, 8vh);
  left: 50%;
  transform: translateX(-50%);
  width: min(640px, calc(100vw - 2rem));
  /* Capped height with breathing room: never taller than 640px, and always
     leaves >= 6rem of viewport visible below so the modal reads as a floating
     panel rather than a full-height sheet. dvh keeps mobile keyboards happy. */
  height: min(640px, calc(100dvh - 6rem));
  max-height: min(640px, calc(100dvh - 6rem));
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 0.75rem;
  background: var(--background);
  color: var(--foreground);
  overflow: hidden;
  box-shadow: 0 8px 32px -8px rgba(0,0,0,0.16), 0 2px 8px -2px rgba(0,0,0,0.06);
}
/* Tiny entry: fade + lift, ~180ms. Runs each time showModal() flips
   [open] on. Pure native — animations attached to the [open] state. */
#mewbo-modal[open] {
  animation: mewbo-modal-in 0.18s ease-out;
}
@keyframes mewbo-modal-in {
  from { opacity: 0; transform: translateX(-50%) translateY(-6px) scale(0.985); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0)    scale(1); }
}
#mewbo-modal::backdrop {
  background: rgba(15,15,20,0.32);
  backdrop-filter: blur(4px);
  animation: mewbo-backdrop-in 0.18s ease-out;
}
@keyframes mewbo-backdrop-in {
  from { background: rgba(15,15,20,0); backdrop-filter: blur(0); }
  to   { background: rgba(15,15,20,0.32); backdrop-filter: blur(4px); }
}
.mewbo-modal__inner {
  display: flex; flex-direction: column;
  height: 100%;
  overflow: hidden;
}

/* Visible scroll affordance for the modal's scrollable regions. Default UA
   scrollbars are too subtle on long DeepWiki answers, so users miss the cue
   that more content is below. */
.mewbo-ask-ai__body,
#mewbo-modal #mkdocs-search-results {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--muted-foreground) 30%, transparent) transparent;
}
.mewbo-ask-ai__body::-webkit-scrollbar,
#mewbo-modal #mkdocs-search-results::-webkit-scrollbar { width: 8px; }
.mewbo-ask-ai__body::-webkit-scrollbar-thumb,
#mewbo-modal #mkdocs-search-results::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--muted-foreground) 25%, transparent);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
.mewbo-ask-ai__body:hover::-webkit-scrollbar-thumb,
#mewbo-modal #mkdocs-search-results:hover::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--muted-foreground) 45%, transparent);
  background-clip: padding-box;
}

/* ── C. Header row: input/label (left) + grouped toggle (right) ─────────── */

.mewbo-modal__header {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  flex-shrink: 0;
  min-height: 3.25rem;
}

#mewbo-modal[data-active="ask-ai"] .mewbo-header__search { display: none; }
#mewbo-modal[data-active="search"] .mewbo-header__label  { display: none; }

.mewbo-header__search {
  display: flex; align-items: center; gap: 0.625rem;
  flex: 1; min-width: 0;
}
.mewbo-header__search > svg { flex-shrink: 0; opacity: 0.55; }
.mewbo-header__search input {
  flex: 1; min-width: 0;
  border: none; outline: none; background: transparent;
  color: var(--foreground); font-size: 0.9375rem; line-height: 1.5;
}
.mewbo-header__search input::placeholder { color: var(--muted-foreground); }

.mewbo-header__label {
  flex: 1; min-width: 0;
  font-size: 0.9375rem; font-weight: 500; color: var(--foreground);
  letter-spacing: -0.005em;
}

/* Segmented toggle group */
.mewbo-toggle {
  display: inline-flex; align-items: center; gap: 0.125rem;
  padding: 0.1875rem;
  background: color-mix(in srgb, var(--muted) 75%, transparent);
  border-radius: 0.5rem;
  flex-shrink: 0;
}
.mewbo-toggle__btn {
  display: inline-flex; align-items: center; gap: 0.35rem;
  height: 1.625rem; padding: 0 0.625rem;
  border: none; border-radius: 0.375rem;
  background: transparent; color: var(--muted-foreground);
  font-size: 0.75rem; font-weight: 500; cursor: pointer; white-space: nowrap;
  transition: color 0.12s, background 0.15s, box-shadow 0.15s;
}
.mewbo-toggle__btn:hover:not([data-active]) { color: var(--foreground); }
.mewbo-toggle__btn svg { flex-shrink: 0; }
#mewbo-modal[data-active="search"] .mewbo-toggle__btn[data-tab="search"],
#mewbo-modal[data-active="ask-ai"]  .mewbo-toggle__btn[data-tab="ask-ai"] {
  background: var(--background);
  color: var(--foreground);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 0 0 1px color-mix(in srgb, var(--border) 35%, transparent);
}

/* ── D. Panel visibility ────────────────────────────────────────────────── */

/* Display flips drive visibility. The keyframe runs every time `display`
   transitions to flex (i.e., when the active tab changes), so swapping
   between Search and Ask AI micro-crossfades instead of snapping. */
#mewbo-search-panel { display: none; flex-direction: column; flex: 1; overflow: hidden; min-height: 0; }
#mewbo-modal[data-active="search"] #mewbo-search-panel {
  display: flex;
  animation: mewbo-panel-in 0.14s ease-out;
}

#mewbo-ask-ai-panel { display: none; flex-direction: column; flex: 1; overflow: hidden; min-height: 0; }
#mewbo-modal[data-active="ask-ai"] #mewbo-ask-ai-panel {
  display: flex;
  animation: mewbo-panel-in 0.14s ease-out;
}

@keyframes mewbo-panel-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── E. Search panel: handoff row + scope filters ───────────────────────── */

#mewbo-ask-ai-shortcut {
  display: flex; align-items: center; gap: 0.625rem;
  margin: 0.625rem 0.75rem 0.5rem;
  padding: 0.625rem 0.875rem;
  border: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
  border-radius: 0.5rem;
  background: color-mix(in srgb, var(--muted) 45%, transparent);
  color: var(--foreground);
  font-size: 0.8125rem; cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
  flex-shrink: 0;
}
#mewbo-ask-ai-shortcut:hover {
  background: color-mix(in srgb, var(--muted) 75%, transparent);
  border-color: color-mix(in srgb, var(--border) 70%, transparent);
}
.mewbo-shortcut__glyph {
  width: 1.5rem; height: 1.5rem; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 0.375rem;
  background: var(--background);
  color: var(--primary);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--border) 50%, transparent);
}
.mewbo-shortcut__label { font-weight: 600; }
.mewbo-shortcut__query {
  color: var(--muted-foreground);
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mewbo-shortcut__query:empty::before {
  content: attr(data-placeholder);
  color: color-mix(in srgb, var(--muted-foreground) 70%, transparent);
}
.mewbo-shortcut__cta {
  margin-left: auto; color: var(--muted-foreground); font-size: 0.7rem; white-space: nowrap;
}

/* Scope filter chips */
.mewbo-search-filters {
  display: none;
  flex-wrap: wrap; gap: 0.375rem;
  padding: 0 0.75rem 0.5rem;
  flex-shrink: 0;
}
.mewbo-search-filters:not(:empty) { display: flex; }
.mewbo-chip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  height: 1.625rem; padding: 0 0.7rem;
  border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  border-radius: 9999px;
  background: var(--background);
  color: var(--muted-foreground);
  font-size: 0.7rem; font-weight: 500; cursor: pointer; white-space: nowrap;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.mewbo-chip:hover { background: var(--muted); color: var(--foreground); }
.mewbo-chip[data-active] {
  background: var(--foreground);
  color: var(--background);
  border-color: var(--foreground);
}
.mewbo-chip[data-active] .mewbo-chip__count { opacity: 0.65; }
.mewbo-chip__count {
  font-variant-numeric: tabular-nums; font-weight: 600;
  font-size: 0.65rem;
  opacity: 0.55;
}

/* ── F. Search results — dense list, distinct loading/empty/populated ────── */

#mewbo-modal #mkdocs-search-results {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  padding: 0 0.25rem 0.5rem;
  display: flex; flex-direction: column;
  max-height: none;
}

/* Populated: dense rows, no card borders, breadcrumb + title-icon + snippet */
#mewbo-modal #mkdocs-search-results article {
  display: flex; flex-direction: column; gap: 0.15rem;
  padding: 0.55rem 0.75rem;
  border: none; border-radius: 0.5rem;
  background: transparent;
  cursor: pointer; text-decoration: none;
  transition: background 0.1s;
  position: relative;
  margin: 0 0.5rem;
}
#mewbo-modal #mkdocs-search-results article:hover,
#mewbo-modal #mkdocs-search-results article:focus-within {
  background: color-mix(in srgb, var(--muted) 55%, transparent);
  outline: none;
}

.mewbo-result__breadcrumb {
  display: flex; align-items: center; gap: 0.35rem;
  font-size: 0.7rem; color: var(--muted-foreground);
  margin-bottom: 0.05rem; line-height: 1.2;
  letter-spacing: 0.005em;
}

#mewbo-modal #mkdocs-search-results article h3 {
  display: flex; align-items: center; gap: 0.45rem;
  margin: 0; font-size: 0.875rem; font-weight: 600;
  line-height: 1.3; color: var(--foreground);
}
#mewbo-modal #mkdocs-search-results article h3 a { color: var(--foreground); text-decoration: none; }
#mewbo-modal #mkdocs-search-results article:hover h3 a,
#mewbo-modal #mkdocs-search-results article:focus-within h3 a { color: var(--primary); }
#mewbo-modal #mkdocs-search-results article h3 svg { flex-shrink: 0; opacity: 0.55; }

#mewbo-modal #mkdocs-search-results article p {
  margin: 0; font-size: 0.75rem;
  color: var(--muted-foreground); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
#mewbo-modal #mkdocs-search-results article p strong { color: var(--foreground); font-weight: 600; }

.mewbo-result__enter-hint {
  position: absolute; right: 0.85rem; top: 50%; transform: translateY(-50%);
  font-size: 0.75rem; color: var(--muted-foreground);
  opacity: 0; transition: opacity 0.1s; pointer-events: none;
}
#mewbo-modal #mkdocs-search-results article:hover .mewbo-result__enter-hint { opacity: 0.6; }

/* Distinct hint variants — no dashed boxes anywhere */
#mewbo-modal #mkdocs-search-results > p.search-hint {
  margin: 0; padding: 1.25rem 1rem;
  text-align: center; font-size: 0.8125rem; line-height: 1.45;
  background: transparent; border: none; border-radius: 0;
  color: var(--muted-foreground);
}
#mewbo-modal #mkdocs-search-results > p.search-hint.empty { color: color-mix(in srgb, var(--muted-foreground) 75%, transparent); }
#mewbo-modal #mkdocs-search-results > p.search-hint.too-short { color: var(--muted-foreground); font-style: italic; }
#mewbo-modal #mkdocs-search-results > p.search-hint.busy {
  color: var(--muted-foreground);
  position: relative;
}
#mewbo-modal #mkdocs-search-results > p.search-hint.busy::after {
  content: ""; display: inline-block; vertical-align: middle;
  width: 0.875rem; height: 0.875rem; margin-left: 0.5rem;
  border: 2px solid color-mix(in srgb, var(--muted-foreground) 45%, transparent);
  border-top-color: var(--primary);
  border-radius: 9999px;
  animation: mewbo-spin 0.7s linear infinite;
}
#mewbo-modal #mkdocs-search-results > p.search-hint.loading { color: var(--muted-foreground); }
@keyframes mewbo-spin { to { transform: rotate(360deg); } }

/* ── G. Ask AI panel ─────────────────────────────────────────────────────── */

.mewbo-ask-ai__body {
  flex: 1; overflow-y: auto;
  padding: 1.25rem 1.25rem 0.5rem;
  display: flex; flex-direction: column; gap: 1.25rem; min-height: 0;
}

#mewbo-welcome { display: flex; flex-direction: column; gap: 1rem; }

/* Greeting block: avatar + multi-line text with inline product pill */
.mewbo-greeting {
  display: flex; gap: 0.875rem; align-items: flex-start;
  padding: 0.25rem 0;
}
.mewbo-greeting__avatar {
  width: 2.25rem; height: 2.25rem; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 9999px;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--primary) 20%, var(--background)),
    color-mix(in srgb, var(--primary) 8%, var(--background)));
  color: var(--primary);
  font-size: 0.95rem; font-weight: 700; letter-spacing: -0.03em;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 18%, transparent);
}
.mewbo-greeting__text {
  display: flex; flex-direction: column; gap: 0.45rem;
  font-size: 0.9375rem; line-height: 1.5; color: var(--foreground);
  padding-top: 0.1rem;
}
.mewbo-greeting__text p { margin: 0; }
.mewbo-greeting__pill {
  display: inline-flex; align-items: center;
  padding: 0.05rem 0.45rem;
  border-radius: 0.3rem;
  background: color-mix(in srgb, var(--muted) 80%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  font-family: ui-monospace, Menlo, monospace; font-size: 0.85em; font-weight: 500;
  color: var(--foreground);
}

/* Examples — pill-shaped, no border, soft fill */
.mewbo-examples__label {
  font-size: 0.65rem; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted-foreground);
}
.mewbo-examples {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
}
.mewbo-example-pill {
  display: inline-flex; align-items: center;
  border: none;
  background: color-mix(in srgb, var(--muted) 60%, transparent);
  color: var(--foreground);
  font-size: 0.8125rem; line-height: 1.3;
  padding: 0.45rem 0.875rem;
  border-radius: 9999px;
  cursor: pointer; text-align: left;
  transition: background 0.12s, transform 0.08s;
}
.mewbo-example-pill:hover { background: color-mix(in srgb, var(--muted) 90%, transparent); }
.mewbo-example-pill:active { transform: translateY(0.5px); }

/* Answer state */
#mewbo-answer {
  display: flex; flex-direction: column; gap: 0.75rem;
}

#mewbo-skeleton { display: flex; flex-direction: column; gap: 0; padding-top: 0.25rem; }
@keyframes mewbo-shimmer {
  from { background-position: -400px 0; }
  to   { background-position:  400px 0; }
}
.mewbo-skeleton__line {
  height: 0.875rem; border-radius: 0.25rem; margin-bottom: 0.6rem;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--muted) 65%, transparent) 25%,
    color-mix(in srgb, var(--muted) 95%, transparent) 50%,
    color-mix(in srgb, var(--muted) 65%, transparent) 75%
  );
  background-size: 800px 100%;
  animation: mewbo-shimmer 1.4s infinite linear;
}
.mewbo-skeleton__line:nth-child(2) { width: 88%; }
.mewbo-skeleton__line:nth-child(3) { width: 72%; }
.mewbo-skeleton__line:nth-child(4) { width: 54%; }

/* A generated answer is read straight through like an article, so it takes
   the body rung rather than the chrome one the rest of the panel uses. */
#mewbo-answer-text { font-size: 0.9rem; color: var(--foreground); line-height: var(--leading-body); }
#mewbo-answer-text > *:first-child { margin-top: 0; }
#mewbo-answer-text > *:last-child  { margin-bottom: 0; }
#mewbo-answer-text h1,
#mewbo-answer-text h2 { font-size: 1.05rem; font-weight: 600; margin: 1rem 0 0.4rem; line-height: 1.3; }
#mewbo-answer-text h3 { font-size: 0.95rem; font-weight: 600; margin: 0.85rem 0 0.3rem; line-height: 1.3; }
#mewbo-answer-text h4 { font-size: 0.875rem; font-weight: 600; margin: 0.7rem 0 0.25rem; line-height: 1.3; }
#mewbo-answer-text p  { margin: 0 0 0.6rem; }
#mewbo-answer-text ul,
#mewbo-answer-text ol { margin: 0 0 0.6rem; padding-left: 1.35rem; }
#mewbo-answer-text ul ul,
#mewbo-answer-text ol ol,
#mewbo-answer-text ul ol,
#mewbo-answer-text ol ul { margin: 0.25rem 0; }
#mewbo-answer-text li { margin: 0.2rem 0; }
#mewbo-answer-text li > p:only-child { margin: 0; }
#mewbo-answer-text strong { font-weight: 600; }
#mewbo-answer-text em { font-style: italic; }
#mewbo-answer-text a {
  color: var(--primary); text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--primary) 35%, transparent);
  text-underline-offset: 0.15em;
}
#mewbo-answer-text a:hover { text-decoration-color: var(--primary); }
#mewbo-answer-text code {
  font-family: ui-monospace, Menlo, monospace; font-size: 0.85em;
  background: color-mix(in srgb, var(--muted) 70%, transparent);
  border-radius: 0.25rem; padding: 0.1em 0.35em;
}
#mewbo-answer-text pre {
  margin: 0.5rem 0 0.75rem; padding: 0.7rem 0.85rem;
  background: color-mix(in srgb, var(--muted) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
  border-radius: 0.5rem;
  overflow-x: auto;
  font-size: 0.8rem; line-height: 1.5;
}
#mewbo-answer-text pre code {
  background: transparent; padding: 0; border-radius: 0;
  font-size: 1em; color: var(--foreground);
}
#mewbo-answer-text blockquote {
  margin: 0.5rem 0 0.75rem; padding: 0.1rem 0 0.1rem 0.75rem;
  border-left: 3px solid color-mix(in srgb, var(--border) 70%, transparent);
  color: var(--muted-foreground);
}
#mewbo-answer-text hr {
  border: none; border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  margin: 0.85rem 0;
}
#mewbo-answer-text table {
  border-collapse: collapse; margin: 0.5rem 0 0.75rem; font-size: 0.825rem;
  display: block; overflow-x: auto; max-width: 100%;
}
#mewbo-answer-text th,
#mewbo-answer-text td {
  border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
  padding: 0.35rem 0.6rem; text-align: left;
}
#mewbo-answer-text th {
  background: color-mix(in srgb, var(--muted) 50%, transparent);
  font-weight: 600;
}

#mewbo-ask-another {
  align-self: flex-start;
  border: none; background: transparent;
  color: var(--primary); font-size: 0.8125rem; font-weight: 500;
  cursor: pointer; padding: 0;
  transition: opacity 0.12s;
}
#mewbo-ask-another:hover { opacity: 0.75; }

/* ── H. Ask AI input footer — soft filled input, quiet glyph send ───────── */

.mewbo-ask-ai__input-footer {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.625rem 0.75rem 0.5rem;
  border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
  flex-shrink: 0;
}
#mewbo-ai-input {
  flex: 1; min-width: 0;
  border: none;
  border-radius: 0.625rem;
  background: color-mix(in srgb, var(--muted) 50%, transparent);
  color: var(--foreground);
  font-size: 0.9375rem; line-height: 1.4;
  padding: 0.625rem 0.875rem;
  outline: none;
  transition: background 0.15s, box-shadow 0.15s;
}
#mewbo-ai-input:focus {
  background: color-mix(in srgb, var(--muted) 75%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--ring) 35%, transparent);
}
#mewbo-ai-input::placeholder { color: var(--muted-foreground); }
#mewbo-ai-send {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; flex-shrink: 0;
  border: none; border-radius: 0.5rem;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
  transition: background 0.12s, color 0.12s, opacity 0.12s;
}
#mewbo-ai-send:not(:disabled):hover {
  background: color-mix(in srgb, var(--muted) 65%, transparent);
  color: var(--primary);
}
#mewbo-ai-send:disabled { opacity: 0.35; cursor: not-allowed; }
#mewbo-ai-send.loading { color: var(--primary); opacity: 0.6; }

/* Attribution footer */
.mewbo-attribution {
  display: flex; align-items: center; gap: 0.3rem;
  padding: 0.5rem 1rem 0.7rem;
  font-size: 0.7rem; color: var(--muted-foreground);
  border-top: 1px solid color-mix(in srgb, var(--border) 20%, transparent);
}
.mewbo-attribution__brand { font-weight: 600; color: var(--foreground); }

/* ── I. Floating FAB ─────────────────────────────────────────────────────── */

#mewbo-fab {
  position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 50;
  display: inline-flex; align-items: center; gap: 0.5rem;
  height: 2.75rem; padding: 0 1.1rem;
  border: none; border-radius: 9999px;
  background: var(--primary); color: var(--primary-foreground);
  font-size: 0.9rem; font-weight: 600;
  box-shadow: 0 4px 16px -4px rgba(0,0,0,0.22), 0 1px 4px -1px rgba(0,0,0,0.12);
  cursor: pointer; transition: transform 0.1s, box-shadow 0.15s;
}
#mewbo-fab:hover { transform: translateY(-1px); box-shadow: 0 6px 20px -4px rgba(0,0,0,0.28); }
#mewbo-fab:active { transform: translateY(0); }
@media (max-width: 639px) { #mewbo-fab { height: 2.5rem; padding: 0 0.9rem; font-size: 0.85rem; } }

/* ── J. Dark mode overrides ──────────────────────────────────────────────── */

.dark #mewbo-modal {
  box-shadow: 0 8px 32px -8px rgba(0,0,0,0.45), 0 2px 8px -2px rgba(0,0,0,0.3);
}
.dark #mewbo-modal::backdrop { background: rgba(0,0,0,0.55); }
.dark #mewbo-modal[data-active="search"] .mewbo-toggle__btn[data-tab="search"],
.dark #mewbo-modal[data-active="ask-ai"]  .mewbo-toggle__btn[data-tab="ask-ai"] {
  background: color-mix(in srgb, var(--background) 90%, var(--foreground));
}
.dark .mewbo-toggle { background: color-mix(in srgb, var(--muted) 50%, transparent); }

/* ── K. Mobile ───────────────────────────────────────────────────────────── */

@media (max-width: 600px) {
  #mewbo-modal {
    top: 1rem; width: calc(100vw - 1rem);
    max-height: calc(100dvh - 2rem);
    border-radius: 0.625rem;
  }
  .mewbo-modal__header { padding: 0.625rem 0.875rem; }
  .mewbo-toggle__btn { font-size: 0.7rem; }
  .mewbo-greeting__text { font-size: 0.875rem; }
}

/* Page actions sit in the same row as the prev/next arrows, which are plain
   shadcn secondary buttons: `size-8 md:size-7 rounded-md bg-secondary`, i.e.
   2rem square dropping to 1.75rem from `md`, radius `--radius-md`, filled
   rather than outlined. This control is a split button rather than a single
   icon, so it is wider — but every other axis has to agree with its
   neighbours or the row reads as two unrelated components, which is exactly
   what an outlined 2.25rem box next to a filled 2rem one did.
   `--page-actions-size` is that shared height, and `--radius-md` the shared
   radius; the seam between the halves is the one thing the arrows have no
   equivalent for.

   Keep the menu outside any clipping so it can extend beyond the control. */
.mewbo-page-actions {
  --page-actions-size: 2rem;
  position: relative;
  display: inline-flex;
  align-items: stretch;
  flex-shrink: 0;
  height: var(--page-actions-size);
  border-radius: var(--radius-md);
  background: var(--secondary);
  color: var(--secondary-foreground);
}
@media (min-width: 768px) {
  .mewbo-page-actions { --page-actions-size: 1.75rem; }
}

/* Override the article button's geometry and variants as one unit, including
   hover/active states. Otherwise the toggle inherits rounded inner corners
   and primary-colour hover while its neighbour keeps secondary styling. */
article .mewbo-page-actions > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--page-actions-size);
  margin: 0;
  padding: 0 0.5rem;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: inherit;
  box-shadow: none;
  transform: none;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
article .mewbo-page-actions > .mewbo-page-actions__main {
  min-width: var(--page-actions-size);
  border-radius: var(--radius-md);
}
article .mewbo-page-actions:has(.mewbo-page-actions__toggle) > .mewbo-page-actions__main {
  border-radius: var(--radius-md) 0 0 var(--radius-md);
}
/* The seam is a foreground tint rather than `--border`: the control is filled
   now, and a border colour picked to read against `--background` disappears
   against `--secondary`. */
article .mewbo-page-actions > .mewbo-page-actions__toggle {
  min-width: calc(var(--page-actions-size) - 0.25rem);
  padding-inline: 0.25rem;
  box-shadow: inset 1px 0 0 color-mix(in oklab, currentColor 18%, transparent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
/* Tailwind expands variant lists into :is(), inheriting the strongest
   selector in the list. Include the component class to beat that hover. */
article .mewbo-page-actions > button:is(.mewbo-page-actions__main, .mewbo-page-actions__toggle):hover,
article .mewbo-page-actions > button:is(.mewbo-page-actions__main, .mewbo-page-actions__toggle):active {
  background: var(--muted);
  color: var(--foreground);
}
article .mewbo-page-actions > button:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: -2px;
}
.mewbo-page-actions__toggle svg {
  transition: transform 0.15s ease;
}
.mewbo-page-actions[data-open] .mewbo-page-actions__toggle svg {
  transform: rotate(180deg);
}

.mewbo-page-actions__menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  z-index: 50;
  width: min(19rem, calc(100vw - 1.5rem));
  max-height: var(--page-menu-max-height, calc(100dvh - 1.5rem));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.35rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--popover);
  color: var(--popover-foreground);
  box-shadow: 0 12px 28px -12px rgb(0 0 0 / 0.35), 0 2px 6px -2px rgb(0 0 0 / 0.2);
}
.mewbo-page-actions__menu[hidden] { display: none; }

/* Two rules in the Tailwind layer have to be undone here, and both are the
   reason the menu looked wrong rather than matters of taste:

   `article button { height: calc(var(--spacing)*8) }` clamps any button in the
   article to 2rem. The "Copy page" row is a <button> while the other three are
   <a>, so it alone rendered 32px tall against their 55px and its hint text
   spilled onto the row below. `height: auto` is the fix; the selector reach
   (0,2,1) is what makes it win.

   `article a` underlines every prose link and paints it clay. These are menu
   rows, not prose. */
article .mewbo-page-actions__menu a.mewbo-page-action,
article .mewbo-page-actions__menu button.mewbo-page-action,
.mewbo-page-action {
  display: flex;
  align-items: flex-start;
  /* The article button default centres the copy row, unlike its link peers. */
  justify-content: flex-start;
  gap: 0.6rem;
  width: 100%;
  height: auto;
  min-height: 0;
  padding: 0.5rem 0.55rem;
  border: 0;
  border-radius: 0.4rem;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 400;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.1s ease;
}
.mewbo-page-action:hover,
.mewbo-page-action:focus-visible {
  background: var(--accent);
  outline: none;
}

/* The icon sits in its own bordered tile, as in the reference. It keeps a
   brand mark (OpenAI, Claude) from reading as part of the label, and gives
   marks of different weights a shared footprint. */
.mewbo-page-action__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 1.85rem;
  height: 1.85rem;
  border: 1px solid var(--border);
  border-radius: 0.45rem;
  color: var(--muted-foreground);
}

.mewbo-page-action__text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
/* `margin: 0` is load-bearing: these are spans, but `.typography > span` and
   the article's own flow rules still reach some of them, and any inherited
   block margin here collapses the two-line row unevenly — which is exactly
   how the second item ended up cramped against the first. */
article .mewbo-page-action__label,
.mewbo-page-action__label {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin: 0;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--foreground);
}
article .mewbo-page-action__hint,
.mewbo-page-action__hint {
  margin: 0;
  font-size: 0.78rem;
  line-height: var(--leading-ui);
  color: var(--muted-foreground);
}
/* Marks the items that navigate away, so a reader can tell them from the one
   that acts in place (Copy page). */
.mewbo-page-action__external {
  display: inline-flex;
  color: var(--muted-foreground);
}

@media (pointer: coarse) {
  article .mewbo-page-actions > button {
    min-width: 2.75rem;
    height: 2.75rem;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Brand footer — THREE HORIZONTAL COLUMNS, side by side.

   Layout, left → right:
     1. Intro     — logo + name + tagline
     2. Connect   — bordered social-icon button cluster
     3. Docs      — auto-derived link list from top-level `nav:`
   (Per-product theme.footer.sections entries become additional columns
   appended after Docs.)

   Below the columns sits a thin baseline strip with copyright + a
   small attribution link.

   The mkdocs-shadcn theme exposes a body-class CSS var `--footer-height`
   for sticky-position math elsewhere (TOC rail, sidebar). The override
   below beats Tailwind's inline arbitrary-property setter on <body> via
   the `.theme-default` class selector specificity.
   ───────────────────────────────────────────────────────────────────────── */

body.theme-default { --footer-height: 320px; }
@media (min-width: 1280px) {
  body.theme-default { --footer-height: 360px; }
}

.mewbo-footer {
  margin-top: 4rem;
  border-top: 1px solid var(--border);
  background: var(--background);
  position: relative;
  isolation: isolate;
}

/* ── Dot-lattice texture ─────────────────────────────────────────────────
   A 65px tile carrying a 5x5 lattice on a 13px pitch, 20 of its 25 cells
   filled — so the eye reads a scatter rather than a grid, and the five gaps
   are what keep it from looking like graph paper. Reproduced as gradients
   rather than shipped as an SVG: at this tile size a 2K viewport draws well
   over a thousand copies, and the file would be re-decoded per tile, on a
   region that is pure decoration.

   Colour comes from tokens, not from opacity. The source art was four
   arbitrary web colours (yellow/red/cyan/violet) on a slate square, which
   belong to no palette here. Dropping them to 10% alpha would only mean
   four muddied hues instead of four loud ones — still off-palette, and
   still shifting with whatever sits behind them. Instead each group is
   mixed from a token toward the footer's own background, so the texture is
   a tint OF the surface: it cannot clash, it inverts correctly between
   light and dark on its own, and a brand fork that changes `--primary`
   gets a matching footer for free.

   The four groups are kept distinct because the pattern's charm is the
   colour scatter; flattening them to one value gives an evenly-speckled
   field. They are deliberately close together — the contrast between dots
   is much smaller than the contrast to the background, so the scatter reads
   as texture up close and as a flat surface from a normal viewing distance.
   ────────────────────────────────────────────────────────────────────── */

.mewbo-footer {
  /* 2.5-4.5%. Tuned by eye against dark, which is the harder case: the clay
     is a saturated hue on a near-black surface, so it reads as colour well
     below the strength at which a neutral reads as anything at all. At the
     7% this started from the dots were legible as dots — a polka field
     behind the columns rather than a texture under them. These are the
     values where the footer still looks like one surface at a normal
     viewing distance and only resolves into a lattice up close. */
  --ms-footer-dot-1: color-mix(in oklab, var(--primary) 4.5%, transparent);
  --ms-footer-dot-2: color-mix(in oklab, var(--foreground) 3%, transparent);
  --ms-footer-dot-3: color-mix(in oklab, var(--primary) 2.5%, transparent);
  --ms-footer-dot-4: color-mix(in oklab, var(--muted-foreground) 3.5%, transparent);
  --ms-footer-dot-size: 65px;
}

/* A pseudo-element, not a second background layer on `.mewbo-footer`: the
   footer's own `background` is a single token today, but a fork that gives
   it a gradient would otherwise have to restate all twenty stops to keep
   the texture. `z-index: -1` with the `isolation` above puts it behind the
   content without creating a stacking context the columns can fall into. */
.mewbo-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(circle 6px at 10% 10%, var(--ms-footer-dot-4) 72%, transparent 100%),
    radial-gradient(circle 6px at 30% 10%, var(--ms-footer-dot-1) 72%, transparent 100%),
    radial-gradient(circle 6px at 50% 10%, var(--ms-footer-dot-3) 72%, transparent 100%),
    radial-gradient(circle 6px at 90% 10%, var(--ms-footer-dot-2) 72%, transparent 100%),
    radial-gradient(circle 6px at 30% 30%, var(--ms-footer-dot-4) 72%, transparent 100%),
    radial-gradient(circle 6px at 50% 30%, var(--ms-footer-dot-3) 72%, transparent 100%),
    radial-gradient(circle 6px at 70% 30%, var(--ms-footer-dot-2) 72%, transparent 100%),
    radial-gradient(circle 6px at 90% 30%, var(--ms-footer-dot-1) 72%, transparent 100%),
    radial-gradient(circle 6px at 10% 50%, var(--ms-footer-dot-4) 72%, transparent 100%),
    radial-gradient(circle 6px at 30% 50%, var(--ms-footer-dot-1) 72%, transparent 100%),
    radial-gradient(circle 6px at 70% 50%, var(--ms-footer-dot-3) 72%, transparent 100%),
    radial-gradient(circle 6px at 90% 50%, var(--ms-footer-dot-1) 72%, transparent 100%),
    radial-gradient(circle 6px at 10% 70%, var(--ms-footer-dot-3) 72%, transparent 100%),
    radial-gradient(circle 6px at 50% 70%, var(--ms-footer-dot-4) 72%, transparent 100%),
    radial-gradient(circle 6px at 70% 70%, var(--ms-footer-dot-2) 72%, transparent 100%),
    radial-gradient(circle 6px at 90% 70%, var(--ms-footer-dot-2) 72%, transparent 100%),
    radial-gradient(circle 6px at 10% 90%, var(--ms-footer-dot-3) 72%, transparent 100%),
    radial-gradient(circle 6px at 30% 90%, var(--ms-footer-dot-2) 72%, transparent 100%),
    radial-gradient(circle 6px at 50% 90%, var(--ms-footer-dot-1) 72%, transparent 100%),
    radial-gradient(circle 6px at 70% 90%, var(--ms-footer-dot-4) 72%, transparent 100%);
  background-size: var(--ms-footer-dot-size) var(--ms-footer-dot-size);
  /* The tile is a scatter, so its edges do not align; repeating it plainly
     would print a seam every 65px. The mask fades the field out toward the
     baseline strip, which both hides the seams at the bottom edge and keeps
     the texture from competing with the copyright line. */
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%);
}

/* Decoration only. Anyone who has asked for less motion or a plain surface
   is asking about the content, not about ornament — but a texture behind
   text is exactly what high-contrast mode exists to remove. */
@media (prefers-contrast: more) {
  .mewbo-footer::before { display: none; }
}

/* ── Container: horizontal grid of columns ──────────────────────────── */

.mewbo-footer__container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 2rem 2.5rem;
  align-items: start;
  max-width: 90rem;
  margin: 0 auto;
  padding: 2.5rem 1.5rem;
}

@media (min-width: 768px) {
  .mewbo-footer__container { padding: 3rem 2rem; }
}

/* ── Generic column shell (intro / connect / docs / extras share this) ── */

.mewbo-footer__col {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  min-width: 0;
}

.mewbo-footer__col-heading {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--foreground);
  margin: 0;
  line-height: 1;
}

/* ── Column 1: Intro ─────────────────────────────────────────────────── */

.mewbo-footer__col--intro {
  gap: 0.625rem;
}

.mewbo-footer__logo {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--foreground);
  width: max-content;
}
.mewbo-footer__logo:hover { color: var(--primary); }

.mewbo-footer__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem; height: 1.5rem;
  flex-shrink: 0;
}
.mewbo-footer__glyph svg,
.mewbo-footer__glyph img {
  width: 100%; height: 100%;
}

.mewbo-footer__name {
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  line-height: 1;
}

.mewbo-footer__tagline {
  color: var(--muted-foreground);
  font-size: 0.85rem;
  line-height: var(--leading-ui);
  margin: 0;
  max-width: 36ch;
}

/* ── Column 2: Connect (bordered social buttons) ─────────────────────── */

.mewbo-footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.mewbo-footer__social-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.125rem; height: 2.125rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border);
  background: var(--background);
  color: var(--muted-foreground);
  transition:
    color 0.15s ease,
    background 0.15s ease,
    border-color 0.15s ease,
    transform 0.12s ease;
}
.mewbo-footer__social-btn:hover {
  color: var(--foreground);
  background: var(--accent);
  border-color: color-mix(in srgb, var(--foreground) 35%, var(--border));
  transform: translateY(-1px);
}
.mewbo-footer__social-btn:active { transform: translateY(0); }
.mewbo-footer__social-btn:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
.mewbo-footer__social-btn svg {
  width: 1rem;
  height: 1rem;
}

/* ── Columns 3+: Docs / extras (link lists under a heading) ──────────── */

.mewbo-footer__col-links {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.mewbo-footer__col-links a {
  color: var(--muted-foreground);
  font-size: 0.85rem;
  line-height: var(--leading-ui);
  text-decoration: none;
  transition: color 0.12s ease;
  display: inline-block;
}
.mewbo-footer__col-links a:hover { color: var(--foreground); }

/* ── Baseline strip ──────────────────────────────────────────────────── */

.mewbo-footer__baseline {
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  max-width: 90rem;
  margin: 0 auto;
  padding: 1rem 1.5rem;
  font-size: 0.78rem;
  color: var(--muted-foreground);
}

@media (min-width: 768px) {
  .mewbo-footer__baseline { padding: 1.25rem 2rem; }
}

.mewbo-footer__copyright { font-variant-numeric: tabular-nums; }

.mewbo-footer__attribution a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted color-mix(in srgb, var(--muted-foreground) 50%, transparent);
  transition: color 0.12s ease, border-color 0.12s ease;
}
.mewbo-footer__attribution a:hover {
  color: var(--foreground);
  border-bottom-color: var(--foreground);
}

/* ── Mobile: collapse to single column ──────────────────────────────── */

@media (max-width: 640px) {
  .mewbo-footer__container {
    grid-template-columns: 1fr;
    gap: 1.75rem;
    padding: 2rem 1.25rem;
  }
  .mewbo-footer__baseline {
    padding: 1rem 1.25rem;
    flex-direction: column;
    align-items: flex-start;
  }
  body.theme-default { --footer-height: 540px; }
}

/* ─────────────────────────────────────────────────────────────────────────
   Prose colour: put body text on the token system

   The upstream `article` rule paints prose with `text-neutral-800` /
   `dark:text-neutral-300` — a COOL grey scale, while this brand's surfaces
   are warm (cream in light, carbon in dark). The effect is subtle and
   persistent: every paragraph sits a few degrees off the chrome around it.

   The upstream INTENT is worth keeping — body text one step recessed from
   headings gives the page a reading hierarchy. So rather than flattening
   prose onto `--foreground`, mix the two tokens: the recession survives and
   the hue now tracks the palette. Headings, in turn, are stated at full
   strength so the contrast between them is deliberate rather than incidental.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --prose-body: color-mix(in oklab, var(--foreground) 82%, var(--muted-foreground));
}

/* ─────────────────────────────────────────────────────────────────────────
   Diagram tokens

   Diagrams are DATA, not chrome, and the two want opposite things from a
   colour. Chrome tokens are tuned to recede: `--card` is the page's own
   surface and `--border` is a whisper-subtle hairline sized for large
   panels. Pointed at a diagram they produced a node filled with exactly the
   colour of the card behind it (measured 1.00:1 in light mode — white on
   white) outlined by a border at 1.25:1. WCAG 1.4.11 asks for 3:1 on the
   boundary of a meaningful graphical object; a hairline for a 700px panel is
   not the same instrument as the outline of a 90px box.

   So diagrams get their own small family, with the contrast stated as the
   reason each value exists. Measured against these values:

     node fill vs surface     light 1.14   dark 1.22   (a visible step)
     node border vs fill      light 3.53   dark 3.23   (>= 3, the real fix)
     node border vs surface   light 3.10   dark 3.94
     edge line vs surface     light 4.60   dark 5.46
     label vs fill            light 19.8   dark 14.6   (>= 4.5)

   `--diagram-surface` backs BOTH the inline figure and the expanded viewer's
   stage. That is deliberate and load-bearing: a diagram that is legible in
   the page cannot then be illegible when expanded, because it is being read
   against the same colour in both places. Giving the two states separate
   backdrops is what let expanded drift out of contrast unnoticed.

   Hues are warm to sit in the brand's cream/carbon palette — a neutral grey
   diagram reads as foreign on this page even when its contrast is correct.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --diagram-surface: #f2f0ea;
  --diagram-node-bg: #ffffff;
  --diagram-node-border: #8c8880;
  --diagram-line: #6f6c64;
  --diagram-label: #0a0a0a;
  --diagram-cluster: #e7e4db;
}

.dark {
  --diagram-surface: #121110;
  --diagram-node-bg: #26241f;
  --diagram-node-border: #767269;
  --diagram-line: #8d8a80;
  --diagram-label: #f8f8f6;
  --diagram-cluster: #1c1b18;
}

article,
.md-content {
  color: var(--prose-body);
}

article h1,
article h2,
article h3,
article h4,
article h5,
article h6,
.md-content h1,
.md-content h2,
.md-content h3,
.md-content h4,
.md-content h5,
.md-content h6 {
  color: var(--foreground);
  text-wrap: balance;
}

/* Paragraphs and list items read better with the browser's newer wrapping
   heuristic: it avoids short widow lines without changing any measurement. */
article p,
article li,
.md-content p,
.md-content li {
  text-wrap: pretty;
}

/* Strong text inside recessed prose needs to come back to full strength, or
   emphasis reads as merely "less grey". */
article strong,
.md-content strong {
  color: var(--foreground);
}

/* Blockquotes ship as a bare left rule + italics, which disappears against a
   busy page. A quiet surface tint plus the accent rule makes the aside read
   as a distinct block without shouting; the text stays recessed. */
article blockquote,
.md-content blockquote {
  color: var(--muted-foreground);
  border-left-color: color-mix(in srgb, var(--primary) 45%, transparent);
  background: color-mix(in srgb, var(--muted) 45%, transparent);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 0.75rem 1rem 0.75rem 1.25rem;
  font-style: normal;
}

article blockquote > :first-child,
.md-content blockquote > :first-child {
  margin-top: 0;
}

/* ─────────────────────────────────────────────────────────────────────────
   Mermaid diagrams

   A diagram is a figure on the page, not a loose SVG: it gets the same matte
   frame content images get a few hundred lines above, so media of every kind
   reads as one family. The card is the click target for the expanded viewer.
   ───────────────────────────────────────────────────────────────────────── */

figure.ms-mermaid {
  position: relative;
  margin: 1.75rem 0;
  padding: 1rem;
  box-sizing: border-box;
  /* NOT --card: that is the colour mermaid fills nodes with, so a card
     background made every node invisible against its own container. */
  background: var(--diagram-surface);
  border: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  border-radius: 0.5rem;
  overflow: hidden;
  cursor: zoom-in;
  transition: border-color 0.15s ease;
}

figure.ms-mermaid:hover {
  border-color: color-mix(in srgb, var(--muted-foreground) 45%, transparent);
}

.ms-mermaid__stage {
  display: flex;
  justify-content: center;
  /* Cross-axis must stay `flex-start`: the default `stretch` would pull the
     SVG to the stage's own height and squash a diagram out of proportion once
     the max-height below clamps it. */
  align-items: flex-start;
  /* A very tall diagram would otherwise push the surrounding prose apart.
     Bound the inline card and let the expanded viewer carry the full size —
     the card is a preview, the viewer is where a large diagram is read. */
  max-height: 32rem;
  overflow: hidden;
}

/* Sizing lives HERE, not in JavaScript. The card used to be laid out by a
   ResizeObserver that measured the stage and wrote width/height back onto the
   SVG; any time that pass did not land the CSS underneath pinned the diagram
   to the top-left at its intrinsic width, which on a wide display is a small
   diagram marooned in the corner of a much wider card.

   `width: 100%` overrides the `max-width` mermaid writes inline on its own
   root (`useMaxWidth`), so the diagram grows into the column instead of
   stopping at the size its text happened to need. `min-width` is the
   legibility floor: below ~0.75 the 16px labels stop being readable, so a
   diagram wider than the column keeps that scale and is cropped — the fade
   and the expand control below say so — rather than shrinking to a thumbnail.
   `height: auto` against the viewBox keeps it proportional. */
.ms-mermaid__stage > svg {
  display: block;
  flex: none;
  width: 100%;
  min-width: calc(var(--ms-diagram-natural, 0px) * 0.75);
  max-width: none;
  height: auto;
}

/* Crop rather than reducing labels to a miniature. Expand shows the whole
   diagram; the fade makes the cut edge explicit without a second scrollbar.
   Faded on BOTH sides because the stage centres its diagram: an over-wide one
   is cut at each edge, and fading only the right would present the left cut as
   a hard, accidental-looking slice. */
figure.ms-mermaid[data-wide] .ms-mermaid__stage {
  -webkit-mask-image: linear-gradient(
    to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(
    to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
}

/* Too tall to show whole: fade the cut edge instead of ending abruptly, so
   the crop reads as "there is more" rather than as a rendering fault. Mirrors
   the scroll fade the left sidebar uses. */
figure.ms-mermaid[data-tall] .ms-mermaid__stage {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 3rem), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 3rem), transparent 100%);
}

/* When content is clipped the way to see the rest must be visible without
   hovering — on touch there is no hover at all. */
figure.ms-mermaid[data-wide] .ms-mermaid__expand,
figure.ms-mermaid[data-tall] .ms-mermaid__expand {
  opacity: 1;
}

/* The expand affordance stays quiet until the diagram is hovered or the
   button itself is focused, so it never competes with the diagram. It is a
   real button, so keyboard users reach the viewer without the card click. */
.ms-mermaid__expand {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1;
  color: var(--muted-foreground);
  background: color-mix(in srgb, var(--background) 85%, transparent);
  border: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  border-radius: calc(var(--radius) - 4px);
  cursor: zoom-in;
  opacity: 0;
  transition: opacity 0.15s ease, color 0.15s ease;
}

figure.ms-mermaid:hover .ms-mermaid__expand,
.ms-mermaid__expand:focus-visible {
  opacity: 1;
}

.ms-mermaid__expand:hover {
  color: var(--foreground);
}

.ms-mermaid__pending,
.ms-mermaid__error {
  margin: 0;
  padding: 1.5rem 0;
  text-align: center;
  font-size: 0.875rem;
  color: var(--muted-foreground);
}

.ms-mermaid__error {
  color: var(--destructive);
}

/* A diagram that failed to render still shows its source — a reader can then
   see what was meant, and an author can see what to fix. */
figure.ms-mermaid--error {
  cursor: default;
}

.ms-mermaid__source {
  margin: 0;
  padding: 0.75rem;
  font-size: 0.8rem;
  white-space: pre-wrap;
  overflow-x: auto;
  color: var(--muted-foreground);
  background: var(--muted);
  border-radius: calc(var(--radius) - 2px);
}

/* Node outlines at 1px read thin even once the colour clears 3:1, because a
   90px box's boundary is carrying more meaning than a panel hairline does.
   A quarter-pixel is enough to make the shape read as drawn rather than
   implied, without turning the diagram into a wireframe. Edge paths are left
   at mermaid's own width — they are already the most visible thing on the
   canvas and thickening them muddies arrowheads. */
figure.ms-mermaid .node rect,
figure.ms-mermaid .node polygon,
figure.ms-mermaid .node circle,
figure.ms-mermaid .node ellipse,
figure.ms-mermaid .node path,
.ms-diagram-viewer .node rect,
.ms-diagram-viewer .node polygon,
.ms-diagram-viewer .node circle,
.ms-diagram-viewer .node ellipse,
.ms-diagram-viewer .node path {
  /* !important because mermaid writes stroke-width as an INLINE style
     attribute on each shape, which a stylesheet rule cannot outrank. This is
     the same narrow exception the theme reserves for beating inline and
     `!`-utilities, not a general licence. */
  stroke-width: 1.25px !important;
}

/* ── Diagram label text ─────────────────────────────────────────────────
   Mermaid is configured with `htmlLabels: true`, so node labels are real
   HTML inside a foreignObject and can be styled. Without these rules a
   label that wraps onto several lines is set at the browser's default
   leading, which is far too loose inside a node box and makes multi-line
   nodes look broken next to single-line ones. */

figure.ms-mermaid .nodeLabel,
figure.ms-mermaid .edgeLabel,
figure.ms-mermaid .label,
.ms-diagram-viewer .nodeLabel,
.ms-diagram-viewer .edgeLabel,
.ms-diagram-viewer .label {
  /* Leading only. Mermaid measures each label and writes its own
     `white-space` inline, so overriding that here lets text re-wrap at a
     width the node box was never sized for, and it spills out. Line height
     is safe to restyle because it does not change the measured width. */
  line-height: 1.35;
  overflow-wrap: break-word;
}

figure.ms-mermaid .nodeLabel p,
.ms-diagram-viewer .nodeLabel p {
  margin: 0;
  line-height: inherit;
}

/* foreignObject labels inherit the page's prose colour rules otherwise. */
figure.ms-mermaid foreignObject div,
.ms-diagram-viewer foreignObject div {
  text-align: center;
}

/* ─────────────────────────────────────────────────────────────────────────
   Expanded diagram viewer

   A native <dialog>, same mechanism as the search modal, so Escape, focus
   containment and the backdrop are the platform's job rather than ours.
   ───────────────────────────────────────────────────────────────────────── */

/* `95vw` alone is a proportion, and a proportion of an ultrawide is still an
   ultrawide: at 5120px the dialog is 4864px across and the diagram inside it
   is scaled to match, so reading it means scanning a metre of glass. The two
   `max-*` values are the real bound; `95vw` stays as the small- and
   medium-screen behaviour, which is the one it was right for. */
dialog.ms-diagram-viewer {
  width: 95vw;
  height: 92dvh;
  max-width: 1800px;
  max-height: 1100px;
  padding: 0;
  margin: auto;
  display: none;
  grid-template-rows: auto 1fr auto;
  color: var(--foreground);
  background: var(--card);
  border: 1px solid var(--border, rgba(120, 120, 120, 0.2));
  border-radius: calc(var(--radius) + 2px);
  overflow: hidden;
}

dialog.ms-diagram-viewer[open] {
  display: grid;
}

dialog.ms-diagram-viewer::backdrop {
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(2px);
}

.ms-diagram-viewer__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 2.75rem;
  padding: 0 0.5rem 0 0.9rem;
  border-bottom: 1px solid var(--border, rgba(120, 120, 120, 0.2));
}

.ms-diagram-viewer__label {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-width: 0;
  font-size: 0.8rem;
  color: var(--muted-foreground);
}

.ms-diagram-viewer__label > span {
  font-family: var(--font-mono);
  color: var(--foreground);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ms-diagram-viewer__actions {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  flex: none;
}

.ms-diagram-viewer__actions button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  font-size: 1rem;
  line-height: 1;
  color: var(--muted-foreground);
  background: none;
  border: 0;
  border-radius: calc(var(--radius) - 4px);
  cursor: pointer;
  transition: color 0.12s ease, background-color 0.12s ease;
}

.ms-diagram-viewer__actions button:hover {
  color: var(--foreground);
  background: var(--muted);
}

.ms-diagram-viewer__stage {
  position: relative;
  overflow: hidden;
  touch-action: none;
  user-select: none;
  cursor: grab;
  /* The SAME surface the inline figure uses, so a diagram is read against
     one colour whether it is on the page or expanded. */
  background-color: var(--diagram-surface);
  /* A dot grid says the area is manipulable before anyone tries to drag it,
     but it is orientation, not content: at 35% it competed with the diagram
     in front of it. Kept faint enough to register only in empty space. */
  background-image: radial-gradient(
    circle,
    color-mix(in srgb, var(--diagram-line) 22%, transparent) 1px,
    transparent 1px
  );
  background-size: 22px 22px;
}

.ms-diagram-viewer__stage[data-dragging] {
  cursor: grabbing;
}

.ms-diagram-viewer__inner {
  position: absolute;
  left: 50%;
  top: 50%;
  transform-origin: center;
  /* Suppressed inline while a drag is in flight so panning tracks the
     pointer exactly; restored for button and wheel steps so those ease. */
  transition: transform 80ms ease-out;
}

.ms-diagram-viewer__inner svg {
  max-width: none;
  height: auto;
  display: block;
}

.ms-diagram-viewer__hint {
  margin: 0;
  padding: 0.4rem 0.9rem;
  font-size: 0.7rem;
  color: var(--muted-foreground);
  border-top: 1px solid var(--border, rgba(120, 120, 120, 0.2));
}

@media (max-width: 640px) {
  dialog.ms-diagram-viewer {
    width: 100vw;
    height: 100vh;
    border-radius: 0;
    border: 0;
  }
  /* Pointer-drag panning is the whole interaction on a phone, and the hint
     costs a row of a small screen to say so. */
  .ms-diagram-viewer__hint {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ms-diagram-viewer__inner {
    transition: none;
  }
}
/* ─────────────────────────────────────────────────────────────────────────
   "On This Page" rail: stop over-reserving for the footer, and say that it
   scrolls

   The rail showed only a few entries and looked terminally cut. Two causes,
   and the interesting part is which one it is NOT: `#toc` already carries
   `overflow-y: auto`, and a flex item whose overflow is not `visible` has an
   automatic minimum size of zero, so it was always free to shrink and scroll.
   Adding `min-height: 0` changes nothing here — measured, not assumed.

   1. The height budget. The rail is sized `100svh - header - footer`, and
      this brand's footer token is 320px against upstream's ~96px, so nearly
      a third of the viewport is reserved for a footer that is not on screen
      while you are reading. On a 768px-tall window that left 248px of rail.
      Reserving a fixed, comfortable gap instead — the same shape as the left
      sidebar's own `100svh-10rem` — returns ~200px at every viewport size.

   2. Nothing said it could scroll. `#toc` also carries `no-scrollbar`, so
      the one affordance that would have shown more content below was hidden,
      and the clipped last entry read as the end of the list. A fade at the
      cut edge restores that cue, matching the left sidebar's scroll fade.
   ───────────────────────────────────────────────────────────────────────── */

div:has(> #toc) {
  height: calc(100svh - var(--header-height) - 10rem);
}

#toc {
  /* The rail's own trailing spacer means this sits over empty space when
     the list is short, so a fully-visible list is not dimmed for no reason. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5rem), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 1.5rem), transparent 100%);
}

/* ─────────────────────────────────────────────────────────────────────────
   Header chrome: give the controls a surface, and the tab rail its own plane

   The search pill and the Ask AI button both sat on `--background` behind a
   single hairline — the same colour as the header they are in, so they read
   as outlines rather than as controls. Filling them one step off the header
   is what makes them look pressable, and it is also what tells a reader the
   search pill is a FIELD rather than a label.

   The tab rail sits between the header and the page and previously shared a
   surface with both, so the three planes read as one. It now takes a deeper
   fill of its own plus a short shadow beneath it. The shadow is bottom-only
   and the rail is not a stacking context above the page content, so it
   cannot bleed sideways or over the sidebar the way the old header shadow
   did (removed in v1.5.0 for exactly that reason).
   ───────────────────────────────────────────────────────────────────────── */

/* ── Search pill ─────────────────────────────────────────────────────── */

/* `--input` is the palette's own fill for a field and it moves the right way
   without being told which mode it is in: in light it sits below the page and
   the field reads as a well, in dark it sits above and the field reads as a
   lift. The previous pair of hand-picked hexes went darker in BOTH modes,
   which is why dark rendered a near-black hole punched into the bar.

   The border is `--border` at full strength and no more. It defines the field
   without outlining it. Stated plainly: at rest that boundary does not reach
   the 3:1 of WCAG 1.4.11 in light, because a border that does is a drawn box
   on a seamless bar. The fill carries the boundary at rest and `:focus-within`
   below raises a full `--ring`, which is the state where the contrast has to
   be unambiguous. */
.mewbo-nav-search-pill {
  background: var(--input);
  border-color: var(--border);
  /* Prompt text, not content: it sits back at --muted-foreground and warms to
     --foreground on hover (the rule below), the same as an unfocused field's
     placeholder anywhere else. Full --foreground here read as pure white in
     dark mode and pulled the eye away from the page. */
  color: var(--muted-foreground);
  transition: background 0.15s ease, border-color 0.15s ease,
    box-shadow 0.15s ease;
}

.mewbo-nav-search-pill:focus-within {
  border-color: var(--ring);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 30%, transparent);
}

.mewbo-nav-search-pill:hover {
  background: color-mix(in srgb, var(--muted) 100%, transparent);
  border-color: color-mix(in srgb, var(--muted-foreground) 40%, transparent);
}

.mewbo-nav-search-pill svg {
  opacity: 0.8;
}

/* The shortcut hint is machine text on a filled field: lift it off the fill
   so it stays legible now that the field itself is no longer transparent.

   It also has to stay QUIETER than the "Search…" placeholder beside it. The
   hint is an affordance, not content: the eye should land on the word first
   and find the caps once it is already there. It was doing the opposite —
   the label measured 11.9:1 against the field in light while the placeholder
   it sits next to measured 4.1:1, so the loudest thing in the header was the
   part nobody needs to read.

   That was a specificity accident, not a colour choice. The prose keycap rule
   `kbd:not(:has(kbd)):not(.mewbo-kbd)` scores (0,1,2) — `:not()` takes the
   specificity of its argument, and `.mewbo-kbd` contributes a whole class —
   while this override scored only (0,1,1) and lost. It set
   `--muted-foreground` and got `--foreground` anyway. Hence
   `.mewbo-nav-search-pill .mewbo-kbd kbd` at (0,2,1): it wins on the merits
   rather than with `!important`, which would then have to be beaten by the
   next person.

   The three colours are all stated as mixes toward the field itself, so they
   track the palette instead of pinning greys that only work on today's two
   themes. `test_search_keycaps_stay_quieter_than_the_placeholder` holds the
   ordering, and holds it RELATIVE to the placeholder rather than to a fixed
   ratio — "subtle" is a claim about the neighbour. */
/* The WRAPPER is not a key. `.mewbo-kbd` is itself a `<kbd>` holding the two
   real caps, and it used to sit in the selector list below alongside them —
   so it painted the same face, border and inset lip AROUND the pair. The
   result was one rectangle with a hairline through it: the caps had no
   outside edge of their own, and the 0.15em between them read as a divider
   rather than as a gap. Stated explicitly rather than merely dropped from
   that list, because the generic `kbd` rules in base.css would otherwise
   still give this element a face. */
.mewbo-nav-search-pill .mewbo-kbd {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: 0;
  /* `.mewbo-kbd` carries a blanket `opacity: 0.85`, which is a second, blunter
     dimmer on top of the palette below: it fades the cap face and its label by
     one factor even though they want to move in opposite directions. Dropping
     the wrapper from the rule below took this reset with it and the label fell
     to 2.70:1, under the 3:1 floor for incidental UI text. Tone is set by the
     mixes below and by nothing else. */
  opacity: 1;
}

.mewbo-nav-search-pill .mewbo-kbd kbd,
.mewbo-nav-search-pill kbd:not(.mewbo-kbd) {
  /* Tone is set by the mixes below and by nothing else. `.mewbo-kbd` carries
     a blanket `opacity: 0.85`, which is a second, blunter dimmer on the same
     control: it fades the face and the label by one factor even though they
     want to move in opposite directions, and it dragged the label under the
     3:1 floor at every mix that made the face quiet enough. One mechanism,
     stated in the palette, is both more legible and easier to retune. */
  opacity: 1;
  /* A whisper of a face — enough that the cap is an object sitting on the
     field, not so much that it reads as a second button inside the first. */
  background: color-mix(in oklab, var(--background) 55%, var(--input));
  border: 1px solid color-mix(in oklab, var(--border) 55%, var(--input));
  /* Pulled back toward the field, but only as far as legibility allows. Not
     `--muted-foreground` itself: that IS the placeholder's colour, and
     matching it exactly makes the hint a peer of the label rather than its
     subordinate. Mixing much past this crosses the 3:1 floor for incidental
     UI text — measured 2.2:1 at 72%, which is unreadable rather than
     understated, so the gap to the placeholder is deliberately narrow. */
  color: color-mix(in oklab, var(--muted-foreground) 88%, var(--input));
  /* The lit top edge and the cast shadow underneath are what made these read
     as raised keys competing with the Ask AI button. What remains is the
     faintest inset lip: a cap that is pressed flat into the field rather than
     sitting proud of it. The cap still has to LOOK like a cap, which
     `test_search_hint_is_two_separate_keycaps` pins by requiring a shadow —
     going to `none` here is what broke it. */
  box-shadow: inset 0 0.06em 0 color-mix(in oklab, var(--background) 45%, transparent);
  text-shadow: none;
}

/* Dark needs its own mix rather than the same numbers. On a near-black field
   a cap built by lightening toward the page barely separates, so the face
   leans slightly brighter while the label pulls back further — the same
   apparent quietness, reached from the other side. */
.dark .mewbo-nav-search-pill .mewbo-kbd kbd,
.dark .mewbo-nav-search-pill kbd:not(.mewbo-kbd) {
  background: color-mix(in oklab, var(--foreground) 7%, var(--input));
  border-color: color-mix(in oklab, var(--foreground) 12%, var(--input));
  color: color-mix(in oklab, var(--muted-foreground) 92%, var(--input));
  box-shadow: inset 0 0.06em 0 color-mix(in oklab, var(--foreground) 8%, transparent);
}

/* ── Ask AI button ───────────────────────────────────────────────────── */

.mewbo-nav-ask-ai-btn {
  /* The primary action in the header, so it carries a tint of the accent
     rather than another neutral outline. Kept low enough that it reads as
     emphasis, not as a call to action competing with the page. */
  background: color-mix(in srgb, var(--primary) 10%, var(--background));
  border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
  /* The tinted fill and the primary-coloured sparkle already carry the
     emphasis, so the label rides quiet with the brand and the search
     placeholder, and warms to --foreground on hover. It is nudged a step
     darker than they are because it alone sits on the accent tint above
     rather than on the header, and --muted-foreground measures 4.14:1 there
     in light mode. The blend reads as the same grey beside the other two. */
  color: color-mix(in srgb, var(--muted-foreground) 85%, var(--foreground));
  box-shadow:
    inset 0 1px 0 0 color-mix(in srgb, var(--background) 55%, transparent),
    0 1px 2px -1px rgb(0 0 0 / 0.08);
}

.mewbo-nav-ask-ai-btn:hover {
  background: color-mix(in srgb, var(--primary) 18%, var(--background));
  border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
  color: var(--foreground);
}

.mewbo-nav-ask-ai-btn svg {
  opacity: 1;
  color: var(--primary);
}

/* ── Tab rail: its own plane between header and page ─────────────────── */

/* Share the header's token-derived finish and retain the single hairlines
   between navigation rows and at the lower edge of the stack. */
.ms-header-tabs {
  background: var(--background);
  background-image: var(--mewbo-chrome-finish);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  position: relative;
  z-index: 1;
}

/* The active tab deliberately stays on `--primary` — the clay accent IS the
   active marker here, and `test_header_tabs_rail_renders` pins it. Raising it
   to full foreground on the new deeper plane looked reasonable and was wrong:
   it removes the only colour cue distinguishing the active tab from its
   neighbours, leaving just the underline. */

.dark .mewbo-nav-search-pill:hover {
  background: color-mix(in srgb, var(--muted) 55%, var(--background));
}

/* Prose links take the text-tuned clay. `article a` resolves to `text-primary`
   in tailwind/article.css, which is the FILL value; on light's cream page that
   measured 2.69:1, well under the 4.5:1 a link has to clear. Dark is
   unchanged, where --primary-text aliases --primary. */
article a,
.md-content a {
  color: var(--primary-text);
}

/* ── Display face: IBM Plex Serif ─────────────────────────────────────────
   Headings and brand lettering are set in a serif against a sans body. The
   contrast is the point: a heading stops being "the same text, larger" and
   announces a section on shape alone, before size or weight register.

   WHAT WEARS IT — the things a reader parses as a LABEL rather than as prose:
   the page title, H1-H3 in the content body, card titles, the brand name
   beside the logo in both header and footer, and the top-level section names
   in the header nav and header tabs.

   WHAT DOES NOT, and why the list stops where it does:
     H4-H6   These sit inside a passage rather than opening one. In practice
             they run close to body size, where a serif stops reading as
             deliberate and starts reading as a font bug. H3 is the floor.
     .doc    mkdocstrings, whose headings are API symbols — a serif
             `get_config(**kwargs)` is a symbol dressed as prose, and those
             headings legitimately contain code spans.
     Hero eyebrow and lede  The text around the hero title, not the title
             itself. The hero HEADING does take the face (see the rule further
             down): on a landing page it is the first and largest thing read,
             and the `<h1>` the document actually has.
     Sidebar, ToC, top nav, body copy, UI chrome  Navigation furniture and
             continuous prose both stay on the sans. That includes the header
             tab rail, which wore the face briefly and made the header read as
             a masthead rather than a toolbar. The serif is for the few words
             that NAME a thing, plus the wordmark.

   TOKEN, NOT LITERAL: everything here goes through --font-display so a consumer
   can repoint the display face in one place. The token is defined in
   tailwind/fonts.css and carries its own fallback chain, so a build that never
   loads plex-serif.css still resolves to a real serif.

   The token is named --font-display rather than --font-serif, and is not a
   repoint of --font-sans, for two separate reasons — both in tailwind/fonts.css
   where it is defined: mermaid canvas-measures labels with --font-sans, and
   Tailwind v4 already owns --font-serif for its `font-serif` utility.

   SPECIFICITY: base.css's prose rules are unlayered and reach (0,1,2)
   (`article .typography h2`), and mewbo.css is linked after base.css, so the
   matched reach below wins on source order without `!important`.

   Inline code inside a heading needs no rule here: `article code:not(pre
   code)` in tailwind/article.css reaches (0,2,2) — the `:not(pre code)` buys
   the extra class-level — so it already outranks this (0,1,2) selector and a
   code span keeps the mono face on specificity alone. Raising this rule's
   reach to (0,2,2) or adding `!important` would take the code span with it;
   `test_code_in_a_heading_keeps_the_mono_face` is what catches that. */
article #page-header h1,
article .typography :is(h1, h2, h3):not(.ms-hero *, .doc *) {
  font-family: var(--font-display);
}

/* Card titles. A card title names the thing the card is about, which is the
   same job the headings above do — and in a grid of cards it is the only text
   doing it.

   `.ms-step__title` is the same idea in the numbered lifecycle component: it
   names a step, and in that grid it is the only text naming anything. It is a
   SEPARATE class from `.ms-card__title` despite looking like a sibling, which
   is exactly how it was missed the first time — the `.ms-*` kit has two card
   families, and styling one by name silently leaves the other behind. */
.ms-card__title,
.ms-step__title {
  font-family: var(--font-display);
}

/* The hero title IS the page's main heading — on a landing page it is the
   first and largest thing read, and the `<h1>` the document actually has. The
   hero was originally excluded wholesale from the display face on the grounds
   that it is brand composition with its own type treatment, but that reasoning
   only holds for the text AROUND the title: the eyebrow and the lede stay on
   the sans, and are not matched here.

   Reach is (0,2,2) to match the hero's own sizing rule above, which is
   unlayered and would otherwise be equal-specificity-and-later. The
   `.ms-hero__title` arm covers heroes written as HTML rather than markdown,
   mirroring that rule's own pair of selectors. */
article .ms-hero :is(h1, h2, h3, h4, h5, h6),
article .ms-hero .ms-hero__title {
  font-family: var(--font-display);
}

/* Brand lettering, header and footer. This is the site's name set as a
   wordmark, so it takes the display face in both places for one identity.
   `.mewbo-brand__docs` — the small bordered "Docs" badge beside it — stays on
   the sans: at 0.525rem a serif is illegible, and it is a status chip rather
   than part of the wordmark. */
.mewbo-brand__name,
.mewbo-footer__name {
  font-family: var(--font-display);
}

/* NOT the top navigation. The inline nav (`theme.topbar_sections`) and the tab
   rail (`theme.header_tabs`) both wore the display face briefly, on the theory
   that a section name in the header is the same noun that heads the page it
   leads to. In use it reads wrong: those tabs are navigation furniture, sat
   directly under a serif wordmark, and setting both made the whole header
   look like a masthead rather than a toolbar. Chrome you click stays on the
   sans; the wordmark above it is the one piece of lettering that does not.
   `test_top_navigation_stays_on_the_sans` pins this. */
