/* Tokens are NOT defined here — they live in tokens.json / tokens.light.json and are
   emitted into :root by the host at render time (or by build-preview.mjs for local
   preview), matching every other Tovu theme's convention. This file is mode-agnostic
   on purpose: it reads var(--x) only, so the same stylesheet serves both modes. */

/* Self-hosted Geist — deliberately vendored, never fetched from a CDN, the same rationale
   NOTICE.md already records for the kUInetic bundle. This site's published Privacy Policy
   states plainly that no third-party resource loads and no visitor data reaches any CDN; a
   `fonts.googleapis.com` stylesheet link here would falsify that document, so the faces are
   declared against files this theme ships itself (`assets/fonts/`, SIL OFL 1.1, licence text
   vendored beside them — see NOTICE.md, "Self-hosted Geist").

   Until this block existed, `tokens.json` named 'Geist'/'Geist Mono' as the display, body and
   mono faces and nothing ever fetched them: the manifest's `fonts` array is read only by
   `fontLink()` inside `pageShell()`, the declarative document shell, which a static-tier theme
   never reaches because it ships its own head. Every page silently rendered in the fallback
   stack.

   These url()s are RELATIVE on purpose. This stylesheet is served at
   `/theme-assets/tovu-theme/css/theme.css`, so `../assets/fonts/` resolves to
   `/theme-assets/tovu-theme/assets/fonts/` with no path rewriting anywhere — the same
   self-resolving arrangement `apps/site-chat/public/remixicon.css` uses for its own woff2.
   Relative is also the only form that survives a base-path static export:
   `platform/export/site-exporter.ts` follows one hop into a fetched stylesheet's own url()
   references, so these files ARE discovered and exported, but it deliberately does not rewrite
   them, so an absolute `/theme-assets/...` url would 404 under any non-root base path.

   One VARIABLE file per family (wght 100-900) rather than the static 400/500 instances the
   manifest used to name: the rules below use 400, 500 and 600, and static 400/500 files would
   have left every 600 rule synthesising a faux bold. `font-display: swap` keeps first paint on
   the fallback stack the tokens already declare instead of blocking on the font. */
@font-face {
  font-family: 'Geist';
  src: url('../assets/fonts/geist-var.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('../assets/fonts/geist-mono-var.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Classic (non-`position: fixed`) sticky footer: on a page shorter than the viewport the footer
   still sits at the bottom instead of floating mid-page; on a long page it scrolls normally and
   never overlays content. This relies on every page template in `render/pages/` rendering the
   SAME flat sibling structure once the header/footer partial-embed markers are substituted
   (embed markers swap the whole marker element — see `.site-header`'s own comment below on why
   the header ends up a sibling, not an ancestor, of page content): `<body><header
   class="site-header">…</header><main>…</main><footer class="site-footer">…</footer></body>`.
   Verified against all 14 templates in `render/pages/`, including the grid/sidebar layouts
   (`docs.html`'s `<main class="wrap docs-layout">`, `posts-sidebar.html`'s `<main>` wrapping a
   nested `.docs-layout` div) — `<main>` is always the one element between header and footer, so
   growing it via `flex: 1 0 auto` below is what pins the footer without a fixed/absolute footer
   and without reserving space with `padding-bottom` (which does not actually pin anything).
   `min-height`, not `height`, so a page taller than the viewport is free to grow past 100vh
   instead of being clipped or forcing internal scroll. `.site-header`'s own `position: sticky`
   is unaffected — sticky positioning works the same whether its containing block is a flex item
   or a plain block child. */
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
main { flex: 1 0 auto; }
/* `video`/`iframe` are constrained on the same terms as `img`, not as an afterthought: all three
   are REPLACED elements, so they size from their own intrinsic dimensions and ignore the width of
   whatever box an author put them in. A media embed resolves to a bare `<video>` tag
   (`widgets/html-embeds.ts`'s SELF_RENDERING_MEDIA_TAGS), so an author writing the documented
   `<div style="max-width:600px" data-embed-config='{"type":"media",…}'>` wrapper got a correctly
   capped 600px div containing a video that rendered at full source resolution and overflowed both
   the wrapper and the page — horizontal scroll on every viewport. `height: auto` is what keeps the
   aspect ratio while the width is being clamped. */
img, video, iframe { max-width: 100%; }
img, video { display: block; }
img, video, iframe { height: auto; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 400; margin: 0; letter-spacing: -0.02em; }
p { margin: 0; color: var(--muted); }

.wrap { max-width: var(--container); margin: 0 auto; padding: 0 24px; }

/* ---------- header / nav ---------- */
/* Single source for the sticky header's rendered height: `.nav-row` below reads `--header-h`
   directly (its 69px content height; `.site-header`'s own 1px border-bottom brings the real box
   to 70px). Declared at `:root` rather than scoped to `.site-header` since the header is injected
   as a partial embed (`posts-sidebar.html`'s
   `data-embed-config='{"type":"partial","id":"nav",...}'`) and ends up a SIBLING of `.docs-layout`
   under `<body>`, not its ancestor. Header height is the same at every breakpoint (`.nav-row` is
   never resized when the header goes compact — the mobile menu is a modal `<dialog>` in the top
   layer, not part of its box), so one value covers desktop and mobile alike.

   NOT `--kui-pin-offset`: kUInetic's docs (https://kuinetic.com/docs/catalog.md) say a pinning
   effect's `offset-top:` defaults to `var(--kui-pin-offset, 0px)`, which reads as "answer the
   header offset once via that token." That default genuinely exists in the vendored bundle
   (`stickyParams`, `kuinetic.all.js`) — but only for the sticky/pin effects (`pin-section`/
   `pin-until`/`pin-spacer`/`stacking-cards`), none of which this theme uses anywhere. `posts-
   sidebar.html` used to drive its docs sidebar with kUInetic's container-form scroll-spy
   (`data-kui="scroll-spy ..."` on `.docs-layout`) — a different code path
   (`prepareScrollSpyContainer`) whose own `offset-top` param defaults to a literal `"0px"` and is
   parsed by a plain JS regex (`toPixels()`/`NUMBER_WITH_UNIT`) that cannot read `var()`/`calc()` at
   all, so that effect's `offset-top:70px` had to be authored directly on the `data-kui` attribute
   instead of this token. Retired 2026-09-24 (docs IA restructure) in favor of `docs-toc.js`'s own
   `IntersectionObserver`, which needs no `--kui-pin-offset`-shaped token at all (its own
   `rootMargin` is authored directly in the script) — kept here as the reason this theme has never
   defined `--kui-pin-offset`. If this theme ever adds a real pin/sticky effect, define
   `--kui-pin-offset: calc(var(--header-h) + 1px)` here for it to pick up automatically; until then
   it would be a var nothing reads. */
:root {
  --header-h: 69px;
}
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in oklch, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s;
}
.site-header.scrolled { border-bottom-color: var(--border); }
/* Light mode: a flat white bar, not a tinted one. The dark-mode rule above blends `--bg` at 82%
   over whatever scrolls beneath it, which in light mode reads as an off-white/tinted strip rather
   than white (owner report). An opaque white here removes both the tint and the show-through; the
   blur above becomes a no-op behind an opaque background, which is the intent. */
:root[data-theme="light"] .site-header { background: #ffffff; }
.nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-h);
}
.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
}
/* The mark PNG itself (`assets/logo.png` / `logo@2x.png`) is the owner's gold artwork only,
   cropped tight with a transparent background -- no tile or backdrop baked in. Previously this
   painted a `--surface`/`--border` tile behind it so the mark had a mode-aware backing; the owner
   looked at light mode and asked for the tile removed (2026-08-31) -- the transparent PNG sits
   directly on the page background instead, which still satisfies "light in light mode, dark in
   dark mode" because the page background itself is mode-aware. Sizing only now, no
   background/border, and no border-radius either: with nothing left to paint, there is no longer a
   square-edged box for a radius to round, and the PNG's own artwork has no square edge to clip. */
.brand .mark {
  width: 28px; height: 28px;
  display: grid;
  place-items: center;
}
.brand .mark img { width: 100%; height: 100%; border-radius: 7px; display: block; }
.main-nav { display: flex; align-items: center; gap: 28px; }
.main-nav a {
  font-size: 14px;
  color: var(--muted);
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.main-nav a:hover, .main-nav a[aria-current="page"] { color: var(--fg); }
.nav-actions { display: flex; align-items: center; gap: 8px; }
/* `.nav-actions` prefix is load-bearing: this button also carries `.btn`, whose
   `display: inline-flex` (below) has IDENTICAL specificity and comes LATER, so a bare
   `.nav-toggle` selector loses on source order and the hamburger shows at every width.
   Same collision class as `.wrap`/`.post-detail`. Fixed 2026-08-30. */
.nav-actions .nav-toggle { display: none; }

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  margin-right: 4px;
  transition: background 0.15s, border-color 0.15s;
}
.theme-toggle:hover { background: rgba(127,127,127,0.12); }
.theme-toggle .icon-stack { position: relative; width: 16px; height: 16px; }
.theme-toggle svg {
  position: absolute; inset: 0; width: 16px; height: 16px;
  transition: opacity 0.25s ease, transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.theme-toggle .icon-sun { opacity: 0; transform: rotate(-90deg) scale(0.6); }
.theme-toggle .icon-moon { opacity: 1; transform: rotate(0deg) scale(1); }
:root[data-theme="light"] .theme-toggle .icon-sun { opacity: 1; transform: rotate(0deg) scale(1); }
:root[data-theme="light"] .theme-toggle .icon-moon { opacity: 0; transform: rotate(90deg) scale(0.6); }

/* ---------- scroll reveal ----------
   Deliberately NO default `opacity: 0` here — and none arrives from anywhere else
   either. Reveals are declared per element as `data-kui="fade-up ..."` and driven by
   the vendored `scripts/vendor/kuinetic.all.js` (MIT, see
   `scripts/vendor/kuinetic-LICENSE.md`), which injects its entire stylesheet itself at
   load. So if that script is blocked or fails, no kUInetic CSS exists at all and every
   element renders in its normal, fully visible resting state: the hidden state lives
   only inside an `@keyframes ... from { opacity: 0 }` block, and with no kUInetic loaded
   there is no animation to render that frame.

   That guarantee covers the script-ABSENT case only. The earlier wording here went on to
   claim the `from` block is "never a resting style" at all, which is wrong and is
   corrected 2026-09-04: an `on:enter` element is compiled with `animation-fill-mode: both`
   plus `animation-play-state: paused`, and a paused fill-both animation renders its `from`
   frame — so between load and the observer firing the element really does rest at
   `opacity: 0`. Harmless when the observer can fire; fatal when it cannot. `threshold` is
   a fraction of the observed element's OWN area, so anything taller than
   `viewport / threshold` never satisfies it and stays invisible for the life of the page.
   Hence the rule the templates in `render/pages/` now follow: no `threshold:` token on any
   element whose height the template does not bound. See NOTICE.md, "Why the page wrappers
   carry no threshold:".

   kUInetic's one rule that hides content in CSS alone —
   `html[data-kui-cloak] [data-kui][data-kui-reveal]:not([data-kui-state])` — is opt-in
   behind a `data-kui-cloak` attribute on `<html>` that this theme deliberately never
   sets (the library only ever removes that attribute, never adds it). Progressive
   enhancement, not a CSS/JS handshake that can desync. */

@media (prefers-reduced-motion: reduce) {
  .theme-toggle svg { transition: none; }
}

/* ---------- buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 38px;
  padding: 0 18px;
  border-radius: 18px;
  font-size: 14px;
  font-weight: 500;
  border: 1px solid transparent;
  cursor: pointer;
  transition: opacity 0.15s, background 0.15s, border-color 0.15s;
  white-space: nowrap;
}
.btn-ghost { border-color: var(--border-strong); color: var(--fg); background: transparent; }
.btn-ghost:hover { background: rgba(255,255,255,0.06); }
.btn-solid { background: var(--fg); color: var(--bg); }
.btn-solid:hover { opacity: 0.88; }
.btn-lg { height: 44px; padding: 0 24px; border-radius: 22px; font-size: 15px; }
.btn-block { width: 100%; }

/* ---------- status pill (the one accent color) ---------- */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: 12px/1 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--accent-text);
  background: color-mix(in oklch, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent) 30%, transparent);
  padding: 6px 10px;
  border-radius: 999px;
}
.status-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }

/* ---------- logo carousel ---------- */
.logo-carousel {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.logo-track {
  display: flex;
  align-items: center;
  gap: 56px;
  width: max-content;
  animation: logo-scroll 26s linear infinite;
}
.logo-carousel:hover .logo-track { animation-play-state: paused; }
.logo-track .logo-ph { font: 13px/1 var(--font-mono); color: var(--muted); letter-spacing: 0.03em; white-space: nowrap; opacity: 0.7; }
@keyframes logo-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .logo-track { animation: none; }
}

/* ---------- hero ---------- */
.hero { padding-block: 96px 72px; text-align: center; }
.hero .eyebrow-row { display: flex; justify-content: center; margin-bottom: 28px; }
.hero h1 {
  font-size: clamp(40px, 6vw, 72px);
  line-height: 1.02;
  letter-spacing: -0.025em;
  max-width: 15ch;
  margin: 0 auto 24px;
}
.hero-line { display: block; }
.hero .lede { font-size: 18px; max-width: 46ch; margin: 0 auto 36px; }
.hero .cta-row { display: flex; gap: 12px; justify-content: center; }

.hero-frame {
  margin-top: 64px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  aspect-ratio: 16/8;
  overflow: hidden;
  position: relative;
}
.ph-img {
  width: 100%; height: 100%;
  background:
    linear-gradient(135deg, var(--surface-2) 25%, transparent 25%) 0 0/16px 16px,
    linear-gradient(225deg, var(--surface-2) 25%, transparent 25%) 0 0/16px 16px,
    var(--surface);
  display: grid;
  place-items: center;
  color: var(--muted);
  font: 12px/1 var(--font-mono);
  letter-spacing: 0.04em;
}

/* ---------- sections ---------- */
section.band { padding-block: 72px; border-top: 1px solid var(--border); }
.section-head { max-width: 56ch; margin-bottom: 48px; }
.section-head .kicker {
  display: block; font: 12px/1 var(--font-mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent-text); margin-bottom: 14px;
}
.section-head h2 { font-size: clamp(28px, 3.4vw, 38px); letter-spacing: -0.02em; margin-bottom: 12px; }
.section-head p { font-size: 16px; }

.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
.feature-card { background: var(--bg); padding: 33px; }
.feature-card .icon { width: 32px; height: 32px; margin-bottom: 20px; color: var(--fg); }
.feature-card h3 { font-size: 17px; font-weight: 500; margin-bottom: 8px; letter-spacing: -0.01em; }
.feature-card p { font-size: 14px; }

/* ---------- footer ---------- */
.site-footer { border-top: 1px solid var(--border); padding: 56px 0 40px; }
/* repeat(3,1fr) -> repeat(2,1fr): the footer's "Account" column was commented out in footer.html
   2026-08-31 (owner request, reversible) leaving 3 visible columns (brand + Resources + Legal),
   not 4. Restore this to repeat(3,1fr) together with uncommenting that column. */
.footer-grid { display: grid; grid-template-columns: 1.4fr repeat(2, 1fr); gap: 32px; margin-bottom: 48px; }
.footer-col h4 { font-size: 13px; color: var(--fg); margin-bottom: 14px; font-family: var(--font-body); font-weight: 500; }
.footer-col a { display: block; font-size: 13px; color: var(--muted); margin-bottom: 10px; }
.footer-col a:hover { color: var(--fg); }
.footer-bottom { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--muted); border-top: 1px solid var(--border); padding-top: 24px; }

/* ---------- pricing ---------- */
.pricing-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.plan-card { border: 1px solid var(--border); border-radius: 16px; padding: 32px; display: flex; flex-direction: column; }
.plan-card.featured { border-color: var(--border-strong); background: var(--surface); }
.plan-card h3 { font-size: 16px; font-weight: 500; margin-bottom: 6px; }
.plan-card .price { font-size: 40px; font-family: var(--font-display); letter-spacing: -0.02em; margin: 16px 0 4px; }
.plan-card .price span { font-size: 14px; color: var(--muted); font-family: var(--font-body); }
.plan-card .desc { font-size: 13px; margin-bottom: 24px; }
.plan-card ul { list-style: none; margin: 24px 0 0; padding: 0; display: flex; flex-direction: column; gap: 12px; flex: 1; }
.plan-card li { font-size: 13px; color: var(--fg); display: flex; gap: 10px; align-items: flex-start; }
.plan-card li svg { width: 14px; height: 14px; margin-top: 2px; flex-shrink: 0; color: var(--muted); }

.faq-list { display: flex; flex-direction: column; border-top: 1px solid var(--border); max-width: 720px; margin: 0 auto; }
.faq-item { border-bottom: 1px solid var(--border); }
.faq-item summary { padding: 22px 0; font-size: 15px; font-weight: 500; cursor: pointer; list-style: none; display: flex; justify-content: space-between; align-items: center; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: '+'; font-size: 20px; color: var(--muted); }
.faq-item[open] summary::after { content: '\2212'; }
.faq-item p { padding-bottom: 22px; font-size: 14px; max-width: 60ch; }

/* ---------- docs ---------- */
/* `padding-block`, not shorthand `padding`: same clobber as `.hero`/`section.band` (fixed
   2026-08-31) — this rule and `.wrap`'s `padding: 0 24px` share specificity, and this one comes
   later in the cascade, so a shorthand here would zero out `.wrap`'s inline padding on every
   `.docs-layout.wrap` element (`docs.html`'s `<main class="wrap docs-layout">`, and
   `posts-sidebar.html`'s `<div class="docs-layout wrap">`), running the sidebar and
   article flush against the viewport edge. `padding-block` only ever touches top/bottom, so it
   can't touch `.wrap`'s left/right padding at all. */
.docs-layout { display: grid; grid-template-columns: 240px 1fr; gap: 48px; padding-block: 48px 96px; }
.docs-side { border-right: 1px solid var(--border); padding-right: 24px; }
.docs-side .group { margin-bottom: 28px; }
.docs-side .group h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 12px; font-family: var(--font-body); font-weight: 500; }
.docs-side a { display: block; font-size: 13.5px; color: var(--muted); padding: 5px 0; }
.docs-side a:hover, .docs-side a[aria-current="page"] { color: var(--fg); }
.docs-main h1 { font-size: 34px; margin-bottom: 14px; }
.docs-main .lede { font-size: 16px; margin-bottom: 32px; max-width: 60ch; }
.docs-main pre {
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  padding: 18px 20px; overflow-x: auto; font: 13px/1.5 var(--font-mono); color: var(--fg);
  white-space: pre;
}
.docs-main pre code { white-space: inherit; line-height: inherit; padding: 0; }
.docs-main h2 { font-size: 20px; margin: 40px 0 14px; }
.docs-main p { font-size: 15px; margin-bottom: 14px; max-width: 60ch; }

/* ---------- changelog ---------- */
.changelog-list { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; }
.changelog-entry { display: grid; grid-template-columns: 140px 1fr; gap: 32px; padding: 40px 0; border-top: 1px solid var(--border); }
.changelog-entry time { font: 13px/1 var(--font-mono); color: var(--muted); padding-top: 4px; }
.changelog-entry h3 { font-size: 19px; margin-bottom: 10px; }
.changelog-entry p { font-size: 14.5px; margin-bottom: 12px; max-width: 56ch; }
.changelog-entry .tag { display: inline-block; font: 11px/1 var(--font-mono); letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--border); padding: 4px 8px; border-radius: 6px; margin-bottom: 12px; }

/* ---------- blog ---------- */
.blog-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.post-card { border: 1px solid var(--border); border-radius: 14px; padding: 24px; display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.post-card.featured { grid-column: span 3; flex-direction: row; align-items: stretch; gap: 32px; padding: 28px; }
.post-card.featured .post-card-media { flex: 0 0 42%; margin-bottom: 0; aspect-ratio: 16/11; }
.post-card-body { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
.post-card-media { aspect-ratio: 16/10; margin-bottom: 16px; font-size: 12px; border-radius: 10px; }
.post-card .tag { display: inline-block; align-self: flex-start; font: 11px/1 var(--font-mono); letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent-text); border: 1px solid color-mix(in oklch, var(--accent) 30%, transparent); background: color-mix(in oklch, var(--accent) 10%, transparent); padding: 4px 8px; border-radius: 6px; margin-bottom: 12px; }
.post-card h2, .post-card h3 { font-weight: 500; letter-spacing: -0.01em; margin-bottom: 10px; }
.post-card h2 { font-size: 22px; }
.post-card h3 { font-size: 17px; }
.post-card p { font-size: 14px; margin-bottom: 16px; flex: 1; }
.post-meta { font: 12.5px/1 var(--font-mono); color: var(--muted); display: flex; gap: 8px; margin-top: auto; }

/* ---------- blog post (single-post template) ---------- */
.post-detail { max-width: 720px; margin: 0 auto; padding-block: 72px 96px; }
.post-detail-header { margin-bottom: 40px; }
.post-detail-header h1 { font-size: clamp(30px, 4.4vw, 46px); line-height: 1.08; letter-spacing: -0.02em; margin-bottom: 16px; }
.post-detail-header .post-meta { font-size: 13px; }
.post-detail-body { font-size: 17px; line-height: 1.7; max-width: 66ch; }
.post-detail-body h2 { font-size: 26px; margin: 40px 0 16px; letter-spacing: -0.01em; }
/* Section divider (2026-09-23) — a docs page like /embeds lists one h3 per marker type
   back-to-back; without a rule between them the previous section's live example runs straight
   into the next heading. Border + extra top spacing reads as a clear break; applies wherever
   `.post-detail-body` renders (not just /embeds) since any adjacent h3 subsections read better
   separated the same way. */
.post-detail-body h3 { font-size: 20px; margin: 40px 0 12px; letter-spacing: -0.01em; border-top: 1px solid var(--border); padding-top: 28px; }
.post-detail-body p { margin-bottom: 18px; }
.post-detail-body ul, .post-detail-body ol { margin: 0 0 18px 22px; }
.post-detail-body li { margin-bottom: 6px; }
.post-detail-body blockquote { border-left: 2px solid var(--border-strong); padding-left: 20px; margin: 24px 0; color: var(--muted); font-style: italic; }
/* Docs callouts (`.qs-note`, `.dx-note`): the pages' own inline <style> paints them
   `var(--surface-2)` grey. Override once here with a faint accent tint so the text reads at full
   contrast in both modes; `:root` lifts specificity over the later in-page rule. */
:root :is(.qs-note, .dx-note) { background: color-mix(in oklch, var(--accent) 7%, var(--bg)); color: var(--fg); }
:root :is(.qs-note, .dx-note) p { color: inherit; }
.post-detail-body code { font: 13px/1 var(--font-mono); background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 2px 6px; }
/* `font` (13px/1.5) set here, on `pre` itself, not just on `pre code` below — `pre` is the block
   box that lays out the forced-newline lines, and CSS gives every line box a "strut" sized off the
   BLOCK's own font/line-height, not the inline `code` descendant's. Without an override here `pre`
   inherited `.post-detail-body`'s 17px/1.7 (line-height: 28.9px), so every line was struck at
   28.9px tall even though the visible glyphs rendered at `code`'s 13px/19.5px — double-spaced text
   sitting inside correctly-sized lines. Matches `.docs-main pre` below, which already sets its own
   font and never had this bug. */
.post-detail-body pre { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 18px 20px; overflow-x: auto; margin: 20px 0; white-space: pre; font: 13px/1.5 var(--font-mono); }
.post-detail-body pre code { border: none; background: none; padding: 0; white-space: inherit; line-height: 1.5; }
/* Live example frame (docs pages, 2026-09-23; white/gold made the default 2026-09-23 same day,
   owner approved the gold-frame prototype and asked to "update throughout") — wraps a rendered
   marker's live output (a `.live-example` div around the marker element) so it reads as a
   distinct demo instead of blending into the surrounding doc prose. The "Live example" text is
   `content` on `::before`, not a real element, so a docs page body only needs the wrapper div,
   never its own label markup.

   A gold border on the theme's own `--surface` background is the default for every live example
   on every docs page (/embeds, /media, /posts, /forms, …) — not a one-off modifier class. `--gold`
   is sampled from this theme's own `assets/logo@2x.png` (~rgb(255,186,62)) because neither
   tokens.json nor tokens.light.json names a fixed gold, and the frame should read the same in
   both modes.

   The background follows the theme (2026-09-26). It used to be a fixed white with light-mode
   `--fg`/`--muted`/`--prose-link` re-declared locally, but embedded widgets style their own
   controls from the page's mode-varying tokens, so in dark mode the contact form's inputs came out
   near-black inside a white box. Following the theme instead means the box and whatever it wraps
   read from the same tokens in both modes.

   `color: var(--fg)` stays on the box itself: a plain-inheriting descendant (e.g. the "Footer
   Resources" widget's `<h3 class="widget-menu-title">`, which has no `color` rule of its own)
   inherits the COMPUTED `color`, not a live re-read of `--fg`, so the box gives its scope a
   freshly computed one. */
:root { --gold: #ffba3e; }
/* `.latest-posts .live-example` (2026-09-23) — /posts's "Latest posts" list is the one live example
   NOT inside `.post-detail-body` (it's the theme-owned post-previews marker rendered by
   listing-default.html, a sibling section to the article body, not authored inside it), so the
   selector above alone would never match it. `.latest-posts` is a class that exists only on that
   one template's wrapper, so this addition changes nothing for any other page's post list. */
.post-detail-body .live-example,
.latest-posts .live-example {
  color: var(--fg);
  position: relative;
  margin: 20px 0;
  padding: 30px 20px 20px;
  border: 2px solid var(--gold);
  border-radius: 10px;
  background: var(--surface);
}
.post-detail-body .live-example::before,
.latest-posts .live-example::before { content: "Live example"; position: absolute; top: 10px; left: 16px; font: 11px/1 var(--font-mono); letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }

/* ---------- code block copy button ---------- */
/* `scripts/main.js` (`setupCodeCopyButtons`) wraps every `<pre>` on the page (docs articles,
   `.post-detail-body` article content, and a bare `<pre>` in custom page body HTML alike — its own
   comment explains why it's unscoped) in a generated `.code-block-wrap` div, and appends the button
   to that wrapper as a SIBLING of `pre`, not a child of it. One unscoped `.code-block-wrap
   .code-copy-btn` selector covers the button wherever a `pre` appears, instead of duplicating the
   rule per ancestor class. Tokens only (`--surface`/`--border`/`--muted`/`--fg`/`--accent`) — no
   hard-coded colors — so the button reads correctly in both the dark default and
   `:root[data-theme="light"]` without its own light/dark branch.

   The button used to be appended as a CHILD of `pre` itself (fixed 8476795c0: a bare `<pre>` in
   custom page body HTML had no positioned ancestor at all, so the button's `top`/`right` resolved
   against the initial containing block and rendered pinned near the top of the whole document).
   That broke again once `pre` itself became the horizontally-scrolling element (8587caa6f, long
   lines scroll instead of wrapping): an absolutely-positioned descendant of a scrolling box is still
   part of that box's own scrollable content, so the button's on-screen position was computed once
   against the unscrolled layout and then translated along with every scroll — mid-scroll it rendered
   on top of whatever code text had scrolled under its old spot instead of staying pinned to the
   corner. Moving the button OUT of the scrolling `pre` and into a non-scrolling wrapper is the fix:
   `pre` still owns `overflow-x: auto` and scrolls its own content, but the button's containing block
   (`.code-block-wrap`) never scrolls, so `top`/`right` stay resolved against the same fixed corner at
   any scroll position.

   This also collapses what used to be three separate `position: relative` declarations — one on
   `.docs-main pre`, one on `.post-detail-body pre`, and one on the unscoped `pre.has-copy-btn`
   catch-all for a bare `<pre>` in custom page body HTML (`pages-default.html` injects page body
   content straight into `.post-detail`, never `.post-detail-body`) — into the single
   `.code-block-wrap` rule below, since the wrapper is generated around every `pre` the script
   touches regardless of ancestor context.

   `padding-right` stays on `pre.has-copy-btn` itself, not the wrapper: it reserves blank space at
   the end of the pre's own scrollable content so a long line's trailing characters, once scrolled
   fully into view, don't land directly under the button's fixed on-screen corner. Keyed off the same
   `has-copy-btn` marker class the script stamps on every `pre` it touches — once, only after a
   button actually lands — so any current or future page body with a bare `<pre>` is covered without
   relying on that page's own styles to remember the declaration. */
.code-block-wrap { position: relative; }
pre.has-copy-btn { padding-right: 48px; }
/* 2026-09-26: `padding-right` alone did not keep text clear of the button. `pre`'s scrollport
   includes its own padding, so an unscrolled long line still ran under the button and read as cut
   off (the /pages embed example). When the text sits in a `<code>` child, that child scrolls
   instead: as a block it ends at `pre`'s content edge, so its scrollport stops 48px short of the
   right border and the button owns that gutter at any scroll position. Lines still scroll rather
   than wrap (8587caa6f). A bare `<pre>` with no `<code>` keeps scrolling on `pre` itself. */
pre.has-copy-btn > code { display: block; overflow-x: auto; }
.code-block-wrap .code-copy-btn {
  position: absolute; top: 10px; right: 10px; width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  color: var(--muted); cursor: pointer; padding: 0; line-height: 0;
}
.code-block-wrap .code-copy-btn:hover { color: var(--fg); border-color: var(--border-strong); }
.code-block-wrap .code-copy-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.code-block-wrap .code-copy-btn svg { width: 15px; height: 15px; display: block; }
.code-block-wrap .code-copy-btn .icon-check { display: none; color: var(--accent); }
.code-block-wrap .code-copy-btn.copied .icon-copy { display: none; }
.code-block-wrap .code-copy-btn.copied .icon-check { display: block; }
/* `height: auto` added (2026-08-12, same dispatch as the admin editor's `.editor-body .tiptap
   img` fix) — confirmed live (headless Chromium against this exact stylesheet, a real PNG
   carrying `width`/`height` HTML attributes) that WITHOUT it, an image squashes instead of
   scaling: `max-width: 100%` alone shrinks the rendered width, but the HTML `height` attribute
   `render.ts` emits (from stored media metadata) still wins as a presentational hint and pins
   the pixel height, distorting the aspect ratio. `renderImageTag` (`src/server/http/site/
   render.ts`) emits `width`/`height` independently whenever an asset has that metadata stored —
   this is a real, reachable shape, not a hypothetical one. This same rule, byte-identical, is
   copied (not inherited — ADR/decision: Tovu themes are copies, see `project_tovu_theme_copy_
   model`) into 5 other themes; all 6 got this same one-line fix in this dispatch. */
.post-detail-body img { max-width: 100%; height: auto; border-radius: 10px; margin: 20px 0; }
/* Highlight mark (`renderDocNode`'s `"highlight"` case, 2026-08-11) — the browser's own UA default
   for `<mark>` (yellow background, black text) is legible on its own, but a bare square block reads
   out of place next to every other inline element here that already carries this theme's own
   rounding (`code` two lines up, `img` just above). An explicit `background`/`color` here also stops
   an author-supplied `background-color` inline style (`safeCssColor`, render.ts) from ever landing
   on illegible black-on-black — the inline style, when present, still wins over this class rule
   (equal-ish specificity but source order + `style=""` beats a stylesheet class), this is only the
   fallback for the toolbar's plain, colorless toggle. */
.post-detail-body mark { background: #fef08a; color: #422006; border-radius: 3px; padding: 0 3px; }
/* Table (`renderDocNode`'s `"table"`/`"tableRow"`/`"tableCell"`/`"tableHeader"` cases, 2026-08-11) —
   an unstyled `<table>` has no visible cell boundaries at all, which is what "renders as unstyled
   garbage publicly" (the brief's own phrase for structural nodes) looks like in practice: `border-
   collapse` alone leaves every cell edge invisible until a border is declared explicitly. `overflow-
   x: auto` on the wrapper block handles a table wider than the reading column (66ch, `.post-detail-
   body`'s own max-width above) without it blowing out the page's horizontal scroll. */
.post-detail-body table { width: 100%; border-collapse: collapse; margin: 24px 0; display: block; overflow-x: auto; }
.post-detail-body th, .post-detail-body td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; vertical-align: top; }
.post-detail-body th { background: var(--surface); font-weight: 600; }
/* Task list (`renderDocNode`'s `"taskList"`/`"taskItem"` cases, 2026-08-11) — without this,
   `ul[data-type="taskList"]` inherits the plain `ul, ol` bullet-indent rule two lines above (a
   redundant bullet glyph next to the checkbox) and the checkbox/label/content sit on three stacked
   lines instead of one row, since `<li data-type="taskItem">` has no layout of its own beyond the
   UA default. `list-style: none` + `padding-left: 0` removes the bullet; the flex row aligns the
   (disabled, public-render-only — see that case's own comment) checkbox with the first line of text
   rather than the block's own vertical center. */
.post-detail-body ul[data-type="taskList"] { list-style: none; padding-left: 0; margin: 0 0 18px; }
.post-detail-body li[data-type="taskItem"] { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 6px; }
.post-detail-body li[data-type="taskItem"] > label { display: flex; align-items: center; margin-top: 0.35em; }
.post-detail-body li[data-type="taskItem"] > div { flex: 1 1 auto; }
.post-detail-body li[data-type="taskItem"] > div p { margin-bottom: 0; }
/* YouTube embed (`renderDocNode`'s `"youtube"` case, 2026-08-11) — a responsive 16:9 box regardless
   of whatever width/height the editor's own node carried (the renderer ignores those attrs
   entirely, see that case's own comment for why); without this an unstyled `<iframe>` renders at
   its raw HTML default size (300x150), which is both tiny and non-responsive on a narrow viewport. */
.post-detail-body .youtube-embed { position: relative; width: 100%; aspect-ratio: 16 / 9; margin: 24px 0; border-radius: 10px; overflow: hidden; }
.post-detail-body .youtube-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Mention link (`renderDocNode`'s `"mention"` case, toolbar-polish pass 2026-08-11) — a bare `<a>`
   inherits `color: inherit; text-decoration: none` from this theme's own reset (`a` rule near the
   top of this file), which would make a mention indistinguishable from the surrounding prose; the
   whole point of a mention is that it reads as a link. `--accent` matches every other inline-accent
   use in this theme (status pills, tags, focus rings). Underline is deferred to `:hover` rather than
   always-on so the link doesn't visually compete with the "@" it already carries as its own signal. */
.post-detail-body .post-mention { color: var(--accent-text); font-weight: 600; }
.post-detail-body .post-mention:hover { text-decoration: underline; }
/* Prose links — the `a` reset near the top makes content links look like plain text, so links
   inside post/page bodies get an explicit blue + underline (owner call 2026-09-22: "obvious it's a
   hyperlink", everywhere for consistency). Mentions and button-styled links keep their own look. */
.post-detail-body { --prose-link: #60a5fa; }
:root[data-theme="light"] .post-detail-body { --prose-link: #2563eb; }
.post-detail-body a:not(.post-mention):not([class*="btn"]) { color: var(--prose-link); text-decoration: underline; text-underline-offset: 3px; }

/* Assigned terms ("filed under" footer, `renderAssignedTermsBlock` in render.ts) — a sibling of
   `.post-detail-body`, not nested in it: that function concatenates `<div class="entry-terms">`
   onto the already-closed body div rather than splicing inside it. A muted, bordered-off block
   below the body, same idiom as `.post-meta`'s label + `.post-card .tag`'s chip for the values
   themselves, so a term reads the same here as it does on a post card. */
.entry-terms {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  font-size: 13px;
}
.entry-terms__group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.entry-terms__taxonomy { color: var(--muted); }
.entry-terms__term {
  display: inline-block;
  font: 11px/1 var(--font-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-text);
  border: 1px solid color-mix(in oklch, var(--accent) 30%, transparent);
  background: color-mix(in oklch, var(--accent) 10%, transparent);
  padding: 4px 8px;
  border-radius: 6px;
}

/* ---------- about ---------- */
.about-hero { padding: 88px 0 56px; }
.about-hero h1 { font-size: clamp(36px, 5vw, 56px); max-width: 16ch; margin-bottom: 20px; }
.about-hero p { font-size: 17px; max-width: 52ch; }
.photo-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.photo-grid .ph-img { aspect-ratio: 3/4; border-radius: 8px; font-size: 11px; }
.values-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; }
.values-grid .value h3 { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
.values-grid .value p { font-size: 14px; }

/* ---------- download ---------- */
.download-hero { padding: 96px 0; text-align: center; }
.install-cmd {
  display: inline-flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  padding: 14px 20px; font: 14px/1 var(--font-mono); color: var(--fg); margin: 32px 0;
}
.install-cmd button { background: transparent; border: 1px solid var(--border-strong); color: var(--muted); border-radius: 6px; padding: 4px 8px; font-size: 11px; cursor: pointer; font-family: var(--font-mono); }
.install-cmd button:hover { color: var(--fg); }
.platform-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 8px; }

/* ---------- auth ---------- */
.auth-wrap { max-width: 380px; margin: 0 auto; padding: 96px 24px 120px; }
.auth-wrap h1 { font-size: 28px; margin-bottom: 8px; text-align: center; }
.auth-wrap .lede { text-align: center; font-size: 14px; margin-bottom: 32px; }
.field { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.field label { font-size: 13px; color: var(--fg); font-weight: 500; }
.field input {
  height: 42px; border-radius: 10px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--fg); padding: 0 14px; font-size: 14px; font-family: var(--font-body);
}
.field input:focus { outline: none; border-color: var(--accent-text); }
.auth-foot { text-align: center; font-size: 13px; color: var(--muted); margin-top: 20px; }
.auth-foot a { color: var(--fg); border-bottom: 1px solid var(--border-strong); }
.divider { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--muted); font-size: 12px; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }

/* ---------- responsive ---------- */
@media (max-width: 860px) {
  .feature-grid { grid-template-columns: 1fr; }
  .pricing-grid { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .docs-layout { grid-template-columns: 1fr; }
  .docs-side { border-right: none; border-bottom: 1px solid var(--border); padding-right: 0; padding-bottom: 24px; }
  .values-grid { grid-template-columns: 1fr; }
  .photo-grid { grid-template-columns: repeat(2, 1fr); }
  .changelog-entry { grid-template-columns: 1fr; gap: 8px; }
  .blog-grid { grid-template-columns: 1fr; }
  .post-card.featured { grid-column: span 1; flex-direction: column; }
  .post-card.featured .post-card-media { aspect-ratio: 16/10; }
}

/* ---------------------------------------------------------------------------
   Docs layout + sidebar/TOC/pager (posts-sidebar.html — the one docs template,
   2026-09-24 docs IA restructure)

   Three grid areas (sidebar / content / toc) placed BY NAME, so the DOM can stay
   in reading order (sidebar disclosure, TOC disclosure, article — see that
   file's own comment) at every width instead of a source-order-dependent column
   layout. `.docs-sidebar`'s `.menu-list`/`.menu-item`/`is-current`/`is-active`/
   `has-children`/`depth-N` hooks are emitted by the engine's tree renderer
   (static-render.ts::renderMenuTree) from the `docs-section` reserved marker —
   these rules work for ANY tree-variant menu, not just this one. `.docs-toc`'s
   `.toc-list`/`.toc-item` hooks are a DIFFERENT, smaller vocabulary: built
   client-side by docs-toc.js from this page's own headings, not server-resolved,
   and every entry is a real destination (no "section heading" treatment needed).
   --------------------------------------------------------------------------- */

.docs-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 200px;
  grid-template-areas: "sidebar content toc";
  gap: 40px;
  align-items: start;
  padding-top: 48px;
  padding-bottom: 96px;
}

.docs-sidebar-toggle { grid-area: sidebar; }
.docs-toc-toggle { grid-area: toc; }

/* Desktop only (mobile re-enables the toggle below): both disclosures render as plain sticky
   columns, not a collapsible control. None of this touches the `open` attribute, so mobile's
   native click-to-toggle (which DOES read `open`) is unaffected.

   `::details-content` is the load-bearing rule. Current engines (Chrome 131+, Safari 18.4+,
   Firefox 143+) hide a closed <details>'s content by giving that pseudo `content-visibility:
   hidden`, which also size-contains it — so `display: block` on the children alone leaves them
   unpainted inside a 0px-tall <details>. That is exactly what shipped 2026-09-24 once the `open`
   attribute came off: no sidebar and no TOC on ANY doc page at desktop width. The `display: block`
   rule stays for engines that predate `::details-content` (their closed slot is `display: none`,
   which a child rule can still override there).

   Sticky lives on the <details> (the grid item), not the <aside> inside it: a sticky box only moves
   within its containing block, and the <details> is exactly as tall as its <aside>, so a sticky
   <aside> never had room to stick. */
@media (min-width: 901px) {
  .docs-sidebar-toggle::details-content,
  .docs-toc-toggle::details-content {
    content-visibility: visible;
    display: block;
  }
  .docs-sidebar-toggle > summary,
  .docs-toc-toggle > summary {
    display: none;
  }
  .docs-sidebar-toggle > *:not(summary),
  .docs-toc-toggle > *:not(summary) {
    display: block;
  }
  .docs-sidebar-toggle,
  .docs-toc-toggle {
    position: sticky;
    top: 88px;
  }
}

.docs-sidebar,
.docs-toc {
  max-height: calc(100vh - 112px);
  overflow-y: auto;
}

/* ---- sidebar (docs-section: groups + pages, server-resolved) ---- */

.docs-nav .menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.docs-nav .menu-list.depth-1 {
  margin: 4px 0 8px;
  padding-left: 14px;
  border-left: 1px solid var(--border);
}

.docs-nav .menu-item {
  margin: 0;
}

.docs-nav .menu-item > a,
.docs-nav .menu-item > .menu-item-label {
  display: block;
  padding: 6px 10px;
  border-radius: 6px;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.45;
  text-decoration: none;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.docs-nav .menu-item > a:hover {
  color: var(--fg);
  background: var(--surface-2);
}

/* A top-level item with children is a section heading (group), not a destination. */
.docs-nav .menu-item.depth-0.has-children > a,
.docs-nav .menu-item.depth-0.has-children > .menu-item-label {
  margin-top: 18px;
  color: var(--fg);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.docs-nav .menu-item.depth-0:first-child.has-children > a,
.docs-nav .menu-item.depth-0:first-child.has-children > .menu-item-label {
  margin-top: 0;
}

/* is-current = this exact item. is-active = this item or a descendant, which is what keeps the
   surrounding group's header visually distinct while you read one of its pages. */
.docs-nav .menu-item.is-current > a,
.docs-nav .menu-item > a[aria-current="page"] {
  color: var(--accent-text);
  background: color-mix(in oklch, var(--accent) 12%, transparent);
  font-weight: 500;
}

.docs-nav .menu-item.is-active:not(.is-current) > a {
  color: var(--fg);
}

/* An unavailable branch renders as inert text so its children survive — dim it so it does not read
   as a link that failed to work. */
.docs-nav .menu-item > .menu-item-label {
  color: var(--muted);
  opacity: 0.7;
  cursor: default;
}

.docs-nav .menu-item-desc {
  display: block;
  color: var(--muted);
  font-size: 12px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

.docs-nav .menu-item-icon {
  display: inline-block;
  width: 1em;
  margin-right: 6px;
  vertical-align: -0.1em;
}

/* A section heading is a label, not a destination, so it takes the accent colour on its own rather
   than the filled pill a real current page gets — the pill on an uppercase heading read as a
   selected row and fought the content for attention. */
.docs-nav .menu-item.depth-0.has-children.is-current > a,
.docs-nav .menu-item.depth-0.has-children > a[aria-current="page"] {
  background: transparent;
  color: var(--accent-text);
}

/* ---- "On this page" (docs-toc.js: built client-side from this page's own headings) ---- */

.docs-toc-nav::before {
  content: "On this page";
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg);
  padding: 0 10px 10px;
}

.toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
}

.toc-list.depth-1 {
  margin: 2px 0 4px;
  padding-left: 12px;
}

.toc-item {
  margin: 0;
}

.toc-item > a {
  display: block;
  padding: 5px 10px;
  border-radius: 6px;
  color: var(--muted);
  line-height: 1.4;
  text-decoration: none;
  border-left: 2px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.toc-item > a:hover {
  color: var(--fg);
}

.toc-item.is-current > a,
.toc-item > a[aria-current="location"] {
  color: var(--accent-text);
  border-left-color: var(--accent-text);
  font-weight: 500;
}

/* ---- content + prev/next pager ---- */

.docs-content {
  grid-area: content;
  min-width: 0;
}

/* The docs article reuses `.post-detail` for its typography, which also carries
   `padding: 72px 0 96px` — correct for a full-width article page, wrong here, because
   `.docs-layout` already supplies the top spacing. The two stacked, so the article started 72px
   below the sidebar's first item instead of level with it. Only the top is zeroed; the 96px
   bottom is still wanted.

   `margin: 0` for the same reason: `.post-detail` also carries `margin: 0 auto` to center a
   full-width article page. On a grid item, auto inline margins opt the item OUT of the grid's
   default stretch sizing — the browser sizes it to its own content (shrink-to-fit, still capped
   by `.post-detail`'s `max-width: 720px`) and uses the margins to center whatever's left inside
   the track, instead of filling it. A wide element inside the article (a `<pre>` code sample,
   here) has no track width to shrink against once that happens, so `overflow-x: auto` on the
   `pre` never engages and the article renders at its content's full intrinsic width — well past
   the sidebar and off the right edge of the viewport on narrow screens. Zeroing the margin here
   restores plain stretch: the article fills the track (still never wider than 720px), so the
   reading column lines up flush with the sidebar's edge and a wide `pre` scrolls inside its own
   box instead of blowing out the page. */
.docs-layout .docs-content {
  padding-top: 0;
  margin: 0;
}

.docs-content h2[id],
.docs-content h3[id] {
  scroll-margin-top: 96px;
}

/* `docs-prev-next` resolves 0-2 items — empty on the Docs landing page, or a page with no
   neighbour on one side. An empty pager bar (just the top border, nothing in it) reads as a
   layout bug, so hide it outright rather than show a bar with nothing in it. */
.docs-pager {
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}

.docs-pager:has(.menu-list:empty) {
  display: none;
}

.docs-pager .menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 16px;
}

.docs-pager .menu-item {
  flex: 1 1 0;
  min-width: 0;
}

/* `flex-direction: column-reverse` puts the eyebrow (`menu-item-desc`, "Previous"/"Next") visually
   ABOVE the page title even though the shared renderer always emits label-then-description in DOM
   order (menuItemInnerHtml, static-render.ts) — reordering visually here avoids a renderer change
   for one caller's own layout preference. */
.docs-pager .menu-item a {
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
  padding: 14px 16px;
  border-radius: 10px;
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--fg);
  font-size: 14px;
  font-weight: 500;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

.docs-pager .menu-item a:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.docs-pager .menu-item-desc {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.docs-pager-next {
  text-align: right;
}

.docs-pager-next a {
  align-items: flex-end;
}

/* ---- mobile: both nav aids collapse to a disclosure above the title ---- */

@media (max-width: 900px) {
  .docs-layout {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "sidebar" "toc" "content";
    gap: 20px;
    padding-top: 32px;
  }

  .docs-mobile-toggle {
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 0 16px;
  }

  .docs-mobile-summary {
    padding: 14px 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--fg);
    cursor: pointer;
    list-style: none;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .docs-mobile-summary::-webkit-details-marker { display: none; }
  .docs-mobile-summary::after { content: '+'; font-size: 18px; color: var(--muted); }
  .docs-mobile-toggle[open] .docs-mobile-summary::after { content: '\2212'; }
  .docs-mobile-toggle[open] .docs-mobile-summary { padding-bottom: 6px; }

  .docs-sidebar,
  .docs-toc {
    position: static;
    max-height: none;
    padding-bottom: 16px;
  }

  .docs-toc-nav::before { padding-top: 0; }

  /* "the disclosure only shows the current group" (docs IA proposal) — the full tree is one tap
     away in the site's own nav drawer already (fe79cabce), so repeating it here is redundant. */
  .docs-sidebar-toggle .menu-item.depth-0.has-children:not(.is-active) {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   Header nav, tree variant (dropdown)

   nav.html opts its marker into {"variant":"tree"} because the flat renderer cannot emit child
   items at all. That swaps the nav's direct <a> children for a single <ul class="menu-list
   depth-0">, which would otherwise collapse `.main-nav`'s flex row to one item — see
   static-render.ts::renderMenuTree's own doc, which names this exact failure as the reason flat
   stays the default. The first rule below is what pays that cost back: the flex row moves off
   `.main-nav` and onto the <ul> the engine emits.

   Hover AND focus-within both open the submenu, so the dropdown is reachable by keyboard and not
   only by pointer. A depth-1 group can itself have children (e.g. Docs > Build > Pages): those
   cascade as a depth-2 flyout to the right of the group, since a second panel can't also drop
   down without overlapping the first. None of this shows when the header is compact: there the
   whole bar collapses into the hamburger and the `.mnav` drawer below renders the same tree as
   accordions instead of floating panels a touch device cannot hover.
   --------------------------------------------------------------------------- */

.main-nav > .menu-list {
  display: flex;
  align-items: center;
  gap: 28px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.main-nav .menu-item { position: relative; }
/* Top-level labels never wrap or shrink, so a bar with too many items overflows measurably and
   main.js's `desktopNavFits()` can collapse it into the hamburger instead of squashing it. */
.main-nav > .menu-list > .menu-item,
.main-nav > a { flex-shrink: 0; white-space: nowrap; }

.main-nav .menu-item > a,
.main-nav .menu-item > .menu-item-label {
  display: block;
  font-size: 14px;
  color: var(--muted);
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

.main-nav .menu-item > a:hover,
.main-nav .menu-item > a[aria-current="page"],
.main-nav .menu-item.is-active > a { color: var(--fg); }

/* Caret on a parent item. Purely decorative, hence a pseudo-element rather than markup: the
   engine's tree renderer owns the <li>'s contents and nothing may be injected into it. */
.main-nav .menu-item.has-children > a::after {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 6px;
  vertical-align: 2px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}

.main-nav .menu-item.has-children:hover > a::after,
.main-nav .menu-item.has-children:focus-within > a::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.main-nav .menu-list.depth-1 {
  position: absolute;
  top: 100%;
  left: -12px;
  min-width: 224px;
  margin: 0;
  padding: 8px;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.28);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}

/* A transparent bridge across the gap between the parent item and the panel, so the submenu does
   not close when the pointer crosses the dead space on its way down. Scoped to depth-0 only: a
   depth-1 group's own children open sideways (see the depth-2 flyout below), where the dead
   space to bridge is horizontal, not vertical. */
.main-nav .menu-item.depth-0.has-children::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 14px;
}

.main-nav .menu-item.has-children:hover > .menu-list.depth-1,
.main-nav .menu-item.has-children:focus-within > .menu-list.depth-1 {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.main-nav .menu-list.depth-1 .menu-item > a {
  padding: 8px 10px;
  border-radius: 8px;
  white-space: nowrap;
}

.main-nav .menu-list.depth-1 .menu-item > a:hover {
  color: var(--fg);
  background: var(--surface-2);
}

/* Depth-2 flyout: a depth-1 group's own children cascade to the RIGHT of the group instead of
   below it (two panels can't both drop down without overlapping). Same panel look as depth-1,
   mirrored onto the horizontal axis — right-pointing caret instead of down, side bridge instead
   of bottom. The caret and bridge selectors below are more specific than depth-0's so they win
   over the shared `.has-children` rules above without needing to touch those rules. */
.main-nav .menu-list.depth-1 .menu-item.has-children > a::after {
  transform: rotate(-45deg);
}

.main-nav .menu-item.depth-1.has-children::after {
  content: '';
  position: absolute;
  top: 0;
  left: 100%;
  bottom: 0;
  width: 14px;
}

.main-nav .menu-list.depth-2 {
  position: absolute;
  top: -8px;
  left: 100%;
  min-width: 224px;
  margin: 0;
  padding: 8px;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.28);
  opacity: 0;
  visibility: hidden;
  transform: translateX(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}

.main-nav .menu-item.has-children:hover > .menu-list.depth-2,
.main-nav .menu-item.has-children:focus-within > .menu-list.depth-2 {
  opacity: 1;
  visibility: visible;
  transform: translateX(0);
}

.main-nav .menu-list.depth-2 .menu-item > a {
  padding: 8px 10px;
  border-radius: 8px;
  white-space: nowrap;
}

.main-nav .menu-list.depth-2 .menu-item > a:hover {
  color: var(--fg);
  background: var(--surface-2);
}


/* ---------------------------------------------------------------------------
   Compact header + mobile menu drawer (nav.html's <dialog class="mnav">)

   Compact = <=900px, or `.nav-compact`, which main.js sets whenever the desktop row does not fit
   (so more menu items collapse the bar at any width instead of overflowing it). The two cases
   can't share one selector (a media query is not a class), hence the small duplicated block.
   The drawer's list is built by main.js from the rendered `.main-nav`; nesting is styled
   recursively (`.mnav-list .mnav-list`), never per depth, so any menu depth renders.
   --------------------------------------------------------------------------- */

/* Theme toggle stays visible in the top bar at every width, including compact/mobile — the
   owner wants it reachable without opening the drawer. Only `.main-nav` (the link row) and
   the desktop-only absence of the hamburger collapse here; `.nav-actions .theme-toggle` is
   deliberately NOT touched by either compact rule below. The drawer keeps its own copy
   (`.mnav-foot .theme-toggle`) for when the drawer itself is open, but the two are never
   visible at once: the drawer is a modal `<dialog>` whose native `::backdrop` covers the
   full viewport, so opening it obscures the top-bar control behind it. */
@media (max-width: 900px) {
  .main-nav { display: none; }
  .nav-actions .nav-toggle { display: inline-flex; }
}
.site-header.nav-compact .main-nav { display: none; }
.site-header.nav-compact .nav-actions .nav-toggle { display: inline-flex; }
.nav-actions .nav-toggle { width: 38px; padding: 0; justify-content: center; }

/* The page behind the open drawer must not scroll. */
html.mnav-open, html.mnav-open body { overflow: hidden; }

.mnav {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(400px, 100vw);
  max-width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  box-shadow: -24px 0 60px rgba(0, 0, 0, 0.3);
}
.mnav::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.mnav[open] { animation: mnav-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); }
@keyframes mnav-in { from { transform: translateX(24px); opacity: 0; } }

.mnav-panel { display: flex; flex-direction: column; height: 100%; }
.mnav-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-h);
  box-sizing: content-box;
  padding: env(safe-area-inset-top, 0px) calc(12px + env(safe-area-inset-right, 0px)) 0 24px;
  border-bottom: 1px solid var(--border);
}
.mnav-title {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.mnav-close {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--fg);
  cursor: pointer;
}
.mnav-close:hover { background: var(--surface-2); }

/* The one scrolling region. `contain` stops a fling at either end from scrolling the page. */
.mnav-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 12px calc(12px + env(safe-area-inset-right, 0px)) 24px 12px;
}
.mnav-list { list-style: none; margin: 0; padding: 0; }
.mnav-list .mnav-list {
  margin: 2px 0 6px 20px;
  padding-left: 8px;
  border-left: 1px solid var(--border);
}
.mnav-list[hidden] { display: none; }
.mnav-row { display: flex; align-items: center; gap: 4px; }

.mnav-link,
.mnav-disclose-label {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 44px;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 16px;
  font-weight: 500;
  color: var(--fg);
  text-align: left;
}
.mnav-list .mnav-list .mnav-link,
.mnav-list .mnav-list .mnav-disclose-label {
  font-size: 15px;
  font-weight: 400;
  color: var(--muted);
}
.mnav-link:hover { background: var(--surface-2); color: var(--fg); }
.mnav-inert { color: var(--muted); }
.mnav-link[aria-current="page"],
.mnav-list .mnav-list .mnav-link[aria-current="page"] {
  color: var(--accent-text);
  font-weight: 600;
  background: color-mix(in oklch, var(--accent) 12%, transparent);
}
.mnav-link .menu-item-desc {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  font-weight: 400;
  color: var(--muted);
}

.mnav-disclose {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  font: inherit;
}
.mnav-disclose:hover { background: var(--surface-2); color: var(--fg); }
.mnav-disclose.mnav-disclose-label {
  width: auto;
  flex-direction: row;
  justify-content: space-between;
}
.mnav-disclose svg { width: 18px; height: 18px; flex: none; transition: transform 0.18s ease; }
.mnav-disclose[aria-expanded="true"] svg { transform: rotate(180deg); }

.mnav-link:focus-visible,
.mnav-disclose:focus-visible,
.mnav-close:focus-visible,
.mnav .theme-toggle:focus-visible,
.mnav .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.mnav-foot {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px calc(24px + env(safe-area-inset-right, 0px)) calc(16px + env(safe-area-inset-bottom, 0px)) 24px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
.mnav-theme {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  color: var(--muted);
}
.mnav-theme .theme-toggle { margin-right: 0; width: 40px; height: 40px; }

@media (prefers-reduced-motion: reduce) {
  .mnav[open] { animation: none; }
  .mnav-disclose svg { transition: none; }
}
