/* A theme is data: one declaration block per family and mode, chosen by the
   `data-theme`/`data-mode` attributes that themes.js always sets. No token is
   ever defined in a media query — the OS preference is one input to the
   resolution in themes.js, not a second source of truth here.
   Adding a skin: one THEMES entry in themes.js, plus a light and a dark block
   below. `node scripts/check_theme_contrast.mjs` gates both.

   Two selectors on the first block, one body: the bare `:root` is what applies
   before JS runs, so it must stand alone — and the default costs no duplicate.

   `--line` is desaturated copper and draws structure only; `--rust` is saturated
   and means data or action only. Keeping them apart is what stops a copper-edged
   panel from competing with the crosshair. */
:root,
:root[data-theme="patina"][data-mode="dark"] {
  --bg:#23272e; --panel:#2c323a; --panel-2:#262b32; --field:#1e232a;
  --ink:#ece5da; --ink-2:#c2b8aa; --muted:#8a8275;
  --line:#5d4132; --rust:#c47c56; --accent:#7cc3b5; --accent-ink:#96d6c8;
  --brass:#c9a86a; --good:#8fc39a; --bad:#d76a4c; --viewer-bg:#101317;
  --radius:6px; --border-w:1px;
  --shadow: 0 1px 2px rgba(0,0,0,.34), 0 8px 20px rgba(0,0,0,.28);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:8px; --pad-x:9px;
  --bg-image:none; --grad-action:var(--rust); --grad-soft:transparent;
  --glow:transparent;
  --on-action:#241209; --on-accent:#0d221e;
  color-scheme: dark;
}

:root[data-theme="patina"][data-mode="light"] {
  --bg:#e9e1d5; --panel:#f7f1e7; --panel-2:#f0e9de; --field:#fffdf8;
  --ink:#2a2620; --ink-2:#574f44; --muted:#7d7466;
  --line:#c9ad91; --rust:#a85a34; --accent:#2b7a6d; --accent-ink:#246d61;
  --brass:#96762f; --good:#4b7d52; --bad:#c0533a; --viewer-bg:#141a1e;
  --radius:6px; --border-w:1px;
  --shadow: 0 1px 2px rgba(70,52,30,.07), 0 8px 20px rgba(70,52,30,.11);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:8px; --pad-x:9px;
  --bg-image:none; --grad-action:var(--rust); --grad-soft:transparent;
  --glow:transparent;
  --on-action:#fff8f1; --on-accent:#ffffff;
  color-scheme: light;
}

/* Oxide: verdigris is the material, copper the fittings. The ground oxidizes
   downward — see texture-oxide.svg — and the panels are translucent so the
   weathering reads through the UI instead of stopping at every card edge.
   `--bg` stays a solid colour (the gradient's midpoint) because NiiVue's
   backColor and several rgba mixes need a colour, not an image. */
:root[data-theme="oxide"][data-mode="dark"] {
  --bg:#28413b; --panel:rgba(15,26,25,.80); --panel-2:rgba(11,20,19,.86); --field:rgba(8,16,15,.74);
  --ink:#e9f0ec; --ink-2:#b6c8c2; --muted:#8ea39e;
  --line:#5d4132; --rust:#cf8a5f; --accent:#6fd6bd; --accent-ink:#8ee3cd;
  --brass:#c9a86a; --good:#8fd3a9; --bad:#e0765c; --viewer-bg:#06090a;
  --radius:7px; --border-w:1px;
  --shadow: 0 1px 2px rgba(0,0,0,.55), 0 10px 26px rgba(0,0,0,.45);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:8px; --pad-x:9px;
  --bg-image: url("texture-oxide.svg"),
              linear-gradient(180deg,#26292a 0%,#243330 34%,#254440 66%,#28564b 100%);
  --grad-action: linear-gradient(100deg,#b8764e,#7fae95 55%,#6fd6bd);
  --grad-soft: linear-gradient(100deg,rgba(162,96,60,.22),rgba(111,214,189,.16));
  --glow:rgba(111,214,189,.26);
  --on-action:#04140f; --on-accent:#04140f;
  color-scheme: dark;
}

:root[data-theme="oxide"][data-mode="light"] {
  --bg:#d7ded2; --panel:rgba(252,250,244,.84); --panel-2:rgba(244,241,232,.88); --field:rgba(255,254,250,.90);
  --ink:#232520; --ink-2:#4e5147; --muted:#6c7367;
  --line:#c9ad91; --rust:#a85a34; --accent:#2b7a6d; --accent-ink:#246d61;
  --brass:#96762f; --good:#4b7d52; --bad:#c0533a; --viewer-bg:#141a1e;
  --radius:7px; --border-w:1px;
  --shadow: 0 1px 2px rgba(60,70,55,.09), 0 10px 24px rgba(60,70,55,.14);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:8px; --pad-x:9px;
  --bg-image: url("texture-oxide-light.svg"),
              linear-gradient(180deg,#e9e5da 0%,#dee2d6 40%,#cfdcd0 72%,#bed4c6 100%);
  --grad-action: linear-gradient(100deg,#9c5230,#537565 58%,#2b7a6d);
  --grad-soft: linear-gradient(100deg,rgba(156,82,48,.14),rgba(47,133,119,.12));
  --glow:rgba(47,133,119,.14);
  --on-action:#fff8f1; --on-accent:#ffffff;
  color-scheme: light;
}

:root[data-theme="clinical"][data-mode="dark"] {
  --bg:#0b0e12; --panel:#141a21; --panel-2:#0f151b; --field:#0e141a;
  --ink:#e7edf3; --ink-2:#a9b6c4; --muted:#6c7887;
  --line:#232c37; --rust:#e2703a; --accent:#38c0cf; --accent-ink:#5fd3e0;
  --brass:#d0b070; --good:#3ecb86; --bad:#e0765c; --viewer-bg:#010409;
  --radius:16px; --border-w:1px;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.34);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:14px; --pad-x:14px;
  --bg-image:none; --grad-action:var(--rust); --grad-soft:transparent;
  --glow:transparent;
  --on-action:#241209; --on-accent:#04262a;
  color-scheme: dark;
}

:root[data-theme="clinical"][data-mode="light"] {
  --bg:#eef1f4; --panel:#fff; --panel-2:#f5f7f9; --field:#fff;
  --ink:#161b21; --ink-2:#455060; --muted:#7c8797;
  --line:#d6dde4; --rust:#b7410e; --accent:#0c8290; --accent-ink:#0b7c8a;
  --brass:#8a6d3b; --good:#1f9d64; --bad:#c0503a; --viewer-bg:#010409;
  --radius:16px; --border-w:1px;
  --shadow: 0 1px 2px rgba(20,30,45,.06), 0 10px 30px rgba(20,30,45,.07);
  --font-ui: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-h: system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace; --pad-y:14px; --pad-x:14px;
  --bg-image:none; --grad-action:var(--rust); --grad-soft:transparent;
  --glow:transparent;
  --on-action:#fff; --on-accent:#ffffff;
  color-scheme: light;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
body {
  /* Colour and image are separate properties, not one shorthand: `--bg` must
     stay a real colour (NiiVue's backColor and several rgba mixes read it), so a
     textured theme keeps the colour and adds the gradient plus weathering on top
     through `--bg-image`. Flat themes set that to `none` and pay nothing.
     `fixed` so the metal does not slide under a scrolling panel. */
  background-color: var(--bg);
  background-image: var(--bg-image);
  background-size: cover;
  background-attachment: fixed;
  color: var(--ink); font: 13px/1.5 var(--font-ui);
  padding: 14px;
  display: flex; flex-direction: column;
  /* The app owns exactly one viewport and never scrolls as a page: every panel
     that can outgrow its box scrolls internally instead. `dvh` rather than
     `vh` so mobile browser chrome does not push the bottom off-screen. */
  height: 100dvh; overflow: hidden;
}
h1, h2, h3 { font-family: var(--font-h); }
code, pre, textarea, input, select { font-family: var(--font-mono); }
/* A hoverable explanation beside a panel's heading. `title` carries the text,
   so it needs no scripting and stays available to assistive tech through
   `aria-label`. */
/* The badge glyph carries its own outline, so the button adds none. */
.info {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 6px; padding: 0; flex: 0 0 auto;
  border: 0; border-radius: 50%; background: none;
  color: var(--muted); line-height: 1; cursor: pointer;
  vertical-align: middle;
  transition: color .15s ease, transform .15s ease, filter .15s ease;
}
/* The glyph is not a hit target of its own. Without this, moving from the icon
   onto the button's own padding fires `pointerleave` *on the glyph*, which the
   delegated handler reads as leaving the control: the tooltip flickers away
   while the pointer is still on it. */
.info > * { pointer-events: none; }
/* Hover lights the mark up and grows it slightly, instead of swapping the
   cursor to a help arrow. A cursor change says "something is here" only once
   the pointer has already arrived, and says nothing about whether it worked. */
.info:hover, .info:focus-visible {
  color: var(--accent);
  transform: scale(1.18);
  /* Derived from the accent rather than `--glow`, which several themes set to
     `transparent` as a deliberate "this theme does not bloom" — the mark still
     has to light up in those. */
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--accent) 55%, transparent));
}
/* Our own tooltip, so it appears immediately — a native `title` cannot be
   shown without the browser's delay. Fixed-position and appended to the body,
   so no ancestor's overflow can clip it. */
.tip {
  position: fixed; z-index: 100; max-width: 34ch; pointer-events: none;
  /* Glass: the panel surface at well under half opacity, over a heavy blur, so
     what is behind stays visible as shape and colour without being readable.
     A tinted *fill* cannot do this. The accent at any usable alpha lightens
     over a pale panel until white copy on it drops to 3.7:1 in the light
     themes, so the accent survives here only as the edge and the glow, and the
     surface is the neutral one whose contrast with `--ink` the theme already
     guarantees (measured worst case 10.9:1 at this alpha, over any backdrop). */
  background: color-mix(in srgb, var(--panel-2) 62%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.8);
  backdrop-filter: blur(16px) saturate(1.8);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: 10px; padding: 8px 11px;
  font: 500 11.5px/1.45 var(--font-ui);
  /* Outer depth, an accent halo to tie it to the mark that opened it, and an
     inset top highlight, which is what makes a translucent panel read as a lit
     edge rather than a washed-out box. */
  box-shadow:
    0 8px 24px rgba(0, 0, 0, .28),
    0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--ink) 14%, transparent);
  animation: tip-in .13s ease-out;
}
@keyframes tip-in {
  from { opacity: 0; transform: translateY(-3px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .tip { animation: none; }
  .info { transition: none; }
  .info:hover, .info:focus-visible { transform: none; }
}
.tip[hidden] { display: none; }
.section-h {
  font: 700 11px/1.4 var(--font-mono); text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); margin: 0 0 8px;
}
.card {
  background: var(--panel); border: var(--border-w) solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: var(--pad-y) var(--pad-x);
}
input, select, textarea, button {
  background: var(--field); color: var(--ink); border: 1px solid var(--line);
  border-radius: 9px; padding: 6px 9px; font: 13px var(--font-mono);
}
input:focus, select:focus, textarea:focus, button:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
button { cursor: pointer; }
button:disabled { opacity: .5; cursor: default; }
/* Fit is the action, so it wears `--grad-action`: a flat colour in the plain
   families, a copper-to-verdigris ramp in Oxide. Colour and image are set
   separately so the flat case still has a real background colour underneath,
   and the halo costs nothing where `--glow` is transparent. */
button.primary {
  background-color: var(--rust);
  background-image: var(--grad-action);
  color: var(--on-action); border-color: var(--line); font-weight: 600;
  box-shadow: 0 0 14px var(--glow);
}
button.primary:hover:not(:disabled) { filter: brightness(1.08); }
.primary.cancel { background: var(--rust); }
#status { color: var(--muted); }

#fallback {
  border: 1px solid var(--line); border-radius: 16px; padding: 12px 14px;
  margin-bottom: 12px; background: var(--panel-2);
}
#fallback pre { overflow-x: auto; }

/* Thin app-identity bar — replaces the old wide topbar status line, whose
   job (per-model summary, fit timing) moved beside the frame-scrub note;
   `#status` here is left for transient app state only (loading, wasm
   unavailable, fit errors), right-aligned so the title always reads first. */
.navbar {
  position: relative;
  display: flex; align-items: center; gap: 10px;
  margin: -14px -14px 12px; padding: 8px 14px;
  border-bottom: 1px solid var(--line); background: var(--panel-2);
}
.navbar-status { font-size: 13.5px; }
/* Indeterminate loading, along the navbar's lower edge: a gradient segment
   sweeping on a loop. It says "still working" without claiming progress, and is
   raised by every long stage — a download, a fit, a simulation, a segmentation —
   because it is the only busy signal in view no matter which panel the reader is
   watching. The determinate readouts each sit in one place and report the amount
   alongside it. Animating `background-position` rather than a child's transform
   keeps it to one pseudo-element with nothing to clip. */
.navbar.loading::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
  background-image: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 38% 100%; background-repeat: no-repeat;
  animation: navbar-sweep 1.9s linear infinite;
}
@keyframes navbar-sweep {
  from { background-position: -38% 0; }
  to { background-position: 138% 0; }
}
/* Everything that is not the status line sits at the right end, reading as one
   cluster of controls: where to go (docs, source), then a rule, then how it looks. */
.navbar-right { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.navbar-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 22px; border-radius: 4px;
  color: var(--muted); text-decoration: none;
}
.navbar-link:hover { color: var(--ink); background: var(--field); }
/* The foldable group and the control that unfolds it. Wide screens have room for
   the whole row, so the group is dissolved into it and the hamburger is absent;
   the narrow rule below is the only place either changes. A name spelled out
   belongs to the dropdown alone — in the bar the glyph and its tooltip are the
   label. */
.navbar-more { display: contents; }
.navbar-burger { display: none; }
.navbar-word { display: none; }
/* At 25px the pack's default 2px stroke reads as heavy beside 14px chrome. */
.navbar-link .icon { stroke-width: 1; }
.navbar-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Separates leaving the page from restyling it — two unrelated kinds of control
   that would otherwise read as one row of six. */
.navbar-rule {
  width: 1px; height: 18px; margin: 0 2px;
  background: var(--line);
}
/* The logo sits in the recipe panel's head, opposite its view toggle. An <img>
   rather than a background: only a real image element carries an intrinsic
   aspect ratio, which is what lets the width follow from the height. */
.panel-logo { height: 56px; width: auto; flex: none; display: block; }
/* Shaped like the docs and source links beside it: these are all "chrome" rather
   than actions, and a bordered pill among borderless glyphs reads as the odd one. */
.theme-pick {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 22px; padding: 0;
  background: none; border: 0; border-radius: 4px;
}
.theme-pick:hover { background: var(--field); }
.theme-pick select {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; border: 0; padding: 0; margin: 0;
  -webkit-appearance: none; appearance: none;
}
.theme-pick-icon { display: flex; color: var(--muted); pointer-events: none; }
/* Icons inherit the text colour of whatever they sit in, so they follow the
   theme with no rule of their own. */
.icon { display: block; }
/* A button that is an icon beside a word, rather than one or the other. */
.icon-btn { display: inline-flex; align-items: center; gap: 5px; }
.theme-pick:hover .theme-pick-icon { color: var(--ink); }
/* The select carries focus, so the ring has to be drawn by its wrapper. */
.theme-pick:focus-within { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A menu anchored to the control that opens it. Fixed rather than absolute, so it
   escapes the card's `overflow: hidden` the way the drawing palette does — its
   position is computed from the button's own rect. Glass recipe: see the
   shared rule below. */
.menu {
  position: fixed; z-index: 60;
  display: flex; flex-direction: column; gap: 2px; padding: 4px;
  border-radius: 10px;
}
.menu[hidden] { display: none; }
.menu-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; border: 0; border-radius: 6px;
  background: none; color: var(--ink-2); cursor: pointer;
  font: 500 12px var(--font-ui); text-align: left; white-space: nowrap;
}
.menu-item:hover { background: var(--field); color: var(--ink); }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.menu-item .icon { color: var(--accent); flex: none; }

/* Shrink/grow sits in the same menu as the segmentation methods but is not a
   command: it holds two buttons and a running description, so it opts out of
   the row hover and keeps its label from pushing the steppers apart. */
.menu-item.mask-steps { cursor: default; gap: 6px; }
.menu-item.mask-steps:hover { background: none; color: var(--ink-2); }
.menu-item.mask-steps span:not(.icon) { flex: 1; }
.step-btn {
  flex: none; width: 20px; height: 20px; padding: 0;
  border: var(--border-w) solid var(--line); border-radius: 5px;
  background: var(--field); color: var(--ink);
  font: 600 13px/1 var(--font-ui); cursor: pointer;
}
.step-btn:hover { border-color: var(--accent); color: var(--accent); }
.step-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Fitting progress: a permanent fixture above the Fit button (idle = empty
   track), not a topbar element that appears/disappears and shifts the rest
   of the page. The stripe is a white sheen rather than a token: it reads as a
   highlight over any accent a theme supplies. */
.fit-progress {
  height: 10px; border-radius: 999px; background: var(--panel-2); overflow: hidden;
  margin: 2px 0 2px;
}
.fit-progress-bar {
  height: 100%; width: 0%; border-radius: 999px;
  background-color: var(--accent);
  background-image: repeating-linear-gradient(
    45deg, rgba(255, 255, 255, 0.35) 0 10px, transparent 10px 20px
  );
  background-size: 28px 28px;
  /* No transition: the last step must land in the frame it is set, or the
     reset that follows the fit overtakes it and 100% never renders. */
}
.fit-progress-bar.active { animation: fit-progress-stripes 0.9s linear infinite; }
@keyframes fit-progress-stripes {
  from { background-position: 0 0; }
  to { background-position: 28px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .fit-progress-bar.active { animation: none; }
  /* Still visibly "loading", just not moving. */
  .navbar.loading::after {
    animation: none; background-image: none; background-color: var(--accent); opacity: .45;
  }
}

.layout {
  display: grid;
  grid-template-columns: 380px 1fr 1fr;
  /* Proportional rows with a zero minimum: they always divide the height the
     grid is given, so the layout cannot sum past the viewport however short it
     is. A fixed height for either row is what used to overflow. */
  grid-template-rows: minmax(0, 1fr) minmax(0, 0.65fr);
  gap: 14px;
  align-items: stretch;
  flex: 1 1 auto;
  min-height: 0;
}
/* Capped, and explicitly opted out of the grid's row-stretch (`align-self:
   start`), so a model with a deeply-nested recipe scrolls internally
   instead of the card stretching to the (now viewport-filling) viewer
   row's height and sitting mostly empty. */
/* The left column is one cell spanning both rows: the recipe grows to fill
   whatever the viewers and curve leave, Fit sits between it and the
   bring-your-own-data panel, and that panel stays at the bottom. */
.left-col {
  grid-column: 1; grid-row: 1 / span 2;
  display: flex; flex-direction: column; gap: 12px; min-height: 0;
}
.recipe-card {
  flex: 1 1 0; min-height: 0;
  display: flex; flex-direction: column;
}
/* Always between the recipe and the drop panel, never scrolled away. */
.fit-strip { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; }
/* Only the editor scrolls, so the model picker stays put at the top and Fit
   stays put at the bottom however deeply nested a recipe is. */
.recipe-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* Collapsing keeps the header, the heading and the model picker — the parts you
   still need to switch models — and hides only the fields. */
.recipe-card.collapsed .recipe-scroll { display: none; }
.recipe-card.collapsed { flex: 0 0 auto; }
/* Just the logo and the view toggle, so it needs less room beneath it than a
   heading row would. */
/* One line: logo left, view toggle right. The logo's width follows its height, so
   it is sized to leave room for the toggle in this column rather than pushing it
   onto a second line or over the card's edge. */
.recipe-head {
  display: flex; align-items: center; gap: 7px; margin-bottom: 6px;
}
/* `.section-h` uppercases and tracks its text, which is right for the heading
   and wrong for the toggle's labels — the pill reads as Form/YAML everywhere
   else in the app, so it must not become FORM/YAML only here. */
.recipe-seg { margin-left: auto; text-transform: none; letter-spacing: normal; }
/* The heading sits with the picker it labels, not with the logo. */
.recipe-title { display: flex; align-items: center; margin: 0 0 4px; }
.collapse-btn {
  display: none; padding: 2px 8px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  font: 600 10px var(--font-mono); text-transform: uppercase; letter-spacing: .06em;
}
.collapse-btn:hover { color: var(--ink); border-color: var(--accent); }
/* The viewer wraps *do* stretch (the grid default) to fill whatever
   vertical space the row has — the point of this layout pass — with their
   own content (`.viewer`) flexed to fill that stretched height in turn. */
.viewer-in-wrap, .viewer-out-wrap {
  grid-row: 1;
  display: flex; flex-direction: column; min-height: 0;
}
.viewer-in-wrap { grid-column: 2; }
.viewer-out-wrap { grid-column: 3; }
/* Both viewers get the same black box, whatever chrome sits below them.
   Letting each one flex to fill its own card made the two panels' image areas
   different heights, since the cards carry different controls — so the box is
   sized here instead, once, and any leftover card height falls below it. */
.viewer-in-wrap, .viewer-out-wrap { justify-content: flex-start; }
/* The two cards are the same height, so matching the chrome below each viewer
   is what makes the two black boxes match. Sizing the viewers directly does
   not work: a fixed height cannot shrink, and overflows the card. */
:root { --foot-h: 68px; }
/* The graph paper's ruling pitch. One square is about a line of the stats table
   below the chart, which is what keeps the paper reading as a background rather
   than as a texture. */
:root { --paper-step: 22px; }
/* Simulating is gold, fitting acquired data is not: the mode changes what the
   numbers on screen *are* — ground truth the reader chose rather than a
   measurement — and that deserves a cue outside the switch that set it. Gold is
   `--brass`, which every theme already guarantees, so the cue retargets per
   family with no per-theme list. Stated as one block, so the set of things that
   turn gold is readable in one place. */
body.sim-mode .recipe-card,
body.sim-mode .sim-wrap { border-color: color-mix(in srgb, var(--brass) 55%, var(--line)); }
body.sim-mode .seg button.active { color: var(--brass); }
/* A fixed box, not a minimum: the two feet carry different controls, and a foot
   that sized itself to its own would put the two viewers at different heights.
   `syncFootHeight` sets the value to the taller foot's content, so the box holds
   the fitted map's controls when a narrow column wraps them onto a second row
   without leaving dead space under both cards when it does not. This declaration
   is the pre-script value. */
.viewer-foot { height: var(--foot-h); flex: 0 0 auto; overflow: hidden; }
.curve-wrap {
  grid-column: 2 / span 2; grid-row: 2;
  display: flex; flex-direction: column; min-height: 0;
}
/* Simulate mode replaces the whole right-hand region: there are no viewers to
   show, and a sweep needs the room a single card can give it. */
.sim-wrap {
  grid-column: 2 / span 2; grid-row: 1 / span 2;
  display: flex; flex-direction: column; min-height: 0;
  /* Graph paper, because what this panel holds is plotted rather than imaged:
     two hairline rulings over the card's own fill, in `--brass` at low mix so
     they read as paper rather than as a chart's own gridlines. The charts draw
     no background, so the ruling shows through behind the curves, which is the
     point of it. `background-attachment: local` pins the ruling to the card, so
     a scrolling stats table does not drag the paper with it. */
  background-image:
    repeating-linear-gradient(to right,
      color-mix(in srgb, var(--brass) 12%, transparent) 0 1px, transparent 1px var(--paper-step)),
    repeating-linear-gradient(to bottom,
      color-mix(in srgb, var(--brass) 12%, transparent) 0 1px, transparent 1px var(--paper-step));
  background-attachment: local;
}
.sim-chart { flex: 1 1 auto; min-height: 220px; }
.sim-note { color: var(--muted); font-size: 11px; padding: 2px 4px 6px; }
.sim-table { overflow: auto; max-height: 40%; }
.sim-table table { border-collapse: collapse; width: 100%; font-size: 11px; }
.sim-table th, .sim-table td {
  text-align: right; padding: 3px 8px; border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
.sim-table th { color: var(--muted); font-weight: 500; }
.sim-table td:first-child, .sim-table th:first-child { text-align: left; }
/* `[hidden]` is a UA rule at specificity 0,1,0, so any class that sets
   `display` defeats it. Every card the page-mode switch toggles therefore
   states its own hidden case. */
.sim-wrap[hidden], .viewer-in-wrap[hidden], .viewer-out-wrap[hidden],
.curve-wrap[hidden], .drop-wrap[hidden] { display: none; }
.drop-wrap { flex: 0 0 auto; }
@media (max-height: 620px) {
  body { height: auto; min-height: 100dvh; overflow: auto; }
  .layout { grid-template-rows: auto auto; }
  .viewer-in-wrap, .viewer-out-wrap { height: 58vh; min-height: 300px; }
  #curve { height: 200px; flex: 0 0 auto; }
}
@media (max-width: 900px) {
  .layout { grid-template-columns: 1fr; grid-template-rows: none; }
  /* `display: contents` dissolves the left column so its three cards become
     grid items in their own right. Keeping it as a flex column is what squashed
     the recipe: `flex: 1 1 0` with no height to distribute collapses to nothing
     while its content spills over the cards below. */
  .left-col { display: contents; }
  .viewer-in-wrap, .viewer-out-wrap, .curve-wrap, .recipe-card, .fit-strip, .sim-wrap {
    grid-column: 1; grid-row: auto;
  }
  /* Everything sizes to its content and the page scrolls, rather than dividing
     a height that is not there. */
  body { height: auto; min-height: 100dvh; overflow: auto; }
  .recipe-card { flex: 0 0 auto; height: auto; }
  .recipe-scroll { max-height: 46vh; }
  .viewer-in-wrap, .viewer-out-wrap { height: 62vh; min-height: 300px; }
  .curve-wrap { height: auto; }
  #curve { height: 220px; flex: 0 0 auto; }
  /* Last on a narrow screen: it is the least-used panel, and burying it keeps
     the model's own data in view first. */
  .drop-wrap { order: 99; grid-column: 1; }
  .collapse-btn { display: inline-flex; }
  /* Show/Hide joins this row only here, and the logo's width was chosen against
     a row without it — three rigid items then measure wider than a phone's
     column, which widens the single grid track and hands the whole page a
     sideways scroll. A smaller mark buys the room at any usual width, and
     wrapping is the guarantee: the row can never demand more than its widest
     item, however narrow the screen. */
  .recipe-head { flex-wrap: wrap; }
  .panel-logo { height: 44px; }
  /* Same guarantee for the frame scrub: a slider with a floor, a label holding
     8em for its value, and the Segment button together measure wider than the
     narrowest phones, and the foot is sized by `syncFootHeight` to hold a second
     row when one appears. */
  .frame-row { flex-wrap: wrap; }
}

/* recipe editor */
.seg {
  display: inline-flex; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 3px; gap: 2px;
  /* `inline-flex` is not enough inside a column flex container, where the
     default cross-axis stretch would widen the pill to the card. The viewer
     heads are row containers, so only the recipe's needed this — but stating
     it here keeps every toggle the same shape wherever one is placed. */
  align-self: flex-start;
}
.seg button {
  border: none; background: transparent; border-radius: 999px; padding: 5px 12px;
  font: 600 12px var(--font-ui); color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px;
}
.seg button.active {
  background-color: var(--panel); background-image: var(--grad-soft);
  color: var(--ink); box-shadow: var(--shadow);
}
.seg button:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
/* The same toggle with a glyph in place of the word. `.seg.seg-icon` rather than
   `.seg-icon`: the generic `.seg button` rules above are declared later at equal
   specificity, so a single class would keep their word-sized padding. */
.seg.seg-icon { padding: 2px; }
.seg.seg-icon button { padding: 4px 8px; }

/* The data/simulate switch: a single checkbox behind two label spans, laid
   out as a two-column grid so each span owns a half. `isolation: isolate`
   gives the label its own stacking context, so the pill and track (`z-index:
   -1`/`-2`) sit below the spans without reaching past the switch itself.
   The input is real and focusable, just visually reduced to a point — a
   native <label> forwards a click or a Space on the focused control to it
   regardless, which is what makes either half, or the label's own padding,
   toggle it. */
.mode-switch {
  position: relative; isolation: isolate; cursor: pointer;
  display: grid; grid-template-columns: 1fr 1fr; padding: 3px;
  /* Transparent: the visible track border is `::after`'s, drawn one layer
     behind. This one only claims the same box-model space a real border
     would, so the switch matches a `.seg` toggle's height pixel for pixel. */
  border: 1px solid transparent;
}
.mode-switch input {
  position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0;
  pointer-events: none;
}
.mode-switch > span {
  position: relative; z-index: 1; text-align: center;
  padding: 5px 12px; font: 600 12px var(--font-ui);
  color: var(--muted); opacity: .75;
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  transition: opacity .3s ease .15s, color .3s ease .15s;
}
.mode-switch > span:hover { opacity: 1; transition-duration: 0s; }
.mode-switch input:not(:checked) ~ span:first-of-type,
.mode-switch input:checked ~ span:last-of-type {
  color: var(--ink); opacity: 1;
}
/* Track, one layer behind the pill. Focus expands it outward rather than
   drawing a separate ring, since the pill already occupies the inside. */
.mode-switch::after {
  content: ""; position: absolute; z-index: -2; inset: 0;
  border-radius: 999px; background: var(--panel-2);
  border: 1px solid var(--line);
  transition: inset .15s ease;
}
.mode-switch:focus-within::after { inset: -0.25rem; }
/* The pill: sized to one column and slid a full pill-width to cross to the
   other half, so the motion is the same distance in either direction without
   depending on the switch's own width. The spring overshoots on the way in,
   which is what tells a reader the action landed rather than merely moved. */
.mode-switch::before {
  content: ""; position: absolute; z-index: -1; top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 5px); border-radius: 999px;
  background-color: var(--panel); background-image: var(--grad-soft);
  box-shadow: var(--shadow);
  transition: transform .5s cubic-bezier(0.47, 1.64, 0.41, 0.8);
}
.mode-switch:has(input:checked)::before { transform: translateX(100%); }
.mode-switch:hover:not(:has(input:checked))::before { transform: translateX(5%); }
.mode-switch:hover:has(input:checked)::before { transform: translateX(calc(100% - 5%)); }
/* Disabled means the payload has nothing to switch to: the whole control
   dims and refuses the pointer, and the reason stays reachable as the
   label's own tooltip since a disabled input does not reliably show one. */
.mode-switch:has(input:disabled) { opacity: .45; cursor: not-allowed; }

/* The navbar on a phone, where the full row runs off the side of the page.
   Only the two controls that change what the page shows and how it looks keep
   their place in the bar, each reduced to the single button its two halves were
   already agreeing on; the links and the citation fold into the hamburger's
   panel. Declared after the switch and the toggle because it overrides them. */
@media (max-width: 700px) {
  .navbar { gap: 8px; padding: 8px 12px; }
  /* Transient text beside permanent controls: it is the one item that gives up
     width, so the switch and the right-hand cluster keep theirs whole instead of
     every item shrinking in proportion and each clipping a little. */
  .navbar-status {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .mode-switch, .navbar-right { flex: none; }
  .navbar-burger { display: inline-flex; width: 28px; height: 26px; }
  .navbar-burger[aria-expanded="true"] { color: var(--ink); background: var(--field); }
  /* The same wrapper, now a panel under the bar's right edge. Glass recipe: see
     the shared rule with `.menu`. */
  .navbar-more {
    display: none; position: absolute; z-index: 60;
    top: calc(100% + 6px); right: 10px;
    flex-direction: column; gap: 2px; padding: 4px; border-radius: 10px;
  }
  .navbar-more.open { display: flex; }
  /* Named rows with a tap-sized footprint, rather than the bar's bare glyphs. */
  .navbar-more .navbar-link {
    width: auto; height: auto; justify-content: flex-start; gap: 8px;
    padding: 8px 10px; color: var(--ink-2);
    font: 500 12px var(--font-ui); white-space: nowrap;
  }
  .navbar-more .icon { width: 18px; height: 18px; color: var(--accent); }
  .navbar-word { display: inline; }
  /* A rule divided a row; a column is divided already. */
  .navbar-more .navbar-rule { display: none; }
  .mode-switch { grid-template-columns: 1fr; }
  .mode-switch input:not(:checked) ~ span:last-of-type,
  .mode-switch input:checked ~ span:first-of-type { display: none; }
  .mode-switch::before { width: calc(100% - 6px); }
  .mode-switch:has(input:checked)::before,
  .mode-switch:hover:not(:has(input:checked))::before,
  .mode-switch:hover:has(input:checked)::before { transform: none; }
  .seg.seg-icon button:not(.active) { display: none; }
}

.model-line { margin: 12px 0; color: var(--ink-2); }
.model-line select { margin-left: 4px; }

/* Model picker: a disclosure tree over the registry's taxonomy. Anchored
   relative so the tree hangs under the button without a fixed-position
   measurement, since this one always has room below it inside the panel. */
.model-label {
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
  color: var(--muted); font: 700 10px var(--font-ui);
  text-transform: uppercase; letter-spacing: .08em;
}
.model-label .icon { color: var(--accent); }

.model-picker { position: relative; }
.model-button {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border: var(--border-w) solid var(--line); border-radius: 8px;
  background: var(--field); color: var(--ink);
  font: 600 13px var(--font-ui); text-align: left; cursor: pointer;
}
.model-button:hover { border-color: var(--accent); }
.model-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.model-button .icon { color: var(--muted); flex: none; transition: transform .15s ease; }
.model-button[aria-expanded="true"] .icon { transform: rotate(90deg); color: var(--accent); }
.model-button-title { flex: 1; }
.model-suffix {
  flex: none; font: 600 10px var(--font-mono); letter-spacing: .04em;
  padding: 2px 6px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
}

.model-tree {
  position: absolute; z-index: 60; left: 0; right: 0; top: calc(100% + 4px);
  max-height: 60vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 1px; padding: 4px;
  /* Opaque, unlike the shared panel token. This hangs directly over the recipe
     form, and every theme's `--panel` is translucent (verdigris is .80), so the
     fields underneath read straight through the list. The tint is kept rather
     than dropped: the panel colour is layered over an opaque base instead of
     being the base. */
  background-color: var(--bg);
  background-image: linear-gradient(var(--panel), var(--panel)), var(--grad-soft);
  border: var(--border-w) solid var(--line); border-radius: 10px; box-shadow: var(--shadow);
}
.model-tree[hidden] { display: none; }

.model-family {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 8px; border: 0; border-radius: 6px;
  background: none; color: var(--ink); cursor: pointer;
  font: 700 11px var(--font-ui); text-transform: uppercase; letter-spacing: .05em;
  text-align: left;
}
.model-family:hover { background: var(--field); }
.model-family:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.model-family span:not(.model-count) { flex: 1; }
/* The chevron is the fold's whole state: right is closed, down is open. */
/* Two glyphs: the fold chevron turns, the family's own icon does not. */
.model-family .icon { flex: none; }
.model-family .icon:first-child { color: var(--muted); transition: transform .15s ease; }
.model-family.open .icon:first-child { transform: rotate(90deg); color: var(--accent); }
.model-family .icon:nth-child(2) { color: var(--accent); }
.model-count {
  flex: none; min-width: 16px; text-align: center;
  font: 600 10px var(--font-mono); color: var(--muted);
}

/* Nesting is shown the way the file tree shows it: one indent step per level
   with a guide line down its left edge, so the two trees on this page read the
   same way rather than each inventing its own depth cue. */
.model-family-body {
  display: flex; flex-direction: column; gap: 1px;
  margin-left: 13px; padding-left: 10px; border-left: 1px solid var(--line);
}
.model-subgroup-body {
  display: flex; flex-direction: column; gap: 1px;
  margin-left: 6px; padding-left: 10px; border-left: 1px solid var(--line);
}
.model-subgroup {
  padding: 5px 8px 2px; color: var(--muted);
  font: 600 10px var(--font-ui); text-transform: uppercase; letter-spacing: .05em;
}
/* The family holding the loaded model, while everything is folded shut. */
.model-family.holds-active { color: var(--accent); }
.model-family.holds-active .model-count { color: var(--accent); }
.model-option {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; border: 0; border-left: 2px solid transparent; border-radius: 6px;
  background: none; color: var(--ink-2); cursor: pointer;
  font: 500 12px var(--font-ui); text-align: left;
}
.model-option span:first-child { flex: 1; }
.model-option:hover { background: var(--field); color: var(--ink); }
.model-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.model-option.active {
  color: var(--ink); background: var(--field); border-left-color: var(--accent);
}
.model-option.active .model-suffix { border-color: var(--accent); color: var(--accent); }

.yaml-wrap { position: relative; }
.yaml-pill {
  position: absolute; top: 8px; right: 8px; font: 700 10px var(--font-mono);
  text-transform: uppercase; letter-spacing: .06em; padding: 2px 8px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
}
.yaml-pill.valid { color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, var(--line)); }
.yaml-pill.invalid { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, var(--line)); }
/* Syntax highlighting for an editable textarea: a highlighted <pre> sits
   behind a transparent-text textarea, the two sharing identical metrics and
   scroll offset so the painted tokens line up under the real caret. The
   textarea stays the single source of truth for text and editing. */
.yaml-edit { position: relative; }
.yaml-edit > #cfg-yaml, .yaml-edit > #yaml-hl {
  font: 12px/1.5 var(--font-mono); padding: 6px 9px; border: 1px solid transparent;
  border-radius: 9px; white-space: pre-wrap; overflow-wrap: break-word;
  tab-size: 2; margin: 0;
}
#yaml-hl {
  position: absolute; inset: 0; overflow: auto; pointer-events: none;
  background: var(--field); border-color: var(--line); color: var(--ink);
}
#cfg-yaml {
  position: relative; width: 100%; min-height: 360px; resize: vertical;
  background: transparent; color: transparent; caret-color: var(--ink);
  border-color: var(--line);
}
#cfg-yaml::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
/* highlight.js token classes, mapped onto this page's own palette rather than
   shipping one of its themes — so the editor tracks light/dark with the rest. */
.hljs-comment { color: var(--good); font-style: italic; }
.hljs-attr { color: var(--accent-ink); }
.hljs-string { color: var(--rust); }
.hljs-number { color: var(--ink); font-weight: 600; }
.hljs-literal, .hljs-type { color: var(--bad); }
.hljs-bullet, .hljs-punctuation { color: var(--muted); }

.form-fields { display: flex; flex-direction: column; gap: 10px; }
.group {
  border: 1px solid var(--line); border-radius: 12px; padding: 10px; background: var(--panel-2);
}
.group > .group-title {
  font: 700 11px var(--font-mono); text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); margin-bottom: 8px;
}
/* The simulation block is gold, like the panel it drives and the card around it:
   what a reader sets under here is ground truth they chose, not a measurement
   read off a dataset, and that difference is worth seeing without reading a
   label. Keyed off the group's own name rather than the page mode, because the
   block is gold for what it is — a recipe declaring a simulation anywhere would
   be marked the same way.

   The nested groups are included, so Ground Truth, Noise and the rest sit in the
   same family instead of leaving neutral boxes patched into a tinted one. The
   tint is weak enough that `--ink` on it stays above 10:1 in every theme, and
   `--brass` clears the mark floor it is already held to. */
.group[data-group="sim"], .group[data-group="sim"] .group {
  border-color: color-mix(in srgb, var(--brass) 55%, var(--line));
  background: color-mix(in srgb, var(--brass) 6%, var(--panel-2));
}
.group[data-group="sim"] .group-title { color: var(--brass); }
.group .form-fields { gap: 8px; }
.field-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center;
}
/* Two lines: the quantity, then what qualifies it. The second line is set in
   the mono face the values use and tinted with the protocol accent, so a
   reader's eye pairs "TI [s]" with the numbers beside it rather than with the
   name above it. */
.field-labels { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.field-label {
  display: flex; align-items: center; gap: 5px;
  color: var(--ink-2); font: 12px var(--font-ui);
}
/* The option hover sits with the name, not the value: it explains the choice,
   and a reader looking for one is reading labels. Dimmer than the panel
   headings' mark so a column of them does not read as a column of warnings. */
.field-label .info { vertical-align: 0; opacity: 0.55; margin-left: 0; width: 14px; height: 14px; }
.field-label .info:hover, .field-label .info:focus-visible { opacity: 1; }
.field-meta { display: flex; align-items: center; gap: 5px; min-height: 14px; }
.field-unit {
  color: var(--muted); font: 10px var(--font-mono);
  letter-spacing: .02em; white-space: nowrap;
}
.field-row input, .field-row textarea { width: 100%; }
.field-row textarea { min-height: 44px; resize: vertical; }
/* A number array is one compact line at rest and one value per line while
   focused, so a long acquisition axis neither dominates the panel nor has to
   be edited by counting commas. Same element in both states. */
.field-row textarea.num-array {
  min-height: 0;
  resize: none;
  overflow-y: hidden;
  white-space: pre;
}
.field-row textarea.num-array.editing {
  overflow-y: auto;
  line-height: 1.5;
}

.cfg-error {
  margin: 0 0 0.75rem;
  padding: 0.5rem 0.7rem;
  border-left: 3px solid var(--bad);
  background: color-mix(in srgb, var(--bad) 10%, transparent);
  color: var(--bad);
  font-size: 0.85rem;
  line-height: 1.4;
}
/* A row whose value comes from the model's default rather than the recipe. */
.field-row.unset .field-label { opacity: 0.72; }
/* A row the sidecars supply: shown for context, not for editing. */
.field-row.locked { opacity: 0.6; }
.field-row.locked input,
.field-row.locked select,
.field-row.locked textarea { pointer-events: none; }
/* A group whose entire contents the sidecars supply (ingest_protocol replaces
   the whole object), same treatment as a locked leaf row. */
.group.locked > .group-title { opacity: 0.6; }
/* A control sourced from the model's acquisition protocol (editable in
   non-BIDS mode, disabled once BIDS mode locks it) — the accent border reads
   as "this is the acquisition" even when `.locked`'s opacity dims the whole
   row uniformly alongside it. */
.protocol-field {
  border-color: var(--accent) !important;
}

.switch { position: relative; display: inline-block; width: 38px; height: 22px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .slider {
  position: absolute; inset: 0; background: var(--line); border-radius: 999px; cursor: pointer;
  transition: background .15s;
}
.switch .slider::before {
  content: ""; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px;
  background: var(--panel); border-radius: 50%; transition: transform .15s;
}
.switch input:checked + .slider { background: var(--accent); }
.switch input:checked + .slider::before { transform: translateX(16px); }
.switch input:focus-visible + .slider { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }


/* viewers */
.viewer-head {
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px; margin-bottom: 8px; flex-wrap: wrap;
}
.viewer-head select { font-size: 12px; padding: 4px 8px; }
.frame-row { display: flex; align-items: center; gap: 8px; flex: 1; margin-top: 8px; }
#frame {
  -webkit-appearance: none; appearance: none;
  flex: 1 1 auto; height: 18px; padding: 0; border: 0; background: none;
  cursor: pointer;
}
#frame:disabled { cursor: default; opacity: 0.5; }
/* `--fill` is set from the value, so the coloured part reaches the right edge
   at the last frame rather than stopping at the thumb's inset. */
#frame::-webkit-slider-runnable-track {
  height: 6px; border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--fill, 0%),
    var(--panel-2) var(--fill, 0%) 100%
  );
  border: 1px solid var(--line);
}
#frame::-moz-range-track {
  height: 6px; border-radius: 999px; background: var(--panel-2);
  border: 1px solid var(--line);
}
#frame::-moz-range-progress { height: 6px; border-radius: 999px; background: var(--accent); }
#frame::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px; margin-top: -5px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow);
}
#frame::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--panel);
}
#frame:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
.frame-row input[type=range] { flex: 1; min-width: 90px; accent-color: var(--accent); }
.frame-label { color: var(--muted); min-width: 8em; text-align: right; font-size: 12px; }
.viewer {
  position: relative; border-radius: 12px; overflow: hidden;
  background: var(--viewer-bg); margin: 0; width: 100%;
  flex: 1 1 auto; min-height: 0;
}
.viewer canvas { width: 100%; height: 100%; display: block; }

/* The files panel occupies the same stretched region as the canvas it
   replaces, and scrolls internally — a dataset with many volumes must not
   grow the card. */
.files-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* An explicit `display` beats the `hidden` attribute's own `display: none`,
   so the tab toggle needs this to hide the panel at all. */
.files-panel[hidden] { display: none; }
.files-summary {
  font-size: 12px; color: var(--muted); padding: 6px 2px;
  border-bottom: 1px solid var(--line);
}
.files-tree {
  flex: 1 1 auto; min-height: 0; overflow: auto; font-size: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 6px 0;
}
/* A real nested tree: each level is a block indented by one step, with a guide
   line down its left edge so deep BIDS paths stay readable. */
.files-node { padding-left: 13px; margin-left: 3px; border-left: 1px solid var(--line); }
.files-tree > .files-node { padding-left: 0; margin-left: 0; border-left: 0; }
.files-dir {
  color: var(--ink-2); padding: 3px 2px 1px; overflow-wrap: anywhere; font-weight: 600;
}
.files-dir::before { content: "▸ "; color: var(--muted); font-weight: 400; }
.files-row {
  display: flex; gap: 8px; align-items: baseline;
  padding: 2px 4px; border-radius: 4px;
}
.files-icon { display: inline-flex; flex: 0 0 auto; color: var(--muted); }
.files-row.unused .files-icon { opacity: .55; }
.files-name { overflow-wrap: anywhere; flex: 1 1 auto; }
/* Both kinds of file open from their name, so both get the same hint. */
.files-row.is-openable .files-name { cursor: pointer; text-decoration: underline dotted; }
.files-role { color: var(--muted); text-align: right; flex: 0 0 auto; }
.files-row.clickable { cursor: pointer; }
.files-row.clickable:hover { background: var(--panel-2); }
.files-row.current { background: var(--panel-2); outline: 1px solid var(--line); }
.files-row.unused .files-name, .files-row.dataset_metadata .files-name { color: var(--muted); }
/* Image data and its sidecars, told apart by colour: the same two roles the
   BIDS layout is built around. Stated after the state colours above, and at
   the same specificity, so a metadata or unused file still shows its kind. */
.files-row .files-name.is-nii { color: var(--rust); }
.files-row .files-name.is-json { color: var(--accent-ink); }

/* Skeleton placeholders, shown while a dataset is being fetched, extracted
   and resolved: the panel keeps the shape of what is coming instead of
   briefly showing a stale or half-built list. */
/* A placeholder has to be clearly *not* content, which means real contrast
   against the panel it sits on — `--panel-2` sits a couple of units from
   `--panel` and read as nothing (1.10:1). The ink colour at low alpha lightens
   the bar on a dark theme and darkens it on a light one, which is the direction
   a placeholder wants either way; 26 % lands around 1.7–2.2:1 across the six
   themes: clearly present, still obviously not text. A band travelling across
   says "coming" in a way a pulse alone does not. */
@keyframes skel-sweep {
  from { background-position: -60% 0; }
  to { background-position: 160% 0; }
}
.skel {
  border-radius: 4px;
  background-color: color-mix(in srgb, var(--ink) 26%, transparent);
  background-image: linear-gradient(
    90deg, transparent, color-mix(in srgb, var(--ink) 20%, transparent), transparent
  );
  background-size: 55% 100%; background-repeat: no-repeat;
  animation: skel-sweep 1.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; background-image: none; }
}
.skel-row { display: flex; gap: 8px; align-items: center; padding: 3px 4px; }
.skel-row .skel { height: 13px; }
.skel-name { flex: 1 1 auto; }
.skel-role { flex: 0 0 auto; width: 96px; }
.skel-dir { height: 13px; width: 40%; margin: 10px 2px 5px; }
/* The tool palette floats above the page rather than inside a card: it takes no
   room in a layout that owns exactly one viewport, and being `fixed` rather than
   absolutely positioned inside the viewer means it can be dragged anywhere —
   including off the map it paints, which is the point of moving it at all.
   A narrow vertical column, draggable by its header. */
/* The eraser's trail, over its viewer's canvas. Never interactive: it exists to
   be looked at while a stroke is in progress. */
.erase-trail {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 2;
}
.draw-palette {
  position: fixed; top: 96px; left: 12px; z-index: 60;
  display: flex; flex-direction: column; gap: 6px; width: 88px;
  padding: 7px; border-radius: var(--radius);
}
.draw-palette[hidden] { display: none; }
.draw-head {
  display: flex; align-items: center; justify-content: space-between;
  cursor: grab; touch-action: none; margin: -1px 0 1px;
}
.draw-head:active { cursor: grabbing; }
.draw-grip { color: var(--muted); font-size: 11px; line-height: 1; }
.draw-close {
  width: 15px; height: 15px; padding: 0; border: 0; border-radius: 3px;
  background: none; color: var(--muted); font-size: 13px; line-height: 1;
}
.draw-close:hover { color: var(--bad); }
/* Two columns: ten buttons in one file would run off a short viewport. */
.draw-tools { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; }
.draw-tool {
  width: 36px; height: 32px; padding: 0; border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--field); border: var(--border-w) solid var(--line);
  color: var(--ink-2); font-size: 11px;
}
.draw-tool:hover:not(:disabled) { color: var(--ink); }
.draw-tool:disabled { opacity: .4; }
/* An icon-only control in the map's button row: square, so it reads as a state
   toggle rather than a truncated labelled button. */
#labels-toggle { padding: 4px 7px; }
#labels-toggle.active { color: var(--brass); border-color: var(--brass); }
/* Brass marks the active tool — the third metal, whose whole job this is. */
.draw-tool.active {
  background: var(--brass); border-color: var(--brass); color: #241c0c;
}
/* What the tool in hand expects. Small, but present: the alternative is a reader
   clicking a bean-shaped button and hoping. */
.draw-hint {
  margin: 0; font: 9.5px/1.35 var(--font-ui); color: var(--muted);
}
.draw-apply {
  width: 100%; padding: 4px 0; border-radius: 4px;
  background-color: var(--rust); background-image: var(--grad-action);
  color: var(--on-action); border: var(--border-w) solid var(--line);
  font: 600 10px var(--font-ui); letter-spacing: .04em; text-transform: uppercase;
}
.draw-apply[hidden] { display: none; }

/* Brush size as a stepper, in the same shape as the tool buttons above it. */
.draw-size { display: flex; align-items: center; justify-content: space-between; }
.draw-step {
  width: 11px; height: 18px; padding: 0; border: 0; border-radius: 3px;
  background: none; color: var(--muted); font: 600 12px var(--font-ui); line-height: 1;
}
.draw-step:hover:not(:disabled) { color: var(--ink); }
.draw-step:disabled { opacity: .35; }
.draw-size-value { font: 10px var(--font-mono); color: var(--ink-2); min-width: 12px; text-align: center; }
.draw-labels { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; }
.draw-swatch {
  width: 16px; height: 16px; padding: 0; border-radius: 3px;
  border: 2px solid transparent; box-shadow: 0 0 0 1px var(--line);
}
.draw-swatch.active { border-color: var(--ink); }
/* The active label's colour is editable, so a reader is not stuck with six
   choices. The drawing stores label values; this only changes how they paint. */
.draw-picker {
  width: 100%; height: 22px; padding: 0; border-radius: 4px;
  border: var(--border-w) solid var(--line); background: none;
}
/* Overlays its viewer rather than replacing it, so the card keeps its exact
   size while data loads — a viewer that resizes mid-load reflows the page.
   `.viewer` is already `position: relative`. */
.viewer-skel {
  position: absolute; inset: 0; background: var(--viewer-bg);
  display: flex; align-items: center; justify-content: center;
}
.viewer-skel[hidden] { display: none; }
/* Fills the skeleton edge to edge with voxel-sized cells that ripple in a
   diagonal sweep, standing in for the image that is coming. `fillSkeleton`
   (skeleton.js) measures the panel and builds the grid: `1fr` tracks are
   what let a JS-computed column and row count tile the panel exactly at any
   size, rather than a fixed cell width that leaves a gap on one edge. */
.loader {
  position: absolute; inset: 0;
  display: grid; gap: 2px;
}
.cell {
  /* One gradient across the whole panel rather than a ramp that repeats: the
     cell's own `--mix`, set by `fillSkeleton` from how far along the diagonal it
     sits, is 0% in the corner the wave starts from and 100% in the corner it
     ends at, so the sweep crosses from `--accent` to `--rust` exactly once.

     Interpolated in oklab rather than srgb so the midpoint keeps a clean hue
     instead of passing through the muddy desaturated grey two saturated colours
     produce in srgb. Every theme guarantees both endpoints, so the gradient
     resolves on-brand per family with no per-family list to maintain. */
  --cell-color: color-mix(in oklab, var(--rust) var(--mix, 50%), var(--accent));
  /* The colour is painted once and the ripple animates `opacity`, never
     `background-color`. A colour animation repaints every cell on the main
     thread each frame, and the volume fit that this panel's skeleton stands in
     for holds that thread for a block at a time — so the wave froze and jumped
     precisely while the work it was reporting was happening. Opacity is
     composited off the main thread, so the wave keeps its own time no matter how
     busy the fit is. Over the viewer's black, a colour at 52% opacity and that
     colour mixed 52% with transparent are the same pixel. */
  background-color: var(--cell-color); opacity: 0; border-radius: 2px;
  /* `--cycle` is set per panel by `fillSkeleton`, because the plateau below has
     to outlast the wave's traverse and the traverse depends on the panel's size.
     The fallback covers a cell somehow rendered outside that path. */
  animation: var(--cycle, 2.2s) ripple linear infinite;
}
/* The grid fills from the top left, holds whole, then clears from the top left.
   The plateau between the two edges is what keeps the first cell lit until the
   last one has arrived, so the grid is never simultaneously filling in one
   corner and emptying in another; `cycleMs` in skeleton.js sizes the cycle
   against this share, and the two must be changed together.

   The lit level is the ramp colour held well short of full strength: at full
   strength the ramp's saturated ends read as a glare over the viewer's black
   rather than as tissue-sized blocks filling in, and a placeholder should not
   out-shout the image it stands in for. */
@keyframes ripple {
  0%   { opacity: 0; }
  8%   { opacity: 0.52; }
  53%  { opacity: 0.52; }
  61%  { opacity: 0; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .cell {
    animation: none;
    opacity: 0.25;
  }
}


/* Bring-your-own-data panel. The BIDS logo sits behind at low opacity — an
   identity cue, not decoration competing with the text. */
.drop-wrap {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; text-align: center; position: relative; overflow: hidden;
  padding: 12px;
  border-style: dashed; border-width: 2px; border-color: var(--line);
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.drop-wrap::before {
  content: ""; position: absolute; inset: 0;
  background: url("./vendor/bids-logo.png") center/72% no-repeat;
  opacity: 0.16; pointer-events: none;
}
/* The logo is near-black artwork on transparency, so on a dark panel it needs
   inverting to read at all — at 7% unfiltered it was invisible. Keyed to the app's
   own mode and never to `prefers-color-scheme`: the theme a reader picked here is
   the only thing that says whether this panel is dark, and a light theme on a dark
   OS would otherwise invert the logo against a pale panel. */
:root[data-mode="dark"] .drop-wrap::before { filter: invert(1); opacity: 0.13; }
.drop-wrap.drop-over {
  border-color: var(--accent);
  background-color: color-mix(in srgb, var(--accent) 8%, var(--panel));
}
.drop-wrap.drop-over::before { opacity: 0.26; }
.drop-title {
  font: 700 13px var(--font-ui); color: var(--ink); position: relative;
}
.drop-sub {
  font-size: 11px; color: var(--muted); max-width: 34ch; position: relative;
  line-height: 1.45;
}
.drop-actions { display: flex; gap: 8px; position: relative; }
.drop-actions button { font-size: 12px; padding: 6px 14px; }
/* Tinted rather than solid: an invitation on a panel that is mostly empty
   space, not a primary action competing with Fit. */
.accent-btn {
  background-color: color-mix(in srgb, var(--accent) 22%, transparent);
  background-image: var(--grad-soft);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--ink); font-weight: 600;
}
.accent-btn:hover {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
  border-color: var(--accent);
}
.drop-privacy {
  font-size: 10px; color: var(--muted); position: relative;
  text-transform: uppercase; letter-spacing: .07em;
}

/* Download progress: navbar, immediately left of the status label. Compact
   enough to sit in a 30px bar, and gone when nothing is downloading. */
.navbar-dl { display: flex; align-items: center; gap: 7px; }
.navbar-dl[hidden] { display: none; }
.dl-ring { width: 18px; height: 18px; }
.dl-track, .dl-arc { fill: none; stroke-width: 10; }
.dl-track { stroke: var(--line); }
.dl-arc {
  stroke: var(--rust); stroke-linecap: round;
  /* 2πr for r=36 in the SVG's own 84-unit box. */
  stroke-dasharray: 226.19; stroke-dashoffset: 226.19;
  transform: rotate(-90deg); transform-origin: 50% 50%;
  transition: stroke-dashoffset 0.15s linear;
}
.dl-pct { font: 700 11px var(--font-mono); color: var(--rust); }
.dl-pct[hidden] { display: none; }
/* Before the response headers arrive there is no total to measure against, so
   the ring spins rather than sitting at 0% through the part of the wait that
   is actually the longest (redirects and time-to-first-byte). */
.navbar-dl.pending .dl-arc {
  stroke-dasharray: 58 170;
  transition: none;
  animation: dl-spin 0.9s linear infinite;
}
@keyframes dl-spin {
  from { transform: rotate(-90deg); }
  to { transform: rotate(270deg); }
}
@media (prefers-reduced-motion: reduce) {
  .navbar-dl.pending .dl-arc { animation: none; stroke-dashoffset: 113; }
}

/* Status severity. "Ready" and "Downloading…" are the two a reader watches
   for, so each gets a colour and weight rather than uniform grey. */
#status.ok { color: var(--good); font-weight: 700; }
#status.busy { color: var(--rust); font-weight: 700; }
#status.error { color: var(--bad); font-weight: 700; }
/* A lit dot ahead of the status text, coloured from the element's own `color`
   rather than a per-kind rule: `currentColor` tracks whichever of the states
   above is active (and the plain `info` state) with one declaration, so a
   future status kind inherits the dot for free. Decorative, so it lives on
   `::before` rather than as a markup element, keeping it out of the
   accessibility tree. */
#status::before {
  content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 6px;
  border-radius: 50%; background: currentColor;
  box-shadow: 0 0 5px currentColor, 0 0 2px currentColor;
  vertical-align: middle; transform: translateY(-1px);
}

.ghost-btn {
  background: none; border: 1px solid var(--line); color: var(--ink-2);
  border-radius: 6px; padding: 4px 9px; font-size: 12px; cursor: pointer;
}
.ghost-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-2); }
.ghost-btn:disabled { opacity: 0.45; cursor: default; }

/* A notice, as opposed to the two viewer modals: this one carries a sentence the
   reader has to act on, so it is narrow, centred and has a single way out. */
.notice-card {
  /* Sizing comes from `.modal-card.compact`; this is only the message layout. */
  padding: 20px 22px 18px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.notice-icon { color: var(--bad); display: flex; }
.notice-title { font: 600 15px var(--font-h); color: var(--ink); margin: 0; }
.notice-body { color: var(--ink-2); font-size: 12.5px; line-height: 1.55; margin: 0; }
.notice-card button { min-width: 84px; }

/* Single-file viewer, so inspecting a path costs neither a tab round-trip
   nor the tree's scroll position. Dimmed rather than blacked out: a blur
   over near-black backdrop reads as flat grey, not glass, so the dim has to
   be light enough that the page still reads through it as shape and colour
   while the card in front still clearly outranks it. */
.modal {
  position: fixed; inset: 0; background: rgba(0,0,0,0.35);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal[hidden] { display: none; }
/* Glass recipe: see the shared rule below. */
.modal-card {
  border-radius: 10px;
  width: min(92vw, 940px); height: min(86vh, 720px);
  display: flex; flex-direction: column; padding: 10px; gap: 8px;
}
/* A dialog that carries a message, not a viewer. `.modal-card` fixes a height
   for an image or a file listing, which leaves a paragraph floating in an empty
   column. This is qualified by `.modal-card` and declared after it: an equally
   specific rule earlier in the file loses to it on source order alone. */
.modal-card.compact {
  width: auto;
  height: auto;
  max-width: min(92vw, 640px);
  max-height: 86vh;
}
.modal-card.compact .modal-body { flex: 0 1 auto; }
.modal-card.notice-card { max-width: 46ch; }
/* Every floating surface (this dialog card, the ROI toolbox, the
   segmentation menu, the navbar's narrow-screen panel) wears the tooltip's glass: the panel surface under
   half opacity over a heavy blur, an accent-tinted edge, and a shadow that
   combines outer depth, an accent halo and an inset top highlight — see
   `.tip` for why a tinted fill cannot do this instead. A dialog carries far
   more body text than a tooltip, so the fill runs richer than `.tip`'s 62%;
   72% is still comfortably inside the contrast `--ink` keeps over
   `--panel-2` at any usable alpha, and is what keeps a paragraph legible
   over a moving backdrop where 62% was only ever measured for a few words. */
.modal-card, .draw-palette, .menu, .navbar-more {
  background: color-mix(in srgb, var(--panel-2) 72%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.8);
  backdrop-filter: blur(16px) saturate(1.8);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, .28),
    0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--ink) 14%, transparent);
}

.modal-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.modal-head .files-name {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
}
.modal-head button {
  background: none; border: 0; color: var(--ink); cursor: pointer; font-size: 16px;
}
.modal-body { display: flex; gap: 12px; flex: 1 1 auto; min-height: 0; }
.json-view {
  flex: 1 1 auto; min-height: 0; overflow: auto; margin: 0;
  background: var(--field); border: 1px solid var(--line); border-radius: 9px;
  padding: 12px; font: 12px/1.55 var(--font-mono); white-space: pre;
}
.modal-body .viewer { flex: 1 1 auto; height: auto; }

/* Window/level control: the map's own value histogram beside a colour bar
   whose two handles are the display window. Reading the histogram is how you
   see *why* a window is wrong — where the tissue actually sits versus where
   the scale currently ends. */
.level { display: flex; gap: 6px; flex: 0 0 auto; padding: 2px 0; }
#mlevel { width: 74px; }
#mlevel .level-hist { width: 46px; }
.level[hidden] { display: none; }
.level-hist {
  width: 52px; height: 100%; border-radius: 6px; background: var(--panel-2);
}
.level-bar {
  position: relative; width: 22px; border-radius: 999px;
  background: var(--panel-2); touch-action: none;
}
.level-grad { position: absolute; inset: 0; border-radius: 999px; }
/* A handle is a rounded cap straddling its edge of the window, like a slider
   thumb on the bar itself. */
.level-handle {
  position: absolute; left: -3px; right: -3px; height: 14px; cursor: ns-resize;
  border-radius: 999px; background: var(--panel);
  border: 2px solid var(--ink-2); box-shadow: var(--shadow);
  transform: translateY(-50%);
}
.level-handle:hover, .level-handle:focus-visible {
  border-color: var(--accent); outline: none;
}
.level-handle.dragging { border-color: var(--accent); }
/* The value that a handle currently sits at, shown while it is touched. */
.level-handle > span {
  position: absolute; right: calc(100% + 8px); top: 50%; transform: translateY(-50%);
  font: 700 11px var(--font-mono); color: var(--ink); background: var(--panel);
  border: 1px solid var(--line); border-radius: 6px; padding: 1px 5px;
  white-space: nowrap; cursor: text;
}
.level-handle:hover > span, .level-handle.dragging > span { border-color: var(--accent); }
/* Typing a bound directly, for when a drag is not precise enough. */
.level-edit {
  position: absolute; right: calc(100% + 8px); top: 50%; transform: translateY(-50%);
  width: 8ch; font: 700 11px var(--font-mono); padding: 1px 4px; border-radius: 6px;
  z-index: 2;
}
/* Value under the pointer as it moves over the bar. */
.level-readout {
  position: absolute; right: calc(100% + 8px); transform: translateY(-50%);
  font: 11px var(--font-mono); color: var(--muted); background: var(--panel);
  border: 1px solid var(--line); border-radius: 6px; padding: 1px 5px;
  white-space: nowrap; pointer-events: none;
}
.level-readout[hidden] { display: none; }
/* Where the voxel under the cursor sits on the scale — the link between a
   place in the image and a number on the colour bar. */
.level-dot {
  position: absolute; left: 50%; width: 13px; height: 13px; border-radius: 50%;
  background: transparent; border: 3px solid #fff;
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.55);
  transform: translate(-50%, 50%); pointer-events: none;
}
.level-dot[hidden] { display: none; }
/* The hovered voxel's own value, offset far enough left to clear the bar.
   Accent-tinted so it reads as "where you are" rather than as one of the
   window's bounds, which are the plain labels at the handles. */
.level-dot > span {
  position: absolute; right: calc(100% + 20px); top: 50%; transform: translateY(-50%);
  font: 700 11px var(--font-mono); color: var(--accent-ink);
  background: var(--panel); border: 1px solid var(--accent);
  border-radius: 6px; padding: 1px 5px; white-space: nowrap;
}

/* Fitted-map viewer + its colour-scale legend, side by side. NiiVue's own
   `isColorbar` only draws horizontally along the canvas's bottom edge in
   this vendored build, with no vertical/right-side placement, so this is a
   plain HTML/CSS bar built from the same LUT `nv.colormap()` returns. */
.viewer-row { display: flex; flex-direction: row; gap: 8px; flex: 1 1 auto; min-height: 0; }
.viewer-row .viewer { flex: 1 1 auto; }


.out-controls { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.out-controls select { font-size: 12px; padding: 4px 8px; }

.voxel-value { color: var(--ink-2); font: 12px var(--font-mono); margin: 2px 0 8px; }

/* ECharts follows this container's size (a ResizeObserver in `curve.js` calls
   its `resize`), so the container flexes and the chart tracks it. */
#curve { width: 100%; flex: 1 1 auto; min-height: 0; }
#curve-note { color: var(--muted); margin-top: 4px; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* The measurements table. Numbers are the product here, so they get the monospace
   column alignment that makes a column of them comparable at a glance. */
.measure-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
}
.measure-summary { color: var(--muted); font-size: 11.5px; flex: 1 1 auto; }
.measure-head button { font-size: 11px; padding: 3px 8px; }
.measure-table { overflow: auto; flex: 1 1 auto; min-height: 0; }
.measure-table table { width: 100%; border-collapse: collapse; }
.measure-table th {
  position: sticky; top: 0; z-index: 1; background: var(--panel);
  font: 600 9.5px var(--font-mono); color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em; text-align: right;
  padding: 4px 6px; border-bottom: var(--border-w) solid var(--line);
}
.measure-table td {
  font: 11px var(--font-mono); color: var(--ink);
  text-align: right; padding: 3px 6px;
  border-bottom: var(--border-w) solid color-mix(in srgb, var(--line) 45%, transparent);
}
.measure-table th:first-child, .measure-table td:first-child { text-align: left; }
.measure-label { display: flex; align-items: center; gap: 6px; }
/* The swatch is the label's own drawing colour, so a row and a region on the map
   are visibly the same object. */
.measure-chip {
  width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto;
  box-shadow: 0 0 0 1px var(--line);
}
.measure-name { font: 11px var(--font-ui); color: var(--ink-2); outline: none; }
.measure-name:focus { color: var(--ink); border-bottom: 1px solid var(--accent); }
#measure-view, #curve-view {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}
/* `[hidden]` is a UA rule at specificity 0,1,0 and loses to the ID selectors
   above, which is why both views rendered at once — the curve squeezed into half
   a card with the table's controls under it. Restated here so hiding wins. */
#measure-view[hidden], #curve-view[hidden] { display: none; }

/* The chart fills the modal it was given room in. */
.chart-body { flex: 1 1 auto; min-height: 0; width: 100%; }


/* The BIDS mark in place of the words "from sidecars": this value came from the
   dataset, not the recipe. Sized to the label's cap height so it reads as part
   of the line rather than as an image dropped into it. */
.bids-badge {
  display: inline-block;
  width: 15px;
  height: 15px;
  margin-left: 6px;
  vertical-align: -3px;
  opacity: 0.9;
  /* Two artworks, one mark: dark ink for a light panel, white for a dark one.
     Keyed to the app's own `data-mode` and never to `prefers-color-scheme` —
     the theme a reader picked here is the only thing that says which. */
  background: url("./vendor/bids-logo-light.png") center/contain no-repeat;
}
:root[data-mode="dark"] .bids-badge {
  background-image: url("./vendor/bids-logo-dark.png");
}
.group-title .bids-badge { vertical-align: -2px; }
/* On the label's second line the mark leads rather than trails, and sits on
   the text baseline of a 10px line rather than a 12px one. */
.field-meta .bids-badge { margin-left: 0; width: 13px; height: 13px; vertical-align: 0; }

/* Slide to override the sidecar-supplied protocol. An iOS slide-to-act pill:
   a recessed track, a raised knob carrying a direction chevron, and a prompt
   that fades as the knob travels. The same control sweeps back to return to
   the dataset's own values.

   Every colour is a theme token — `--bad` for the warning accent, `--on-action`
   for ink on it, `--field`/`--line` for the recess — so each palette dresses
   it in its own scheme rather than a fixed red. */
.override {
  --p: 0;                       /* knob travel, 0..1, set from the slider */
  --knob: 40px;
  /* Leaving the dataset behind is a warning, so the locked state wears the
     palette's warning tone; `.override.unlocked` below swaps in the accent one,
     because coming back to the dataset is not a warning. */
  --tone: var(--bad);
  --tone-ink: var(--on-action);
  position: relative;
  width: min(320px, 100%);
  margin: 18px auto 4px;
  height: 48px;
  border-radius: 999px;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--tone) 26%, var(--field)),
      color-mix(in srgb, var(--tone) 13%, var(--field)));
  box-shadow:
    inset 0 2px 5px rgb(0 0 0 / 0.45),
    inset 0 -1px 0 color-mix(in srgb, var(--tone) 22%, transparent),
    inset 0 0 0 1px color-mix(in srgb, var(--tone) 34%, transparent);
  overflow: hidden;
  isolation: isolate;
}

/* The interaction layer: a real range input, kept invisible so the drawn knob
   can carry a glyph a native thumb cannot. Every border and shadow the global
   input rule would paint is cleared here. */
.override-slider {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  outline: none;
  box-shadow: none;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  z-index: 3;
  cursor: grab;
}
.override-slider:active { cursor: grabbing; }
.override-slider:focus, .override-slider:focus-visible {
  border: none; box-shadow: none; outline: none;
}
.override-slider:focus-visible + .override-knob {
  box-shadow:
    0 2px 6px rgb(0 0 0 / 0.5),
    0 0 0 3px color-mix(in srgb, var(--tone) 50%, transparent);
}
.override-slider::-webkit-slider-runnable-track { background: transparent; height: 100%; }
.override-slider::-moz-range-track { background: transparent; height: 100%; }
.override-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  height: var(--knob); width: var(--knob);
  border: none; background: transparent;
}
.override-slider::-moz-range-thumb {
  height: var(--knob); width: var(--knob);
  border: none; background: transparent;
}

.override-knob {
  position: absolute;
  top: 4px;
  width: var(--knob);
  height: var(--knob);
  border-radius: 50%;
  /* Lit from above, with a rim highlight and a contact shadow: the two cues
     that read as a physical knob sitting in the recess. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--tone) 82%, var(--ink)),
    var(--tone) 55%,
    color-mix(in srgb, var(--tone) 88%, #000));
  box-shadow:
    0 2px 5px rgb(0 0 0 / 0.5),
    0 1px 0 color-mix(in srgb, var(--ink) 28%, transparent) inset,
    0 -2px 4px rgb(0 0 0 / 0.25) inset;
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 2;
  /* Travel spans the track minus the knob and its 4px insets; a percentage
     here resolves against `.override`, the containing block. */
  left: calc(4px + var(--p) * (100% - var(--knob) - 8px));
  transition: left 60ms linear;
}
.override-knob svg {
  width: 19px;
  height: 19px;
  /* The palette's own ink for action-coloured surfaces. */
  color: var(--tone-ink);
}

.override-label {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding-left: calc(var(--knob) + 6px);
  pointer-events: none;
  z-index: 1;
  font: 600 12.5px var(--font-ui);
  letter-spacing: 0.015em;
  color: color-mix(in srgb, var(--tone) 88%, var(--ink));
  text-shadow: 0 1px 1px rgb(0 0 0 / 0.35);
  transition: opacity 80ms linear;
}

/* Unlocked: the same pill in the accent tone, the sweep running the other way.
   Returning to the dataset's own values is the safe direction, so it should not
   wear the warning colour. */
.override.unlocked {
  --tone: var(--accent);
  --tone-ink: var(--on-accent);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--tone) 34%, var(--field)),
    color-mix(in srgb, var(--tone) 20%, var(--field)));
}
.override.unlocked .override-label {
  padding-left: 0;
  padding-right: calc(var(--knob) + 6px);
}

/* "Cite this work". The citation strings are pre-rendered into
   data/citation.json, so this dialog only picks between them.

   The shared `.modal-card` is a fixed viewer-sized box — right for a volume or
   a file listing, far too big for a paragraph of text. This one sizes to its
   content instead, capped so a long BibTeX entry scrolls rather than growing
   past the viewport. */

.cite-body {
  display: grid;
  gap: 10px;
  /* Rows take their content's height rather than splitting the card between
     them, which is what left the citation box mostly empty. */
  align-content: start;
  padding: 4px 4px 8px;
}
.cite-lead { margin: 0; color: var(--ink-2); font: 12.5px var(--font-ui); line-height: 1.5; }
.cite-controls { display: flex; gap: 8px; align-items: center; }
.cite-controls select { flex: 0 0 auto; }
#cite-copy { display: inline-flex; align-items: center; gap: 6px; }
.cite-text {
  margin: 0;
  padding: 10px 12px;
  /* A floor so switching between a two-line style and a long BibTeX entry does
     not make the dialog jump, and a ceiling so the long ones scroll. */
  min-height: 5.5em;
  max-height: 42vh;
  overflow: auto;
  background: var(--field);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font: 12px/1.6 var(--font-mono);
  color: var(--ink);
  /* BibTeX and RIS are line-oriented and keep their newlines; a prose citation
     is one long line and must wrap instead of scrolling sideways. */
  white-space: pre;
}
.cite-text.wrap { white-space: pre-wrap; overflow-wrap: anywhere; }
.cite-doi { color: var(--accent); font: 11.5px var(--font-mono); text-decoration: none; }
.cite-doi:hover { text-decoration: underline; }
/* A button in the navbar link row: same footprint and colour as its <a> siblings. */
button.navbar-link { background: none; border: none; padding: 0; cursor: pointer; }

/* A term named in a message: a config key, a field, a filename. Set apart from
   the prose without shouting, and it must not break the line box height. */
.notice-body code {
  font: 0.94em var(--font-mono);
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--field);
  border: 1px solid var(--line);
  color: var(--ink);
  white-space: nowrap;
}
