/* RXXR manual page.
   Tokens come from tokens.css (shared with the builder).
   This file is standalone — style.css is NOT loaded here. */

* { box-sizing: border-box; }

/* Outlines on this page run dimmer than the builder's: long-form reading, so
   the frames should stay behind the type instead of drawing the eye. */
:root {
  --line: #1a1a1c;
  --line-hot: #3d3d42;

  /* Type scale. Seven steps, and every piece of text on the page picks one —
     no per-selector pixel values, so a step can be retuned in one place.
     Kept local to the manual: tokens.css is shared with the builder, and the
     builder's own type lives on a different rhythm. */
  --fs-1: 9px;    /* eyebrows / overlines, always tracked */
  --fs-2: 10px;   /* labels, meta, values, nav */
  --fs-3: 11px;   /* captions, notes, small UI text */
  --fs-4: 13px;   /* prose */
  --fs-5: 15px;   /* parameter names, card titles */
  --fs-6: 19px;   /* section head */
  --fs-7: 26px;   /* page title */
  --fs-display: 38px; /* the number on an empty card — not part of the text scale */

  /* Leading. Long-form prose gets the loose one; anything that reads as a
     label or a caption sits tighter. */
  --lh-tight: 1.45;
  --lh-normal: 1.6;
  --lh-prose: 1.75;

  /* A hovered tile lifts off the page and pushes the rest of the grid back.
     Both numbers read off the page's own ground, so they move with the theme. */
  --tile-shadow: rgba(0, 0, 0, 0.7);
  --tile-dim: 0.5;

  /* How far a label is allowed to sink toward the page ground. A fade costs
     more contrast on a light page than on a dark one, so the light theme
     holds its labels higher rather than the rules carrying two numbers. */
  --fade: 0.6;

  /* Ink and hairline for the few chips that sit ON a media surface rather than
     on the page — the before/after tags. --media-bg stays dark in both themes,
     so anything laid over it has to stay light in both: these two are defined
     once here and are NOT repeated in the light block. They hold the dark
     theme's own --text and --line, so nothing shifts there. */
  --on-media: #f2f2ef;
  --on-media-line: #1a1a1c;
}

/* The light branch. Stamped on <html> by the switch in the sidebar — never by
   the system setting: the page is dark by design and a reader who wants it
   light says so once, after which it is remembered.

   Only tokens live here. Every rule below is written against them, so the two
   themes share one stylesheet and a retune stays in this block.

   The page ground is the lightest thing on it and every plate sits a step
   BELOW it, the mirror of the dark theme where a plate sits a step above the
   ground. Running it the other way — an off-white page with white cards —
   left the cards reading as holes and the page as the plate.

   Two roles deliberately do NOT invert. The accent splits by job rather than
   by shade: lime is unreadable as type on white at any darkness worth calling
   lime, so type, rails and links hand over to the violet (--accent) while lime
   keeps every surface it fills (--accent-fill, dark ink over it). And
   --media-bg stays dark — the videos and screenshots are captures of a dark
   plugin UI, and a white letterbox around them reads as a rendering fault. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #ffffff;
  --ink: #f7f7f4;     /* the plate one step off the page, as --ink is in the dark */
  --panel: #f7f7f4;
  --panel-2: #eeeee9;
  --line: #dededa;
  --line-hot: #a9a9a3;

  /* The ramp is pitched against --panel-2, the darkest plate on the page, not
     against the white ground: nearly all of this text is 9-13px mono, and a
     value that only clears AA on white fails the moment the same label lands
     on a card. */
  --text: #16161a;
  --muted: #5c5c63;
  --label: #35353c;
  --pin: #16161a;

  --focus: #3a3a3e;
  --accent-focus: var(--focus);
  /* Type and rails leave the lime family entirely here. Even the darkest lime
     that still reads as lime tops out around 6.9:1 on white and 5.6:1 on a
     plate; the violet is the brand's other colour and lands on 7.7 / 6.3
     without going grey. The lime stays where it is a plate, not type. */
  --accent: var(--violet);     /* type, rails, links, active states */
  --accent-fill: var(--lime);  /* the plate keeps the lime it has */
  --on-accent: #0a0a0a;
  --accent-subtle: rgba(71,0,255,0.10);
  --accent-line: rgba(71,0,255,0.30);

  --selected-bg: rgba(0,0,0,0.07);
  --lift: rgba(0,0,0,0.06);
  --lift-soft: rgba(0,0,0,0.04);
  --hairline: rgba(0,0,0,0.12);

  --media-bg: #0f0f10;
  /* the menu and the page are the same tone in the dark, where a hairline
     separates them. On a light ground it needs to actually float. */
  --menu-bg: var(--panel);
  --menu-shadow: rgba(0, 0, 0, 0.10);

  --tile-shadow: rgba(0, 0, 0, 0.22);
  --tile-dim: 0.82;
  --fade: 0.86;
}

html {
  color-scheme: dark;
  scrollbar-width: thin;
  scrollbar-color: var(--line) var(--panel);
  scroll-behavior: smooth;
}

body.manual-page {
  margin: 0;
  min-height: 100%;
  font-family: "Roboto Mono", ui-monospace, monospace;
  font-size: var(--fs-4);
  line-height: var(--lh-prose);
  /* Flat, unlike the builder's diagonal texture — this page is long-form
     reading and the texture only added noise behind the type. */
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* ============================ view transitions ============================ */
/* Enter-only. The outgoing view is replaced instantly; the incoming one slides
   in along the chapter order — forward when going deeper, back when returning. */

/* One duration for the page change, enter-only path and view transition alike.
   The crossfade carries two layers where the enter-only one carried one, and
   260ms reads clipped with the outgoing page still on screen. */
:root { --dur-page: 300ms; }

@keyframes m-view-in {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes m-view-fwd {
  from { opacity: 0; transform: translate3d(18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes m-view-back {
  from { opacity: 0; transform: translate3d(-18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes m-stagger-in {
  from { opacity: 0; transform: translate3d(0, 12px, 0); }
  to   { opacity: 1; transform: none; }
}

/* Swapping a whole view out from under the viewport is exactly the case
   scroll anchoring tries to "help" with, and it fights our own positioning. */
#manualRoot, .m-body { overflow-anchor: none; }

.m-view { animation: m-view-in var(--dur-page) var(--ease-out) both; }
.m-view--fwd { animation-name: m-view-fwd; }
.m-view--back { animation-name: m-view-back; }

/* cards, list rows and sections cascade in behind the view itself */
.m-stagger {
  animation: m-stagger-in var(--dur-page) var(--ease-out) both;
  animation-delay: var(--stagger, 0ms);
}

/* The sidebar exists only inside a chapter, so it re-enters from display:none
   and its animation replays. Opacity only — a transform would fight `sticky`. */
@keyframes m-fade-in { from { opacity: 0; } to { opacity: 1; } }
.m-toc { animation: m-fade-in var(--dur-page) var(--ease-out) both; }

/* Sections of the open chapter arrive one at a time, as the reader gets to
   them — the whole block moves as one piece, nothing inside it animates on its
   own. .m-reveal is added per section by JS, and only to the ones that start
   below the fold: the first screenful is already carried in by the page
   entrance, and a second move on top of that is the page arriving twice.
   Without scripting, or with reduced motion, nothing is marked and the page
   stays fully visible. */
.m-section.m-reveal {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  transition:
    opacity 460ms var(--ease-out) var(--reveal-delay, 0ms),
    transform 460ms var(--ease-out) var(--reveal-delay, 0ms);
}
.m-section.m-reveal.is-in { opacity: 1; transform: none; }


@media (prefers-reduced-motion: reduce) {
  .m-view, .m-stagger, .m-toc { animation: none; }
  .m-section.m-reveal { opacity: 1; transform: none; }
}

/* ---------------------- native view transitions ----------------------
   Where startViewTransition() exists, JS routes through it and the rules above
   are never applied (`.m-view` is not added) — the crossfade below IS the
   entrance, and the outgoing page is finally in the frame instead of being cut.
   Everything here is scoped to the pseudo tree, so a browser without the API
   simply ignores it and keeps the enter-only path.

   Both halves run `mix-blend-mode: normal`. The UA default is plus-lighter,
   which is built for a straight opacity crossfade of identical geometry; with
   the offsets below it lightens the page ground for the length of the move. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(root),
  ::view-transition-group(m-top),
  ::view-transition-group(m-toc) {
    animation-duration: var(--dur-page);
    animation-timing-function: var(--ease-out);
  }
  ::view-transition-old(root),
  ::view-transition-new(root) {
    mix-blend-mode: normal;
    animation-duration: var(--dur-page);
    animation-timing-function: var(--ease-out);
    animation-fill-mode: both;
  }

  /* No direction — a language switch redraws the chapter in place. */
  ::view-transition-old(root) { animation-name: m-vt-out; }
  ::view-transition-new(root) { animation-name: m-vt-in; }

  :root[data-nav="fwd"]::view-transition-old(root) { animation-name: m-vt-out-fwd; }
  :root[data-nav="fwd"]::view-transition-new(root) { animation-name: m-vt-in-fwd; }
  :root[data-nav="back"]::view-transition-old(root) { animation-name: m-vt-out-back; }
  :root[data-nav="back"]::view-transition-new(root) { animation-name: m-vt-in-back; }

  /* The bar and the sidebar are lifted out of the page snapshot: left in it
     they would crossfade and drift with the content under them. The sidebar
     only exists inside a chapter, so on the way to home it simply leaves. */
  .m-top { view-transition-name: m-top; }
  .m-toc { view-transition-name: m-toc; }

  /* Where the API exists, the pseudo below owns every appearance of the
     sidebar, so the element's own fade is off for good. Toggling it per
     transition instead would restart it the moment the toggle lifts — which is
     a blink at the end of every page change, not a fade at the start of one. */
  @supports (view-transition-name: none) {
    .m-toc { animation: none; }
  }

  /* Its list is rebuilt on every route and its height changes with the chapter,
     so a crossfade here is a second fade competing with the page's own, over a
     snapshot the group is busy rescaling. It swaps in place instead; only
     arriving in a chapter and leaving for home are worth a fade, and those are
     the two cases where one half of the pair is missing. */
  ::view-transition-group(m-toc) { animation: none; }
  ::view-transition-old(m-toc) { animation: none; opacity: 0; }
  ::view-transition-new(m-toc) { animation: none; }
  ::view-transition-new(m-toc):only-child {
    animation: m-fade-in var(--dur-page) var(--ease-out) both;
  }
  ::view-transition-old(m-toc):only-child {
    animation: m-vt-out var(--dur-page) var(--ease-out) both;
  }
}

/* The outgoing half leaves early: it is under the incoming one, and holding it
   at full opacity for the whole move only muddies the type. */
@keyframes m-vt-out {
  from { opacity: 1; }
  60%  { opacity: 0; }
  to   { opacity: 0; }
}
@keyframes m-vt-in {
  from { opacity: 0; transform: translate3d(0, 6px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes m-vt-out-fwd {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(-12px, 0, 0); }
}
@keyframes m-vt-in-fwd {
  from { opacity: 0; transform: translate3d(18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes m-vt-out-back {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(12px, 0, 0); }
}
@keyframes m-vt-in-back {
  from { opacity: 0; transform: translate3d(-18px, 0, 0); }
  to   { opacity: 1; transform: none; }
}

::selection { background: var(--accent-subtle); color: var(--text); }

a { color: inherit; }

/* ============================ top bar ============================ */

.m-top {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--header-h);
  padding: 0 var(--sp-3);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.m-brand { display: inline-flex; align-items: center; flex: 0 0 auto; }
.m-brand-logo { display: block; height: 20px; width: auto; }
/* The wordmark's lime is baked into logo.svg and it is loaded as an <img>, so
   no token reaches it. brightness(0) crushes every channel to zero, which
   lands the mark on flat black — the wordmark reads as type here, and on a
   white page that is the same black the headings carry. */
:root[data-theme="light"] .m-brand-logo { filter: brightness(0); }

.m-top-title {
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
  padding-left: var(--sp-1);
  border-left: 1px solid var(--line);
  flex: 0 0 auto;
}

/* search — a glyph until it is asked for. At rest it is a 32px square with the
   magnifier in it; focusing the input (the whole square is the input) opens it
   to full width, and it closes again on blur unless something was typed. */
.m-search {
  position: relative;
  display: flex;
  align-items: center;
  margin-left: auto;
  width: 32px;
  /* Shrinkable, so a long locale can never push the open field into the
     language selector: every other item in the bar is `0 0 auto`, and the
     field is the one that can give ground. */
  flex: 0 1 auto;
  min-width: 32px;
  transition: width var(--dur-menu) var(--ease-out);
}
.m-search.is-open { width: min(280px, 40vw); }
.m-search-icon {
  position: absolute;
  left: 9px;
  color: var(--muted);
  pointer-events: none;
}
.m-search-input {
  width: 100%;
  /* A flex item will not shrink past its own content-based minimum, and for an
     input that floor is its intrinsic ~20-character width. Without this the
     field ignores the folded 32px box and spills over the language selector. */
  min-width: 0;
  height: 32px;
  padding: 0 28px 0 28px;
  font: inherit;
  font-size: var(--fs-3);
  letter-spacing: 0.5px;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: border-color var(--dur-ui) var(--ease-out);
  -webkit-appearance: none;
  appearance: none;
}
.m-search-input::placeholder { color: var(--muted); }
/* Closed, the box is narrower than its own padding: nothing of the field
   shows, and the placeholder must not flicker through on the way out. */
.m-search:not(.is-open) .m-search-input {
  cursor: pointer;
  text-overflow: clip;
  /* `box-sizing: border-box` cannot take an element below its own padding and
     border, so with 28px on both sides the folded field bottomed out at 58px
     and its right edge ran 14px into the language selector — over the pill,
     and stealing its clicks. Closed, the field is only the magnifier, so the
     trailing padding (which the clear button sits in) comes off. */
  padding-right: 0;
}
.m-search:not(.is-open) .m-search-input::placeholder { color: transparent; }
.m-search-input::-webkit-search-cancel-button { display: none; }
.m-search-input:hover { border-color: var(--line-hot); }
.m-search-input:focus { outline: none; border-color: var(--accent-focus); }
.m-search-clear {
  position: absolute;
  right: 4px;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 15px; /* the × glyph, not text — stays off the type scale */
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
/* Same trap as .m-banner: the `display` here outranks the UA's [hidden] rule,
   so the × sat over the field on every load. Invisible while the input was
   full width, obvious once the box folded down onto the magnifier. */
.m-search-clear[hidden] { display: none; }
.m-search-clear:hover { color: var(--text); }

/* language selector — mirrors .lang-select in style.css */
.lang-select {
  align-self: center;
  height: 32px;
  padding: 0 26px 0 10px;
  font: inherit;
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--muted);
  background-color: transparent;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%237d7d80' stroke-width='2.5' 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 9px center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  flex: 0 0 auto;
  -webkit-appearance: none;
  appearance: none;
  transition: color var(--dur-ui) var(--ease-out), border-color var(--dur-ui) var(--ease-out);
}
.lang-select:hover { color: var(--text); border-color: var(--line-hot); }
.lang-select:focus { outline: none; border-color: var(--accent-focus); color: var(--text); }
.lang-select option { background: var(--bg); color: var(--text); }
/* Once dropdown.js runs, the native select is hidden and .lang-dropdown (styled
   in dropdown.css) stands in its place. The header owns the spacing here, so
   the builder's own trailing margin comes off. */
.m-top .lang-dropdown { margin-right: 0; }

/* Collapses the chapter sidebar. Lives in the top bar rather than in the
   sidebar itself: once the sidebar is gone, a control inside it would go with
   it. Narrow screens have their own drawer toggle, and the home screen has no
   sidebar at all, so it is hidden in both. */
.m-toc-collapse {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--text);
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  flex: 0 0 auto;
  transition: color var(--dur-ui) var(--ease-out), border-color var(--dur-ui) var(--ease-out);
}
.m-toc-collapse svg { display: block; }
.m-toc-collapse:hover { border-color: var(--line-hot); }
.m-toc-collapse:focus-visible { outline: none; border-color: var(--accent-focus); }
/* collapsed: the glyph goes quiet, the way an off switch reads */
.m-toc-collapse[aria-expanded="false"] { color: var(--muted); }

.m-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--muted);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  flex: 0 0 auto;
  transition: color var(--dur-ui) var(--ease-out), border-color var(--dur-ui) var(--ease-out);
}
.m-back:hover { color: var(--text); border-color: var(--line-hot); }
.m-back svg { flex: 0 0 auto; display: block; }

/* ============================ shell ============================ */

/* Full-bleed: the sidebar is pinned to the left edge of the viewport, not
   floated inside a centred column. Reading width is capped further down,
   on the content blocks themselves. */
.m-shell {
  --toc-w: 264px; /* also the offset the reading column's break-out budgets for */
  display: grid;
  grid-template-columns: var(--toc-w) minmax(0, 1fr);
  gap: 0;
  margin: 0;
  padding: 0;
  align-items: start;
}

/* ---- table of contents ---- */

.m-toc-toggle { display: none; }

/* Always exactly the height of the viewport below the header, so the divider
   runs edge to edge instead of stopping where the list happens to end. */
.m-toc {
  position: sticky;
  top: var(--header-h);
  /* Above a hovered tile (z-index 3), below the top bar (30) — a clip that
     grows past the reading column must not cover the chapter list. */
  z-index: 10;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  height: calc(100vh - var(--header-h));
  /* The scrolling moved one level in, so the switches at the bottom stay put
     however long the chapter list gets. */
  overflow: hidden;
  padding: var(--sp-3) var(--sp-2) var(--sp-3) var(--sp-3);
  border-right: 1px solid var(--line);
}
.m-toc-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Quick-start video in the sidebar: a 16:9 plate with a play badge. Disabled
   until the clip exists — the placeholder shows the slot without pretending
   to be a working link. */
.m-toc-video {
  flex: 0 0 auto;
  display: block;
  margin-top: var(--sp-2);
  color: var(--muted);
  text-decoration: none;
}
.m-toc-video-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 9;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  transition: color var(--dur-ui) var(--ease-out), border-color var(--dur-ui) var(--ease-out);
}
.m-toc-video-frame svg { width: 26px; height: 26px; opacity: var(--fade); }
.m-toc-video-label {
  display: block;
  margin-top: 7px;
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
}
.m-toc-video[aria-disabled="true"] { cursor: default; opacity: var(--fade); }
.m-toc-video:not([aria-disabled="true"]):hover { color: var(--accent); }
.m-toc-video:not([aria-disabled="true"]):hover .m-toc-video-frame { border-color: var(--accent); }
.m-toc-video:not([aria-disabled="true"]):hover .m-toc-video-frame svg { opacity: 1; }

/* Reading switches on the floor of the sidebar — out of the scroller, so they
   are in the same place on every chapter. */
.m-toc-tools {
  flex: 0 0 auto;
  display: flex;
  gap: 2px;
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}
.m-toc-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease-out), background-color var(--dur-ui) var(--ease-out);
}
.m-toc-tool svg { display: block; }
.m-toc-tool:hover { color: var(--text); background: var(--lift-soft); }
.m-toc-tool:focus-visible { outline: 1px solid var(--accent-focus); outline-offset: 1px; }
/* pressed means the plain mode is on — the switch is doing something */
.m-toc-tool[aria-pressed="true"] { color: var(--accent); background: var(--accent-subtle); }

/* The theme switch is the odd one out in this row: it is not on or off, so it
   carries no pressed state and shows where a click would take you instead —
   a sun on the dark page, a moon on the light one. It also belongs to a
   different job than the two reading switches beside it — the page, not the
   text — so the auto margin banks it against the far corner of the sidebar
   and the gap does the grouping. */
.m-toc-tool--theme { margin-left: auto; }
/* `.m-toc-tool svg` sets display on every glyph in this row, so the pair has to
   be hidden at the same weight or the two icons stack on top of each other. */
.m-toc-tool .m-theme-sun { display: none; }
:root[data-theme="light"] .m-toc-tool .m-theme-sun { display: block; }
:root[data-theme="light"] .m-toc-tool .m-theme-moon { display: none; }

/* ---- what the switches do ---- */

/* A chip loses its ground and keeps only the monospace; a bold term keeps its
   meaning in the markup but stops standing out. The .m-hint override is beaten
   on specificity, so a chip on the lime plate flattens with the rest. */
body.is-plain-marks code,
body.is-plain-marks .m-code {
  padding: 0;
  color: inherit;
  background: none;
  border-radius: 0;
}
body.is-plain-marks strong { font-weight: inherit; }

/* Links stay links — they still navigate, they just stop shouting. */
body.is-plain-links .m-inline-link {
  color: inherit;
  border-bottom: 0;
}

.m-toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.m-toc-item + .m-toc-item { margin-top: 1px; }

.m-toc-link {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 5px 10px;
  font-size: var(--fs-3);
  letter-spacing: 0.5px;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-ui) var(--ease-out);
}
.m-toc-link:hover { color: var(--text); }
/* Accent type is the whole active state — a filled row plus a stripe was two
   markers doing one job, and the fills tiled the sidebar into blocks. (Lime on
   the dark page, violet on the light one — see the theme block.) */
.m-toc-link.active { color: var(--accent); }

.m-toc-label { flex: 1 1 auto; }

.m-toc-group {
  margin: var(--sp-3) 0 6px;
  padding: 0 10px;
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
  opacity: var(--fade);
}
.m-toc-group:first-child { margin-top: 0; }

.m-toc-link--home {
  margin-bottom: var(--sp-1);
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1.5px;
}

/* sub-sections of the chapter you are currently reading */
.m-toc-sub {
  position: relative;
  list-style: none;
  margin: 2px 0 6px;
  padding: 0 0 0 22px;
}

/* One accent stripe that drives to the active item, instead of every row
   repainting its own border the instant the scroll spy switches. */
.m-toc-rail {
  position: absolute;
  left: 22px; /* the .m-toc-sub padding — the rail rides the sublinks' own line */
  top: 0;
  width: 1px;
  height: var(--rail-h, 0px);
  background: var(--accent);
  opacity: 0;
  transform: translate3d(0, var(--rail-y, 0px), 0);
  transition:
    transform 240ms var(--ease-in-out),
    height 240ms var(--ease-in-out),
    opacity var(--dur-ui) var(--ease-out);
  pointer-events: none;
}
.m-toc-rail.is-on { opacity: 1; }
.m-toc-sublink {
  display: block;
  padding: 3px 10px;
  font-size: var(--fs-2);
  letter-spacing: 0.5px;
  color: var(--muted);
  text-decoration: none;
  border-left: 1px solid var(--line);
  transition: color var(--dur-ui) var(--ease-out), border-color var(--dur-ui) var(--ease-out);
}
.m-toc-sublink:hover { color: var(--text); border-left-color: var(--line-hot); }
.m-toc-sublink.active { color: var(--accent); border-left-color: transparent; }

/* ---- content column ---- */

/* Sidebar hugs the viewport edge, but the text does not: one reading column,
   centred in whatever space is left over. Prose sets the measure and everything
   else lines up on it — headings, media and the slider share the same two edges.
   The three grid blocks are the exception: they break out of the measure
   symmetrically (see --bleed), so a pair of parameter cards sits side by side
   without dragging the prose out to a 140-character line. */
.m-body {
  --measure: 1040px;
  /* Whatever is left beside the measure once the sidebar and the page padding
     are paid for, capped so a break-out block never grows past 1680px. Goes to
     0 on its own when there is no room, which is what narrow screens want. */
  --bleed: clamp(
    0px,
    calc((100vw - var(--toc-w) - var(--measure)) / 2 - var(--sp-3)),
    320px
  );
  min-width: 0;
  width: 100%;
  max-width: calc(var(--measure) + var(--sp-3) * 2);
  margin: 0 auto;
  padding: var(--sp-3) var(--sp-3) 64px var(--sp-3);
}

.m-body > * { max-width: none; }

.m-params,
.m-shots,
.m-gradients { margin-inline: calc(-1 * var(--bleed)); }

.m-banner {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin: 0 0 var(--sp-3);
  padding: 10px 12px;
  font-size: var(--fs-3);
  line-height: var(--lh-normal);
  color: var(--text);
  background: var(--accent-subtle);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius);
}
/* display:flex outranks the UA rule for [hidden] — say it again here */
.m-banner[hidden] { display: none; }
.m-banner svg {
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--accent);
}


.m-empty {
  padding: 40px 0;
  font-size: var(--fs-4);
  color: var(--muted);
  text-align: center;
}

/* ============================ home: chapter cards ============================ */

/* The home screen is the chapter grid at full width — the sidebar and the
   reading column only make sense once you are inside a chapter. */
body.is-home .m-shell {
  grid-template-columns: minmax(0, 1fr);
  max-width: 1600px;
  margin: 0 auto;
  padding: 0 var(--sp-3);
}
body.is-home .m-toc,
body.is-home .m-toc-toggle,
body.is-home .m-toc-collapse { display: none; }

/* --toc-w goes to zero as well as the column: the reading column's break-out
   budget is measured from it, and a collapsed sidebar frees exactly that. */
body.is-toc-collapsed .m-shell {
  --toc-w: 0px;
  grid-template-columns: minmax(0, 1fr);
}
body.is-toc-collapsed .m-toc { display: none; }
body.is-home .m-body { max-width: none; padding-left: 0; padding-right: 0; padding-bottom: 80px; }
body.is-home .m-body > * { max-width: none; }
body.is-home .m-banner { max-width: 720px; }
body.is-home .m-foot { max-width: none; }

.m-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 32px var(--sp-3);
}

.m-card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
}

.m-card-media {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--media-bg);
  border-radius: var(--radius);
  overflow: hidden;
}
.m-card:focus-visible { outline: none; }
.m-card:focus-visible .m-card-media { outline: 1px solid var(--accent-focus); outline-offset: 2px; }
.m-card-media video,
.m-card-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* no clip recorded yet — a deliberate slot, not a broken thumbnail.
   Kept flat: the page background is already a diagonal texture. */
.m-card-media--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
}
.m-card-num {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-display);
  font-weight: 800;
  letter-spacing: 1px;
  color: var(--text);
  opacity: 0.09;
  font-variant-numeric: tabular-nums;
  transition: opacity var(--dur-ui) var(--ease-out);
}
.m-card:hover .m-card-num { opacity: 0.16; }

/* NEW plate on a chapter cover. The accent fill, not the subtle tint the shot
   badge uses: this one sits on artwork and has to hold against any frame. */
.m-card-new {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--accent-fill);
  color: var(--on-accent);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 1px;
  line-height: 1.5;
  pointer-events: none;
}

.m-card-body {
  display: flex;
  flex-direction: column;
  gap: 5px;
  flex: 1 1 auto;
  padding: var(--sp-2) 2px 0;
}
.m-card-title {
  margin: 0;
  font-size: var(--fs-5);
  font-weight: 700;
  letter-spacing: 0.2px;
  color: var(--text);
  transition: color var(--dur-ui) var(--ease-out);
}
.m-card:hover .m-card-title { color: var(--accent); }
.m-card-blurb {
  margin: 0;
  flex: 1 1 auto;
  /* Prose size: this is a sentence about the chapter, and the home screen is
     the one place where it is the only thing to read. */
  font-size: var(--fs-4);
  line-height: var(--lh-normal);
  color: var(--label);
}

/* secondary chapters — no card, just a list */
.m-home-more-label {
  margin: 56px 0 var(--sp-1);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
}
.m-more {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 2px;
}
.m-more-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-1);
  padding: 12px var(--sp-2);
  font-size: var(--fs-3);
  letter-spacing: 0.5px;
  color: var(--label);
  text-decoration: none;
  background: var(--panel);
  border-radius: var(--radius);
  transition: color var(--dur-ui) var(--ease-out), background-color var(--dur-ui) var(--ease-out);
}
.m-more-link:hover { color: var(--accent); background: var(--panel-2); }
.m-more-link svg { flex: 0 0 auto; opacity: 0.5; }

/* ============================ chapter page ============================ */

.m-crumb {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-bottom: var(--sp-3);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-ui) var(--ease-out);
}
.m-crumb:hover { color: var(--accent); }
.m-crumb svg { flex: 0 0 auto; }

.m-page-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-7);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.5px;
}
.m-page > .m-section:first-of-type { border-top: 0; padding-top: var(--sp-2); }

/* prev / next */
.m-pagenav {
  display: flex;
  gap: var(--sp-2);
  margin-top: 48px;
}
.m-pagenav-link {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  flex: 1 1 0;
  min-width: 0;
  padding: var(--sp-2);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: border-color var(--dur-ui) var(--ease-out);
}
.m-pagenav-link:hover { border-color: var(--line-hot); }
.m-pagenav-link svg { flex: 0 0 auto; color: var(--muted); }
.m-pagenav-link--next { margin-left: auto; justify-content: flex-end; text-align: right; }
.m-pagenav-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.m-pagenav-dir {
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
}
.m-pagenav-title {
  font-size: var(--fs-4);
  font-weight: 700;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m-pagenav-link:hover .m-pagenav-title { color: var(--accent); }

/* ============================ search results ============================ */

.m-results-label {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 2px;
  color: var(--muted);
}
.m-results-count {
  margin-left: 8px;
  color: var(--accent);
  letter-spacing: 1px;
}

/* A result is one row: where it lives, what it is called, the line it matched
   on. The chapter itself is one click away, so nothing is reprinted here. */
.m-hits {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--line);
}
.m-hit {
  display: block;
  padding: 14px 10px;
  border-bottom: 1px solid var(--line);
  color: inherit;
  text-decoration: none;
}
.m-hit:hover { background: var(--panel); }
.m-hit:focus-visible { outline: 2px solid var(--accent-focus); outline-offset: -2px; }

.m-hit-crumb {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 1.5px;
  color: var(--muted);
  text-transform: uppercase;
}
.m-hit-chapter { color: var(--accent); }
.m-hit-sep { color: var(--line-hot); opacity: 0.5; }
.m-hit-kind {
  margin-left: auto;
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: 2px;
  font-size: var(--fs-1);
  letter-spacing: 1px;
}

.m-hit-title {
  display: block;
  margin-top: 6px;
  font-size: var(--fs-5);
  color: var(--text);
}
.m-hit:hover .m-hit-title { color: var(--accent); }
.m-hit-snip {
  display: block;
  margin-top: 4px;
  font-size: var(--fs-3);
  line-height: 1.6;
  color: var(--muted);
}
.m-hit-mark {
  background: var(--accent-subtle);
  color: var(--accent);
  border-radius: 2px;
}

/* ---- section ---- */

.m-section {
  padding: 44px 0;
  border-top: 1px solid var(--line);
  scroll-margin-top: calc(var(--header-h) + var(--sp-2));
}
.m-section[hidden] { display: none; }

.m-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-6);
  font-weight: 700;
  letter-spacing: -0.2px;
}

.m-anchor {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px; /* the § glyph, not text — stays off the type scale */
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease-out), color var(--dur-ui) var(--ease-out);
}
.m-section:hover .m-anchor,
.m-anchor:focus-visible { opacity: 1; }
.m-anchor:hover { color: var(--accent); }
.m-anchor.copied { opacity: 1; color: var(--accent); }

.m-lead {
  margin: 0 0 28px;
  font-size: var(--fs-4);
  color: var(--label);
}

/* ---- blocks ---- */

/* Prose runs grey, the way parameter bodies, tips and leads already did —
   white was making every paragraph shout as loudly as the headings. What
   stays white is what the sentence is about: a bold term, a control name in
   `code`. */
.m-text { margin: 0 0 var(--sp-2); color: var(--label); }
.m-text:last-child { margin-bottom: 0; }

/* A heading inside a section — between the 19px section head and the 13px
   prose. The top margin runs far wider than the bottom one: the heading
   belongs to what follows it, not to what it interrupts. */
.m-subhead {
  margin: 32px 0 10px;
  font-size: var(--fs-5);
  font-weight: 700;
  letter-spacing: -0.1px;
  color: var(--text);
}
.m-subhead:first-child { margin-top: 0; }
/* the aside some headings carry — same line, quieter, so the heading stays
   short enough to scan */
.m-subhead-note {
  margin-left: 8px;
  font-size: var(--fs-3);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--muted);
}

.m-list {
  margin: 0 0 var(--sp-2);
  padding-left: 18px;
  color: var(--label);
}
.m-list li { margin-bottom: 6px; }
/* Lime is spent on links, the note glyph and the open chapter — a bullet does
   not need it too. */
.m-list li::marker { color: var(--muted); }

/* The ground alone marks a parameter name. With a frame around it too, a list
   whose every line is two or three of these read as a chain of boxes.

   A translucent lift rather than the plate tone: a chip is quoted inside tips
   and captions as often as on the page itself, and a flat --panel chip would
   disappear into a --panel plate. This way it sits one step above whatever it
   lands on. */
code, .m-code {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.92em;
  padding: 2px 6px;
  color: var(--text);
  background: var(--lift);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* The weight already carries the emphasis. White on top of it made every
   bold term as loud as a heading, and in grey prose it read as a link that
   was not one — links are lime. Chips with a ground of their own keep their
   white: there the colour belongs to the chip, not to the sentence. */
strong { font-weight: 700; }

/* parameter rows */
/* Two columns, never three. Letting the grid fill by a 300px minimum meant a
   wide screen packed in a third one, and a parameter card is a heading, a
   paragraph and a scale — at a third of the width the paragraph broke into
   four ragged lines. */
.m-params {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 40px;
  row-gap: 10px;
  margin: 0 0 40px;
  border-top: 1px solid var(--line);
}
.m-param {
  display: flex;
  flex-direction: column;
  padding: 22px 0 26px;
  border-bottom: 1px solid var(--line);
}
.m-param[hidden] { display: none; }
.m-param-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}
/* control-type glyph in front of the name — slider / checkbox / popup /
   colour. Muted, so it marks the row without competing with the name. */
.m-param-type {
  flex: 0 0 auto;
  align-self: center;
  display: inline-flex;
  color: var(--muted);
}
.m-param-type svg { display: block; }

.m-param-name {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-5);
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.2px;
}
/* Parameter added since 1.1.0: the same plate the example tiles use. */
.m-param-new {
  flex: none;
  align-self: center;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--accent-subtle);
  color: var(--accent);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 1px;
  line-height: 1.5;
}
/* Both live in the head next to the name: what a popup can be set to, and the
   bounds a number lives in. Reference data you look up rather than read, so
   both run a size below the name and never take its weight. They sit on a
   bubble, and the bubble is a fill with no outline: at 11px a hairline reads as
   noise this close to the name, while a plate sets them apart without drawing a
   line. --lift carries the plate through both themes. */
.m-param-values,
.m-param-range {
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  background: var(--lift);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-3);
  line-height: 1.5;
  letter-spacing: 0.5px;
  color: var(--label);
  font-variant-numeric: tabular-nums;
}

.m-inline-link {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
}
.m-inline-link:hover { border-bottom-color: var(--accent); }
/* [`Main > Input`](#input) — the link already says it is a link. The chip
   inside it added a ground of its own and overrode the lime with white. */
.m-inline-link code {
  padding: 0;
  color: inherit;
  background: none;
  border-radius: 0;
}

.m-param-body { margin: 0; color: var(--label); }
.m-param-body code { color: var(--text); }

/* Currently unused: the spec is hidden behind SHOW_PARAM_SPEC in manual.js.
   Kept whole so flipping that flag brings it back unchanged.

   range + default, generated from the plugin source.
   Nothing on this page is set to anything, so there is no fill to draw and no
   reason to spend the builder's 24 ticks on it: one hairline for the range,
   one mark for where the default sits. */
.m-slider {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: 360px;
  margin-top: auto;
  padding-top: 22px;
}

.m-track {
  position: relative;
  width: 100%;
  height: 12px;
}
.m-track::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: var(--line-hot);
  transform: translateY(-0.5px);
}
/* Full height, so the default reads as a mark on the scale rather than a dot
   sitting on the line. Half a pixel of overhang at either end is deliberate —
   a default at min or max belongs on the end of the track. */
.m-track-pin {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--pin);
  transform: translateX(-0.5px);
}

.m-slider-scale {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-1);
  letter-spacing: 0.5px;
  font-variant-numeric: tabular-nums;
}
.m-slider-range { flex: 1; color: var(--muted); }

/* "DEFAULT" — one label for both spec lines, so a number and a mode read the
   same way down the column. */
.m-spec-label {
  font-size: var(--fs-1);
  letter-spacing: 1px;
  color: var(--muted);
}
.m-slider-value {
  font-size: var(--fs-2);
  letter-spacing: 0;
  font-weight: 700;
  color: var(--text);
}

/* non-numeric parameters state their default next to a mock of their own
   control — a checkbox, a popup, a colour swatch — so the kind of control is
   visible before the line is read. Same measure and baseline as the slider
   spec, value flush right, so a column of mixed parameters still lines up. */
.m-default {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 360px;
  min-height: 22px;
  margin-top: auto;
  padding-top: 22px;
  font-family: "JetBrains Mono", ui-monospace, monospace;
}
.m-default .m-spec-label { flex: 1; }
.m-default-value {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-2);
  letter-spacing: 0;
  font-weight: 700;
  color: var(--text);
}
.m-default-value.is-on { color: var(--accent); }
.m-default-value.is-off { color: var(--muted); }

/* checkbox — off is an empty box, on is filled with the accent and ticked,
   the same read as the plugin's own panel. */
.m-check {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--lift);
}
.m-check.is-on {
  border-color: var(--accent-fill);
  background: var(--accent-fill);
  color: var(--on-accent);
}
.m-check svg { display: block; }

/* popup — a caret is what makes a value read as "one of a list". Borrows the
   builder's .dropdown-trigger feel without being interactive. */
.m-select {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 3px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--lift);
}
.m-select-caret {
  font-size: var(--fs-1);
  line-height: 1;
  color: var(--muted);
}

/* colour — the border keeps both #000000 and #FFFFFF readable on the page's
   own dark ground. */
.m-swatch {
  flex: 0 0 auto;
  display: block;
  width: 14px;
  height: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* tip / note callout — a plate a shade off the page, no rail. The block is
   set apart by its ground, and its own heading names it: at eyebrow size the
   mark read as a caption on the paragraph rather than as a title over it. */
.m-tip {
  margin: 0 0 20px;
  padding: 18px 20px;
  border-radius: var(--radius);
  background: var(--panel);
}
.m-tip-mark {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  font-size: var(--fs-4);
  font-weight: 700;
  letter-spacing: 0.3px;
  color: var(--text);
}
.m-tip-mark svg { flex: 0 0 auto; color: var(--accent); }
/* the quiet tone, for limitations that shouldn't spend the accent — with the
   rail gone the glyph is the only thing carrying it */
.m-tip--quiet .m-tip-mark svg { color: var(--muted); }
.m-tip-body {
  margin: 0;
  font-size: var(--fs-4);
  line-height: var(--lh-normal);
  color: var(--label);
}

/* symptom → fix, collapsed until asked for */
.m-faq {
  margin: 0 0 var(--sp-3);
  border-top: 1px solid var(--line);
}
.m-faq-item { border-bottom: 1px solid var(--line); }
.m-faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 14px 2px;
  font-size: var(--fs-4);
  font-weight: 700;
  color: var(--label);
  cursor: pointer;
  list-style: none;
  transition: color var(--dur-ui) var(--ease-out);
}
.m-faq-q::-webkit-details-marker { display: none; }
.m-faq-q:hover { color: var(--text); }
.m-faq-item[open] .m-faq-q { color: var(--accent); }
.m-faq-q svg {
  flex: 0 0 auto;
  opacity: 0.5;
  transition: transform var(--dur-ui) var(--ease-out);
}
.m-faq-item[open] .m-faq-q svg { transform: rotate(180deg); }
.m-faq-body { margin: 0 0 10px; color: var(--label); }
.m-faq-item .m-list { margin-top: 0; padding-bottom: 6px; }

/* gradient presets — swatches built from the plugin's own stop table.
   Cards rather than table rows: the palette is the content here, so the ramp
   leads and the name reads as its caption underneath. */
.m-gradients {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  column-gap: 20px;
  row-gap: 22px;
  margin: 0 0 40px;
}
.m-gradient {
  display: flex;
  flex-direction: column;
  gap: 7px;
  min-width: 0;
}
.m-gradient-name {
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.m-gradient-bar {
  height: 46px;
  border-radius: var(--radius-sm);
}

/* video example */
.m-video {
  margin: 0 0 var(--sp-3);
  padding: 0;
}
.m-video-frame {
  position: relative;
  display: block;
  width: 100%;
}
.m-video-clip {
  display: block;
  width: 100%;
  background: var(--media-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.m-video-clip video {
  display: block;
  width: 100%;
  height: auto;
}
/* Only .m-compare-caption renders today — the video and shots-grid captions
   are hidden behind SHOW_MEDIA_CAPTIONS in manual.js, and this rule is kept
   whole so flipping that flag brings them back unchanged.

   Caption under a media block — video, shots grid or before/after. One rule
   for all three: the compare caption had none of its own and fell through to
   the body colour, which made it the only white line under a picture.
   Centred, because it describes the frame above it rather than continuing the
   paragraph before it. */
.m-video-caption,
.m-shots-caption,
.m-compare-caption {
  margin: 8px 0 var(--sp-3);
  padding: 10px 14px;
  /* Prose size, not caption size: on a plate of its own this line reads as a
     sentence about the figure, and at 11px it sat a step below every other
     sentence on the page. */
  font-size: var(--fs-4);
  color: var(--muted);
  text-align: center;
  background: var(--panel);
  border-radius: var(--radius);
}

/* before / after: both shots stacked, the "after" one clipped at --pos.
   --pos is a bare 0–100 number so the label fades can do maths on it. */
.m-compare {
  --pos: 50;
  /* Matches the parameter table's trailing space — a media block needs more
     air under it than the 16px prose rhythm, or the next paragraph sits on it. */
  margin: 0 0 40px;
  padding: 0;
}
.m-compare-frame {
  position: relative;
  display: block;
  width: 100%;
  background: var(--media-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: ew-resize;
  touch-action: pan-y; /* vertical scrolling still belongs to the page */
  user-select: none;
  -webkit-user-select: none;
}
.m-compare-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
}
.m-compare-after {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 calc(var(--pos) * 1%));
}
.m-compare-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--pos) * 1%);
  width: 1px;
  background: var(--accent-fill);
  pointer-events: none;
}
.m-compare-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 34px;
  height: 34px;
  margin: 0;
  padding: 0;
  color: var(--on-accent);
  background: var(--accent-fill);
  border: none;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  cursor: ew-resize;
  pointer-events: auto;
  transition: transform var(--dur-ui) var(--ease-out);
}
.m-compare-handle svg { display: block; }
.m-compare-frame:hover .m-compare-handle { transform: translate(-50%, -50%) scale(1.08); }
.m-compare-frame.is-dragging .m-compare-handle { transform: translate(-50%, -50%) scale(0.94); }
.m-compare-handle:focus-visible {
  outline: 2px solid var(--accent-fill);
  outline-offset: 3px;
}

/* Side labels sit clear of the divider and fade out once it reaches them. */
.m-compare-tag {
  position: absolute;
  top: 10px;
  padding: 4px 8px;
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--on-media);
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid var(--on-media-line);
  border-radius: var(--radius-sm);
  pointer-events: none;
}
.m-compare-tag--before { left: 10px; opacity: clamp(0, calc((var(--pos) - 14) / 6), 1); }
.m-compare-tag--after { right: 10px; opacity: clamp(0, calc((86 - var(--pos)) / 6), 1); }

@media (prefers-reduced-motion: reduce) {
  .m-compare-handle { transition: none; }
}

/* ===================== hover: zoom + lime caption plate =====================
   Any clip on the page behaves the same under the cursor: the frame grows and
   the caption it already carries is repeated on a lime plate that hangs off
   the frame's bottom edge — never over the picture, which is the thing the
   cursor came to look at. The plate lives outside the clip, so `top: 100%`
   in the frame's own coordinates lands on the *visual* bottom edge of the
   zoomed frame for free; the counter-scale keeps its type at natural size. */
.m-hint {
  position: absolute;
  top: 100%;
  /* Laid out `--zoom` times too wide and then counter-scaled by the same
     number, the plate renders at exactly the zoomed frame's width — the two
     edges have to line up, or the plate reads as a stray tag under the clip.
     The extra width is split either side so the centres stay put. */
  left: calc(50% - 50% * var(--zoom));
  width: calc(100% * var(--zoom));
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 7px 10px;
  background: var(--accent-fill);
  color: var(--on-accent);
  /* Clip and plate are one card, so the corners have to be shared: the clip
     gives up its bottom pair while hovered (below) and the plate carries them.
     Both radii are quoted in the frame's *visual* scale, and the two elements
     get there differently — the clip is drawn at --zoom, so its 6px renders at
     6 * zoom, while the plate is counter-scaled by 1 / --zoom. Landing the
     plate on that same arc therefore takes zoom squared. */
  border-radius: 0 0
    calc(var(--radius) * var(--zoom) * var(--zoom))
    calc(var(--radius) * var(--zoom) * var(--zoom));
  pointer-events: none;
  opacity: 0;
  transform-origin: top center;
  transform: scale(calc(1 / var(--zoom))) translate3d(0, 4px, 0);
  transition: opacity 260ms var(--ease-out),
              transform 260ms var(--ease-out);
}
.m-hint-label {
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
}
.m-hint-note {
  font-size: var(--fs-4);
  line-height: var(--lh-tight);
  opacity: 0.78;
}
/* The plate is lime and its type is ink. A chip carries its own white text on
   a white-tinted ground, both of which vanish here — on this one surface it
   borrows the plate's colour and lifts with a dark wash instead. Same for a
   link, which would otherwise be lime on lime. */
.m-hint code {
  color: inherit;
  background: rgba(0, 0, 0, 0.12);
}
.m-hint .m-inline-link {
  color: inherit;
  border-bottom-color: rgba(0, 0, 0, 0.35);
}
/* The growing frame belongs inside a chapter, where the tiles are the subject
   of the page. The home grid keeps its old, quiet lift. */
.m-shot,
.m-video {
  position: relative;
  --zoom: 1.15; /* one number for the zoom and the plate's counter-scale */
}
/* The frame outranks its own caption — the grown tile must not end up under
   the label that belongs to it. */
.m-shot-frame,
.m-video-frame { z-index: 2; }
.m-shot-cap,
.m-video-caption { position: relative; z-index: 1; }

@media (hover: hover) and (pointer: fine) {
  .m-shot-frame,
  .m-video-frame { transition: transform 260ms var(--ease-out); }
  /* The corners square off on the same curve the plate fades in on, so the
     card closes up as one move instead of snapping open under an empty edge. */
  .m-shot-clip,
  .m-video-clip {
    transition: box-shadow 260ms var(--ease-out),
                border-radius 260ms var(--ease-out);
  }

  /* Driven from the block, not the frame: the caption belongs to the tile,
     so crossing it must not drop the tile back down. */
  .m-shot:hover .m-shot-frame,
  .m-video:hover .m-video-frame {
    transform: scale(var(--zoom));
  }
  .m-shot:hover .m-shot-clip,
  .m-video:hover .m-video-clip {
    box-shadow: 0 28px 70px var(--tile-shadow);
    /* the plate takes over these two — see .m-hint */
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }

  /* The hovered tile sits over everything around it. */
  .m-shot:hover,
  .m-video:hover { z-index: 3; }

  /* The rest of the grid drops out of focus. `:has` keeps the gaps neutral —
     the blur belongs to a hovered tile, not to the grid. */
  .m-shots-grid .m-shot { transition: filter 260ms var(--ease-out); }
  .m-shots-grid:has(.m-shot:hover) .m-shot:not(:hover) {
    filter: blur(4px) brightness(var(--tile-dim));
  }

  .m-shot:hover .m-hint,
  .m-video:hover .m-hint {
    opacity: 1;
    transform: scale(calc(1 / var(--zoom)));
  }

  /* The plate says the same thing as the caption under the tile, and it sits
     right on top of it — one copy at a time, or the tail of the caption reads
     as a second, ragged line under the lime. */
  .m-shot-cap,
  .m-video-caption { transition: opacity 260ms var(--ease-out); }
  .m-shot:hover .m-shot-cap,
  .m-video:hover .m-video-caption { opacity: 0; }
}


/* Reduced motion keeps the frame at rest; the plate still appears under it,
   full tile width, and still clears the picture and the native controls. */
@media (prefers-reduced-motion: reduce) {
  .m-shot,
  .m-video { --zoom: 1; }
  .m-hint { transform: none; }
  .m-shot:hover .m-shot-frame,
  .m-video:hover .m-video-frame { transform: none; }
  .m-shots-grid:has(.m-shot:hover) .m-shot:not(:hover) { filter: none; }
}

/* shot grid: one variant per tile, breaking out past the reading measure.
   Two across is the default because the tiles carry fine texture — a dither
   pattern read at 270px is just noise, at ~670px it is the actual pattern. */
.m-shots {
  --cols: 2;
  margin: 0 0 40px; /* same trailing air as the compare block and the param table */
  padding: 0;
}
.m-shots-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  /* The caption sits under its own tile, so the row gap has to beat the
     caption's own offset by a wide margin — otherwise a label reads as if it
     belonged to the tile below it. */
  column-gap: 10px;
  row-gap: 28px;
}
.m-shot-frame { position: relative; }
.m-shot-clip {
  background: var(--media-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.m-shot-clip img,
.m-shot-clip video,
/* drawn tiles (grid markers) scale exactly like a still */
.m-shot-clip svg {
  display: block;
  width: 100%;
  height: auto;
}
.m-shot-cap {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 6px;
}
.m-shot-labelrow {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 6px;
}
/* The parameter name in front of the value: it repeats the head of the row,
   so it stays quiet and a step below the value it introduces. */
.m-shot-param {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--fs-3);
  font-weight: 700;
  letter-spacing: 0.5px;
  color: var(--muted);
}
/* The value is what the tile is about — read at 10px next to a full-width
   still it was a footnote, so it takes prose size and tabular figures. */
.m-shot-label {
  font-size: var(--fs-4);
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* Thumbnail tiles carry no prefix and have no width to spend on a big value. */
.m-shots--compact .m-shot-label { font-size: var(--fs-2); }
.m-shot-new {
  flex: none;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--accent-subtle);
  color: var(--accent);
  font-size: var(--fs-1);
  font-weight: 700;
  letter-spacing: 1px;
  line-height: 1.5;
}
.m-shot-note {
  /* Prose size, like the blurb under a chapter card: the label above it names
     the tile, this line is a sentence about it. */
  font-size: var(--fs-4);
  line-height: var(--lh-normal);
  color: var(--muted);
}
.m-shots-caption { margin-top: 12px; }

/* the marker gallery: vector tiles with a 2x2 patch of cells in each. They
   stay legible at thumbnail size, so they run as one dense row instead of
   eating a stills-sized grid — and they keep that row on narrow screens. */
.m-shots--compact .m-shots-grid {
  grid-template-columns: repeat(auto-fit, minmax(78px, 1fr));
  column-gap: 8px;
  row-gap: 14px;
}
/* The hover plate is exactly as wide as the tile it hangs off, and these tiles
   run ~78px — prose size would break the note into one word per line. */
.m-shots--compact .m-hint-note { font-size: var(--fs-3); }

/* a grid living inside a parameter row: the row gives it the full table width,
   the figure drops the trailing air a standalone block needs */
.m-param--wide { grid-column: 1 / -1; }
/* A card with examples breaks out to the whole grid width, so its spec has to
   follow: clipped at the 360px column measure the default hung in the middle
   of the card instead of lining up with the edge of the tiles under it. */
.m-param--wide .m-slider,
.m-param--wide .m-default { max-width: none; }
.m-shots--inline { margin: 16px 0 0; }
.m-shots--inline .m-shots-caption,
.m-shots--inline .m-compare-caption { margin-top: 10px; }

/* Once the break-out is gone the pair would be back to postage stamps, so the
   tiles go single file and keep their size instead. */
@media (max-width: 900px) {
  .m-params { grid-template-columns: minmax(0, 1fr); }
  .m-shots-grid { grid-template-columns: minmax(0, 1fr); }
  .m-shots--compact .m-shots-grid {
    grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
  }
}

/* ---- footer ---- */

.m-foot {
  display: flex;
  gap: var(--sp-3);
  margin-top: 48px;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.m-foot-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-2);
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-ui) var(--ease-out);
}
.m-foot-link svg { flex: 0 0 auto; display: block; opacity: var(--fade); }
.m-foot-link:hover { color: var(--accent); }
.m-foot-link:hover svg { opacity: 1; }

/* ============================ responsive ============================ */

@media (max-width: 1000px) {
  .m-shell {
    grid-template-columns: minmax(0, 1fr);
    padding: 0 var(--sp-2);
  }

  .m-toc-collapse { display: none; }

  .m-toc-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    margin-top: var(--sp-2);
    padding: 10px 12px;
    font: inherit;
    font-size: var(--fs-2);
    font-weight: 700;
    letter-spacing: 2px;
    color: var(--muted);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
  }
  .m-toc-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
  .m-toc-toggle svg { transition: transform var(--dur-menu) var(--ease-out); }

  .m-toc {
    position: static;
    height: auto;
    max-height: 50vh;
    padding: var(--sp-2) 0;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .m-toc-video,
  .m-toc-tools { margin-inline: var(--sp-2); }
  .m-toc[hidden] { display: none; }

  .m-body { max-width: none; padding: var(--sp-3) 0 48px; }
  .m-body > * { max-width: none; }
}

@media (max-width: 720px) {
  .m-top {
    height: auto;
    flex-wrap: wrap;
    padding: var(--sp-1) var(--sp-2);
    row-gap: var(--sp-1);
  }
  .m-top-title { order: 2; }
  .lang-select,
  .m-top .lang-dropdown { order: 3; margin-left: auto; }
  .m-back { order: 4; }
  /* No room to spend on a reveal here — the field is full width either way. */
  .m-search,
  .m-search.is-open { order: 5; width: 100%; margin-left: 0; }

  /* The headline steps come down as a set — the scale bends, the selectors
     that use it stay put. */
  :root {
    --fs-6: 17px;
    --fs-7: 21px;
    --fs-display: 32px;
  }

  /* A Windows plug-in path is wider than the column here, and `nowrap` would
     push the page sideways rather than wrap. */
  code, .m-code {
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .m-toc { max-height: 60vh; }
  .m-section { padding: 26px 0; }

  .m-cards { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--sp-2); }
  .m-pagenav { flex-direction: column; }
  .m-pagenav-link--next { margin-left: 0; }
}

/* ---------------- Japanese ----------------
   Roboto Mono has no kana or kanji, so without a named fallback every browser
   picks its own CJK face for the prose. Latin, digits and `code` keep their
   monospace faces; strict line breaking keeps 。、ー and small kana off the start
   of a line. The wide tracking on the small caps labels suits Latin capitals
   but pulls Japanese labels apart, so it is dropped here. */
html[lang="ja"] body.manual-page {
  font-family: "Roboto Mono", "Hiragino Sans", "Hiragino Kaku Gothic ProN",
    "Noto Sans JP", "Yu Gothic UI", "Yu Gothic", Meiryo, ui-monospace, monospace;
  line-break: strict;
}
html[lang="ja"] :is(.m-top-title, .m-back, .m-toc-group, .m-toc-link--home,
  .m-home-more-label, .m-crumb, .m-pagenav-dir, .m-results-label, .m-hit-crumb,
  .m-hit-kind, .m-spec-label, .m-compare-tag, .m-shot-label, .m-toc-toggle,
  .m-foot-link) {
  letter-spacing: 0.05em;
}
