/* Styles for app.html. Split out of the page 2026-08-28: the stylesheet is
   274 lines that an agent editing the JS never needs to read, and vice versa.
   check_app.py asserts across app.html + app.css + web/app/*.js together, so
   a rule asserted there may live in either file. */
:root{
  --bg:#151515; --chrome:#1a1a1a; --panel:#212121; --sunken:#111111;
  --hover:#2b2b2b; --rule:#2e2e2e; --rule-strong:#3a3a3a; --rule-soft:#2a2a2a;
  --text:#ececec; --dim:#9a9a9a; --faint:#878787;
  --primary:#E8501F; --contextual:#B8391A; --error:#B8394B;
  --focus:#E8501F;
  /* feat 85: ink for text/icons drawn on top of --primary. Dark's value
     matches --bg (already AA on --primary); light needs true white --
     --text there fails on --primary, see the light block below. */
  --on-primary:#151515;
  /* feat 84 (A1): a five-step type scale, replacing 20 hand-picked sizes down
     to 9px. Floor is 11px -- nothing in the app chrome goes below it. */
  --fs-xs:11px; --fs-sm:12px; --fs-md:13px; --fs-lg:14px; --fs-xl:16px;
  /* Review status (feat 36 row 7): red / amber / green. `--bad` and `--ok` are
     colours the page already uses -- --error, and the axis gizmo's Y arm in
     app.html -- so amber is the one new value here, and it is the only place
     three-way compliance is expressed. Referenced by --bad/--warn/--ok rather
     than by hex at each use, so a colour-blind alternative later is one edit. */
  --bad:var(--error); --warn:#C9A227; --ok:#4C9A5C;
  /* feat 153 A4: step bar. Connector rust (done) / grey (not yet), and the selected step's orange text. */
  --step-rust:#B8391A; --step-line:#3A3A3A; --step-accent:#E8501F;
  /* ponytail: --rule is below 3:1 (WCAG 1.4.11) for input edges in both
     themes -- predates feat 85, a Phase 2 accessibility pass picks it up. */
  color-scheme: dark;
}

/* ===== light theme (feat 85) =====
   :root.light overrides every dark hex token above. Neutrals are NODEMENA's
   light Slate; dim/muted are replaced (NODEMENA's fail AA on --hover at
   2.56:1) and the accents are darkened until they pass AA as text. Ratios
   computed 2026-09-29 (WCAG relative luminance); worst case is on --hover.
   check_app.py's token-parity check (row 4) keeps this list matching :root. */
:root.light{
  --bg:#f8fafc; --chrome:#f1f5f9; --panel:#ffffff; --sunken:#f1f5f9;
  --hover:#e2e8f0; --rule:#cbd5e1; --rule-soft:#e2e8f0;
  --text:#0f172a; --dim:#475569; --faint:#556070;
  --primary:#B33B0B; --contextual:#9A3412; --error:#B91C1C;
  --focus:#B33B0B;
  --on-primary:#ffffff;
  --warn:#8A5A06; --ok:#13733A;
  --step-rust:#8F2D10; --step-line:#cbd5e1; --step-accent:#C43D14;
  color-scheme:light;
}
/* Self-hosted type (brand DESIGN.md "Delivery"); Inter replaced the Google Fonts
   link. Archivo and Plex Mono are used by the lab overlay only. */
@font-face{ font-family:'Archivo'; src:url(fonts/archivo-latin.woff2) format('woff2'); font-weight:600 800; font-stretch:100% 125%; font-display:swap; }
@font-face{ font-family:'Inter'; src:url(fonts/inter-latin.woff2) format('woff2'); font-weight:400 700; font-display:swap; }
@font-face{ font-family:'IBM Plex Mono'; src:url(fonts/plexmono-400-latin.woff2) format('woff2'); font-weight:400; font-display:swap; }
@font-face{ font-family:'IBM Plex Mono'; src:url(fonts/plexmono-500-latin.woff2) format('woff2'); font-weight:500; font-display:swap; }
:root{
  --font-display:'Archivo','Inter',system-ui,sans-serif;
  --font-body:'Inter',system-ui,-apple-system,"Segoe UI",sans-serif;
  --font-mono:'IBM Plex Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
}
/* Lab boot cover (app.html <head>): the modeler stays unpainted until lab.js
   has built its overlay. visibility, not display, so the viewport still sizes
   its canvas against a real layout while hidden. */
html.lab-boot body > :not(.lab):not(.boot){ visibility:hidden; }

/* ===== boot cover (feat 92 row 8, app.html #boot) ========================
   Shown in both modes from first paint until `nodevsp:ready`. Above everything,
   including the lab overlay, which is only built after ready. */
.boot{
  /* Brand values, as .lab does below (the modeler's own --primary is darker in light). */
  --primary:#E8501F; --contextual:#B8391A; --glow:rgba(232,80,31,.22); --glow2:rgba(232,80,31,.15);
  position:fixed; inset:0; z-index:10000; background:var(--bg); color:var(--text); isolation:isolate; overflow:hidden;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:12px; padding:24px; text-align:center; font-family:var(--font-body);
  transition:opacity .2s ease;
}
:root.light .boot{ --primary:#C43D14; --contextual:#8f2d10; --glow:rgba(196,61,20,.12); --glow2:rgba(196,61,20,.08); }
.boot.boot-out{ opacity:0; pointer-events:none; }
/* Glow From Above: two soft blobs whose centres stay in the top ~25% of the screen,
   drifting by transform only (no blur, no gradient animation). Offset in the
   ::before/::after box (inset -20%): screen y = -20% + box y * 1.4. */
.boot::before, .boot::after{ content:""; position:absolute; inset:-20%; z-index:-1; pointer-events:none; will-change:transform; animation-timing-function:ease-in-out; animation-iteration-count:infinite; animation-direction:alternate; }
.boot::before{ background:radial-gradient(ellipse 55% 30% at 30% 18%, var(--glow), transparent 65%); animation-name:bootDriftA; animation-duration:57s; }
.boot::after{ background:radial-gradient(ellipse 45% 26% at 72% 26%, var(--glow2), transparent 65%); animation-name:bootDriftB; animation-duration:43s; animation-delay:-17s; }
@keyframes bootDriftA{ from{ transform:translate(-6%,0) scale(1); } to{ transform:translate(9%,5%) scale(1.12); } }
@keyframes bootDriftB{ from{ transform:translate(7%,4%) scale(1.08); } to{ transform:translate(-8%,-1%) scale(1); } }
.boot .boot-mark{ width:88px; height:88px; }   /* beats .c-mark (15px, later in the file) */
.boot-word{ font-family:var(--font-display); font-weight:800; font-stretch:125%; font-size:22px; letter-spacing:.1em; padding-left:.1em; }
.boot-phrase{ margin:0; height:20px; display:flex; align-items:center; gap:8px; font-family:var(--font-mono); font-weight:500; font-size:13px; letter-spacing:.02em; color:var(--dim); }
.boot-dot{ width:6px; height:6px; border-radius:50%; background:var(--primary); flex:none; }
.boot-phrase span{ display:block; animation:bootPhrase 2.2s cubic-bezier(.16,1,.3,1) both; }
@keyframes bootPhrase{ 0%{ opacity:0; transform:translateY(4px); } 18%,82%{ opacity:1; transform:none; } 100%{ opacity:0; transform:translateY(-4px); } }
/* #boot-msg is the one stable status text: screen-reader only until it carries an error. */
.boot:not(.boot-err) .boot-msg{ position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; }
.boot.boot-err .boot-phrase{ display:none; }
.boot.boot-err::before, .boot.boot-err::after{ animation-play-state:paused; }
.boot-msg{ margin:0; font-size:var(--fs-lg); }
.boot-sub{ margin:0; max-width:36ch; min-height:1.4em; font-size:var(--fs-md); color:var(--dim); }
.boot.boot-err .boot-msg{ color:var(--error); }
.boot.boot-err .c-mark, .boot.boot-err .c-mark *{ animation:none; }
.boot-retry{
  margin-top:4px; min-height:36px; padding:0 18px; border:0; border-radius:6px; cursor:pointer;
  background:var(--primary); color:var(--on-primary); font-size:var(--fs-md); font-weight:600;
}
.boot-retry[hidden]{ display:none; }
@media (prefers-reduced-motion:reduce){ .boot{ transition:none; } .boot::before, .boot::after{ animation:none; } .boot-phrase span{ animation:none; } }
*{ box-sizing:border-box; }
html,body{ height:100%; }
html{ scrollbar-color:var(--rule) var(--bg); scrollbar-width:thin; }
body{
  margin:0; background:var(--bg); color:var(--text); overflow:hidden;
  font-family:Inter,system-ui,-apple-system,"Segoe UI",sans-serif;
  -webkit-font-smoothing:antialiased;
  display:flex; flex-direction:column;
}
::-webkit-scrollbar{ width:10px; height:10px; }
::-webkit-scrollbar-track{ background:var(--bg); }
::-webkit-scrollbar-thumb{ background:var(--rule); border-radius:5px; border:2px solid var(--bg); }
button,input{ font-family:inherit; }
:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
[disabled]{ opacity:.38; cursor:default; }

/* ---- tab strip ----
   .app-tabs is the outer bar; the generated tab list lives one level deeper
   in .app-tabs-tabs (id="tabs") so the static collapse/prefs buttons that
   also live in .app-tabs don't inherit tab-button styling from a bare
   descendant selector. position:relative makes .app-tabs the containing
   block for .prefs-panel's absolute popover. */
.app-tabs{
  display:flex; align-items:stretch; height:38px; background:var(--chrome);
  border-bottom:1px solid var(--rule); flex:none; position:relative;
}
.app-brand{ display:flex; align-items:center; gap:10px; padding:0 14px 0 12px; flex:none; font-family:var(--font-display); font-weight:800; font-stretch:125%; letter-spacing:.1em; font-size:var(--fs-md); }   /* the nodevsp.com wordmark: Archivo 800, 125 % wide */
.acct-slot{ display:flex; align-items:center; flex:none; }
.acct-slot:empty{ display:none; }
@media (max-width:1099px){ .app-brand span{ display:none; } }
.app-tabs-tabs{ display:flex; align-items:stretch; flex:1; min-width:0; overflow-x:auto; }
.app-tabs-tabs button{
  border:0; background:transparent; color:var(--dim); font:inherit; font-size:var(--fs-md);
  padding:0 18px; cursor:pointer; position:relative;
}
/* feat 153 A4: the selected tab is orange text only -- no box, no outline, no capsule. */
.app-tabs-tabs button:not([disabled]):hover{ color:var(--text); }
.app-tabs-tabs button[aria-selected="true"]{ color:var(--step-accent); }
.app-tabs-tabs button[aria-selected="true"]::after{ content:""; position:absolute; left:8px; right:8px; bottom:0; height:2px; background:var(--primary); }   /* the orange underline (restored 2026-10-28) */
.app-tabs-tabs button[data-label="File"]{ margin-right:10px; }
.app-tabs-tabs button[data-label="Help"]{ margin-left:10px; }
.app-tabs-tabs button.tabico{ display:flex; align-items:center; gap:7px; flex:none; white-space:nowrap; padding:0 10px; }   /* the step tabs' padding, so the underline hangs the same */
.app-tabs-tabs button.tabico .ico{ width:16px; height:16px; }

.app-tabs-tabs button.step{ display:flex; align-items:center; gap:7px; flex:none; padding:0 10px; white-space:nowrap; }
.st-badge{
  width:18px; height:18px; border-radius:50%; flex:none; display:inline-flex; align-items:center; justify-content:center;
  border:1.5px solid transparent; color:var(--faint); font-size:var(--fs-xs); font-weight:600; line-height:1;
}
.st-badge[data-state="done"]{ background:var(--ok); color:var(--on-primary); }
.st-badge[data-state="current"]{ border-color:var(--dim); color:var(--text); }
.st-badge[data-state="stale"]{ border-color:var(--warn); color:var(--warn); }
.st-link{ flex:1 1 12px; min-width:12px; height:2px; align-self:center; background:var(--step-line); }
.st-link[data-done="true"]{ background:var(--step-rust); }
/* Narrow bar: icons, plus the label of the current step only. */
@media (max-width:1099px){
  .app-tabs-tabs button.step{ padding:0 6px; }
  .app-tabs-tabs button.step:not([aria-selected="true"]) .st-label{ display:none; }
}
/* Step sidebar (filled by later feat 153 rows) and the one-line status at the bottom. */
.stepside{ width:var(--wt-w, 250px); flex:none; background:var(--chrome); border-right:1px solid var(--rule); overflow-y:auto; }
.stepside[hidden]{ display:none; }
body[data-step="Mesh"] :is(.worktree, .options), body[data-step="Analysis"] :is(.worktree, .options),
body[data-step="Results"] :is(.worktree, .options), body[data-step="File"] :is(.worktree, .options), body[data-step="Examples"] :is(.worktree, .options), body[data-step="Help"] :is(.worktree, .options){ display:none; }   /* Home swaps the tree and the options for the summary; opening a model sends the user to Geom (chrome.js nodevsp:step) */
/* The 2D previews (front/side panes, tables, wing 2D) and their splitters belong to Geom: steps 2-4 and Home get the whole viewport. */
body:not([data-step="Geom"]) :is(.body-overlay, .body-split, .body-dock-preview){ display:none !important; }   /* !important: bodyoverlay.js sets display inline for the wing planform/front/side panes */
body:not([data-step="Geom"]) .centre{ grid-template-columns:minmax(0,1fr) !important; grid-template-rows:minmax(0,1fr) !important; }
.statusline{
  flex:none; height:22px; display:flex; align-items:center; padding:0 12px; background:var(--chrome);
  border-top:1px solid var(--rule); color:var(--dim); font-size:var(--fs-xs);
}

/* ===== ribbon + step bar, to the frames (feat 153) =====
   Group captions under the groups, hairline dividers, the large item's icon bigger, the tab's primary action in
   orange, an active item outlined in orange. On the three step tabs (Mesh, Analysis, Results) the small items are
   checkbox rows, as in the frames; Home, Geom and Help keep icon rows. */
.r-btn:not(.sm) .ico{ width:28px; height:28px; }
.r-btn.accent .ico{ color:var(--primary); }
.r-btn[aria-pressed="true"]{ outline:1px solid var(--primary); outline-offset:-1px; }
/* step labels: STEP n over the name */
.st-label{ display:flex; flex-direction:column; align-items:flex-start; line-height:1.15; }
.st-no{ font-size:9px; font-weight:600; letter-spacing:.08em; color:var(--faint); }
.st-name{ font-size:var(--fs-md); }
.app-tabs-tabs button[aria-selected="true"] .st-no{ color:var(--step-accent); }
/* Every tab, step or page, shows its selection the same way: orange text and the 2 px orange underline on the bar's edge
   (no pill; one inset, one padding -- homogenised 2026-10-28). */

/* ===== Preferences dialog (feat 153, Preferences frame) =====
   The old popover's controls under a side navigation; the backdrop is the dialog's own box-shadow. */
.prefs-panel.prefs-modal{
  position:fixed; top:50%; left:50%; right:auto; transform:translate(-50%,-50%); z-index:90;
  width:min(740px, 94vw); height:min(540px, 88vh); max-height:none; padding:0; border-radius:12px; overflow:hidden;
  display:grid; grid-template-columns:200px minmax(0,1fr); box-shadow:0 0 0 100vmax rgba(0,0,0,.55);
}
.prefs-panel.prefs-modal[hidden]{ display:none; }
.pm-nav{ display:flex; flex-direction:column; gap:2px; padding:20px 14px; border-right:1px solid var(--rule); background:var(--chrome); }
.pm-nav h2{ margin:0 0 14px 8px; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; }
.pm-nav button{ display:flex; align-items:center; gap:10px; padding:8px; border:0; background:transparent; color:var(--dim); font:inherit; text-align:left; cursor:pointer; border-radius:6px; }
.pm-nav button::before{ content:""; width:7px; height:7px; box-sizing:border-box; border:1.5px solid var(--dim); border-radius:50%; flex:none; }
.pm-nav button:hover{ color:var(--text); }
.pm-nav button[aria-current="true"]{ color:var(--text); font-weight:600; }
.pm-nav button[aria-current="true"]::before{ background:var(--primary); border-color:var(--primary); }
.pm-nav button:focus-visible{ outline:2px solid var(--focus); outline-offset:-2px; }
.pm-main{ display:flex; flex-direction:column; min-height:0; padding:24px 28px 0; }
.pm-sec{ flex:1; min-height:0; overflow-y:auto; padding-right:4px; }
.pm-sec[hidden]{ display:none; }
.pm-sec > h2{ margin:0 0 14px; font-family:var(--font-display); font-size:24px; font-weight:700; }
.pm-sec h3{ border-top:0; padding-top:10px; }
.pm-system{ margin-bottom:12px; }
.pm-rows{ border-top:1px solid var(--rule); }
.pm-row{ display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:44px; border-bottom:1px solid var(--rule-soft); font-size:var(--fs-md); }
.pm-val{ font-family:var(--font-mono); color:var(--dim); }
.pm-fixed .pm-val{ color:var(--faint); }
.pm-row .hdr-select{ margin:0; width:auto; border:0; text-align:right; color:var(--text); font-family:var(--font-mono); }
.pm-foot{ flex:none; display:flex; justify-content:flex-end; gap:10px; padding:14px 0 18px; }
.pm-foot .an-btn{ min-width:110px; min-height:36px; }

/* ===== collapse toggles + layout presets (feat 14) ======================
   .panel-collapse is the one small icon-button look shared by the three
   collapse buttons (#tree-collapse, #options-collapse, #ribbon-collapse) --
   same shape as .chat .c-toggle below, just not scoped to .chat. */
.panel-collapse{
  border:0; background:transparent; color:var(--faint); cursor:pointer;
  width:24px; height:24px; flex:none; align-self:center;
  display:flex; align-items:center; justify-content:center; border-radius:4px;
}
.panel-collapse:hover{ background:var(--hover); color:var(--text); }

/* feat 84 row 1 (A7/A11 decision): classes for two controls chrome.js builds
   with `element.style.cssText` today (the header length-unit <select> and the
   tree header's clear/trash button) -- row 2 switches those call sites to
   these classes, this row only adds the rule. */
.hdr-select{
  align-self:center; margin-left:4px; background:var(--panel); color:var(--dim);
  border:1px solid var(--rule-strong); border-radius:4px; padding:4px 7px;
  font:inherit; font-size:var(--fs-sm); cursor:pointer;
}
.hdr-select:hover{ color:var(--text); }
.wt-clear{
  background:var(--panel); color:var(--dim); border:1px solid var(--rule-strong);
  border-radius:4px; padding:4px 5px; display:flex; align-items:center;
  cursor:pointer; margin-right:auto;
}
.wt-clear:hover{ color:var(--error); border-color:var(--error); }

/* Ribbon has no header bar of its own (unlike worktree/options) -- collapse
   just removes it outright rather than hiding a body under a kept bar.
   `.ribbon-clip` (app.html) wraps it for hero mode's slide, below --
   `display:contents` here means it is not a layout box outside hero mode. */
#ribbon.collapsed{ display:none; }
.ribbon-clip{ display:contents; }

/* Preferences gear + popover -- proportions ported from flow.html:470-494,
   colours reused from this app's own tokens (--chrome/--rule/--dim/--hover/
   --text/--faint/--primary, all defined at app.css:5-11). */
.prefs-btn{
  align-self:center; margin-left:4px; width:28px; height:28px; flex:none;
  display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; border-radius:4px; color:var(--dim); cursor:pointer;
}
.prefs-btn:hover, .prefs-btn[aria-expanded="true"]{ background:var(--hover); color:var(--text); }
/* Its own `display:flex` above beats the UA [hidden] rule, same trap as
   .chat/.param/.foil-fields (web/README.md documents it) -- lab mode
   (chrome.js guard 5) sets `.hidden` on both the button and the popover. */
.prefs-btn[hidden]{ display:none; }

.prefs-panel{
  position:absolute; top:44px; right:8px; width:260px; z-index:20;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:8px; padding:14px;
  box-shadow:0 16px 36px rgba(0,0,0,.45);
}
.prefs-panel h3{ margin:0 0 3px; font-size:var(--fs-md); font-weight:600; color:var(--text); }
.prefs-panel .prefs-sub{ margin:0 0 12px; font-size:var(--fs-sm); color:var(--faint); }
/* feat 85: the Theme .seg sits above Layout's h3 -- same 12px a prefs-sub
   leaves under itself, so the two stacked groups read as separate. */
.prefs-panel .seg{ margin-bottom:12px; }
.prefs-row{
  display:block; width:100%; text-align:left; border:0; background:transparent;
  padding:8px 9px; border-radius:5px; cursor:pointer; margin-bottom:2px; font:inherit;
}
.prefs-row:hover{ background:var(--hover); }
.prefs-row .pr-name{ display:block; font-size:var(--fs-md); font-weight:600; color:var(--text); }
.prefs-row .pr-desc{ display:block; font-size:var(--fs-xs); color:var(--faint); margin-top:1px; }

/* ---- hero mode (dev only, ?hero -- app.html sets body.hero-mode) ----
   A capture aid for recording a collapse-then-reveal video, not a feature:
   the three existing collapse buttons (#tree-collapse/#options-collapse/
   #ribbon-collapse, wired in chrome.js) slide instead of instantly popping.
   Their `.collapsed` class and click handlers are untouched -- this only
   adds a transition on top of the same state change.
   Worktree/options: `width` already carries a real from/to value (var(--wt-w)
   or 250px fallback <-> 40px, app.css below), so transitioning it directly is
   the "reclaim real layout space" case a transform/opacity substitute cannot
   cover -- .viewport's own ResizeObserver (viewport.js) already re-syncs the
   canvas on every intermediate frame, same as a manual drag. `.resizing`
   (chrome.js's drag handles) turns the transition back off for that drag --
   a live pointermove sets the same property every frame, and a transition
   would lag a step behind the cursor. `overflow-x:hidden` throughout (not
   only while `.collapsed`) keeps the icon-rail content clipped mid-slide. */
body.hero-mode .worktree, body.hero-mode .options{
  transition:width .3s ease; overflow-x:hidden;
}
body.hero-mode .worktree.resizing, body.hero-mode .options.resizing{ transition:none; }
/* Ribbon: `#ribbon.collapsed` is plain `display:none` above, an all-or-
   nothing swap with no from/to size to animate. `.ribbon-clip` instead
   animates `grid-template-rows` (0fr <-> 1fr) around the un-collapsed
   `.ribbon` -- the auto-height-reveal pattern, since the ribbon's own height
   varies with fitRibbon()'s zoom and is never a fixed
   number here. `:has()` is Chromium-only, same precedent already used below
   (`.tree .item-row:has(...)`) -- this app already targets Chromium. */
body.hero-mode .ribbon-clip{
  display:grid; grid-template-rows:1fr; overflow:hidden;
  transition:grid-template-rows .3s ease;
}
body.hero-mode .ribbon-clip:has(#ribbon.collapsed){ grid-template-rows:0fr; }
body.hero-mode #ribbon.collapsed{ display:flex; }

/* ---- ribbon ---- */
.ribbon{
  display:flex; align-items:stretch; background:var(--panel);
  border-bottom:1px solid var(--rule); flex:none; overflow-x:auto;
}
.r-group{
  display:flex; flex-direction:column; padding:6px 10px 0;
  border-right:1px solid var(--rule-soft); flex:none;
}
/* A fixed row height -- three 22px small buttons + two 1px gaps -- so every
   tab's ribbon is the same height by construction, whatever its groups hold
   (File's Import + unit column outgrew it once the icons went to 24px). */
.r-row{ display:flex; align-items:center; gap:4px; height:68px; }
/* Large buttons centre on the row; a short column of small ones stays on the
   top rows of the three-row grid, like File's Exchange group. */
.r-col{ display:flex; flex-direction:column; gap:1px; align-self:flex-start; }
/* margin-top:auto pins the caption to the bottom, so a group with one short
   column (Propulsion) lines its label up with its neighbours'. */
.g-label{ margin-top:auto; text-align:center; font-size:var(--fs-xs); color:var(--dim); letter-spacing:.05em; padding:4px 0 5px; }
/* width:auto up to 84px keeps "Landing Gear" on one line (a 60px box wrapped
   it and made that group read taller than the rest), while a really long
   caption still wraps. */
.r-btn{
  min-width:60px; max-width:84px; display:flex; flex-direction:column; align-items:center; gap:3px;
  padding:3px 4px; border:0; background:transparent; color:var(--dim);
  border-radius:4px; cursor:pointer; transition:background-color .1s ease, color .1s ease;
}
.r-btn:not([disabled]):hover{ background:var(--hover); color:var(--text); }
.r-btn[aria-pressed="true"]{ color:var(--primary); }   /* Help -> Viewport tips */
.r-btn .cap{ font-size:var(--fs-xs); text-align:center; line-height:1.15; }
.r-btn.sm{
  min-width:0; max-width:none; flex-direction:row; align-items:center; gap:6px;
  height:22px; padding:0 7px; text-align:left;
}
/* Small-button caption stays one step up from the large row's (--fs-sm not
   --fs-xs): a .sm row has room and reads better a notch bigger. */
.r-btn.sm .cap{ font-size:var(--fs-sm); white-space:nowrap; text-align:left; }
/* Icon as a CSS mask so the theme colour paints it and a disabled button dims
   it. The filename is the wiring -- see icons/README.md. */
.ico{
  width:17px; height:17px; flex:none; background:currentColor;
  -webkit-mask:var(--i) center/contain no-repeat; mask:var(--i) center/contain no-repeat;
}
/* Ribbon icons were 17/14px; 24/18 (owner, 2026-09-26: "make the icons
   bigger"). Both still fit the existing boxes -- a large button is shorter
   than its three-row column of small ones, and a small row is 22px -- so the
   ribbon keeps its height. Scoped to .r-btn: tree rows keep 17px. */
.r-btn .ico{ width:24px; height:24px; }
.r-btn.sm .ico{ width:18px; height:18px; }
/* examples.js: the model-header Examples button is LAB MODE only; everyone
   else opens the gallery from File -> Project -> Examples. */


/* ---- work area ---- */
.workarea{ flex:1; display:flex; min-height:0; position:relative; }

/* width is a custom prop (feat 64 row 1: drag-resize), 250px fallback keeps
   the old default for anyone reading just this rule. `position:relative`
   anchors the resize handle appended into this panel below -- checked live,
   nothing else in here assumes `absolute` against a different ancestor (the
   appearance popover and length-unit select are parked on <body>, not here). */
.worktree{
  width:var(--wt-w, 250px); flex:none; background:var(--chrome); border-right:1px solid var(--rule);
  display:flex; flex-direction:column; overflow-y:auto; position:relative;
}
.worktree h2, .options h2, .chat h2{
  margin:0; padding:14px 16px 8px; font-size:var(--fs-xs); letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim);
}
/* Header row wrapping the h2 + collapse button (feat 14). .collapsed hides
   everything else in the panel and leaves this row as the panel's whole
   footprint -- same "keep the bar" idea as .chat.compact, different shape
   since these panels have a real header instead of an always-on bar. */
.wt-head, .o-head{ display:flex; align-items:center; justify-content:space-between; padding-right:12px; }
/* Collapsed shrinks the panel itself to an icon rail -- not just its content
   -- so .viewport reclaims the freed width (ResizeObserver in viewport.js
   picks up the resulting box change and keeps the canvas in sync). */
.worktree.collapsed, .options.collapsed{ width:40px; overflow-x:hidden; }
.options.collapsed > *:not(.o-head){ display:none; }
/* The worktree keeps its TREE when collapsed -- an icon rail, so the panel
   still says what has been built and stays clickable, instead of going blank
   the way the options panel does (which has no such list). Everything with a
   width of its own goes: headings, names, the per-row toggles and the x. The
   rows themselves are untouched markup -- renderTree() does not know the panel
   is collapsed, and must not learn. */
.worktree.collapsed > *:not(.wt-head):not(.tree){ display:none; }
.worktree.collapsed .tree{ padding:0 4px 10px; }
.worktree.collapsed .tree-head, .worktree.collapsed .t-name,
.worktree.collapsed .t-tog, .worktree.collapsed .item-rm,
.worktree.collapsed .item-add, .worktree.collapsed .item-caret,
.worktree.collapsed .tree .empty{ display:none; }
.worktree.collapsed .t-row{ padding:6px 0; gap:0; justify-content:center; }
/* The rail is icons alone, so they carry the whole meaning: 22px, not 17. */
.worktree.collapsed .t-row .ico{ width:22px; height:22px; }
.worktree.collapsed .item-children{ margin-left:0; }
/* feat 21: a nested geom's row carries its own inline `margin-left` (depth *
   18px, chrome.js's renderTree()) -- meaningless once the rail is icon-only,
   and it would push a deep row's icon half out of the 40px strip. `!important`
   is the only way to beat an inline style from a stylesheet rule; same
   renderTree()-must-not-learn constraint as the rest of this block. */
.worktree.collapsed .item-row{ margin-left:0 !important; }
/* The rail's whole job is "what exists, and what is selected", so the icon
   itself carries the selection colour -- at 40px wide the row's own tint and
   left rule are too small a signal to read at a glance. */
.worktree.collapsed .t-row[aria-selected="true"] .ico{ color:var(--primary); }
.worktree.collapsed .wt-head, .options.collapsed .o-head{ justify-content:center; padding-right:0; }
.worktree.collapsed .wt-head h2, .options.collapsed .o-head h2{ display:none; }
.worktree.collapsed .panel-collapse svg, .options.collapsed .panel-collapse svg{ transform:rotate(180deg); }

.tree{ display:flex; flex-direction:column; padding:0 8px 14px; }
.tree .empty, .params .empty{ padding:6px 10px; font-size:var(--fs-sm); color:var(--faint); }
/* Sub-heading inside #tree for a top-level family (Surfaces, Bodies -- feat
   09 row 9). Smaller and inline vs. the panel's own .wt-head h2, which stays
   the panel's one true title; this is a second-level label, not a duplicate
   of it. */
.tree-head{
  margin:10px 0 4px; padding:0 2px; font-size:var(--fs-xs); letter-spacing:.06em;
  text-transform:uppercase; color:var(--dim);
}
.tree-head:first-child{ margin-top:2px; }
.t-row{
  display:flex; align-items:center; gap:7px; padding:6px 8px; border-radius:4px;
  cursor:pointer; font-size:var(--fs-md); color:var(--dim); text-align:left;
  background:transparent; border:0; border-left:2px solid transparent;
}
.t-row:hover{ background:var(--hover); color:var(--text); }
/* Selected PARENT: a full outline round the row. `box-shadow: inset` rather
   than `border`, so the ring appears without changing the row's box and
   shifting every label 1px when selection moves. */
.t-row[aria-selected="true"]{
  background:color-mix(in srgb, var(--primary) 16%, transparent);
  color:var(--text); border-left-color:var(--primary); font-weight:600;
  box-shadow:inset 0 0 0 1px var(--primary);
}
/* Selected CHILD (a control surface under its wing): text colour only. A
   child is a detail of the outlined parent, not a peer of it -- giving it
   the same ring reads as two things selected at once. */
.item-children .t-row[aria-selected="true"]{
  background:transparent; border-left-color:transparent; box-shadow:none;
  color:var(--primary);
}
.item-children .t-row[aria-selected="true"] .ico{ color:var(--primary); }
.item-row.picked .t-row{
  background:color-mix(in srgb, var(--primary) 9%, transparent);
  color:var(--text); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--primary) 55%, transparent);
}
.multi-actions{ display:flex; gap:6px; align-items:center; padding:4px 8px; font-size:var(--fs-sm); color:var(--dim); }
.multi-actions button{ background:none; border:0; padding:0; color:var(--primary); cursor:pointer; font:inherit; text-decoration:underline; }
.multi-actions{ flex-wrap:wrap; }
/* feat 70: wing-body blend group (blend.js). The tag is an outline chip, not a
   fill: it labels risk, it is not a button. */
.blend-tag{ font-size:var(--fs-xs); font-weight:600; letter-spacing:.04em; text-transform:uppercase;
  color:var(--text); border:1px solid var(--warn); border-radius:8px; padding:0 6px; margin-left:6px; vertical-align:middle; }
.blend-warn{ font-size:var(--fs-sm); color:var(--text); border:1px solid var(--warn); border-radius:6px; padding:4px 8px; }
.blend-warn[hidden]{ display:none; }
.blend-warn.busy{ border-color:var(--rule); color:var(--dim); }   /* feat 76: the fillet is computing, not failing */
.blend-remove{ align-self:flex-start; min-height:28px; padding:0 10px; background:none; color:var(--text);
  border:1px solid var(--rule-strong); border-radius:6px; cursor:pointer; font:inherit; font-size:var(--fs-sm); }
.blend-remove:hover{ border-color:var(--primary); }
.blend-remove:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }
.t-row .ico{ width:16px; height:16px; color:var(--contextual); }
.t-name{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* A row being renamed (widgets.js: onRename swaps the button for this field on
   a double click). Same height and radius as .t-row, so the list does not jump
   as the two swap. */
.t-rename{
  flex:1; min-width:0; padding:5px 7px; border-radius:4px; font:inherit; font-size:var(--fs-md);
  color:var(--text); background:var(--sunken); border:1px solid var(--primary);
}
.t-rename:focus{ outline:none; }
/* Per-geom visibility / edit-lock toggles. aria-pressed true == hidden/locked.
   24x24 minimum hit target (was 19x19 -- a 15px icon in 2px of padding). */
.t-tog{
  flex:none; display:flex; align-items:center; justify-content:center;
  min-width:24px; min-height:24px; padding:2px; border:0; border-radius:3px;
  background:transparent; color:var(--faint); cursor:pointer;
}
.t-tog:hover{ background:var(--rule-soft); color:var(--text); }
.t-tog[aria-pressed="true"]{ color:var(--primary); }
.t-tog .ico{ width:15px; height:15px; color:inherit; }

/* feat 84 (A7): eye/lock/remove read as always-on chrome on every row even
   when nothing is happening there. Dim them until the row is worth looking
   at -- hovered, focused, selected -- and always show a PRESSED toggle (a
   hidden or locked geom must stay visibly so, not just on hover).
   `:has()` is Chromium-only, which this app already targets. Scoped to
   `.tree` so the same classes elsewhere (station/curve item lists) are
   unaffected -- those rows have no hover-to-reveal convention. */
.tree .item-row .t-tog, .tree .item-row .item-rm{
  opacity:.45; transition:opacity .12s ease;
}
.tree .item-row:hover .t-tog, .tree .item-row:hover .item-rm,
.tree .item-row:focus-within .t-tog, .tree .item-row:focus-within .item-rm,
.tree .item-row:has(.t-row[aria-selected="true"]) .t-tog,
.tree .item-row:has(.t-row[aria-selected="true"]) .item-rm,
.tree .item-row .t-tog[aria-pressed="true"]{
  opacity:1;
}
@media (prefers-reduced-motion:reduce){
  .tree .item-row .t-tog, .tree .item-row .item-rm{ transition:none; }
}

.viewport{ flex:1; background:var(--sunken); position:relative; min-width:0; }

/* ===== centre column: 3D cell + docked body panes (feat 108 row 1) =====
   `.centre` holds #view and, when a FUSELAGE/STACK is selected, the front
   pane (right) and side pane (bottom, full width) with two splitters. The
   grid tracks are written by bodyoverlay.js's layout(); this block only
   places the items. #view stays the 3D cell, untouched. */
.centre{ flex:1; min-width:0; min-height:0; display:grid; position:relative; }
.centre > .viewport{ grid-column:1; grid-row:1; min-width:0; min-height:0; }
.body-split{ position:relative; touch-action:none; }
.body-split[hidden]{ display:none; }
.body-split::after{ content:""; position:absolute; background:var(--rule); }
.body-split-v{ grid-column:2; grid-row:1; cursor:col-resize; }
.body-split-v::after{ top:0; bottom:0; left:50%; width:1px; transform:translateX(-50%); }
.body-split-h{ grid-column:1 / -1; grid-row:2; cursor:row-resize; }
.body-split-h::after{ left:0; right:0; top:50%; height:1px; transform:translateY(-50%); }
.body-split:hover::after, .body-split:focus-visible::after, .body-split.dragging::after{ background:var(--primary); }
.body-split:focus-visible{ outline:2px solid var(--primary); outline-offset:-2px; }
/* The size here is load-bearing, not tidiness. viewport.js calls
   `renderer.setSize(w, h, false)` -- the `false` is "CSS owns the layout size,
   do not write style.width" -- so without these two the canvas falls back to
   its INTRINSIC size, which is the drawing buffer: w * devicePixelRatio CSS
   px. That is only right at dpr 1. On a Windows display at 125%/150% (the
   laptop default) the canvas came out 25%/50% wider and taller than its box,
   overflowed, painted over the Options panel, and showed a cropped corner of
   the render. Stays in normal flow (never `position:absolute`), or it would
   paint over the gizmo/HUD/chat overlays instead of under them. */
.viewport canvas{ display:block; width:100%; height:100%; user-select:none; -webkit-user-select:none; }
.hint{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--dim); font-size:var(--fs-md); pointer-events:none;
}
.hud{
  position:absolute; top:12px; left:14px; pointer-events:none; white-space:pre;
  font:var(--fs-xs)/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; color:var(--faint);
}
/* feat 84 (A5): the dev HUD is opt-in via the viewport toolbar's Stats toggle,
   not always-on chrome a user never asked to see. */
body:not(.show-hud) .hud{ display:none; }

/* ---- the sync-error banner (feat 62 row 3) ----
   Replaces `.hud.err`, which was the whole report a rejected /aircraft POST
   ever got: 11px monospace in --faint, pointer-events:none, in the corner.
   Every later edit re-posts the same invalid spec, so the app reads as frozen
   rather than as having refused something. This sits across the top of the
   viewport instead, above the HUD's own corner (which app/viewport.js clears
   while this is up, so the two never stack), and it is the ONE thing in the
   viewport that takes the pointer -- `Ver detalle` has to be clickable and
   keyboard-reachable, which is why there is no pointer-events:none here.
   `[hidden]` needs restating because display:flex would otherwise win. */
.sync-err{
  position:absolute; top:10px; left:14px; right:14px; z-index:6;
  display:flex; flex-wrap:wrap; align-items:baseline; gap:8px 12px;
  padding:10px 12px; border-radius:5px;
  background:var(--panel); border:1px solid var(--error);
  box-shadow:0 6px 20px rgba(0,0,0,.45);
}
.sync-err[hidden]{ display:none; }
.sync-err-msg{
  margin:0; flex:1 1 220px; min-width:0; color:var(--text);
  font-size:var(--fs-md); line-height:1.45;
}
.sync-err-more{
  flex:none; background:transparent; border:1px solid var(--rule-strong); color:var(--dim);
  font:inherit; font-size:var(--fs-sm); padding:5px 10px; border-radius:4px; cursor:pointer;
}
.sync-err-more:hover{ color:var(--text); border-color:var(--error); }
.sync-err-raw{
  flex-basis:100%; margin:0; max-height:130px; overflow:auto; white-space:pre-wrap;
  word-break:break-word; color:var(--faint);
  font:var(--fs-xs)/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
}

/* ---- the "updating" cue (app/viewport.js: updateSyncCue) ----
   Lives in .vp-status (top-right, under .viewbar; feat 96) with .blend-busy, so a
   sync and a fillet compute stack instead of overlapping; top-left is the HUD and
   toast's. .sync-err spans the top (and hides this while up). The outer div is a
   permanent live region with no box of its own; the pill is what shows. The
   boot cover's phrase pattern: the animated V mark + Plex Mono label (Three Voices Rule). */
/* feat 96: one right-aligned column under the toolbar (.viewbar top:10 + ~36px). */
.vp-status{
  position:absolute; top:52px; right:12px; z-index:3; pointer-events:none;
  display:flex; flex-direction:column; align-items:flex-end; gap:6px;
}
/* .blend-busy shares it: every pill in .vp-status looks the same. */
.sync-cue-pill, .blend-busy{
  display:flex; align-items:center; gap:8px; padding:5px 11px; border-radius:999px;
  background:var(--panel); border:1px solid var(--rule-strong); white-space:nowrap;
  font-family:var(--font-mono); font-weight:500; font-size:var(--fs-sm); letter-spacing:.02em; color:var(--dim);
  animation:cueIn .2s ease-out both;
}
.sync-cue-pill[hidden], .blend-busy[hidden]{ display:none; }
@keyframes cueIn{ from{ opacity:0; } to{ opacity:1; } }
@media (prefers-reduced-motion:reduce){ .sync-cue-pill, .blend-busy{ animation:none; } }   /* the V's own rule stills it (.c-mark) */

/* ---- preset toast (feat 62 row 2b) ----
   The fuselage preset's one-line description. It was a <p class="hint"> in the
   Options panel, and `.hint` is the EMPTY-VIEWPORT PLACEHOLDER -- inset:0 over
   .workarea -- so the sentence rendered centred across the entire work area
   and could not be clicked away. It is now what the owner asked for: the
   viewport's top-left corner, left-aligned, no box, fading out on its own.
   top:52 clears the toolbar row; with the HUD on (3 lines) it drops to 70.
   Transparent means the text sits straight on the render, so the shadow is
   load-bearing legibility, not decoration. Below the HUD's own corner, and
   pointer-events:none so it can never take a click meant for the canvas. */
.preset-toast{
  position:absolute; top:52px; left:16px; max-width:min(44ch, 46%); z-index:4;
  pointer-events:none; text-align:left;
  opacity:1; transition:opacity .6s ease;
  text-shadow:0 1px 4px var(--bg), 0 0 2px var(--bg);
}
.preset-toast.fade{ opacity:0; }
body.show-hud .preset-toast{ top:70px; }   /* clears the 3-line HUD (feat 84 A5) */
.preset-toast strong{
  display:block; color:var(--text); font-size:var(--fs-md); font-weight:600; letter-spacing:-.01em;
}
.preset-toast span{
  display:block; margin-top:3px; color:var(--dim); font-size:var(--fs-sm); line-height:1.5;
}
@media (prefers-reduced-motion: reduce){ .preset-toast{ transition:none; } }
/* An error outranks a caption. Measured at 1024px: with both up the toast's
   title rendered behind the banner and its body text hung out below, reading
   as one broken paragraph. A sibling combinator rather than :has() -- the two
   are both children of #view, and the banner is created at viewport.js's
   module load, so it is always the earlier sibling. */
.sync-err:not([hidden]) ~ .preset-toast{ display:none; }

/* ---- body cross-section overlays (feat 67 row 1, revised again) ----
   Two draggable, resizable boxes in #view holding fuselage.js's front/side
   SVG pictures -- the previewer that used to live inline in the Options
   panel. Frosted glass: a dark, semi-transparent fill over a
   `backdrop-filter` blur, so the picture reads with contrast against ANY
   render behind it (a light fuselage over a light patch of sky was the
   failure a fully transparent background had) while the viewport stays
   visible, softened, underneath -- not the opaque `var(--panel)` card
   `.sync-err` uses, and not `.preset-toast`'s "no box at all" either; this
   is the middle case, a box that still keeps `pointer-events` on because it
   must stay draggable/resizable. `.bodyview` (app.css:1398) carries its OWN
   opaque `--sunken` fill and border for its other context (a plot inline in
   the Options panel); the override below strips that back to nothing
   specifically INSIDE this overlay, so the sidebar's own use of `.bodyview`
   (the preset gallery, via `.bodyview-tile`, and any future inline plot) is
   untouched -- the frosted card is the ONE background now, not two stacked
   boxes. `overflow:hidden` keeps the picture's own square corners from
   poking past the card's rounded ones. No padding, so the resize handle's
   corner sits flush on the picture's own corner rather than offset out into
   empty space -- unaffected by the card's own edge now existing, since a
   background matches an element's box exactly regardless of padding.
   `z-index:5` (feat 139 R2, was 3): above `.hd-layer`'s 4 so the section grips
   never draw over or take clicks meant for the table, level with `.pe-readout`'s
   5, below `.hd-label`/`.hd-field`'s 6 and `.sync-err`'s 6. It now also sits
   over `.chat`'s 4 and `.preset-toast`'s 4: a box dragged down onto the
   assistant bar covers it.
   `max-width` gives `.bodyview{width:100%;height:auto}` (feat 63 row 2)
   something concrete to size against until the user drags the resize handle
   (`.body-overlay-resize`, bodyoverlay.js's makeResizable -- clears
   `max-width` on first grab so an explicit `style.width` can exceed it).
   Position/size are clamped fully inside #view on every drag and resize
   (bodyoverlay.js's clampToView) -- never merely "partially visible" -- so
   this box can never spill into the ribbon or either sidebar, which sit
   OUTSIDE #view entirely.
   `touch-action:none` keeps a touchscreen drag from also scrolling.
   `[hidden]` is restated for the same reason `.sync-err[hidden]` is (:271):
   belt-and-suspenders against a later rule giving this a non-`none` display. */
.body-overlay{
  position:relative; z-index:5; max-width:240px;   /* in the dock; [data-moved] leaves it. feat 139 R2: 3 -> 5 */
  cursor:grab; touch-action:none;
  background:color-mix(in srgb, var(--panel) 62%, transparent);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
  border-radius:6px; overflow:hidden;
  /* Same grey rule the sidebars are edged in (.worktree/.options, :141/:413),
     so a floating card reads as part of the same chrome rather than a
     stray box -- reinstated after an earlier pass removed it; the frosted
     fill is now the thing carrying contrast, the border is purely edge
     definition, same division of labour the sidebars already use. */
  border:1px solid var(--rule-strong);
}
/* The dock: covers #view (so a dragged box's absolute left/top share its
   origin), stacks default-placed boxes bottom-left ABOVE the gizmo (bottom:74 +
   64 + 10 = 148), growing upward (feat 96), and passes pointer events through.
   `safe` clips a tall stack at the bottom rather than past the top. */
.body-dock{
  position:absolute; inset:0; padding:0 0 148px 12px; pointer-events:none;
  display:flex; flex-direction:column; justify-content:safe flex-end; align-items:flex-start; gap:8px;
}
.body-dock > .body-overlay{ pointer-events:auto; }
.body-overlay[data-moved]{ position:absolute; }
.body-overlay .bodyview{ background:none; border:0; border-radius:0; }
.body-overlay.dragging{ cursor:grabbing; }
.body-overlay[hidden]{ display:none; }
/* Float/dock button: top-right, left of the x (bodyoverlay.js makeOverlay). */
.body-overlay .body-overlay-dock{ top:2px; right:28px; z-index:1; }   /* .hint-close (later) sets 6px at the same weight */
.body-overlay-dock:focus-visible{ outline:2px solid var(--primary); outline-offset:-2px; }
.body-overlay .body-overlay-max{ top:2px; right:54px; z-index:1; }   /* feat 133 B5: maximise, left of the dock button */
.body-overlay-max:focus-visible{ outline:2px solid var(--primary); outline-offset:-2px; }
/* ===== docked pane (feat 108 row 1): the same box, in the centre grid =====
   Opaque, not frosted: it sits beside the 3D view, not over it. Placement is
   the grid's (`.centre > .body-overlay-front` = column 3, row 1; side = the
   bottom row, full width). No drag, no resize: the splitters size it. */
.centre > .body-overlay[data-docked]{
  position:relative; left:auto; top:auto; max-width:none; width:auto; height:auto;
  min-width:0; min-height:0; display:flex; flex-direction:column;
  background:var(--panel); backdrop-filter:none; -webkit-backdrop-filter:none; cursor:default;
}
.centre > .body-overlay[hidden]{ display:none; }   /* beats the docked display:flex above: a hidden pane must not hold a track */
.centre > .body-overlay-front[data-docked]{ grid-column:3; grid-row:1; }
.centre > .body-overlay-side[data-docked]{ grid-column:1 / -1; grid-row:3; }
/* feat 139: the station table docks into the same bottom row as the side view (one at a time, bodyoverlay.js setTablePlace). */
.centre > .body-overlay-table[data-docked]{ grid-column:1 / -1; grid-row:3; }
.body-overlay[data-docked] .body-overlay-body{ flex:1 1 0; min-height:0; min-width:0; position:relative; overflow:hidden; }
.body-overlay[data-docked] .body-overlay-resize{ display:none; }
.body-overlay[data-docked] .bodyview{ width:100%; height:100%; }
/* Default positions -- the ONLY source of truth for "reset to default" on
   toggle-off-then-on (bodyoverlay.js's setFrontOn/setSideOn): the dock above,
   bottom-left over the gizmo, clear of `.hud` and `.preset-toast` (top-left). */
/* Floating front/side: the box size is JS state (bodyoverlay.js floatSize) and the
   svg FILLS it. Never let the svg size the box (height:auto + max-width) -- the
   resize observer repainted from that measurement and a fractional width grew forever. */
.body-overlay-front, .body-overlay-side, .body-overlay-table, .body-overlay-shape{ max-width:none; }
/* feat 139: the table and the shape window scroll inside their box (a docked table is as tall as the strip). */
.body-overlay.body-overlay-table .body-overlay-body, .body-overlay.body-overlay-shape .body-overlay-body{ overflow:auto; }
.body-overlay-front .body-overlay-body, .body-overlay-side .body-overlay-body{ overflow:hidden; position:relative; }
.body-overlay-front .bodyview, .body-overlay-side .bodyview{ width:100%; height:100%; }
/* feat 89 round 2: the gear side view, never open together with a body's two
   previews in practice (different selection) but laid out clear of them anyway. */
.body-overlay-gear{ max-width:320px; }
/* Title (labels the drag handle) and the how-to-reopen line: small and quiet. */
.body-overlay-title{ padding:4px 52px 0 8px; font-size:var(--fs-xs); font-weight:600; color:var(--dim); line-height:1.3; }
/* The title is the drag handle: never a text-selection start (2026-10-21: undocking selected the whole page). */
.body-overlay-title, .body-overlay.dragging{ user-select:none; -webkit-user-select:none; }
.body-overlay-hint{ padding:0 8px 4px; font-size:var(--fs-xs); color:var(--faint); line-height:1.3; }
/* The gear box's attitude caption: one quiet line hugging the drawing. */
.body-overlay-cap{ margin:0; padding:2px 8px 4px; font-size:var(--fs-xs); color:var(--dim); line-height:1.3; }
/* Close x, top-right. Smaller than the shared .hint-close's 24px hit area is
   not an option (real hit targets), so it keeps 24px and just sits tighter. */
.body-overlay .body-overlay-close{ top:2px; right:2px; z-index:1; }   /* the dock's weight: .hint-close's 6px put the x 4px below it */
.body-overlay-close:focus-visible{ outline:2px solid var(--primary); outline-offset:-2px; }
/* Bottom-right resize tab, flush on the picture's own corner (no padding on
   `.body-overlay` to offset it). Same "invisible until hover, a wide hit
   target around a slim visible cue" shape as `.wt-handle`/`.opt-handle`
   (:389), a corner bracket instead of a straight line since this resizes
   off one dragged corner (only `width` is actually written -- the SVG's own
   `height:auto` follows it, keeping the picture's aspect ratio). */
.body-overlay-resize{
  position:absolute; right:0; bottom:0; width:16px; height:16px;
  cursor:nwse-resize; touch-action:none;
  border-right:2px solid transparent; border-bottom:2px solid transparent;
}
.body-overlay-resize:hover, .body-overlay-resize:focus-visible{
  border-color:var(--primary);
}
/* feat 139 R2 F1: the dock is a flex column that shrinks its items, so a 240px
   table was squashed to 158px around 258px of rows. The table keeps its height. */
.body-overlay-table{ flex-shrink:0; }
/* feat 139 R2 F2: the frosted fill put --dim text at 1.81:1 over a side-view line.
   Table and shape windows are opaque, with a drop shadow. */
.body-overlay-table, .body-overlay-shape{
  background:var(--panel); backdrop-filter:none; -webkit-backdrop-filter:none;
  box-shadow:0 8px 24px rgba(0,0,0,.35);
}
/* feat 139 R2 F3: the shape window's fields in two columns, tighter inputs. */
.body-overlay-shape .body-overlay-body{ display:grid; grid-template-columns:1fr 1fr; gap:8px 10px; padding:6px 10px 10px; }
.body-overlay-shape .param input{ padding:4px 6px; font-size:var(--fs-sm); }
.body-overlay-shape .param .unit{ padding:0 6px; }
/* feat 139 R2 (contract with bodyoverlay.js): the drag-to-dock preview, one element
   appended to #view and moved with inline left/top/width/height. The sidebar zone
   is placed in fixed coordinates over #params. Never a hit target. */
.body-dock-preview{
  position:absolute; z-index:7; pointer-events:none;
  border:2px solid var(--primary); border-radius:6px;
  background:color-mix(in srgb, var(--primary) 18%, transparent);
}
.body-dock-preview[data-zone="sidebar"]{ position:fixed; }
.body-dock-preview[hidden]{ display:none; }
/* feat 139 R2 (contract): the table box's "Back to sidebar" icon, in the .body-overlay-max
   slot. Same focus ring as the dock button. */
.body-overlay .body-overlay-unpin{ top:2px; right:54px; z-index:1; }
.body-overlay-unpin:focus-visible{ outline:2px solid var(--primary); outline-offset:-2px; }

/* Native <select>: three mutually exclusive styles, no custom menu to maintain. */
.viewstyle{
  position:absolute; top:10px; right:12px; background:var(--panel);
  border:1px solid var(--rule-strong); color:var(--dim); font:inherit; font-size:var(--fs-sm);
  padding:4px 7px; border-radius:4px; cursor:pointer;
}
.viewstyle:hover{ color:var(--text); }
/* feat 76's .blend-busy is styled with .sync-cue-pill (feat 96, the "updating" cue). */

/* Axis gizmo -- bottom-left, clearing the compact chat bar (16px + 42px). The
   arms are positioned by app/viewport.js; only colour, size and the hit state
   live here. `.ax-hit` is an invisible 22px pad behind each 12px head, so the
   click target meets the pointer-target minimum without growing the drawing. */
.gizmo{ position:absolute; left:18px; bottom:74px; z-index:3; overflow:visible; }
/* Below the 11px floor everywhere else, but the label sits centred in a 12px
   head circle on a 64px svg (viewport.js: GIZMO_R=22) -- var(--fs-xs) here
   still fits inside that circle with margin, checked against the markup. */
.gizmo text{ font-size:var(--fs-xs); font-weight:600; pointer-events:none; }
.gizmo .ax, .gizmo .ax-home{ cursor:pointer; }
.gizmo .ax:hover circle:not(.ax-hit){ filter:brightness(1.35); }
/* the home pad is transparent; the sibling AFTER it is the dot you see */
.gizmo .ax-home:hover + circle{ filter:brightness(1.6); }
/* parallel projection on: the centre dot reads as a pressed button, which is
   the only thing on screen that says which projection you are looking at. */
.gizmo.ortho .ax-home + circle{ fill:var(--primary); r:4; }
.gizmo .ax:focus-visible, .gizmo .ax-home:focus-visible{ outline:2px solid var(--primary); outline-offset:2px; }

/* Same custom-prop/fallback/position:relative story as .worktree above. */
.options{
  width:var(--opt-w, 280px); flex:none; background:var(--chrome); border-left:1px solid var(--rule);
  display:flex; flex-direction:column; position:relative;
}
/* ---- panel resize handles (feat 64 row 1) ----
   One handle per panel, anchored to the edge that faces .viewport (right for
   the tree, left for options) so it moves for free when the panel's own
   width changes -- app/chrome.js only ever writes --wt-w/--opt-w, it never
   repositions the handle. 8px, not a bare 1px line, for a real hit target;
   the visible line is the hover/focus highlight, not a permanent rule, so an
   untouched panel edge still reads as a plain border. Hidden while
   `.collapsed` -- a collapsed panel has no width left to drag. */
.wt-handle, .opt-handle{
  position:absolute; top:0; bottom:0; width:8px; cursor:ew-resize;
  touch-action:none; background:transparent;
}
.wt-handle{ right:0; }
.opt-handle{ left:0; }
/* The 8px box above is the hit target, kept wide for usability; the visible
   highlight is a separate 2px line centered in it, via ::after, so a wider
   grab area doesn't read as a thick bar on the panel edge. */
.wt-handle::after, .opt-handle::after{
  content:''; position:absolute; top:0; bottom:0; width:2px; background:transparent;
}
.wt-handle::after{ right:0; }
.opt-handle::after{ left:0; }
.wt-handle:hover::after, .wt-handle:focus-visible::after,
.opt-handle:hover::after, .opt-handle:focus-visible::after{ background:var(--primary); }
.wt-handle:focus-visible, .opt-handle:focus-visible{
  outline:2px solid var(--focus); outline-offset:-2px;
}
.worktree.collapsed .wt-handle, .options.collapsed .opt-handle{ display:none; }
/* Options sub-tabs (generated per selection, render.js paintTabs): native radios styled as underlined labels, same
   precedent as flow.html's o-tabs -- a radiogroup is the correct native role
   for a mutually-exclusive set, so no fake tablist ARIA is layered on. */
.o-tabs{ display:flex; flex-wrap:wrap; gap:4px 18px;   /* up to 5 tabs wrap to a second row in a 300 px sidebar: none scrolls away or hides */ padding:0 16px; border-bottom:1px solid var(--rule); margin-bottom:2px; }
.o-tabs input{ position:absolute; opacity:0; width:1px; height:1px; }
.o-tabs label{ font-size:var(--fs-md); color:var(--dim); padding:0 0 10px; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; }
.o-tabs input:checked + label{ color:var(--text); font-weight:600; border-color:var(--primary); }
.o-tabs input:focus-visible + label{ outline:2px solid var(--focus); outline-offset:2px; }
.params{ display:flex; flex-direction:column; gap:12px; padding:0 16px 16px; overflow-y:auto; }
.params h3{
  margin:6px 0 -4px; font-size:var(--fs-xs); letter-spacing:.06em; text-transform:uppercase;
  color:var(--faint); font-weight:600;
}
/* One line of explanation under a heading or a toggle. --dim, not --faint:
   the axis convention and the symmetry consequence are not guessable from the
   field names, so this is text that has to be readable, not decoration. */
.params .note{ margin:-4px 0 0; font-size:var(--fs-xs); line-height:1.45; color:var(--dim); }
/* a note under a segmented control has no label of its own to tuck under: the standard gap, not the -4px pull-up */
.params .param:has(> .seg) + .note{ margin-top:4px; }
/* feat 74: the body's overall size under its header -- a reminder read while
   typing, so full --text and figures that do not jitter as they change. */
/* The Basic tab's derived numbers (render.js derivedBlock): clear of the tab bar, not
   pulled up like a note, and label/value in two columns so each figure reads alone. */
.params .note.derived{
  margin:12px 0 2px; padding:10px 12px; border:1px solid var(--rule-strong); border-radius:8px;
  display:grid; grid-template-columns:1fr auto; gap:6px 16px;
  font-size:var(--fs-sm); line-height:1.3; font-variant-numeric:tabular-nums;
}
.params .note.derived > div{ display:contents; }
.params .note.derived strong{ color:var(--text); font-weight:600; text-align:right; }
/* feat 132: an editable size row (render.js editableValue) becomes a number field. The dotted underline is ALWAYS
   on (owner 2026-10-22: hover-only, nobody found that Pod/Ellipsoid/Fuselage sizes edit); hover adds a fill. */
.params .note.derived strong.editable{ cursor:text; border-radius:3px; padding:0 3px; margin-right:-3px;
  text-decoration:underline dotted var(--faint); text-underline-offset:3px; }
.params .note.derived strong.editable:hover{ background:var(--hover); text-decoration-color:var(--text); }
.params .note.derived strong.editable:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.params .note.derived strong input{
  width:7ch; box-sizing:border-box; padding:1px 6px; font:inherit; text-align:right; color:var(--text);
  background:var(--chrome); border:1px solid var(--primary); border-radius:4px;
}
.params .note.derived strong input:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.params .note.derived strong input.bad{ border-color:var(--error); }
.params .note.derived strong .unit{ margin-left:4px; color:var(--faint); font-weight:400; }
/* The same size, in the viewport's bottom-right while something is selected
   (dims.js). bottom:74px mirrors .gizmo on the left: clear of the compact chat
   bar (bottom:16px). pointer-events:none -- never eats a
   pick or an orbit. */
.dims-badge{
  position:absolute; right:16px; bottom:74px; z-index:3; pointer-events:none;
  display:flex; flex-direction:column; gap:2px; padding:8px 12px;
  background:color-mix(in srgb, var(--panel) 85%, transparent);
  border:1px solid var(--rule-strong); border-radius:8px;
  font-size:var(--fs-sm); line-height:1.4; color:var(--dim); font-variant-numeric:tabular-nums;
}
.dims-badge strong{ color:var(--text); font-weight:600; }
.dims-badge[hidden]{ display:none; }
/* Collapsible option group: native <details>, no JS open/close state to
   keep. feat 84 (A6): the summary used to be 11px uppercase grey text with a
   6px triangle, and read as a label rather than a clickable row -- now a
   full-width 32px row with a real chevron and a hover fill, sentence case,
   still shut by default. */
.o-sect{ display:flex; flex-direction:column; gap:12px; }
/* Hairline between consecutive groups (not before the first, or after the
   last, which would double up against .params's own padding). */
.o-sect + .o-sect{ border-top:1px solid var(--rule-soft); padding-top:4px; margin-top:-8px; }
/* A shut <details> still lays out its (empty) content slot as a flex item,
   so the 12px gap made every shut group ~70px tall. */
.o-sect:not([open]){ gap:0; }
/* feat 139 R2 F4: the Section parameters group lays its fields in two columns, the
   summary spanning both (beats the flex column above by specificity). */
.o-sect[data-key="SectionParams"][open]{ display:grid; grid-template-columns:1fr 1fr; gap:12px 10px; }
.o-sect[data-key="SectionParams"] > summary{ grid-column:1 / -1; }
/* feat 139 R2 F9: the sidebar row holding the note and the icon button (Pop out / Back to sidebar).
   The note takes the spare width; the close-style icon keeps its 24 px box, not absolute. */
.sect-tools{ display:flex; align-items:center; gap:8px; }
.sect-tools .note{ flex:1; }
.sect-tools .hint-close{ position:static; }
.o-sect > summary .sect-pop{ position:static; margin-left:auto; }   /* Sections: pop out the table, in the header */
.sect-tools .hint-close:disabled{ opacity:.35; cursor:default; }   /* feat 142 row 1: the table's Add/Delete at a rule's limit */
.o-sect > summary{
  list-style:none; cursor:pointer; display:flex; align-items:center; gap:6px;
  min-height:32px; padding:6px 8px; margin:0 -8px; border-radius:4px;
}
.o-sect > summary:hover{ background:var(--hover); }
.o-sect > summary::-webkit-details-marker{ display:none; }
/* A CSS-drawn chevron (two borders, mitred to a corner) rather than a glyph --
   crisp at any zoom, and the colour is a plain `border-color`, not a font
   fighting antialiasing at 7px. Points right shut, down open, same sense as
   the tree's own .item-caret. */
.o-sect > summary::before{
  content:""; flex:none; width:7px; height:7px;
  border-right:1.6px solid var(--dim); border-bottom:1.6px solid var(--dim);
  transform:rotate(-45deg); margin-left:1px;
  transition:transform .12s ease;
}
.o-sect[open] > summary::before{ transform:rotate(45deg); }
/* "More…" inside a group (feat 90 rows 3/5a): one native <details>, shut by
   default; params.js moreBox() builds it. 32px summary = a real hit target. */
.o-more > summary{
  cursor:pointer; display:flex; align-items:center; gap:8px; min-height:32px;
  width:100%; box-sizing:border-box; padding:0 4px; border-radius:4px;
  color:var(--text); font-size:var(--fs-sm); font-weight:600;
}
.o-more > summary::before{
  /* no colour of its own: the chevron is the label's text colour -- the
     whole button is white in dark theme (owner, 2026-10-02), not orange */
  content:""; flex:none; width:7px; height:7px;
  border-right:1.6px solid currentColor; border-bottom:1.6px solid currentColor;
  transform:rotate(-45deg); transition:transform .12s ease;
}
.o-more[open] > summary::before{ transform:rotate(45deg); }
.o-more > summary:hover{ background:var(--hover); }
.o-more > summary:focus-visible{ outline:2px solid var(--focus); outline-offset:-2px; }
.o-more-count{ color:var(--dim); font-weight:400; font-variant-numeric:tabular-nums; }
.o-more[open] .o-more-count{ display:none; }
/* render.js soloGroup: a tab's only group, always open -- no chevron, no hover, no toggle;
   no row at all when its title is the tab's label. */
.o-sect.solo > summary{ cursor:default; }
.o-sect.solo > summary:hover{ background:none; }
.o-sect.solo > summary::before{ display:none; }
.o-sect.solo-same > summary{ display:none; }   /* the count says what is inside; open, the rows say it */
.o-more-body{ display:flex; flex-direction:column; gap:12px; padding-top:8px; }
@media (prefers-reduced-motion:reduce){
  .o-sect > summary::before, .o-more > summary::before{ transition:none; }
}
.o-sect > summary h3{
  margin:0; font-size:var(--fs-md); font-weight:600; color:var(--text);
  text-transform:none; letter-spacing:0;
}

.param{ display:flex; flex-direction:column; gap:5px; }
/* An author `display` beats the UA sheet's `[hidden]{display:none}`, so every
   `el.hidden = true` in foil.js was a no-op on these three -- which is why the
   "+ Upload airfoil" button showed for shapes that have no file to upload.
   One rule, not `!important`: they are the only flex boxes the JS hides.
   `.chat` (line 555, also an author `display:flex`) joined this list for lab
   mode (feat 60 row 2), which hides the assistant panel entirely. */
.param[hidden], .foil-custom-row[hidden], .foil-fields[hidden], .chat[hidden]{ display:none; }
.param label{ font-size:var(--fs-sm); color:var(--dim); }
.p-control{ display:flex; align-items:stretch; }
.param input{
  flex:1; min-width:0; background:var(--sunken); border:1px solid var(--rule-strong);
  color:var(--text); padding:7px 9px; font-size:var(--fs-md); border-radius:4px;
  font-variant-numeric:tabular-nums;
}
/* The colour swatch (widgets.js: colorField) is the one .param input whose
   JOB is to show a colour, and the rule above was hiding it: 7px of padding
   over a --sunken background squashed the swatch to a thin strip, so the
   control read as an empty grey box whatever colour it held. Zero the
   padding, give it a real size, and let the swatch fill it. The two -webkit-
   pseudo-elements are how chromium exposes the inner swatch; without them the
   default 2px inset border keeps a visible frame around the colour. */
.param input[type="color"]{
  flex:none; width:52px; height:26px; padding:2px; background:var(--sunken);
  cursor:pointer;
}
.param input[type="color"]::-webkit-color-swatch-wrapper{ padding:0; }
.param input[type="color"]::-webkit-color-swatch{ border:0; border-radius:2px; }
.param input:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
/* Set from script, not :invalid -- :invalid also matches a step mismatch, and
   an off-grid value like 1.50 is perfectly good geometry. */
.param input.bad{ border-color:var(--error); }
.param.has-unit input{ border-right:0; border-radius:4px 0 0 4px; }
.param .unit{
  background:var(--panel); border:1px solid var(--rule-strong); color:var(--faint);
  font-size:var(--fs-sm); padding:0 9px; display:flex; align-items:center; border-radius:0 4px 4px 0;
}
.param select{
  flex:1; min-width:0; background:var(--sunken); border:1px solid var(--rule-strong);
  color:var(--text); padding:7px 9px; font-size:var(--fs-md); border-radius:4px;
}
.param select:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }

/* Inherit-vs-override checkboxes (section dihedral / section airfoil). */
.inherit-toggle{
  display:flex; align-items:center; gap:7px; font-size:var(--fs-sm); color:var(--dim); cursor:pointer;
}
.inherit-toggle input{ accent-color:var(--primary); width:14px; height:14px; margin:0; }

/* ===== generated-field widgets (widgets.js toggleField/choiceField) =====
   Used by the server-generated component panel and the Pose tab. The
   `.inherit-toggle` style above stays for the two hand-written flags that
   read as a sentence ("Mirror about the centreline"); these two are for a
   LABELLED field, so they keep .param's own label treatment. */

/* A bool. `.param` is a column, so the switch row overrides it to a row --
   the label and the switch are one line, unlike a number field. */
.param.param-row{ flex-direction:row; align-items:center; justify-content:space-between; gap:10px; }
.param.param-row label{ flex:1; }
input.switch{
  appearance:none; -webkit-appearance:none; flex:none;
  width:34px; height:19px; padding:0; border-radius:999px;
  background:var(--sunken); border:1px solid var(--rule-strong); cursor:pointer;
  position:relative; transition:background .12s, border-color .12s;
}
input.switch::after{
  content:''; position:absolute; top:2px; left:2px; width:13px; height:13px;
  border-radius:50%; background:var(--faint); transition:transform .12s, background .12s;
}
input.switch:checked{ background:var(--primary); border-color:var(--primary); }
input.switch:checked::after{ transform:translateX(15px); background:#fff; }
input.switch:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
input.switch:disabled{ opacity:.45; cursor:not-allowed; }

/* A small int that names states rather than counting -- gender, BOR surface
   mode, gear retraction config. Segmented radios: same hidden-input +
   styled-label trick as `.o-tabs`, boxed rather than underlined so it does
   not read as a second tab bar inside the panel. */
/* position:relative contains the visually-hidden radios below: without it
   their containing block sat OUTSIDE the scrolling Options panel, so a seg
   deep in a long panel (feat 89's gear strut rows) put its radio below the
   window and the click's focus scrolled the whole page off the viewport. */
.seg{ position:relative; display:flex; border:1px solid var(--rule-strong); border-radius:4px; overflow:hidden; }
.seg input{ position:absolute; opacity:0; width:1px; height:1px; }
.seg label{
  flex:1; text-align:center; font-size:var(--fs-sm); color:var(--dim); cursor:pointer;
  padding:6px 8px; background:var(--sunken); border-left:1px solid var(--rule);
  white-space:nowrap;
}
.seg label:first-of-type{ border-left:0; }
.seg input:checked + label{ background:var(--primary); color:var(--on-primary); font-weight:600; }
.seg input:focus-visible + label{ outline:2px solid var(--focus); outline-offset:-2px; }
.seg input:disabled + label{ opacity:.45; cursor:not-allowed; }
.seg-icons label{ display:inline-flex; align-items:center; justify-content:center; padding:4px 10px; }   /* feat 170: a section picked from its outline */
.seg-icons svg{ width:22px; height:22px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linejoin:round; }

/* a seg whose labels do not fit one row at the 200 px sidebar (Follow position): the labels wrap to a second row; the 1px gap is the rule between them */
.seg.seg-wrap{ flex-wrap:wrap; gap:1px; background:var(--rule); }
.seg.seg-wrap label{ flex:1 1 auto; border-left:0; padding:6px 6px; }

/* A generated field the Pose tab has greyed out (a left-side angle while
   left/right symmetry is on) -- it still SHOWS the mirrored value, so it must
   read as driven rather than as broken. */
.foil-field-ctl input:disabled{ opacity:.45; cursor:not-allowed; }

/* Compact Root/Tip airfoil EDIT CURSOR (both tabs) -- pill buttons, not the
   checkbox-with-sentence style above. Single-select: at most one .active and
   at most one aria-pressed="true" (params.js airfoilCursorToggles). They sit
   at the right-hand end of the Shape field's LABEL row (widgets.js
   shapeSelect's optional labelExtra), so the field itself keeps .param's
   label-over-control column. */
.param-label-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; }
.part-toggle-group{ display:flex; gap:4px; }
.part-toggle-btn{
  font-size:var(--fs-xs); padding:2px 8px; border-radius:4px; border:1px solid var(--rule-strong);
  background:transparent; color:var(--dim); cursor:pointer;
}
/* A tile in a preset grid (repeat(3, 1fr)) must shrink with the sidebar: a grid item's min-width:auto kept
   "Bicycle + Outrigger Gear" and its art 20 px past #params at 200 px (feat 150 row 6). */
.part-toggle-btn{ min-width:0; overflow-wrap:anywhere; }
.part-toggle-btn > svg{ max-width:100%; }
.part-toggle-btn:hover:not(:disabled){ background:var(--hover); color:var(--text); }
.part-toggle-btn.active{ border-color:var(--primary); color:var(--text); }
.part-toggle-btn:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
/* Root on station 2+ : its root IS the previous station's tip, one shared
   XSec. `disabled` drops it out of the tab order and its title with it, so the
   same explanation is ALSO a visible note() in the group -- a tooltip is not
   an accessible way to state a rule. */
.part-toggle-btn:disabled{ color:var(--faint); border-style:dashed; cursor:not-allowed; }
/* The one state with no single current airfoil to draw: root and tip differ
   and no cursor is set. Takes the plot's own box so the panel does not jump,
   and reads as information, not as the error .foil-status normally carries. */
.foil-status.foil-notice{
  font-size:var(--fs-sm); line-height:1.45; color:var(--dim); text-align:center;
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px;
  padding:22px 14px; min-height:0;
}

/* ---- station list editor (Advanced tab) ---- */
.station-list-wrap{ display:flex; flex-direction:column; gap:6px; margin-bottom:2px; }

/* ---- station table (feat 106 row 5b, web/app/stationtable.js) ----
   One row per station, every number an inline input. Inputs fill their cell
   so each column reads as a column; numbers right-align, the shape select does
   not. The selected row takes the 16%-of-primary mix the gate strip already uses. */
.station-table{ width:100%; table-layout:fixed; border-collapse:collapse; font-size:var(--fs-xs); font-variant-numeric:tabular-nums; }
.station-table th{ font-size:var(--fs-xs); font-weight:600; color:var(--dim); text-align:left; padding:2px 2px; }
/* 2026-10-21: headers are short (x / y / z, the full name in the title) and never hyphenate; the
   unit sits on its own line and every header sits on the row's bottom, so the columns line up. */
.station-table thead th{ vertical-align:bottom; line-height:1.25; padding-bottom:4px; white-space:nowrap; }
.station-table th .st-unit{ display:block; font-weight:400; color:var(--faint); }
.station-table th[title]{ cursor:help; }
/* Fixed layout so six columns fit the panel: # narrow, Shape widest, the four numbers share the rest. */
.station-table th:nth-child(1){ width:1.6em; }
.station-table th[scope=row]{ white-space:nowrap; }
/* feat 139 R2 F10: the sidebar's Shape select truncated at 22% (28 px of text). Fuselage only:
   the wing table's third column is a number; its widest is the airfoil, last. */
.station-table:not(.wing-table) th:nth-child(3){ width:28%; }
.wing-table th:last-child{ width:20%; }
/* Measured (Inter 11px): a 3-decimal value needs ~43 px of cell, "Cuerda" 39 px of header. Seven columns
   need 296 px; a narrower sidebar scrolls the table sideways (wingpanes.js .station-scroll) instead of clipping. */
.wing-table{ min-width:296px; }
.station-scroll{ overflow-x:auto; }
/* feat 139 R2 F7: numbers right-aligned under their right-aligned inputs; the
   table capped at 560px in a wide window; the header stays put while the window scrolls. */
.station-table th:nth-child(n+2){ text-align:right; padding-right:5px; }
.station-table:not(.wing-table) th:nth-child(3), .wing-table th:last-child{ text-align:left; padding-right:2px; }
/* The airfoil name is its own edit button: one box like the inputs, the name cut with an ellipsis. */
.wing-airfoil button{ display:block; width:100%; height:24px; padding:2px 4px; font:inherit; color:var(--text); text-align:left;
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:3px; cursor:pointer;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.wing-airfoil button:hover{ border-color:var(--primary); }
.wing-airfoil button:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
.wing-airfoil span{ display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.body-overlay-table .station-table{ max-width:560px; }
.body-overlay-table thead th{ position:sticky; top:0; z-index:1; background:var(--panel); }
.station-table td{ padding:1px 2px; border-top:1px solid var(--rule-soft); }
.station-table tbody tr{ cursor:pointer; }
/* The selected row (owner, 2026-10-12: "more visible"). The inputs paint their own
   --sunken over the cell, so a row tint alone showed only in the padding: tint the
   inputs too, edge them in the accent, and mark the # cell with a bar. */
.station-table tr.is-selected td, .station-table tr.is-selected th{ background:color-mix(in srgb, var(--primary) 22%, transparent); }
/* feat 139 R2 F6: the number was --primary on a --primary tint (3.55:1). The bar carries the selection. */
.station-table tr.is-selected th[scope=row]{ color:var(--text); font-weight:700; box-shadow:inset 3px 0 0 var(--primary); }
.station-table tr.is-selected input, .station-table tr.is-selected select, .station-table tr.is-selected .wing-airfoil button{
  border-color:var(--primary); background:color-mix(in srgb, var(--primary) 12%, var(--sunken)); }
/* feat 139 R2 F5: a Point row's disabled width/height read as broken at .38 opacity
   (2.38:1 light). Shown as plain dim text, no box. The selected-row variant is
   listed too: its tint rule is more specific and would otherwise repaint them. */
.station-table input:disabled, .station-table tr.is-selected input:disabled{
  opacity:1; color:var(--dim); background:transparent; border-color:transparent; }
/* feat 139 R2 F8: 24 px hit targets (was 20-22). Windows get the larger text and padding. */
.station-table input, .station-table select{
  width:100%; min-width:0; box-sizing:border-box; height:24px; padding:2px 2px; font:inherit; color:var(--text);
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:3px;
}
.body-overlay .station-table input, .body-overlay .station-table select{ font-size:var(--fs-sm); }
.body-overlay .station-table input{ padding:2px 5px; }
.station-table input[type="number"]{ text-align:right; -moz-appearance:textfield; appearance:textfield; }
.station-table input[type="number"]::-webkit-inner-spin-button, .station-table input[type="number"]::-webkit-outer-spin-button{ -webkit-appearance:none; margin:0; }
.station-table input:focus-visible, .station-table select:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
.station-actions{ display:flex; gap:6px; margin-top:6px; }
.station-actions .item-add{ flex:1; text-align:center; }

/* ---- shape from dimensions (feat 106 row 5c, web/app/bodyshape.js) ---- */
.body-shape-form{ display:flex; flex-direction:column; gap:4px; }
.body-shape-form > button{ align-self:flex-start; }
.body-shape-error{ color:var(--error); }
.body-shape-error:empty{ display:none; }
.item-list{ display:flex; flex-direction:column; gap:2px; }
.item-row{ display:flex; align-items:stretch; gap:4px; }
.item-row .t-row{ flex:1; }
/* 24x24 minimum hit target -- was 26x~18 (a 14px glyph, no explicit height). */
.item-rm{
  flex:none; width:26px; min-height:24px; display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; color:var(--faint);
  border-radius:4px; cursor:pointer; font-size:var(--fs-lg); line-height:1;
}
.item-rm:hover{ background:var(--hover); color:var(--error); }
.item-add{
  align-self:flex-start; background:transparent; border:1px dashed var(--rule-strong); color:var(--dim);
  font-size:var(--fs-sm); padding:5px 10px; border-radius:4px; cursor:pointer;
}
.item-add:hover:not([disabled]){ background:var(--hover); color:var(--text); }
/* ---- nested children under an item-row (control surfaces under a wing in
   the tree, renderItemList's `childrenFor`) -- NOT `.item-list`, a distinct
   class on purpose (check_app.py counts item-list containers). 24x24 minimum
   hit target -- was 16x~14. ---- */
.item-caret{
  flex:none; width:24px; min-height:24px; display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; color:var(--faint);
  cursor:pointer; font-size:var(--fs-xs); transform:rotate(0deg); transition:transform .12s;
}
.item-caret:hover{ color:var(--text); }
.item-caret[aria-expanded="true"]{ transform:rotate(90deg); }
.item-children{ display:flex; flex-direction:column; gap:2px; margin:0 0 2px 22px; }
.item-children[hidden]{ display:none; }

/* ---- foil editor: shared by the Basic tab's wing airfoil and the Advanced
   tab's per-station override (renderFoilEditor is one function, mounted
   twice with different get/set/preview callbacks -- see script). ---- */
.foil-editor{ display:flex; flex-direction:column; gap:10px; margin-top:2px; }
.foil-plot{ background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px; padding:6px; }
.foil-plot svg{ display:block; width:100%; height:auto; }
.foil-curve{ fill:color-mix(in srgb, var(--contextual) 30%, transparent); stroke:var(--contextual); stroke-width:1.5; }
.foil-chord{ stroke:var(--rule); stroke-width:1; stroke-dasharray:3 3; }
.foil-status{ font-size:var(--fs-xs); color:var(--error); min-height:14px; }
.foil-status:empty{ display:none; }
.foil-custom-row{ display:flex; }
.foil-custom-btn{
  flex:1; background:transparent; border:1px solid var(--rule-strong); color:var(--dim);
  font-size:var(--fs-sm); padding:6px 10px; border-radius:4px; cursor:pointer;
}
.foil-custom-btn:hover{ background:var(--hover); color:var(--text); }
.foil-custom-btn[aria-expanded="true"]{ color:var(--text); border-color:var(--primary); }
.foil-upload .item-add{ width:100%; text-align:center; }
.foil-fields{ display:flex; flex-direction:column; gap:10px; }
.foil-fields h3{ margin:4px 0 -2px; }
.foil-field{ display:flex; flex-direction:column; gap:4px; }
.foil-field label{ font-size:var(--fs-sm); color:var(--dim); }
.foil-field-ctl{ display:flex; align-items:center; gap:8px; }
.foil-field-ctl input[type="range"]{ flex:1; accent-color:var(--primary); background:transparent; }
.foil-field-ctl input[type="number"]{
  width:64px; flex:none; background:var(--sunken); border:1px solid var(--rule-strong);
  color:var(--text); padding:5px 7px; font-size:var(--fs-md); border-radius:4px;
  font-variant-numeric:tabular-nums;
}
.foil-field-ctl input[type="number"]:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
.foil-field-ctl input[type="number"].bad{ border-color:var(--error); }
/* placementField's unit label, reusing the foil editor's slider+number row --
   the foil editor itself has no unit (curve params are all 0..1 or degrees
   folded into the label), so this is the one addition placement needs. */
.foil-field-ctl .unit{ flex:none; font-size:var(--fs-xs); color:var(--faint); }
/* Placement's number box needs more digits than the foil editor's 0..1
   fields ever do -- min/max is +-10000, and a value truncated in the box is
   a value the user cannot read before it goes in a report. */
.placement-field .foil-field-ctl input[type="number"]{ width:84px; }

/* ---- blade curve editor (feat 27 row 6, web/app/curve.js) -- a compact
   two-column (r, value) row, genuinely new: .param's full label+control row
   would make a 10-40 point CEDIT curve (the corpus's real point counts,
   feat doc "The nine curves") an unreadably long panel. ---- */
.curve-plot{ background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px; padding:4px; margin-bottom:6px; }
.curve-plot svg{ display:block; width:100%; height:40px; }
.curve-plot polyline{ fill:none; stroke:var(--contextual); stroke-width:1.5; vector-effect:non-scaling-stroke; }
.curve-points{ display:flex; flex-direction:column; gap:3px; max-height:220px; overflow-y:auto; margin-bottom:6px; }
.curve-row{ display:flex; gap:6px; }
.curve-row input{
  flex:1; min-width:0; background:var(--sunken); border:1px solid var(--rule-strong);
  color:var(--text); padding:4px 6px; font-size:var(--fs-sm); border-radius:4px;
  font-variant-numeric:tabular-nums;
}
.curve-row input:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
.curve-row input.bad{ border-color:var(--error); }
.curve-actions{ display:flex; gap:6px; }
.curve-actions .item-add{ flex:1; text-align:center; }

/* ===== cross-section editor (feat 19 row 7, web/app/xsec.js) ==========
   Two plots: .xsec-plot (read-only preview, the engine's real (y,z) outline
   -- foil.js's TRUE-1:1-aspect rule, same reason) and .xsec-edit-plot
   (EDIT_CURVE only, draggable/nudgeable control points -- square viewBox,
   fit to the current points' own extent). Both square so the section is
   never visually stretched. */
.xsec-status{ font-size:var(--fs-xs); color:var(--error); min-height:14px; }
.xsec-status:empty{ display:none; }
.xsec-plot{
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px;
  padding:6px; margin-bottom:8px; aspect-ratio:1/1;
}
.xsec-plot svg{ display:block; width:100%; height:100%; }
.xsec-centerline{ stroke:var(--rule); stroke-width:1; stroke-dasharray:3 3; }
.xsec-curve{ fill:color-mix(in srgb, var(--contextual) 30%, transparent); stroke:var(--contextual); stroke-width:1.5; }

.xsec-controls{ display:flex; flex-direction:column; gap:2px; margin-bottom:4px; }
.xsec-outline{
  fill:color-mix(in srgb, var(--primary) 12%, transparent);
  stroke:var(--dim); stroke-width:.6; vector-effect:non-scaling-stroke;
}
.xsec-outline.xsec-open{ fill:none; }              /* close_flag off: a path, not a fillable loop */
.xsec-handle-stem{ stroke:var(--faint); stroke-width:.4; vector-effect:non-scaling-stroke; }
.xsec-point, .xsec-handle{
  fill:var(--panel); stroke:var(--primary); stroke-width:.6; cursor:grab;
  vector-effect:non-scaling-stroke;
}
.xsec-handle{ fill:var(--sunken); stroke:var(--dim); }
.xsec-point.sel, .xsec-handle.sel{ fill:var(--primary); }
.xsec-point:active, .xsec-handle:active{ cursor:grabbing; }
/* No UA focus ring, ever: an svg child's outline is drawn in USER units, so
   Chrome's 5px `outline:auto` on a clicked point became a 5-unit disk over a
   ~1-unit viewBox (feat 108 X9). Keyboard focus shows as the stroke below. */
.xsec-point:focus, .xsec-handle:focus{ outline:none; }
/* Mirror left/right: points on the half the engine overwrites (xsec.js mirrorRL). */
.xsec-mirrored{ opacity:.35; pointer-events:none; }
.xsec-point:focus-visible, .xsec-handle:focus-visible{ stroke:var(--focus); stroke-width:1.2; }
.xsec-actions{ display:flex; gap:6px; margin-top:6px; }
.xsec-actions .item-add{ flex:1; text-align:center; }

/* ===== assistant panel (app/chat.js) ================================== */

/* Always on, like flow.html's .chatbar mock -- there is no hidden state, only
   two SHAPES of the same element (`.compact` / `.expanded`, toggled in
   app/chat.js's setMode). Positioned inside `.viewport` (which is the
   `position:relative` anchor) rather than as a `.workarea` flex sibling, so it
   floats OVER the viewport and is centered on it regardless of how wide
   worktree/options are -- the bug in the first version was exactly that: a
   flex-sibling `.chat` squeezed a narrow window until the panel's own text
   clipped. An absolutely positioned overlay cannot be squeezed. */
.chat{ position:absolute; z-index:6; display:flex; flex-direction:column; }   /* feat 139 R2: stays above .body-overlay (now 5) */

.chat .c-head{ display:flex; align-items:center; justify-content:space-between; }
.chat .c-toggle{
  border:0; background:transparent; color:var(--faint); cursor:pointer;
  width:24px; height:24px; flex:none; display:flex; align-items:center; justify-content:center;
  border-radius:4px;
}
.chat .c-toggle:hover{ background:var(--hover); color:var(--text); }
.chat .c-icon{ display:none; width:16px; height:16px; color:var(--contextual); flex:none; }

.c-msg{
  font-size:var(--fs-md); line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere;
  border-radius:6px; padding:7px 10px;
}
.c-hint{ color:var(--faint); background:transparent; padding:6px 0; }
.c-msg.user{ background:var(--panel); color:var(--text); align-self:flex-end; max-width:88%; }
.c-msg.bot{ background:transparent; color:var(--dim); padding:0; }
.c-msg.err{ background:transparent; color:var(--error); padding:0; }

.c-status{ font-size:var(--fs-sm); color:var(--faint); }
/* :not([hidden]), NOT a bare `.c-status{display:flex}` -- any display value
   here beats the hidden attribute's own display:none, and the mark then
   animates over the viewport forever. */
.c-status:not([hidden]){ display:flex; align-items:center; gap:7px; }

/* The thinking mark: flow.html's #screen-load V, drawn on a 1.8s loop instead
   of 4.5s and with no wordmark. Same geometry, same two tokens, so "the agent
   is working" reads as the same brand beat as "the app is starting". */
.c-mark{ width:15px; height:15px; flex:none; }
@keyframes cmDotL { 0%,4%{opacity:0;transform:scale(.55);} 12%,100%{opacity:1;transform:scale(1);} }
@keyframes cmLine1{ 0%,12%{stroke-dashoffset:227;} 30%,100%{stroke-dashoffset:0;} }
@keyframes cmDotV { 0%,26%{opacity:0;transform:scale(.55);} 34%,100%{opacity:1;transform:scale(1);} }
@keyframes cmLine2{ 0%,34%{stroke-dashoffset:227;} 52%,100%{stroke-dashoffset:0;} }
@keyframes cmDotR { 0%,50%{opacity:0;transform:scale(.55);} 58%,100%{opacity:1;transform:scale(1);} }
@keyframes cmFade { 0%,80%{opacity:1;} 94%,100%{opacity:0;} }
.cm-dot-l{ transform-origin:126.21px 155.32px; animation:cmDotL 1.8s ease-out infinite; }
.cm-line-1{ stroke-dasharray:227; animation:cmLine1 1.8s ease-out infinite; }
.cm-dot-v{ transform-origin:256px 340.68px; animation:cmDotV 1.8s ease-out infinite; }
.cm-line-2{ stroke-dasharray:227; animation:cmLine2 1.8s ease-out infinite; }
.cm-dot-r{ transform-origin:385.79px 155.32px; animation:cmDotR 1.8s ease-out infinite; }
.c-mark{ animation:cmFade 1.8s ease-in-out infinite; }
@media (prefers-reduced-motion:reduce){
  .c-mark, .cm-dot-l, .cm-dot-v, .cm-dot-r{ animation:none; opacity:1; transform:scale(1); }
  .cm-line-1, .cm-line-2{ stroke-dashoffset:0; animation:none; }
}

.c-form{ display:flex; gap:8px; }
.c-form textarea{
  flex:1; min-width:0; resize:none; background:transparent; color:var(--text);
  border:0; padding:0; font:inherit; font-size:var(--fs-md); line-height:1.4;
}
.c-form textarea:focus{ outline:0; }
.c-form textarea::placeholder{ color:var(--dim); }
.c-form button[type="submit"]{
  align-self:flex-end; background:var(--primary); color:var(--on-primary); border:0;
  border-radius:4px; padding:8px 14px; font:inherit; font-size:var(--fs-md);
  font-weight:600; cursor:pointer; flex:none;
}
.c-form button[type="submit"]:hover:not([disabled]){ background:var(--contextual); }

/* ---- compact: the always-visible bar, flow.html's .chatbar geometry ----
   Only `bottom` is set (no `top`), so the box is content-sized and grows
   UPWARD as the log preview / status line above the bar gain height -- the
   bar itself never moves. */
.chat.compact{
  left:16px; right:16px; bottom:16px; gap:8px;
}
.chat.compact .c-head{ display:none; }
.chat.compact .c-icon{ display:block; }
.chat.compact .c-form{
  background:var(--panel); border:1px solid var(--rule-strong); border-radius:6px;
  box-shadow:0 10px 28px rgba(0,0,0,.35); align-items:center;
  height:42px; padding:0 8px 0 14px; flex:none;
}
.chat.compact .c-form textarea{ height:20px; }
.chat.compact .c-form button[type="submit"]{ display:none; }   /* Enter-only, like the mock */

/* The log has no visible chrome in compact -- every bubble but the LAST is
   hidden, and that one floats above the bar as a card, styled after
   flow.html's `.chat-pill`. This needs no JS: it is the exact same #chat-log
   append() already uses for full history, so a reply STREAMS into this card
   live, and switching to `.expanded` shows the very same nodes in full. */
.chat.compact .c-log{ display:flex; flex-direction:column; flex:none; }
.chat.compact .c-log > *{ display:none; }
.chat.compact .c-log > .c-hint{ display:none; }        /* placeholder lives in the textarea instead */
.chat.compact .c-log > *:last-child:not(.c-hint){
  display:block; max-height:160px; overflow-y:auto;
  transition:opacity .7s ease;
  background:color-mix(in srgb, var(--panel) 94%, transparent); border:1px solid var(--rule-strong);
  border-radius:10px; padding:9px 13px; color:var(--text);
  box-shadow:0 12px 28px rgba(0,0,0,.4);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
}
/* app/chat.js hangs `.faded` on the log ten seconds after a turn that APPLIED
   a change -- the answer is on screen in the viewport, the sentence is not
   worth the clutter. Compact only: `.expanded` is the history panel and
   nothing there ever fades. */
.chat.compact .c-log.faded > *:last-child:not(.c-hint){ opacity:0; pointer-events:none; }

.chat.compact .c-status:not([hidden]){
  background:color-mix(in srgb, var(--panel) 94%, transparent); border:1px solid var(--rule-strong);
  border-radius:8px; padding:6px 13px; box-shadow:0 12px 28px rgba(0,0,0,.4);
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px); flex:none;
}

/* ---- expanded: the original right-hand panel, full history ----
   `top` clears .viewstyle/.hud instead of `top:0`, or the panel would sit
   under the view-style dropdown at the same corner. */
.chat.expanded{
  top:52px; right:16px; bottom:16px; width:340px;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:8px;
  box-shadow:0 10px 28px rgba(0,0,0,.35); padding:0;
}
.chat.expanded .c-head{ padding:14px 16px 8px; }
.chat.expanded .c-head h2{ margin:0; }
/* flex:1 + min-height:0 is what makes the log scroll instead of pushing the
   composer off the bottom of a fixed-height column. */
.chat.expanded .c-log{
  flex:1; min-height:0; overflow-y:auto; display:flex; flex-direction:column;
  gap:10px; padding:4px 16px 12px;
}
.chat.expanded .c-status:not([hidden]){ padding:0 16px 8px; }
.chat.expanded .c-form{ flex-direction:column; padding:10px 16px 14px; border-top:1px solid var(--rule); }
.chat.expanded .c-form textarea{
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px; padding:7px 9px;
  font-size:var(--fs-md); min-height:44px;  /* rows=1 is the compact bar's height; grow it back here */
}
.chat.expanded .c-icon{ display:none; }
.chat.expanded .c-form .c-toggle{ display:none; }   /* #chat-expand is compact-only; the header's #chat-collapse is the one toggle here */

/* ===== interface presets (feat 14) =======================================
   body[data-preset] is set by app/chrome.js (task 4); default/unset means
   'studio'. Studio shows everything, so it needs no rule of its own here --
   only the three presets that hide something get one, and an unset
   data-preset falls through to the same "show everything" baseline as an
   explicit studio, satisfying the doc's "absent == studio" requirement for
   free rather than repeating :not([data-preset]) on every selector below. */
body[data-preset="focus"] #ribbon, body[data-preset="focus"] .app-tabs-tabs,
body[data-preset="copilot"] #ribbon, body[data-preset="copilot"] .app-tabs-tabs{
  display:none;
}
body[data-preset="focus"] .worktree, body[data-preset="focus"] .options,
body[data-preset="copilot"] .worktree, body[data-preset="copilot"] .options,
body[data-preset="direct"] .worktree, body[data-preset="direct"] .options{
  display:none;
}
body[data-preset="focus"] .chat, body[data-preset="direct"] .chat{ display:none; }
/* Copilot wants the chat forced to its compact "prompt bar" reading. The
   chat starts .compact by default (app.html) and chat.js's setMode() only
   ever adds .expanded on an explicit click of #chat-expand, so hiding that
   one button is enough to keep it compact for the preset's own session --
   this deliberately does not fight an .expanded state the user already
   opened in another preset (see feat doc's ".chat:not(.expanded)" escape
   hatch); switching a still-expanded chat back to compact is chat.js's own
   setMode(), not a CSS concern. */
body[data-preset="copilot"] #chat-expand{ display:none; }

/* ===== appearance popover (feat 35) =====
   Right-click a geometry tree row. `position:fixed` because chrome.js anchors
   it to the row's viewport rect and clamps it into the window -- an absolute
   box would be positioned against the tree's own scrolled origin instead.
   Same visual language as .prefs-panel above (the app's other popover). */
.geom-menu{
  position:fixed; z-index:40; width:224px;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:8px;
  padding:10px 12px 8px; box-shadow:0 16px 36px rgba(0,0,0,.45);
}
.geom-menu .gm-head{
  font-size:var(--fs-md); font-weight:600; color:var(--text);
  margin-bottom:8px; padding-bottom:6px; border-bottom:1px solid var(--rule);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* The colour swatch arrives as widgets.js's standard `.param` row. */
.geom-menu .param{ margin:0 0 8px; }
.geom-menu .gm-row{ display:flex; align-items:center; gap:8px; }
.geom-menu .gm-row label{ font-size:var(--fs-sm); color:var(--dim); flex:none; }
.geom-menu .gm-row input[type="range"]{ flex:1; min-width:0; }
/* Tabular figures and a fixed width, so the number does not jitter the slider
   sideways as it counts through 9% -> 10% -> 100% during a drag. */
.geom-menu .gm-pct{
  font-size:var(--fs-sm); color:var(--dim); flex:none;
  width:34px; text-align:right; font-variant-numeric:tabular-nums;
}
/* Mirror popover (edit.js: mirrorFields): two rows of .part-toggle-btn --
   planes, then rotation icon | X Y Z | Instances [n]. Wider than the
   appearance popover so the rotation row stays on one line. */
/* Everything here is 1.4x the appearance popover's scale (owner, 2026-09-26:
   "the mirror popover altogether, 40% bigger") -- sizes, not `zoom`, since
   edit.js positions the box from the button's rect in unzoomed px. */
/* Docked in #view's bottom-left, stacked above .gizmo (bottom:74px + its 64px
   + a gap), not anchored to the ribbon button -- anchored, it covered the
   ribbon's other menus (owner, 2026-10-01). z-index above .gizmo's 3. */
#mirror-menu{
  position:absolute; left:18px; bottom:148px; z-index:5;
  max-height:calc(100% - 160px); overflow:auto;
  width:auto; padding:14px 17px 11px; border-radius:11px;
}
#mirror-menu .gm-head{ cursor:grab; touch-action:none; font-size:17.5px; margin-bottom:11px; padding-bottom:8px; padding-right:24px; }
#mirror-menu .hint-close{ top:10px; right:10px; }
#mirror-menu .params .note{ font-size:var(--fs-xl); margin:-2px 0 0; }
.geom-menu .sym-row{ display:flex; align-items:center; gap:6px; margin-bottom:11px; }
.geom-menu .sym-row .part-toggle-btn{
  display:flex; align-items:center; justify-content:center;
  min-width:39px; height:39px; padding:0 8px; font-size:17px; border-radius:6px;
}
.geom-menu .sym-row .ico{ width:25px; height:25px; }
.geom-menu .sym-row > .ico{ color:var(--dim); margin-right:3px; }
/* Pressed reads at a glance: the accent paints the icon/letter and a tint
   fills the pill, on top of .active's accent border. */
.geom-menu .sym-row .part-toggle-btn.active{ color:var(--primary); background:var(--hover); }
.geom-menu .sym-row .param{ flex-direction:row; align-items:center; gap:8px; margin:0 0 0 11px; }
.geom-menu .sym-row .param label{ font-size:17px; }
.geom-menu .sym-row .param input{ width:78px; height:39px; box-sizing:border-box; padding-block:0; font-size:17px; }

/* ===== example gallery (feat 36) ========================================
   A full-window overlay, lifted from flow.html's `#screen-welcome` Examples
   tab -- the mock's `.p-tile` grid, `.thumb`/`.meta`/`.p-name`/`.p-date` class
   names kept verbatim so the two stay recognisably the same screen when the
   real welcome screen lands and this overlay goes away. Nothing here is a new
   colour: --chrome/--panel/--sunken/--rule/--dim/--faint/--primary/
   --contextual are all app.css:5-11's own tokens. */
.gallery{
  position:fixed; inset:0; z-index:60; display:flex; flex-direction:column;
  background:var(--bg); padding:22px 28px 8px;
}
.gallery[hidden]{ display:none; }
.gal-top{ display:flex; align-items:baseline; gap:14px; flex:none; }
.gal-top h2{ margin:0; font-size:20px; font-weight:600; letter-spacing:-.01em; }
.gal-count{ font-size:var(--fs-sm); color:var(--faint); flex:1; }
.gal-x{
  flex:none; width:28px; height:28px; border-radius:4px; border:0; cursor:pointer;
  background:transparent; color:var(--dim); font-size:20px; line-height:1;
}
.gal-x:hover{ background:var(--hover); color:var(--text); }
.gal-search{
  flex:none; margin:14px 0 0; max-width:320px; font:inherit; font-size:var(--fs-md);
  background:var(--chrome); border:1px solid var(--rule-strong); color:var(--text);
  border-radius:5px; padding:8px 12px;
}
.gal-search::placeholder{ color:var(--faint); }
/* The Phase 1 / Phase 2 strip (feat 55). `.gate-seg-btn` is flex:1, which in a
   full-screen overlay stretches two tabs across the whole window; capped to the
   search box's width so the pair reads as one control above it. */
.gate-seg.gal-phase{ flex:none; max-width:320px; padding:14px 0 0; }
/* Two banners share this look and nothing else: `.gal-note` (why an example
   cannot open) and `.gal-save` (a note that failed to save). They are separate
   classes so each is uniquely addressable -- they were one, and `.gal-note`
   then matched both. */
.gal-banner{
  flex:none; margin-top:12px; padding:9px 12px; border-radius:5px;
  background:color-mix(in srgb, var(--contextual) 16%, transparent);
  border:1px solid var(--contextual); color:var(--text);
  font-size:var(--fs-sm); line-height:1.5; max-width:640px;
}
.gal-scroll{ flex:1; overflow-y:auto; margin-top:16px; padding:2px 6px 20px 2px; }
.gal-grid{
  display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:16px;
}
.gal-grid .p-tile{
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:6px;
  overflow:hidden; display:flex; flex-direction:column; text-align:left;
  cursor:pointer; color:inherit; padding:0; font:inherit;
  transition:border-color .15s ease, transform .15s ease;
}
.gal-grid .p-tile:hover:not(:disabled){
  border-color:var(--contextual); transform:translateY(-3px);
}
.gal-grid .p-tile:disabled{ opacity:.45; cursor:not-allowed; }
@media (prefers-reduced-motion:reduce){
  .gal-grid .p-tile{ transition:none; }
  .gal-grid .p-tile:hover:not(:disabled){ transform:none; }
}
.gal-grid .thumb{
  height:104px; background:var(--sunken); display:flex;
  align-items:center; justify-content:center; color:var(--rule);
}
.gal-grid .thumb svg{ width:46px; height:46px; }
.gal-grid .meta{ padding:11px 13px 12px; display:flex; flex-direction:column; gap:3px; }
.gal-grid .p-name{ font-size:var(--fs-md); font-weight:600; color:var(--text); overflow-wrap:anywhere; }
.gal-grid .p-date{ font-size:var(--fs-sm); color:var(--dim); overflow-wrap:anywhere; }
/* How deep into the parity roadmap this model's last blocker sits. The list is
   ordered by it, and an order with nothing explaining it reads as no order. */
.p-step{
  align-self:flex-start; margin-top:4px; padding:1px 6px; border-radius:3px;
  font-size:var(--fs-xs); letter-spacing:.04em; text-transform:uppercase;
  color:var(--faint); border:1px solid var(--rule-soft);
}
.p-step-0{ color:var(--primary); border-color:var(--contextual); }

/* ---- the tree, while an example is open. Read-only rows: examples.js draws
   them through the same renderItemList the real tree uses, so .t-row and the
   depth indentation need no rules of their own here. */
.ex-title{ padding:0 2px; font-size:var(--fs-md); font-weight:600; color:var(--text); overflow-wrap:anywhere; }
.ex-sub{ padding:0 2px 8px; font-size:var(--fs-xs); color:var(--faint); }
.ex-warn{
  margin:0 0 8px; padding:7px 9px; border-radius:4px; font-size:var(--fs-xs); line-height:1.45;
  color:var(--text); background:color-mix(in srgb, var(--error) 18%, transparent);
  border:1px solid var(--error); overflow-wrap:anywhere;
}
.ex-tag{
  /* .item-row is `align-items:stretch`, so without the inner centring the
     label sits at the top of a row whose height the button sets. */
  display:flex; align-items:center; flex:none;
  padding-left:6px; font-size:var(--fs-xs); letter-spacing:.03em;
  text-transform:uppercase; color:var(--faint); white-space:nowrap;
}
/* A geom that exported no triangles AND should have. It is in the file and in
   the tree and it is NOT in the viewport -- half of what the compliance report
   is asking. A surfaceless geom (BLANK/HINGE/ROUTING) is NOT given this: it
   draws a marker glyph instead and is not a finding (feat 37 F1). */
.ex-tag-none{ color:var(--error); }
/* A sub-surface -- a control surface, a line, a rectangle (feat 37 F4). Not a
   geom and not a row: it hangs off its parent's row, because `blendedwingbody`
   carries five of them and the tree showed none. Dimmer than .ex-tag and NOT
   uppercased, because a sub-surface carries the modeller's own name and
   `SS_CONT_0` shouted in caps reads as an error code. */
.ex-tag-sub{
  text-transform:none; letter-spacing:0;
  color:var(--dim); opacity:.75;
}
.worktree.collapsed .ex-title, .worktree.collapsed .ex-sub,
.worktree.collapsed .ex-warn, .worktree.collapsed .ex-tag,
.worktree.collapsed .ex-tag-sub{ display:none; }

/* ---- the notepad (feat 36 row 7). The gallery tile carries the three
   compliance columns of docs/local/EXAMPLE_COMPLIANCE.md, and the SAME editor
   is mounted in the tree panel beside an open model. */

/* The previz colour IS the review status, which is the point of colouring it:
   the board reads at a glance from wherever you have scrolled to. Unset stays
   the app's own inert grey -- an unreviewed model must not look like a verdict. */
.gal-grid .p-tile[data-status="no"]     { border-color:var(--bad); }
.gal-grid .p-tile[data-status="partial"]{ border-color:var(--warn); }
.gal-grid .p-tile[data-status="yes"]    { border-color:var(--ok); }
.gal-grid .p-tile[data-status="no"]      .thumb{ color:var(--bad);
  background:color-mix(in srgb, var(--bad) 14%, var(--sunken)); }
.gal-grid .p-tile[data-status="partial"] .thumb{ color:var(--warn);
  background:color-mix(in srgb, var(--warn) 14%, var(--sunken)); }
.gal-grid .p-tile[data-status="yes"]     .thumb{ color:var(--ok);
  background:color-mix(in srgb, var(--ok) 14%, var(--sunken)); }

/* Shorter than it was: the tile now holds three controls under the glyph, and
   the glyph is a placeholder anyway (no thumbnails -- see examples.js THUMB). */
.gal-grid .thumb{ height:74px; }
.gal-grid .thumb svg{ width:34px; height:34px; }

.ex-open{
  margin-top:8px; width:100%; padding:6px 8px; font:inherit; font-size:var(--fs-sm);
  background:var(--panel); color:var(--dim); border:1px solid var(--rule-strong);
  border-radius:4px; cursor:pointer;
}
.ex-open:hover:not(:disabled){ background:var(--hover); color:var(--text); border-color:var(--contextual); }
.ex-open:disabled{ opacity:.5; cursor:not-allowed; }

.ex-note{ display:flex; flex-direction:column; gap:6px; padding:0 13px 13px; }
/* In the tree panel there is no tile padding to inherit. */
.tree .ex-note{ padding:2px 2px 8px; }

.ex-seg{ display:flex; gap:4px; }
.ex-sw{
  flex:1; min-width:0; padding:5px 2px; font:inherit; font-size:var(--fs-xs);
  letter-spacing:.03em; text-transform:uppercase; cursor:pointer;
  background:transparent; color:var(--faint);
  border:1px solid var(--rule-soft); border-radius:4px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.ex-sw:hover{ background:var(--hover); color:var(--text); }
/* Only the CHOSEN one is coloured. Three lit swatches would say the status is
   all three at once, which is what a plain hover/idle tint would look like. */
.ex-sw-no[aria-checked="true"]{
  background:color-mix(in srgb, var(--bad) 26%, transparent);
  border-color:var(--bad); color:var(--text);
}
.ex-sw-partial[aria-checked="true"]{
  background:color-mix(in srgb, var(--warn) 26%, transparent);
  border-color:var(--warn); color:var(--text);
}
.ex-sw-yes[aria-checked="true"]{
  background:color-mix(in srgb, var(--ok) 26%, transparent);
  border-color:var(--ok); color:var(--text);
}

.ex-field{
  width:100%; font:inherit; font-size:var(--fs-sm); padding:6px 8px;
  background:var(--sunken); color:var(--text);
  border:1px solid var(--rule-soft); border-radius:4px;
}
.ex-field::placeholder{ color:var(--faint); }
.ex-field:focus-visible{ border-color:var(--primary); }
.ex-text{ resize:vertical; min-height:44px; line-height:1.4; font-family:inherit; }

/* A save failure. Same shape as .gal-note but --error, because an optimistic
   editor that lost a write looks exactly like one that succeeded. */
.gal-save{
  background:color-mix(in srgb, var(--error) 18%, transparent);
  border-color:var(--error);
}
.worktree.collapsed .ex-note{ display:none; }

/* ===== the agent gallery (feat 39 row 4) ===== */
/* The gate's review strip, under the example tree. Deliberately quiet: it sits
   beside the compliance notepad and must not compete with the viewport, which
   is the thing actually being reviewed. */
.gate-panel {
  border-top: 1px solid var(--rule);
  margin-top: 8px;
  padding-top: 8px;
  /* body sets no size, so without this the panel read at the UA's 16px beside
     a 13px tree. Everything inside inherits it unless it sets its own. */
  font-size: var(--fs-sm);
}
.gate-head {
  font-weight: 600;
  padding: 0 8px 6px;
}
.gate-seg {
  gap: 4px;
  padding: 0 8px 6px;
}
/* The Geometry/Agent tab strip (chrome.js) and the gallery's Phase 1/Phase 2
   strip (examples.js) both carry `.ex-tabs` and stay a plain flex row of two,
   same as before the grid rule below existed. */
.gate-seg.ex-tabs { display: flex; }
/* feat 58 row 7: gate.js's own segment strip (Reference/Agent/Batch/Optimiser
   [/Ladder]) grew a 5th button and no longer fits one flex row at 12px in the
   ~250px tree column -- "Reference Agent Batch Optimiser" already filled it
   (screenshot, 2026-09-16). A 3-column grid wraps to a second row instead of
   shrinking the buttons further; the level strip below reuses this same
   selector (it is a `.gate-seg` too, five chips of its own). Phase 2's
   2-button case (Reference/Agent only, same bare `.gate-seg` with no `.ex-tabs`)
   still lands on one row -- 2 items in 3 columns just leaves the third empty. */
.gate-seg:not(.ex-tabs) {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}
.gate-seg-btn { flex: 1; min-width: 0; justify-content: center; font-size: var(--fs-sm); padding: 5px 4px; }
/* The selected pipeline. Same affordance the compliance strip's segmented
   status buttons already use, so the two strips read as one review surface. */
.gate-seg-btn[aria-selected="true"] {
  /* The 16%-of-primary mix is this stylesheet's own selected-row
     convention -- see `.item-children .t-row[aria-selected="true"]`. Reused
     rather than invented so the two strips cannot drift apart. */
  background: color-mix(in srgb, var(--primary) 16%, transparent);
  color: var(--text);
}
.gate-status {
  padding: 0 8px 6px;
  font-size: var(--fs-sm);
  color: var(--dim);
}

/* The brief, always visible -- see gate.js for why it is not a <details>.
   Scrolls in its own box rather than growing the tree column: at ~170 words it
   would push the notepad off screen, and the notepad is what the grader is
   here to fill in. */
.gate-brief-head { padding-top: 4px; }
.gate-brief-body {
  margin: 0 8px 8px;
  max-height: 220px;
  overflow-y: auto;
  white-space: pre-wrap;
  line-height: 1.5;
  color: var(--dim);
}

/* ===== the optimiser stepper (feat 43 row 2) =====
   Prev/next over the scored iterations of a `gate_optimise.py` run. Toggled by
   `hidden` rather than rebuilt, because gate.js's segment switches views
   without going through renderTree() -- so both flex containers need their own
   `[hidden]` rule: an author `display` beats the UA sheet's, and without these
   `hidden = true` is a silent no-op (see web/README.md). */
.gate-opt[hidden], .gate-step[hidden], .gate-opt-notes[hidden]{ display:none; }
.gate-opt{ padding-bottom:2px; }
/* Label above, buttons below. Between them the label wrapped to two lines in a
   ~230px tree column and squeezed both buttons; stacked, each button takes half
   the width, which is also the bigger hit target. */
.gate-step{
  display:flex; align-items:center; gap:6px; padding:0 8px 6px;
}
/* A soft outline the segment buttons do not have: those are a pick-one-of-three
   whose selected state carries the affordance, these are two ordinary controls
   and read as plain text without it. */
.gate-step-btn{
  flex:1; min-width:0; justify-content:center;
  border:1px solid var(--rule-soft); border-left-width:1px;
}
.gate-step-btn:not([disabled]):hover{ border-color:var(--rule); }
.gate-step-label{
  padding:0 8px 4px; text-align:center; font-size:var(--fs-sm); color:var(--text);
  font-variant-numeric:tabular-nums;
}
/* The measured numbers. Tabular figures so stepping does not shuffle the row
   sideways as digits change width. */
.gate-step-meta{
  padding:0 8px 4px; font-size:var(--fs-sm); color:var(--dim);
  font-variant-numeric:tabular-nums;
}
.gate-step-meta[hidden]{ display:none; }
/* A qualifying sentence: skipped iterations, the rejected-rewrite note, and
   the "written AFTER this iteration" label under REVIEWER's notes. */
.gate-hint{
  padding:0 8px 6px; font-size:var(--fs-sm); line-height:1.4; color:var(--faint);
}
.gate-hint[hidden]{ display:none; }
.gate-step-note{ color:var(--warn); }
/* The promote command, printed rather than run -- promotion writes the owner's
   ledger. Selectable, wraps rather than truncates: a command the reader cannot
   see the end of is a command they cannot run. */
.gate-cmd{
  margin:0 8px 8px; padding:6px 8px; font-size:var(--fs-sm); line-height:1.4;
  font-family:ui-monospace,SFMono-Regular,Consolas,monospace;
  background:var(--sunken); color:var(--dim);
  border:1px solid var(--rule-soft); border-radius:4px;
  overflow-wrap:anywhere; user-select:text;
}
.gate-cmd[hidden]{ display:none; }
/* REVIEWER's notes reuse .gate-brief-body's scroll box: same kind of content
   (prose, in its own box, under the brief) and one rule to keep them alike. */
.gate-opt-notes .gate-brief-body{ max-height:180px; }

/* ===== the Ladder segment (feat 58 row 7) =====
   The level strip is a `.gate-seg` (the grid rule above already wraps its
   five L0-L4 chips), disabled state and selected tint both borrowed for free
   from `.gate-seg-btn`/`[disabled]`. The turn stepper reuses `.gate-opt`/
   `.gate-step*` verbatim -- ladder.js gives it the extra `ladder-step-wrap`/
   `ladder-step-btn` classes only so a test can tell it apart from the
   optimiser's own stepper; `.gate-opt[hidden]` above already covers it. Only
   the run picker is new layout: a plain wrapping row, hidden like everything
   else here with an explicit rule because it gets an author `display`. */
.ladder-runs{
  display:flex; flex-wrap:wrap; gap:4px; padding:0 8px 6px;
}
.ladder-runs[hidden]{ display:none; }

/* The example column's two tabs (Geometry / Agent review). Same segmented
   affordance as .gate-seg, whose rules it borrows by carrying both classes --
   one visual language for "pick one of two" across the review surfaces. */
.ex-tabs { padding-top: 8px; }
/* First thing in the column, so the panel below it needs no second divider. */
.ex-tabs + .gate-panel { border-top: 0; margin-top: 0; }

/* ===== lab flow (feat 60, restyled to the brand system 2026-10-01) ==========
   The consent -> begin -> survey -> done screens of web/app/lab.js. Follows nodevsp-branding/DESIGN.md: three voices (Archivo for
   the wordmark and headings, Inter for everything read, Plex Mono only for a
   one-line value: the code), pill buttons, raised panels with a
   strong hairline, and one soft glow falling from the top edge. No shadows.
   The brand's values are set as the app's own token NAMES, scoped to `.lab`, so every rule
   below keeps reading var(--text) etc. while the modeler's :root tokens, and
   check_app.py's parity check on them, are untouched. Both themes are defined below.
   `.lab-btn` is also the dialog's button (modal.js): its SHAPE is app-wide, its
   brand colours live under `.lab`, so the dialog stays right in the light theme. */
.lab{
  --chrome:#1a1a1a; --panel:#212121; --hover:#2b2b2b;
  --rule:#3a3a3a; --rule-soft:#2e2e2e;
  --text:#ececec; --dim:#a3a3a3; --faint:#858585;
  --hot:#f2622f; --ember:#f0916c; --ease:cubic-bezier(.16,1,.3,1);
  /* Dark brand values; the light ones follow below, under :root.light. */
  --bg:#151515; --sunken:#111111; --primary:#E8501F; --error:#B8394B; --on-primary:#151515;
  color:var(--text);
  font-family:var(--font-body);
}
:root.light .lab{
  /* DESIGN.md "Light theme (toggle)": ground #fafafa, raised #fff, step #f0f0f0,
     hairlines #e5e5e5/#d4d4d4, text #151515/#4b4b4b/#6b6b6b, accent #C43D14. */
  --bg:#fafafa; --chrome:#ffffff; --panel:#f0f0f0; --sunken:#f0f0f0; --hover:#f0f0f0;
  --rule:#d4d4d4; --rule-soft:#e5e5e5;
  --text:#151515; --dim:#4b4b4b; --faint:#6b6b6b;
  --primary:#C43D14; --hot:#a93410; --ember:#C43D14; --on-primary:#ffffff; --error:#B3261E;
}
:root.light .lab{ background-image:radial-gradient(ellipse 90% 35% at 50% 0, rgba(196,61,20,.12), transparent 70%); }
:root.light .lab-err{ color:var(--error); }
.lab{
  position:fixed; inset:0; z-index:80; overflow-y:auto; background:var(--bg);
  background-image:radial-gradient(ellipse 90% 35% at 50% 0, rgba(232,80,31,.16), transparent 70%);
  display:flex; flex-direction:column;
}
.lab[hidden]{ display:none; }
.lab ::selection{ background:color-mix(in srgb, var(--primary) 45%, transparent); color:var(--text); }
.lab a{ color:var(--ember); }
.lab-bar{ display:flex; align-items:center; gap:10px; padding:20px clamp(20px,4vw,48px) 0; flex:none; }
.lab-bar svg{ width:22px; height:22px; }
.lab-bar span{
  font-family:var(--font-display); font-size:13px; font-weight:800; font-stretch:125%;
  letter-spacing:.1em; text-transform:uppercase;
}

.lab-screen{
  width:100%; max-width:1080px; margin:0 auto; flex:1;
  padding:clamp(20px,5vh,56px) clamp(20px,4vw,48px) 64px;
  display:grid; gap:clamp(28px,5vw,64px); align-content:start; align-items:center;
}
.lab-screen[data-wide]{ grid-template-columns:minmax(0,1.05fr) minmax(0,1fr); }
@media (max-width:860px){
  .lab-screen[data-wide]{ grid-template-columns:1fr; }
  .lab-hero{ display:none; }
}
.lab h1{
  margin:0; font-family:var(--font-display); font-size:clamp(28px,3.9vw,46px); line-height:1.08;
  font-weight:800; font-stretch:118%; letter-spacing:-.02em; text-wrap:balance; outline:none;
}
.lab-lead{ margin:18px 0 0; max-width:52ch; font-size:clamp(15px,1.5vw,16.5px); line-height:1.65; color:var(--dim); }
.lab-fine{ margin:14px 0 0; max-width:56ch; font-size:13.5px; line-height:1.55; color:var(--dim); }

.lab-panel{ margin:26px 0 0; padding:26px 28px; border:1px solid var(--rule-strong); border-radius:14px; background:var(--chrome); max-width:520px; }
.lab-panel h2{ margin:0 0 14px; font-family:var(--font-display); font-size:15px; font-weight:700; font-stretch:112%; letter-spacing:-.015em; }
.lab-panel ul{ margin:0; padding:0; list-style:none; display:grid; gap:10px; font-size:14px; line-height:1.5; color:var(--dim); }
.lab-panel strong{ color:var(--text); font-weight:600; }
/* The landing's .lab-goals marker: an 11px orange dot. The controls list keeps
   its glyphs instead. */
.lab-panel ul:not(.lab-controls) li{ position:relative; padding-left:26px; }
.lab-panel ul:not(.lab-controls) li::before{
  content:""; position:absolute; left:0; top:.4em; width:11px; height:11px; border-radius:50%; background:var(--primary);
}
.lab-controls li{ display:flex; align-items:center; gap:10px; }
.lab-ico{ flex:none; width:22px; height:22px; color:var(--dim); }
.lab-ico svg{ display:block; width:100%; height:100%; }

.lab-agree{ display:flex; gap:12px; align-items:flex-start; margin:22px 0 0; min-height:44px; font-size:14.5px; line-height:1.5; cursor:pointer; }
.lab-agree input{ flex:none; width:20px; height:20px; margin:2px 0 0; accent-color:var(--primary); cursor:pointer; }
.lab-actions{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; margin-top:22px; }

.lab-btn{
  display:inline-flex; align-items:center; justify-content:center; min-height:48px; padding:0 26px;
  font-family:var(--font-body); font-size:15px; font-weight:600; text-decoration:none; cursor:pointer;
  color:var(--text); background:transparent; border:1px solid var(--rule-strong); border-radius:999px;
  transition:background-color .25s var(--ease), border-color .25s var(--ease), color .25s var(--ease), transform .25s var(--ease);
}
.lab-btn:hover:not(:disabled){ background:var(--hover); }
.lab-btn:active:not(:disabled){ transform:translateY(1px); }
.lab-btn:focus-visible{ outline:2px solid var(--primary); outline-offset:3px; }
.lab-btn:disabled{ cursor:not-allowed; color:var(--faint); background:var(--chrome); border-color:var(--rule-soft); }
.lab-btn-go{ color:var(--on-primary); background:var(--primary); border-color:var(--primary); }
.lab-btn-go:hover:not(:disabled){ background:var(--hot, color-mix(in srgb, var(--primary) 86%, #fff)); border-color:var(--hot, transparent); }
.lab-btn-go:disabled{ color:var(--faint); background:var(--chrome); border-color:var(--rule-soft); }

/* The ES/EN and Light/Dark switches (lab.js prefsSwitches): quiet pills at the bar's far edge. */
.lab-prefs{ margin-left:auto; display:flex; gap:8px; }
.lab-lang{
  min-width:48px; min-height:44px; padding:0 14px; cursor:pointer;
  font-family:var(--font-body); font-size:13px; font-weight:600; letter-spacing:.04em; color:var(--text);
  background:transparent; border:1px solid var(--rule-strong); border-radius:999px;
  transition:background-color .25s var(--ease), border-color .25s var(--ease);
}
.lab-lang:hover{ background:var(--hover); }
.lab-lang:focus-visible{ outline:2px solid var(--primary); outline-offset:3px; }

/* The reference set, as pictures. One authored moment: the tiles rise in once,
   exponential ease-out, from a default that is already visible. */
.lab-hero{ display:grid; grid-template-columns:1fr 1fr; gap:12px; margin:0; padding:0; list-style:none; }
.lab-hero li{
  border:1px solid var(--rule-soft); border-radius:14px; overflow:hidden; background:var(--sunken);
  animation:lab-rise .6s var(--ease) backwards;
}
.lab-hero li:nth-child(even){ position:relative; top:20px; }
.lab-hero li:nth-child(2){ animation-delay:.08s; }
.lab-hero li:nth-child(3){ animation-delay:.16s; }
.lab-hero li:nth-child(4){ animation-delay:.24s; }
.lab-hero img{ display:block; width:100%; aspect-ratio:16/10; object-fit:cover; }
@keyframes lab-rise{ from{ opacity:0; transform:translateY(14px); } }
@media (prefers-reduced-motion:reduce){
  .lab-hero li{ animation:none; }
  .lab-btn, .lab-lang, .lab-scale span{ transition:none; }
}

.lab-form{ display:grid; gap:34px; max-width:640px; margin-top:8px; }
.lab-q{ display:block; margin:0; padding:0; border:0; min-width:0; }
.lab-label{ display:block; padding:0; margin-bottom:10px; font-size:15px; font-weight:600; line-height:1.4; }
.lab-hint{ display:block; margin-top:2px; font-size:13.5px; font-weight:400; color:var(--faint); }
.lab-field{
  display:block; width:100%; font:inherit; font-size:15px; line-height:1.5; color:var(--text);
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:12px; padding:12px 16px;
  caret-color:var(--primary);
}
.lab-field:focus-visible{ outline:2px solid var(--primary); outline-offset:3px; border-color:var(--primary); }
textarea.lab-field{ resize:vertical; min-height:96px; }
.lab-brief{ min-height:150px; font-size:16px; }

.lab-scale{ display:grid; grid-auto-flow:column; grid-auto-columns:1fr; gap:6px; }
.lab-scale label{ position:relative; display:block; }
.lab-scale input{ position:absolute; inset:0; width:100%; height:100%; margin:0; opacity:0; cursor:pointer; }
.lab-scale span{
  display:flex; align-items:center; justify-content:center; min-height:44px; padding:6px 8px; text-align:center;
  font-size:14px; font-weight:500; color:var(--dim); background:var(--chrome);
  border:1px solid var(--rule-strong); border-radius:12px; transition:background-color .25s var(--ease), color .25s var(--ease);
}
.lab-scale label:hover input:not(:checked) + span{ background:var(--hover); color:var(--text); }
.lab-scale input:checked + span{ color:var(--on-primary); background:var(--primary); border-color:var(--primary); font-weight:600; }
.lab-scale input:focus-visible + span{ outline:2px solid var(--primary); outline-offset:3px; }
.lab-ends{ display:flex; justify-content:space-between; margin-top:6px; font-size:12.5px; color:var(--faint); }

/* role=alert, so it stays in the layout (display:none regions are announced
   unreliably) and collapses to nothing while empty. */
.lab-err{
  margin:0; padding:12px 16px; border:1px solid color-mix(in srgb, var(--error) 70%, transparent); border-radius:12px;
  background:color-mix(in srgb, var(--error) 14%, transparent);
  color:color-mix(in srgb, var(--error) 40%, #fff); font-size:14px; line-height:1.5;
}
.lab-err:empty{ padding:0; border:0; background:none; }

.lab-code{
  display:inline-block; margin:8px 0 0; padding:14px 22px; border:1px solid var(--rule-strong); border-radius:14px; background:var(--chrome);
  font-family:var(--font-mono); font-weight:500; font-size:clamp(28px,5vw,40px); letter-spacing:.06em;
}
.lab-shot{ margin:0; }
.lab-shot canvas{
  display:block; width:100%; aspect-ratio:4/3; border:1px solid var(--rule-soft); border-radius:14px;
  background:var(--sunken); cursor:grab; touch-action:none;
}
.lab-shot canvas:active{ cursor:grabbing; }
.lab-shot figcaption{ margin:8px 0 18px; font-size:13px; color:var(--faint); }
/* Four buttons under a half-width canvas: tighter pills that share the row,
   so they sit on one line down to a phone-width column; narrower still, they wrap. */
.lab-shot .lab-actions{ gap:8px; margin-top:0; }
.lab-shot .lab-btn{ flex:1 1 auto; padding:0 14px; white-space:nowrap; }
@media (max-width:480px){ .lab-shot .lab-actions{ gap:6px; } .lab-shot .lab-btn{ padding:0 10px; font-size:14px; } }

/* The operator's "this isn't me" escape (lab.js: resetSession). A text link,
   not a lab-btn: it must read as a quiet exit, never as the next step. */
.lab-reset{
  margin-left:auto; display:inline-flex; align-items:center; min-height:44px; background:none; border:0; padding:0 8px; cursor:pointer;
  font:inherit; font-size:12px; color:var(--faint); text-decoration:underline;
  text-decoration-color:var(--rule); text-underline-offset:2px;
}
.lab-reset:hover{ color:var(--dim); text-decoration-color:var(--dim); }
.lab-reset:focus-visible{ outline:2px solid var(--primary); outline-offset:3px; }

/* ---- the one confirmation dialog (feat 62 row 8, app/modal.js) ----
   A native <dialog>, so the focus trap, Escape, the backdrop and the top layer
   are the browser's; only the box is ours. `color`/`background` are set because
   the UA stylesheet hands a dialog `canvas`/`canvastext`, which is white on a
   dark app. Buttons wear the lab screens' `.lab-btn` rather than a second pair
   of button rules -- those declarations are not lab-specific, only lab-named,
   and this dialog is app-wide (chrome.js's Clear uses it outside lab mode). */
.confirm{
  max-width:min(420px, calc(100vw - 32px)); padding:22px 24px 18px;
  color:var(--text); background:var(--chrome);
  border:1px solid var(--rule-strong); border-radius:10px;
  box-shadow:0 18px 48px rgba(0,0,0,.55);
}
.confirm::backdrop{ background:rgba(0,0,0,.55); }
.confirm p{ margin:0; max-width:46ch; font-size:15px; line-height:1.55; }
.confirm-actions{ display:flex; flex-wrap:wrap; gap:10px; justify-content:flex-end; margin-top:20px; }
/* feat 108 row 6 / feat 129 row 3: the reference WORKSPACE (refdialog.js). Not modal: a fixed, draggable
   window the viewport stays live behind. Left the views list, right the stage (image + marks in one
   SVG whose viewBox is the stage's own pixel size over the zoom) with a ruler band on the left and bottom. */
.confirm.blueprint{ position:fixed; margin:0; right:auto; bottom:auto; width:min(980px, calc(100vw - 16px)); max-width:none; max-height:calc(100vh - 16px);
  overflow:auto; padding:0 14px 10px; z-index:8000; box-shadow:0 12px 36px rgba(0,0,0,.45); }   /* above the viewport's own floating bars (<= 90), below the lab screens (9000) */
.confirm.blueprint [hidden]{ display:none !important; }
.confirm.blueprint .lab-btn{ min-height:34px; padding:0 12px; font-size:var(--fs-md); gap:6px; }
.confirm.blueprint .confirm-actions{ margin-top:10px; }
.bp-head{ position:sticky; top:0; z-index:2; margin:0 -14px 8px; padding:8px 14px 4px; background:var(--chrome); border-bottom:1px solid var(--rule-soft); cursor:move; user-select:none; }
.bp-title{ margin:0; font-size:var(--fs-xl); font-weight:600; }
.confirm .bp-note{ font-size:var(--fs-sm); color:var(--dim); max-width:none; line-height:1.4; }
.bp-top{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
.blueprint-file{ position:absolute; width:1px; height:1px; opacity:0; overflow:hidden; }
.bp-top label.lab-btn{ position:relative; cursor:pointer; }
.bp-top label.lab-btn:focus-within{ outline:2px solid var(--primary); outline-offset:1px; }
.bp-ask{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; padding:8px 10px; border:1px solid var(--primary); border-radius:6px; font-size:var(--fs-md); }
.bp-ask > span{ flex:1 1 200px; }
.bp-body{ display:flex; gap:14px; margin-top:10px; align-items:flex-start; }
.bp-views{ flex:0 0 280px; display:flex; flex-direction:column; gap:6px; }
.bp-view{ border:1px solid var(--rule-soft); border-radius:6px; padding:6px; font-size:var(--fs-sm); }
.bp-view.is-active{ border-color:var(--primary); box-shadow:inset 3px 0 0 var(--primary); }
.bp-view-sel{ display:flex; gap:8px; align-items:center; width:100%; padding:2px; background:none; border:0; color:var(--text); font:inherit; text-align:left; cursor:pointer; min-height:44px; border-radius:4px; }
.bp-view .lab-btn{ font-size:var(--fs-sm); padding:0 8px; min-height:30px; }
.bp-view-sel:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.bp-thumb{ flex:0 0 84px; height:48px; background:var(--sunken) no-repeat center; border:1px solid var(--rule-soft); border-radius:4px; }
.bp-view-txt{ display:flex; flex-direction:column; min-width:0; }
.bp-name{ color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bp-status{ color:var(--dim); }
.bp-view-ctl{ display:flex; flex-wrap:wrap; gap:4px 8px; align-items:center; margin-top:4px; }
.bp-slider, .bp-ink{ display:inline-flex; align-items:center; gap:4px; }
.bp-slider input{ width:64px; accent-color:var(--primary); }
.bp-work{ flex:1 1 0; min-width:0; }
.bp-tools{ display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
.bp-tool.is-on{ background:var(--primary); color:var(--on-primary); border-color:var(--primary); }
.confirm.blueprint .bp-icon{ min-width:34px; padding:0 8px; }
.bp-icon span{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }   /* icon only; the aria-label and title name it */
.confirm .bp-hint{ margin:6px 0 0; font-size:var(--fs-sm); color:var(--dim); max-width:none; min-height:1lh; }
.confirm .bp-hint-note.is-bad{ color:var(--bad); }
.bp-grid{ display:grid; grid-template-columns:46px minmax(0, 1fr); grid-template-rows:clamp(180px, 34vh, 380px) 22px; margin-top:6px; }
.bp-ruler{ display:block; width:100%; height:100%; overflow:hidden; background:var(--chrome); }
.bp-ruler line{ stroke:var(--dim); stroke-width:1; }
.bp-ruler text{ fill:var(--dim); font-size:var(--fs-xs); font-family:var(--font-mono); }
.bp-corner{ font-size:var(--fs-xs); color:var(--dim); background:var(--chrome); display:flex; align-items:center; justify-content:center; }
.blueprint-stage{ position:relative; overflow:hidden; background:var(--sunken); outline:1px solid var(--rule); }
.blueprint-marks{ position:absolute; inset:0; width:100%; height:100%; cursor:crosshair; touch-action:none; user-select:none; }
.blueprint-stage[data-tool="frame"] .blueprint-marks{ cursor:cell; }
.blueprint-stage.is-pan .blueprint-marks{ cursor:grab; }
.bp-img{ pointer-events:none; }
.bp-mask{ fill:rgba(0,0,0,.55); pointer-events:none; }   /* outside the active frame: the other drawings of a sheet recede */
.bp-fields{ display:flex; flex-wrap:wrap; gap:6px 12px; align-items:center; margin-top:8px; font-size:var(--fs-sm); }
.bp-fields input{ width:4.5rem; min-height:28px; background:var(--panel); color:var(--text); border:1px solid var(--rule-strong); border-radius:4px; }
.bp-fields input[aria-invalid]{ border-color:var(--bad); }
.bp-pbar{ display:flex; gap:8px; margin-top:8px; }
.confirm .bp-res{ margin:6px 0 0; font-size:var(--fs-sm); color:var(--dim); max-width:none; min-height:1lh; }
/* the frames: a distinct token per view, a faint tint, the label inside the top-left corner */
.bp-frame{ --fc:var(--primary); }
.bp-frame-top{ --fc:var(--ok); }
.bp-frame-front{ --fc:var(--warn); }
.bp-frame-body{ fill:var(--fc); fill-opacity:.08; stroke:var(--fc); stroke-width:2; vector-effect:non-scaling-stroke; pointer-events:none; }
.bp-frame.is-active .bp-frame-body{ fill-opacity:.16; stroke-width:3; }
/* feat 133 W2: outside the Frame tool the active frame is a thin outline, no tint, no grips (paintFrames hides the grips) */
.blueprint-stage:not([data-tool="frame"]) .bp-frame-body{ fill:none; fill-opacity:0; stroke-width:1.5; }
.blueprint-stage:not([data-tool="frame"]) .bp-frame-label{ display:none; }
.blueprint-stage[data-tool="frame"] .bp-frame.is-active .bp-frame-body{ pointer-events:visible; cursor:move; }
/* Round 4: only the ACTIVE view's frame is interactive; the others are drawn faint so a press inside them draws the active frame */
.blueprint-stage[data-tool="frame"] .bp-frame:not(.is-active){ opacity:.4; }
.bp-frame-label{ fill:var(--fc); font-weight:700; pointer-events:none; paint-order:stroke; stroke:var(--sunken); stroke-width:3px; }
.bp-grip{ fill:var(--fc); stroke:var(--sunken); stroke-width:1; vector-effect:non-scaling-stroke; pointer-events:none; }
.blueprint-stage[data-tool="frame"] .bp-frame.is-active .bp-grip{ pointer-events:visible; }   /* one action at a time: frames act only in the Frame tool; `visible`, not `all`, so a hidden (cleared) frame takes no pointer */
.blueprint-pt.is-off, .bp-link.is-off, .bp-axes.is-off{ opacity:.35; pointer-events:none; }   /* another tool's marks: dimmed and inert */
.bp-axis{ stroke-width:2; vector-effect:non-scaling-stroke; pointer-events:none; }
.bp-axis-head{ pointer-events:none; }
.bp-axis-label{ font-weight:700; pointer-events:none; paint-order:stroke; stroke:var(--sunken); stroke-width:3px; }
.bp-grip-nw, .bp-grip-se{ cursor:nwse-resize; } .bp-grip-ne, .bp-grip-sw{ cursor:nesw-resize; }
.bp-grip-n, .bp-grip-s{ cursor:ns-resize; } .bp-grip-e, .bp-grip-w{ cursor:ew-resize; }
.bp-frame-body:focus, .bp-grip:focus{ outline:none; }   /* an svg child's UA ring is drawn in user units (image px here): see .xsec-point */
.bp-frame-body:focus-visible{ stroke:var(--text); stroke-width:4; }
.bp-grip:focus-visible{ stroke:var(--text); stroke-width:3; }
.blueprint-dim{ stroke:var(--primary); stroke-width:2; vector-effect:non-scaling-stroke; pointer-events:none; }
.bp-hintrow{ display:flex; gap:10px; align-items:center; min-height:34px; }   /* the hint, and the Scale tool's distance box beside it (feat 133 W3) */
.bp-hintrow .bp-hint{ flex:1 1 0; min-width:0; margin:0; }
.bp-dim{ flex:none; display:flex; gap:4px; align-items:center; }
.bp-dim-unit{ color:var(--text); font-size:var(--fs-sm); text-shadow:0 0 3px var(--sunken); }
.blueprint-dim-input{ width:6.5rem; min-height:28px; text-align:center; font:inherit;
  background:var(--panel); color:var(--text); border:1px solid var(--primary); border-radius:4px; }
.blueprint-dim-input:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.blueprint-pt{ fill:var(--primary); fill-opacity:.35; stroke:var(--primary); stroke-width:2; vector-effect:non-scaling-stroke; cursor:grab; touch-action:none; }
.bp-pt-ends{ fill:var(--ok); stroke:var(--ok); }
.bp-pt-origin{ fill:var(--warn); stroke:var(--warn); }
.blueprint-pt:focus{ outline:none; }   /* an svg child's UA ring is drawn in user units (image px here): see .xsec-point */
.blueprint-pt:focus-visible{ stroke:var(--text); stroke-width:3; }
.bp-link{ stroke:var(--primary); stroke-width:2; vector-effect:non-scaling-stroke; pointer-events:none; }
.blueprint-loupe{ position:absolute; top:8px; width:140px; height:140px; box-sizing:border-box; border:2px solid var(--primary); border-radius:4px;
  background-color:var(--panel); background-repeat:no-repeat; background-origin:border-box; image-rendering:pixelated; pointer-events:none; }   /* 140 = LOUPE_PX in refdialog.js; border-box: the JS centres on 140/2 from the OUTER edge, where the crosshair (50% of the padding box + the 2px border) also sits */
.blueprint-loupe::before, .blueprint-loupe::after{ content:""; position:absolute; background:var(--primary); }
.blueprint-loupe::before{ left:50%; top:0; bottom:0; width:1px; }
.blueprint-loupe::after{ top:50%; left:0; right:0; height:1px; }
@media (max-width:760px){ .bp-body{ flex-direction:column; } .bp-views{ flex-basis:auto; width:100%; } }
/* feat 133 W4: a narrow window (the dialog is min(980px, 100vw - 16px)): the views list becomes a compact strip above
   the stage, one row per view, and the tool buttons draw their icon alone (title + aria-label name them) */
@media (max-width:900px){
  .bp-body{ flex-direction:column; gap:8px; }
  .bp-views{ flex:0 0 auto; width:100%; gap:3px; }
  .bp-view{ display:flex; align-items:center; gap:8px; padding:2px 6px; }
  .bp-view-sel{ flex:1 1 140px; min-width:0; min-height:34px; }
  .bp-thumb{ display:none; }
  .bp-view-txt{ flex-direction:row; flex-wrap:wrap; align-items:baseline; gap:0 8px; }   /* name, file and status run together: ~38px a row */
  .confirm .bp-note{ display:none; }
  .bp-grid{ grid-template-rows:clamp(150px, 30vh, 380px) 22px; }
  .bp-tools{ gap:4px; }
  .bp-view-ctl{ flex:0 1 auto; margin:0; flex-wrap:nowrap; }
  .bp-view-ctl .lab-btn span, .bp-tools .lab-btn:has(.ui-icon) span{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  .confirm.blueprint .bp-view-ctl .lab-btn, .confirm.blueprint .bp-tools .lab-btn:has(.ui-icon){ min-width:34px; padding:0 8px; }
}

/* ===== body views (feat 63 row 2, web/app/bodyviews.js) ==============
   The front/side pair above the fuselage panel, and the preset gallery's
   silhouettes. There is deliberately no orange rule here: the highlighted
   outline wears `.xsec-curve` (line 610) unchanged, so the cross-section and
   airfoil tabs and these two views cannot drift into two different
   --contextual treatments. Only the GREY half is new. `.xsec-centerline` is
   reused the same way.
   The svg carries its own box rather than sitting in a wrapper div like
   .foil-plot/.xsec-plot: bodyviews.js returns a bare detached <svg>, and its
   viewBox is already proportioned to the body (one scale on both axes), so
   `height:auto` is what keeps the aspect honest -- an aspect-ratio here would
   fight it and squash a 12 m fuselage back into a square. */
.bodyview{
  display:block; width:100%; height:auto;
  background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px;
}
.bodyview-ghost{ fill:none; stroke:var(--dim); stroke-width:1; }
/* --faint, not --rule: a station tick on --sunken has to stay visible at
   3:1, and --rule is a divider colour that all but vanishes on it. */
.bodyview-tick{ stroke:var(--faint); stroke-width:1; }
/* A preset tile is art beside its own name, not a plot: no box, and no
   centreline either -- the dashes run the full padded width, which on a
   boxless tile reads as a stray line rather than an axis. */
.bodyview-tile{ background:none; border:0; border-radius:0; }
.bodyview-tile .xsec-centerline{ display:none; }

/* ----- profile editor (feat 106 row 6, web/app/profileedit.js) ---------------
   The side overlay is editable: grips on the selected station, an x-handle
   for its position, and a Side/Top choice above it. Grips and handles carry
   data-kind (viewport-free: bodyviews.js), so the cursors follow the kind, and
   the host carries data-axis, so a top-view grip is the width grip (left/right). */
/* A stroke in the pane colour knocks out any line under the label (paint-order puts it beneath the fill). */
.bodyview-label{ font-size:var(--fs-xs); fill:var(--dim); font-variant-numeric:tabular-nums; font-weight:600;
  paint-order:stroke; stroke:var(--bp-paper, var(--panel)); stroke-width:3px; stroke-linejoin:round; }
.bodyview-label.is-selected{ fill:var(--primary); font-weight:800; }
/* The editor panes' lines: 2 px on screen whatever the viewBox scale. A 1 px line at a
   fractional x antialiased to half strength, so stations faded in and out as the view
   moved. :where() keeps the weight at one class, under .has-underlay's colours and the
   .is-hover widths below. The preset tiles (outside .body-overlay) keep their art widths. */
:where(.body-overlay) .bodyview-tick, :where(.body-overlay) .bodyview-ghost,
:where(.body-overlay) .xsec-curve{ vector-effect:non-scaling-stroke; stroke-width:2; }
:where(.body-overlay) .bodyview-tick{ stroke:var(--dim); }
:where(.body-overlay) line.xsec-curve{ stroke-width:2.5; }
/* Over a blueprint image (bodyviews.js drawUnderlay adds .has-underlay): the label
   halo (above) takes --bp-paper here, so it matches the image's paper.
   The ink follows the IMAGE, not the theme: drawUnderlay sets --bp-ink, --bp-dim
   and --bp-accent from the image's brightness at its opacity (bodyviews.js
   inkFor): light-theme ink on a light image, dark-theme ink on a dark one, and in
   between the brand hues moved in lightness to 4.5:1 (text) / 3:1 (lines). Until
   the brightness is measured, the theme's own tokens. */
.has-underlay{ --bp-ink:var(--text); --bp-dim:var(--dim); --bp-accent:var(--primary); --bp-paper:var(--sunken); background:var(--bp-paper); }
.body-overlay .bodyview.has-underlay{ background:var(--bp-paper); }   /* beats .body-overlay .bodyview{background:none} */
.has-underlay .bodyview-ghost, .has-underlay .xsec-centerline{ stroke:var(--bp-ink); }
.has-underlay .bodyview-ghost{ stroke-width:2; }
.has-underlay .bodyview-tick, .has-underlay .bodyview-ruler{ stroke:var(--bp-dim); }
.has-underlay .xsec-curve{ stroke:var(--bp-accent); fill:color-mix(in srgb, var(--bp-accent) 30%, transparent); }
.has-underlay .bodyview-label{ fill:var(--bp-ink); }
.has-underlay .bodyview-label.is-selected{ fill:var(--bp-accent); }
.bodyview-ruler{ stroke:var(--faint); stroke-width:1; }
.profile-head{ padding:0 8px 4px; display:flex; align-items:center; gap:6px; }   /* left: the title row's float/x sit top-right */
.profile-btn{ font:inherit; font-size:var(--fs-xs); padding:2px 8px; color:var(--text);
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:4px; cursor:pointer; }
.profile-btn{ display:inline-flex; align-items:center; justify-content:center; gap:6px; height:26px; box-sizing:border-box; }
.profile-btn[hidden]{ display:none; }   /* the wing's Fit to image hides with no reference image; inline-flex beat [hidden] */
.profile-btn:not(:has(span)){ width:26px; padding:0; }   /* icon-only (zoom): square, icon centred */
/* The Side/Top switch at the buttons' 26px and size (it is 29px at --fs-sm elsewhere). */
.profile-head .seg{ height:26px; box-sizing:border-box; }
.profile-head .seg label{ font-size:var(--fs-xs); padding:0 10px; display:flex; align-items:center; }
.ui-icon{ flex:none; }
.profile-btn:hover{ background:var(--hover); }
/* The fuselage side pane's head always overlays the svg's top rows (fuselage.js .head-over), which the
   plot fit reserves: a full-width row in the flow clipped a zoomed outline under its empty space.
   Over an underlay (.bp-over and the --bp-* vars on the pane wrapper) its controls also take the image's ink. */
.head-over{ position:relative; }
.head-over > .profile-head{ position:absolute; top:calc(var(--bp-title-h, 0px) + 4px); left:0; right:0; z-index:4; pointer-events:none; }
.head-over > .profile-head > *{ pointer-events:auto; }
.bp-over .profile-btn, .bp-over .seg label{ color:var(--bp-ink); border-color:var(--bp-dim);
  background:color-mix(in srgb, var(--bp-paper) 80%, transparent); }
.bp-over .profile-btn:hover{ background:var(--bp-paper); }
.bp-over .seg{ border-color:var(--bp-dim); }
.bp-over .seg label{ border-left-color:var(--bp-dim); }
.bp-over .seg input:checked + label{ background:var(--bp-accent); color:var(--bp-paper); }
.profile-btn:focus-visible{ outline:2px solid var(--primary); outline-offset:1px; }
/* The title row floats too (bodyoverlay.js setTitleOver sets .bp-title-over and the --bp-* vars on the
   box): out of the flow, so the pane's content (the image) starts at the box's top edge. */
.bp-title-over > .body-overlay-title{ position:absolute; top:0; left:0; right:0; z-index:4; color:var(--bp-ink); pointer-events:none; }
.bp-title-over > .hint-close{ color:var(--bp-ink); border:1px solid var(--bp-dim); border-radius:4px;
  background:color-mix(in srgb, var(--bp-paper) 80%, transparent); }
.bp-title-over > .hint-close:hover{ background:var(--bp-paper); color:var(--bp-ink); }
.bp-title-over > .hint-close:focus-visible, .bp-over .profile-btn:focus-visible{ outline:2px solid var(--bp-ink); outline-offset:-2px; }

.profile-host[data-caption]{ position:relative; }
.profile-host[data-caption]::before{ content:attr(data-caption); position:absolute; left:6px; top:2px;
  font-size:var(--fs-xs); color:var(--faint); pointer-events:none; }
/* a fin's wing panes sit side by side (wingpanes.js setMode 'row'): front left, planform right, widths set inline */
.wing-row{ display:flex; align-items:flex-start; }
.wing-row > .profile-host{ flex:none; }
.profile-host{ touch-action:none; border-radius:4px; }
.profile-host svg{ touch-action:none; }
.profile-host:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
/* Hover (profileedit.js setHover): the cursor lives on the host, from hitTest, since
   the hit areas are larger than what is drawn. is-hover marks what a press would grab. */
.profile-host .is-hover{ transition:stroke-width .08s; }
.profile-host line.is-hover{ stroke:var(--primary); stroke-width:3.5; }
.profile-host text.is-hover{ fill:var(--primary); font-weight:700; }
.profile-host circle.is-hover, .profile-host polygon[data-kind="rise"].is-hover{
  fill:var(--primary); transform-box:fill-box; transform-origin:center; transform:scale(1.35); }
.profile-host polygon[data-kind="outline"].is-hover{ stroke:var(--primary); stroke-width:2; }

/* ===== feat 108 round 4: numeric entry, live readout, move banner, corner grip ===== */
.profile-host{ position:relative; }
.pe-field{ position:absolute; z-index:5; width:64px; box-sizing:border-box; padding:2px 6px; font:inherit;
  font-size:var(--fs-xs); font-variant-numeric:tabular-nums; font-family:var(--font-mono);
  color:var(--text); background:var(--chrome); border:1px solid var(--primary); border-radius:4px; }
.pe-field:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.pe-field[aria-invalid="true"]{ border-color:var(--error); }
.pe-readout{ position:absolute; z-index:5; pointer-events:none; padding:1px 7px; white-space:nowrap;
  font-size:var(--fs-xs); font-variant-numeric:tabular-nums; color:var(--text);
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:999px; }
.bp-banner{ position:absolute; z-index:5; left:0; right:0; top:0; display:flex; align-items:center; gap:8px;
  padding:3px 8px; font-size:var(--fs-xs); color:var(--text); background:var(--chrome); border-bottom:1px solid var(--primary); }
.bp-banner .profile-btn{ margin-left:auto; }
/* feat 133 R3 M3: the move-mode banner in #view sits bottom-centre, above the viewport hint and the chat bar,
   so it never covers the view bar (a pane's banner keeps the top strip above). */
#view > .bp-banner{ left:50%; right:auto; top:auto; bottom:104px; transform:translateX(-50%);
  width:max-content; max-width:min(640px, calc(100% - 260px)); border:1px solid var(--primary); border-radius:6px; }
.profile-host rect.is-corner, .profile-host polygon.is-corner{ rx:1px; }

/* ===== profile context menu (feat 108 round 2: profilemenu.js) =============
   One element on <body>, same chrome as .geom-menu / .prefs-panel. */
.pmenu{
  position:fixed; z-index:60; min-width:180px; max-width:320px; padding:4px;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:8px;
  box-shadow:0 16px 36px rgba(0,0,0,.45); font-size:var(--fs-md); color:var(--text);
}
.pmenu[hidden]{ display:none; }
.pmenu-item{
  display:flex; align-items:center; gap:8px; min-height:32px; padding:4px 10px 4px 8px;
  border-radius:5px; cursor:pointer; user-select:none; white-space:nowrap;
}
.pmenu-item:hover, .pmenu-item:focus-visible, .pmenu-item[data-open]{ background:var(--hover); outline:none; }
.pmenu-item:focus-visible{ box-shadow:inset 0 0 0 2px var(--primary); }
.pmenu-item[aria-disabled="true"]{ color:var(--faint); cursor:default; }
.pmenu-item[aria-disabled="true"]:hover{ background:none; }
.pmenu-item[aria-disabled="true"] .pmenu-icon{ color:inherit; }
.pmenu-check{ width:14px; flex:none; text-align:center; color:var(--primary); font-weight:700; }
.pmenu-icon{ width:24px; height:24px; flex:none; display:inline-flex; align-items:center; justify-content:center; color:var(--dim); }
.pmenu-icon .shape-icon{ width:24px; height:24px; fill:none; stroke:currentColor; stroke-width:1.5; }
.pmenu-label{ flex:1; overflow:hidden; text-overflow:ellipsis; }
.pmenu-sub{ flex:none; color:var(--dim); margin-left:12px; }
.pmenu-sep{ height:1px; margin:4px 2px; background:var(--rule); }

/* ===== ruler (feat 83) ===================================================
   The label for app/ruler.js's drag ruler -- a pointer-events:none overlay in
   #view, same shape as .dims-badge above: a translucent panel over the
   render, never eating a pick or an orbit. The 3D line and ticks are drawn in
   the scene itself (ruler.js), not here -- there is nothing to style on a
   THREE.Line. Positioned in JS (style.left/top, screen px, following the
   dragged point) rather than in CSS, since that point moves every frame. */
.ruler-label{
  position:absolute; z-index:4; pointer-events:none;
  padding:2px 7px; border-radius:5px; font-size:var(--fs-sm); line-height:1.3;
  font-variant-numeric:tabular-nums; white-space:nowrap;
  background:color-mix(in srgb, var(--panel) 85%, transparent);
  border:1px solid var(--rule-strong); color:var(--text);
}
.ruler-label[hidden]{ display:none; }

/* ===== viewport toolbar, tooltip, shortcuts, empty state (feat 84) ==========
   Row 2's one appended section -- everything above is Row 1's (app.css's
   existing sections plus the untouched lab block). Four unrelated features
   share it only because the file-split rule is "one new section per feature",
   not "one per file this feature touches". */

/* ---- viewport toolbar (app/viewbar.js, A4/A5) ------------------------------
   Top-right of #view, where the old <select id="viewstyle"> sat (top:10px
   right:12px) -- a horizontal pill of 28px icon buttons instead of a native
   dropdown and a separate tab-bar text button. */
.viewbar{
  position:absolute; top:10px; right:12px; z-index:3;
  display:flex; align-items:center; gap:2px;
  padding:3px; background:var(--panel); border:1px solid var(--rule-strong);
  border-radius:6px; box-shadow:0 4px 14px rgba(0,0,0,.3);
}
.vb-group{ display:flex; gap:2px; }
.vb-sep{ width:1px; height:18px; margin:0 3px; flex:none; background:var(--rule); }
.vb-btn{
  width:28px; height:28px; flex:none;
  display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; border-radius:4px; color:var(--dim); cursor:pointer;
}
.vb-btn:hover{ background:var(--hover); color:var(--text); }
.vb-btn[aria-checked="true"], .vb-btn[aria-pressed="true"]{ background:var(--hover); color:var(--primary); }

/* ---- chat Send (A11) --------------------------------------------------------
   #chat-send: an id selector, so this beats .c-form button[type="submit"]'s
   class+attribute rule (app.css, existing) for every property listed here --
   `display` is deliberately NOT one of them, so
   `.chat.compact .c-form button[type="submit"]{ display:none; }` (Enter-only
   in the compact bar) still wins for that property. Centred with
   line-height/text-align instead of flex for the same reason: `display:flex`
   here would also beat that `display:none`. */
#chat-send{
  width:32px; height:32px; padding:0; border-radius:50%;
  text-align:center; line-height:32px;
}
#chat-send svg{ vertical-align:middle; }

/* ---- empty state card (app.html's #hint, A8) -------------------------------
   `.hint` itself (app.css, existing) already centres its content and sets
   pointer-events:none -- the card opts back in so its buttons are clickable,
   the rest of the overlay still lets an orbit drag through to the canvas. */
.hint.onboard{ align-items:flex-start; padding-top:14vh; }   /* above the origin, not on it */
.hint-card{
  pointer-events:auto; display:none; flex-direction:column; align-items:center;
  gap:10px; padding:14px 18px 12px; text-align:center; position:relative;
  background:color-mix(in srgb, var(--panel) 70%, transparent);
  -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
  border:1px solid var(--rule-soft); border-radius:10px;
}
.hint-card h2{ margin:0; font-size:var(--fs-md); font-weight:600; color:var(--dim); }
.hint-actions{ display:flex; flex-wrap:wrap; justify-content:center; gap:6px; }
.hint-actions button{
  display:flex; align-items:center; gap:6px; padding:6px 11px;
  background:transparent; border:1px solid var(--rule-strong); border-radius:6px;
  color:var(--text); font:inherit; font-size:var(--fs-sm); cursor:pointer;
}
.hint-actions button:hover{ border-color:var(--primary); background:var(--hover); }
.hint-actions svg{ flex:none; width:16px; height:16px; color:var(--dim); }
.hint-sub{ margin:0; font-size:var(--fs-xs); color:var(--faint); }
.hint-close{
  position:absolute; top:6px; right:6px; width:24px; height:24px;
  display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; border-radius:4px; color:var(--faint); cursor:pointer;
}
.hint-close:hover{ background:var(--hover); color:var(--text); }
/* viewport.js setHint (feat 111): the one-line "what can I do with this handle" cue,
   bottom-centre of #view above the compact chat bar (16..58px). Side margins clear the
   bottom-left gizmo and docked Mirror popover and the right-hand overlays. Only its x takes a
   click (2026-10-21: closing a tip hides it for good). */
.vp-hint{
  position:absolute; left:50%; bottom:66px; transform:translateX(-50%); z-index:4;
  max-width:min(560px, calc(100% - 260px)); padding:4px 30px 4px 10px; pointer-events:none; text-align:center;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:6px;
  font-size:var(--fs-xs); line-height:1.4; color:var(--dim);
}
.vp-hint[hidden]{ display:none; }
.vp-hint .hint-close{ top:50%; right:3px; width:20px; height:20px; transform:translateY(-50%); pointer-events:auto; }
.hint-lab{ margin:0; }
/* feat 84: the card is opt-in. `.onboard` is added by app.html's entry script
   (lab mode too, with Import and the assistant line hidden there), and the
   close button removes it again, which drops back to that one line. */
.hint-actions button[hidden], .hint-sub[hidden]{ display:none; }
.hint.onboard .hint-card{ display:flex; }
.hint.onboard .hint-lab{ display:none; }

/* ---- tooltip (app/tooltip.js, A10) ------------------------------------------
   One floating element, positioned in JS (place()) because its anchor moves
   with whatever it is attached to; only its rest-state look lives here. */
.tip{
  position:fixed; z-index:30; pointer-events:none;
  display:flex; align-items:center; gap:6px;
  max-width:260px; padding:5px 8px; border-radius:5px;
  background:var(--chrome); border:1px solid var(--rule-strong); color:var(--text);
  font-size:var(--fs-xs); line-height:1.35; box-shadow:0 6px 18px rgba(0,0,0,.4);
  opacity:0; transform:translateY(-4px); transition:opacity .1s ease, transform .1s ease;
}
.tip.show{ opacity:1; transform:none; }
.tip.above{ transform:translateY(4px); }
.tip.above.show{ transform:none; }
.tip-kbd{
  flex:none; padding:1px 5px; border-radius:3px;
  background:var(--sunken); border:1px solid var(--rule-strong); color:var(--faint);
  font:10px ui-monospace,SFMono-Regular,Menlo,monospace;
}

/* ---- shortcuts dialog (app/shortcuts.js, A9) --------------------------------
   `.confirm` (app.css, existing) is app/modal.js's own dialog box -- reused
   here for the background/border/radius/shadow/::backdrop, with a wider
   max-width and its own padding overriding it below (same specificity, later
   in the file wins). Its Close button wears `.lab-btn`/`.lab-btn-go`, same
   reuse `.confirm`'s own buttons make (web/README.md: those classes are
   lab-NAMED, not lab-SPECIFIC). */
.shortcuts-dialog{ max-width:min(560px, calc(100vw - 32px)); padding:22px 26px 20px; }
.shortcuts-dialog h2{ margin:0 0 4px; font-size:var(--fs-lg); font-weight:600; color:var(--text); }
.shortcuts-list{
  list-style:none; margin:14px 0 0; padding:0; display:grid; gap:2px;
  max-height:60vh; overflow:auto;
}
.shortcuts-list li{
  display:flex; align-items:baseline; justify-content:space-between; gap:16px;
  padding:6px 0; border-bottom:1px solid var(--rule-soft);
  font-size:var(--fs-sm); color:var(--text);
}
.shortcuts-list li:last-child{ border-bottom:0; }
.shortcuts-list kbd{
  flex:none; padding:1px 6px; border-radius:3px;
  background:var(--sunken); border:1px solid var(--rule-strong); color:var(--faint);
  font:11px ui-monospace,SFMono-Regular,Menlo,monospace;
}

/* ---- image export dialog (app/capture.js) ---------------------------------
   `.confirm` box and `.lab-btn` buttons, same reuse as .shortcuts-dialog. */
.capture-dialog h2{ margin:0 0 4px; font-size:var(--fs-lg); font-weight:600; color:var(--text); }
.capture-dialog p{ color:var(--dim); font-size:var(--fs-sm); margin-bottom:12px; }
.capture-dialog label{ display:flex; align-items:center; gap:8px; padding:4px 0; font-size:var(--fs-sm); cursor:pointer; }
.capture-size{ display:flex; flex-wrap:wrap; align-items:center; gap:8px 12px; }
.capture-dialog input[type=number], .capture-dialog select{
  background:var(--panel); color:var(--text); border:1px solid var(--rule-strong);
  border-radius:4px; padding:3px 6px; font:inherit;
}
.capture-size input{ width:10ch; }
.capture-dialog .capture-out{ margin:8px 0 10px; font-variant-numeric:tabular-nums; }

/* ===== idle pause (feat 102) =================================================
   app/idle.js: shown when a visible tab has had no input for 20 min. Same scrim
   idiom as the boot cover; above the app, below the boot cover and modal dialog. */
.idle-box{
  position:fixed; inset:0; z-index:9000; background:color-mix(in srgb, var(--bg) 86%, transparent);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; padding:24px; text-align:center; font-family:var(--font-body); color:var(--text);
  animation:idleIn .2s ease both;
}
.idle-box[hidden]{ display:none; }
.idle-msg{ margin:0; max-width:40ch; font-size:var(--fs-lg); }
.idle-go{
  min-height:36px; padding:0 18px; border:0; border-radius:6px; cursor:pointer;
  background:var(--primary); color:var(--on-primary); font-size:var(--fs-md); font-weight:600;
}
.idle-go:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
@keyframes idleIn{ from{ opacity:0; } to{ opacity:1; } }
@media (prefers-reduced-motion:reduce){ .idle-box{ animation:none; } }

/* ===== early access (feat 100 row 10a) ======================================
   The screens reuse the `.lab*` brand classes above (web/app/ea.js adds `.lab ea-screen`);
   only what has no lab twin is here: the persistent banner, the account menu and
   the clause list. App tokens, both themes. The overlay must clear the boot cover (z 10000). */
.ea-screen{ z-index:10001; }
.ea-clauses{ margin:26px 0 0; padding:0 0 0 22px; max-width:62ch; display:grid; gap:14px; font-size:14.5px; line-height:1.55; color:var(--dim); }
.ea-clauses li::marker{ color:var(--faint); }
.ea-clauses .lab-agree{ margin:0; color:var(--text); }
.ea-banner{
  flex:none; display:flex; align-items:center; gap:10px; min-height:28px; padding:0 8px 0 12px;
  background:var(--chrome); border-bottom:1px solid var(--rule); font-size:12px; color:var(--dim);
}
.ea-banner[hidden]{ display:none; }
.ea-dot{ flex:none; width:8px; height:8px; border-radius:50%; background:var(--warn); }
.ea-banner-link, .ea-account, .ea-menu-item{
  font:inherit; color:inherit; background:none; border:0; cursor:pointer; padding:0 6px; min-height:28px; border-radius:4px;
}
.ea-banner-link{ color:var(--text); font-weight:600; text-decoration:underline; text-underline-offset:3px; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.ea-banner-link:hover, .ea-account:hover, .ea-menu-item:hover{ background:var(--hover); }
.ea-acct{ position:relative; margin-left:auto; min-width:0; }
.ea-account{ max-width:34ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ea-menu{
  position:absolute; right:0; top:100%; z-index:90; min-width:180px; padding:4px; display:grid;
  background:var(--panel); border:1px solid var(--rule-strong); border-radius:6px;
}
.ea-menu[hidden]{ display:none; }
.ea-menu-item{ text-align:left; padding:0 10px; min-height:36px; color:var(--text); }

/* feat 153 B3: account avatar in #acct-slot, sign-in in the dark style, invite code in the negative style. */
.acct-slot .ea-acct{ margin:0 6px; display:flex; align-items:center; }
.ea-account{ max-width:none; padding:0; width:26px; height:26px; min-height:26px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center;
  background:var(--primary); color:var(--on-primary); font-weight:700; font-size:var(--fs-sm); }
.ea-account{ position:relative; }
.ea-account:empty{ display:none; }   /* nothing until the gate opens and paintBanner() fills it */
.ea-account:hover{ background:var(--primary); filter:brightness(1.1); }
.ea-account:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.ea-mail{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
.ea-menu{ border-radius:10px; border-color:var(--rule-strong, var(--rule)); }
.ea-menu-item{ border-radius:6px; }
.ea-screen .lab-screen{ grid-template-columns:minmax(0,520px); justify-content:center; }
.ea-screen .lab-btn{ border-radius:999px; }
.ea-screen .lab-field{ background:transparent; border-color:var(--rule-strong, var(--rule)); }
.lab-neg .lab-field{ background:#ececec; color:#151515; border-color:#ececec; font-family:var(--font-mono); font-size:20px; letter-spacing:.12em; text-align:center; }
.lab-neg .lab-field:focus-visible{ outline-color:#ececec; }
.lab-neg .lab-label{ color:var(--dim); text-transform:uppercase; font-size:12px; letter-spacing:.08em; }
.lab-neg .lab-btn-go{ background:#ececec; color:#151515; border-color:#ececec; }
.lab-neg .lab-btn-go:hover:not(:disabled){ background:#fff; border-color:#fff; }
.lab-neg .lab-btn-go:disabled{ background:transparent; color:var(--faint); border-color:var(--rule); }

/* ===== help panel (feat 101 rows 1-2, web/app/help.js) =====================
   One <aside>, two shapes chosen by the page's kind (help.js setMode), no
   setting. doc: a sheet over #view's right edge -- absolute, so the layout and
   the canvas size do not change, and tree/Options/ribbon stay usable. coach: a
   fixed 64px strip under .workarea (the viewport loses height, never width).
   App tokens only; both themes come from them. Pages are our own HTML
   fragments, styled here by element (h1/h2/ol/kbd), never by class. */
.help-panel{
  display:flex; flex-direction:column; background:var(--chrome); min-height:0;
}
/* z-index 7: above .sync-err (6), .chat/.preset-toast (4) and .gizmo (3), all
   absolute inside #view. The sheet is a SIBLING of the canvas, so a drag on it
   never reaches OrbitControls (checked live, help-panel layout doc). */
.help-panel[data-mode="doc"]{
  position:absolute; top:0; right:0; bottom:0; z-index:7; width:min(var(--help-w, 520px), 100%);
  border-left:1px solid var(--rule); box-shadow:-8px 0 24px color-mix(in srgb, var(--bg) 70%, transparent);
}
.help-panel[data-mode="coach"]{
  flex:none; height:64px; border-top:2px solid var(--primary); position:relative;
}
.help-panel[data-mode="coach"] > :is(.help-head, .help-body){ display:none; }
.help-panel[data-mode="doc"] > .help-coach{ display:none; }
.help-coach{ flex:1; min-height:0; display:flex; align-items:center; gap:14px; padding:0 12px 0 16px; }
.help-count{ flex:none; min-width:64px; font-size:var(--fs-sm); font-weight:600; color:var(--dim); }
.help-step{ flex:1; min-width:0; max-height:100%; overflow-y:auto; padding:3px 0; box-sizing:border-box; font-size:var(--fs-md); line-height:1.4; color:var(--text); }
/* padding: an inline <kbd>'s border sticks out of its line box, and overflow-y clips it at the edge. */

.help-step > *{ margin:0; }
.help-step > * + *{ margin-top:4px; }
.help-step h1{ font-size:var(--fs-md); font-weight:600; display:inline; margin:0; }
.help-step ol, .help-step ul{ padding:0; list-style:none; }
.help-step .help-start{ margin:0 0 0 10px; min-height:28px; padding:0 12px; vertical-align:middle; }
.help-step [data-ref-sample]{ margin:0 8px 0 0; min-height:28px; padding:0 12px; vertical-align:middle; }
.help-step kbd{
  font:inherit; font-size:var(--fs-sm); padding:1px 6px; border:1px solid var(--rule-strong); border-radius:4px;
  background:var(--panel); color:var(--text);
}
.help-step kbd[data-ribbon], .help-step [data-select], .help-step [data-param]{ cursor:pointer; border-color:var(--contextual); }
.help-step :is([data-ribbon],[data-select],[data-param]):hover{ background:var(--hover); }
.help-coach-btns{ flex:none; display:flex; align-items:center; gap:6px; }
.help-coach-btn{
  min-height:32px; padding:0 12px; border:1px solid var(--rule-strong); border-radius:999px; cursor:pointer;
  background:transparent; color:var(--text); font:inherit; font-size:var(--fs-sm);
}
.help-coach-btn:hover:not(:disabled){ background:var(--hover); }
.help-coach-btn:disabled{ opacity:.35; cursor:default; }
.help-coach-next{ background:var(--primary); border-color:var(--primary); color:var(--on-primary); font-weight:600; }
.help-coach-next:hover:not(:disabled){ background:var(--primary); filter:brightness(1.1); }
/* A "Show me"/status message in the strip: a transient note above it, over the
   viewport's bottom edge, rather than a taller strip. */
.help-panel[data-mode="coach"] > .help-status{
  position:absolute; left:0; right:0; bottom:100%; z-index:1; padding:6px 16px;
  background:var(--chrome); border-top:1px solid var(--rule); border-bottom:0;
}
@media (max-width:860px){
  .help-panel[data-mode="coach"]{ height:auto; min-height:64px; max-height:140px; }
  .help-coach{ flex-wrap:wrap; padding:6px 12px; row-gap:6px; }
  .help-step{ flex:1 1 100%; order:3; }
}
.help-panel[hidden]{ display:none; }
.help-head{ flex:none; display:flex; align-items:center; gap:2px; padding:6px 8px; border-bottom:1px solid var(--rule); }
.help-title{
  flex:1; min-width:0; margin:0 6px; font-size:var(--fs-md); font-weight:600; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-transform:none; letter-spacing:0; padding:0;
}
.help-nav, .help-x, .help-topics{
  flex:none; min-width:32px; height:32px; border:0; border-radius:4px; cursor:pointer;
  background:transparent; color:var(--dim); font:inherit; font-size:var(--fs-xl); line-height:1;
}
.help-topics{ font-size:var(--fs-sm); padding:0 8px; }
.help-nav:hover:not(:disabled), .help-x:hover, .help-topics:hover{ background:var(--hover); color:var(--text); }
.help-nav:disabled{ opacity:.35; cursor:default; }
.help-panel button:focus-visible, .help-body [tabindex]:focus-visible, .help-step [tabindex]:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.help-body{ flex:1; min-height:0; overflow-y:auto; padding:4px 16px 20px; font-size:var(--fs-md); line-height:1.6; color:var(--text); }
.help-body:focus{ outline:none; }
.help-body h1{ margin:14px 0 8px; font-size:var(--fs-xl); font-weight:600; letter-spacing:-.01em; }
.help-body h2, .help-body h3{
  margin:18px 0 6px; padding:0; font-size:var(--fs-xs); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim);
}
.help-body p{ margin:0 0 10px; color:var(--text); }
.help-body ol, .help-body ul{ margin:0 0 12px; padding-left:20px; }
.help-body li{ margin:0 0 8px; }
.help-body li::marker{ color:var(--faint); }
/* high-lift documentation (scripts/gen_highlift_doc.py): figures and MathML; same layout rules as web/help/page.css */
.hp-fig{ margin:14px 0 20px; max-width:580px; }
.hp-fig svg{ width:100%; height:auto; display:block; }
.hp-fig figcaption{ color:var(--dim); font-size:var(--fs-sm); }
.hp-leg{ display:flex; flex-wrap:wrap; gap:2px 14px; margin:4px 0; font-size:var(--fs-sm); }
.hp-leg span{ display:inline-flex; align-items:center; }
.hp-leg svg{ display:inline-block; flex:none; width:22px; height:8px; margin-right:6px; }
.hp-fig details{ font-size:var(--fs-sm); } .hp-fig summary{ cursor:pointer; color:var(--dim); }
.hp-fig table{ font-size:var(--fs-sm); }
/* booktabs-style tables (rules above the header, below it and below the body; no verticals) */
.hp-tbl{ border-collapse:collapse; margin:12px auto 18px; font-family:inherit; font-size:var(--fs-md); font-variant-numeric:tabular-nums; }
.hp-tbl caption{ caption-side:top; text-align:center; padding:0 0 8px; font-family:inherit; color:var(--dim); font-size:var(--fs-sm); }
.hp-tbl th, .hp-tbl td{ padding:4px 16px; text-align:center; border:0; background:none; }
.hp-tbl thead th{ font-weight:600; border-top:2px solid currentColor; border-bottom:1px solid currentColor; }
.hp-tbl tbody tr:last-child td{ border-bottom:2px solid currentColor; }
.hp-fig .hp-tbl td:last-child{ text-align:left; }
.hp-fig .hp-tbl td:first-child{ text-align:left; }
math{ font-size:1.1em; }
math[display="block"]{ display:block math; margin:.7em 0; overflow-x:auto; }
.help-body a{ color:var(--text); text-underline-offset:3px; }
.help-body [href]:hover{ color:var(--primary); }
.help-body kbd{
  font:inherit; font-size:var(--fs-sm); padding:1px 6px; border:1px solid var(--rule-strong); border-radius:4px;
  background:var(--panel); color:var(--text);
}
/* A "Show me" target: it does something, so it looks like it does. */
.help-body kbd[data-ribbon], .help-body [data-select], .help-body [data-param]{ cursor:pointer; border-color:var(--contextual); }
.help-body :is([data-ribbon],[data-select],[data-param]):hover{ background:var(--hover); }
.help-list{ list-style:none; padding:0; margin:0 0 6px; }
.help-list li{ margin:0; }
.help-list button{
  display:grid; grid-template-columns:1fr auto auto; align-items:baseline; column-gap:10px; width:100%; min-height:44px; padding:8px 10px; margin:0 0 4px;
  border:1px solid var(--rule-soft); border-radius:6px; background:var(--panel); color:var(--text); font:inherit; text-align:left; cursor:pointer;
}
.help-list button:hover{ background:var(--hover); border-color:var(--rule); }
.help-list strong{ font-weight:600; }
.help-list span{ color:var(--faint); font-size:var(--fs-sm); }
.help-list .help-done{ color:var(--text); font-weight:600; }
.help-list small{ grid-column:1 / -1; margin-top:2px; color:var(--dim); font-size:var(--fs-sm); line-height:1.4; }
.help-lead{ margin:6px 0 10px; color:var(--dim); }
/* "Show me" targets read as buttons: a primary-tinted pill that never changes shape. */
:is(.help-step, .help-body) :is([data-ribbon],[data-select],[data-param]){
  background:color-mix(in srgb, var(--primary) 16%, var(--panel)); border-color:var(--primary); cursor:pointer;
}
/* The "Show me" label under a step's first target (help.js tipFirst): fixed on <body>,
   outside every layout, and only it fades. help.js removes it at 4 s either way. */
.help-tipbox{
  position:fixed; z-index:60; transform:translateX(-50%); pointer-events:none; white-space:nowrap;
  padding:0 5px; border-radius:3px; background:var(--primary); color:var(--on-primary);
  font-size:9px; font-weight:600; letter-spacing:.03em; line-height:1.5; text-transform:uppercase;
  animation:helpTipFade 4s ease-in forwards;
}
@keyframes helpTipFade{ 0%,65%{ opacity:1; } 100%{ opacity:0; } }
@media (prefers-reduced-motion: reduce){ .help-tipbox{ animation:none; } }
.help-start, .help-err + button, [data-ref-sample]{
  min-height:36px; padding:0 14px; margin:2px 0 8px; border:0; border-radius:999px; cursor:pointer;
  background:var(--primary); color:var(--on-primary); font:inherit; font-size:var(--fs-md); font-weight:600;
}
.help-err{ color:var(--error); }
.help-status{ flex:none; margin:0; padding:8px 16px; min-height:0; font-size:var(--fs-sm); color:var(--dim); border-top:1px solid var(--rule-soft); }
.help-status:empty{ display:none; }
/* "Show me": a ring that grows off the real ribbon button, twice. Reduced
   motion gets a steady outline for the same 2.6 s instead. */
.r-btn.help-pulse{ position:relative; animation:helpPulse 1.3s ease-out 2; }
@keyframes helpPulse{
  0%{ box-shadow:0 0 0 0 color-mix(in srgb, var(--primary) 80%, transparent); }
  100%{ box-shadow:0 0 0 12px transparent; }
}
/* "Show me" for an Options field: the same ring on the row, plus the focus the
   step gave its input. */
.help-hit{ animation:helpPulse 1.3s ease-out 2; border-radius:6px; }
@media (prefers-reduced-motion:reduce){
  .r-btn.help-pulse, .help-hit{ animation:none; outline:3px solid var(--primary); outline-offset:2px; }
}

/* ===== analysis panel + sweep popover (feat 103 row 4) =====
   Panel docks top-right of #view (absolute, like .prefs-panel); the popover is
   fixed to the Alpha Sweep button's rect. Theme tokens only. */
.centre-col{ flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; position:relative; }
.centre-col > .centre{ flex:1; }
.andock{
  flex:none; max-height:40%; overflow:auto; background:var(--chrome); border-top:1px solid var(--rule);
  padding:12px 14px; color:var(--text); font-size:var(--fs-sm);
}
.andock[hidden], .an-pop[hidden]{ display:none; }
.andock.stale > :not(.an-dock-msg){ opacity:.4; }
.an-dock-msg{ margin:0 0 8px; color:var(--text); }
.stepside{ padding:10px 12px; color:var(--text); font-size:var(--fs-sm); }
.stepside.stale .rs-body{ opacity:.4; }   /* stale results stay readable but recede; the banner and its button do not */
.an-sec{ display:none; margin-bottom:14px; }
body[data-step="Mesh"] .an-sec[data-for="Mesh"], body[data-step="Analysis"] .an-sec[data-for="Analysis"],
body[data-step="Results"] .an-sec[data-for="Results"]{ display:block; }
/* Home summary (feat 153 B1): hairline rows, no boxes, orange only on the current step. */
.hm-sec{ display:none; }
body[data-step="File"] .hm-sec{ display:block; }
.hm-sec h3{ margin:0 0 8px; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; }
.hm-sec h4{ margin:16px 0 4px; font-size:var(--fs-sm); color:var(--dim); font-weight:600; }
.hm-spine{ list-style:none; margin:0; padding:0; }
.hm-step{ display:flex; align-items:center; gap:10px; width:100%; min-height:32px; padding:0; border:0; border-top:1px solid var(--rule);
  background:transparent; color:var(--text); font:inherit; text-align:left; cursor:pointer; }
.hm-step:hover .hm-name{ text-decoration:underline; }
.hm-step:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.hm-n{ width:18px; height:18px; border-radius:50%; flex:none; display:inline-flex; align-items:center; justify-content:center;
  border:1.5px solid transparent; color:var(--faint); font-size:var(--fs-xs); font-weight:600; }
.hm-step[data-state="done"] .hm-n{ background:var(--ok); color:var(--on-primary); }
.hm-step[data-state="current"] .hm-n{ border-color:var(--dim); color:var(--text); }
.hm-step[data-state="stale"] .hm-n{ border-color:var(--warn); color:var(--warn); }
.hm-step[data-state="current"] .hm-name{ color:var(--step-accent); }
.hm-name{ flex:1; }
.hm-state{ color:var(--dim); font-size:var(--fs-xs); }
.hm-row{ display:flex; justify-content:space-between; gap:8px; padding:4px 0; border-top:1px solid var(--rule-soft); color:var(--dim); }
.hm-v{ color:var(--text); font-family:var(--font-mono); font-variant-numeric:tabular-nums; }
.hm-empty{ margin:4px 0; color:var(--faint); }
.hm-req{ align-items:center; justify-content:flex-start; }
.hm-req .hm-k{ flex:1; color:var(--text); }
.hm-dot{ width:8px; height:8px; border-radius:50%; flex:none; background:var(--faint); }
.hm-dot[data-state="pass"]{ background:var(--ok); }
.hm-dot[data-state="fail"]{ background:var(--bad); }
.hm-req-rm{ background:none; border:0; color:var(--faint); cursor:pointer; font:inherit; }
.hm-req-rm:hover{ color:var(--error); }
.hm-req-form{ display:flex; flex-wrap:wrap; gap:4px; margin-top:6px; }
.hm-req-form select, .hm-req-form input{ background:var(--sunken); color:var(--text); border:1px solid var(--rule-strong); border-radius:4px; font:inherit; font-size:var(--fs-sm); padding:3px 5px; }
.hm-req-form input{ width:5.5em; }
.hm-req-add{ background:transparent; color:var(--dim); border:1px solid var(--rule-strong); border-radius:999px; padding:3px 12px; font:inherit; font-size:var(--fs-sm); cursor:pointer; }
.hm-req-add:hover{ color:var(--text); background:var(--hover); }

.an-sec h3{ margin:0 0 8px; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; }
.an-btn{ min-height:28px; padding:0 14px; border:1px solid var(--rule-strong, var(--rule)); border-radius:999px;
  background:transparent; color:var(--text); font:inherit; cursor:pointer; }
.an-btn.primary{ background:var(--primary); border-color:var(--primary); color:#fff; }
.an-btn:disabled{ opacity:.5; cursor:default; }
.an-btn:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.an-num{ margin:6px 0; color:var(--dim); font-variant-numeric:tabular-nums; }
.an-err{ margin:6px 0; color:color-mix(in srgb, var(--error) 55%, var(--text)); }
.an-chk{ display:flex; align-items:center; gap:8px; margin-top:6px; }
.an-field{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; color:var(--dim); }
.an-field input{ width:90px; min-height:28px; background:var(--panel); color:var(--text);
  border:1px solid var(--rule-strong); border-radius:4px; padding:2px 6px; font:inherit; font-family:var(--font-mono); font-weight:500; }
.an-field input.bad{ border-color:var(--error); }
.an-field input:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.an-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
.an-head h3{ margin:0; font-family:var(--font-display); font-size:var(--fs-xl); font-weight:700; }
.an-close{ min-width:28px; min-height:28px; border:0; background:transparent; color:var(--dim);
  display:inline-flex; align-items:center; justify-content:center; border-radius:6px; cursor:pointer; }
.an-close:hover{ background:var(--hover); color:var(--text); }
.an-close:focus-visible, .an-pop input:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.an-status{ min-height:16px; color:var(--dim); margin-bottom:6px; overflow-wrap:anywhere; }
.an-status.err{ color:color-mix(in srgb, var(--error) 55%, var(--text)); }   /* --error alone is < 4.5:1 on the panel */
.stepside.stale .an-status{ color:var(--text); }
.an-adv, .an-ds{ margin:6px 0; }
.an-adv summary, .an-ds summary{ cursor:pointer; color:var(--dim); margin-bottom:6px; }
/* data table (feat 153 E2) */
.an-data{ margin-top:8px; }
.an-data summary{ cursor:pointer; color:var(--dim); }
.rs-table{ border-collapse:collapse; margin-top:6px; font-family:var(--font-mono); font-variant-numeric:tabular-nums; font-size:var(--fs-xs); }
.rs-table th, .rs-table td{ padding:2px 10px; text-align:right; border-top:1px solid var(--rule-soft); }
/* AeroDataset (feat 104 U1/U2) */
.ad-panel h4{ margin:12px 0 4px; font-size:var(--fs-sm); color:var(--dim); font-weight:600; }
.ad-table td:first-child, .ad-table th:first-child{ text-align:left; }
.ad-na{ color:var(--dim); font-style:italic; }
.ad-warn{ margin:6px 0; padding:0 0 0 14px; font-size:var(--fs-xs); color:var(--warn); }
.ad-cut{ stroke-dasharray:3 2; }
.ad-s2{ stroke-dasharray:4 2; }   /* the second and later vortex sheets of the lift chart (tail, fin) */
/* Mesh step: the geometry icon of the part(s) a node, chip or heading is about */
.mp-nl, .mc-chip, h4.mp-comp{ display:inline-flex; align-items:center; gap:5px; }
.mp-nl .ico, .mc-chip .ico, h4.mp-comp .ico{ width:15px; height:15px; }

.ad-bridge{ stroke-dasharray:1 3; opacity:.55; }   /* the lift chart across a body between the two halves of a wing */


/* mesh panel (feat 153 D1; frames 36/37): one column, Compute pinned to the bottom */
body[data-step="Mesh"] .stepside{ display:flex; flex-direction:column; --wt-w:300px; padding-bottom:0; }
body[data-step="Mesh"] .an-sec[data-for="Mesh"]{ display:flex; flex-direction:column; flex:1; min-height:0; margin-bottom:0; }
.stepside.stale .an-sec[data-for="Mesh"] > :not(.an-status){ opacity:1; }   /* a stale mesh does not dim the controls that fix it */
.mp-wb{ flex:1; min-height:0; overflow-y:auto; }
.mp-compute{ flex:none; width:100%; min-height:34px; margin:12px 0 14px; }
.mp-label{ color:var(--dim); font-size:var(--fs-xs); margin-bottom:6px; }
/* feat 165: Performance and Propeller studies (perfstudy.js) and the Reynolds-band badge (aerodata.js bandBadge) */
.pf-band{ display:inline-block; margin:4px 0 6px; padding:1px 10px; border:1px solid var(--ok); border-radius:999px; font-size:var(--fs-xs); color:var(--ok); }
.pf-band:not([data-band="light"]){ border-color:var(--warn); color:var(--warn); }
.pf-head{ margin-top:10px; color:var(--text); font-weight:600; }
.pf-src{ margin:-4px 0 6px; font-size:var(--fs-xs); color:var(--faint, var(--dim)); overflow-wrap:anywhere; }
.mp-seg{ display:flex; border:1px solid var(--rule-strong); border-radius:4px; overflow:hidden; }
.mp-seg button{ flex:1; min-height:28px; border:0; background:transparent; color:var(--dim); font:inherit; font-size:var(--fs-sm); cursor:pointer; }
.mp-seg button + button{ border-left:1px solid var(--rule); }
.mp-seg button[aria-pressed="true"]{ background:var(--hover); color:var(--text); }
.mp-seg button:focus-visible, .mp-link:focus-visible, .mp-item:focus-visible, .mp-back:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.mp-est{ margin:12px 0; color:var(--dim); font-size:var(--fs-xs); line-height:1.4; }
.mp-table{ width:100%; border-collapse:collapse; margin:10px 0 14px; font-size:var(--fs-sm); }
.mp-table th{ padding:6px 0; text-align:left; font-weight:600; font-size:var(--fs-xs); color:var(--text); border-bottom:1px solid var(--rule); }
.mp-table td{ padding:7px 0; border-top:1px solid var(--rule-soft); overflow-wrap:anywhere; }
.mp-table th.mp-n{ text-align:right; }
.mp-n{ text-align:right; font-variant-numeric:tabular-nums; font-family:var(--font-mono); padding-left:8px; }
.mp-link, .mp-back, .mp-item{ border:0; background:transparent; color:var(--text); font:inherit; cursor:pointer; padding:0; text-align:left; }
.mp-link:hover, .mp-item:hover{ text-decoration:underline; }
.mp-checks{ border-top:1px solid var(--rule); padding-top:10px; }
.mp-checks summary{ cursor:pointer; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; margin-bottom:8px; }
.mp-check{ display:flex; justify-content:space-between; padding:3px 0; color:var(--dim); }
.mp-val{ font-variant-numeric:tabular-nums; } .mp-val.warn{ color:var(--warn); } .mp-val.ok{ color:var(--ok); }
.mp-back{ color:var(--dim); font-size:var(--fs-xs); margin-bottom:10px; display:block; }
/* feat 153 mesh controls (meshcontrols.js) */
/* the Mesh step's two panes: tree = #stepside (left), settings = #stepopts (right) */
.stepopts{ width:300px; flex:none; background:var(--chrome); border-left:1px solid var(--rule); overflow-y:auto; padding:10px 12px; color:var(--text); font-size:var(--fs-sm); }
.stepopts[hidden]{ display:none; }
.so-pane{ display:none; }
body[data-step="Mesh"] .so-pane[data-for="Mesh"], body[data-step="Analysis"] .so-pane[data-for="Analysis"], body[data-step="Results"] .so-pane[data-for="Results"]{ display:block; }
.stepopts .mp-comp:first-child{ margin-top:0; padding-top:0; border-top:0; }
.mp-grp{ margin:14px 0 4px; text-transform:uppercase; letter-spacing:.04em; }
.mp-node{ display:flex; justify-content:space-between; gap:8px; width:100%; padding:7px 8px; border:0; border-radius:4px; background:transparent; color:var(--dim); font:inherit; text-align:left; cursor:pointer; }
.mp-node:hover{ background:var(--hover); }
.mp-node[aria-current="true"]{ background:var(--hover); color:var(--text); }
.mp-node:focus-visible{ outline:2px solid var(--focus); outline-offset:-2px; }
.mp-sub{ padding-left:18px; }
.mp-nl{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mp-nr{ flex:none; font-variant-numeric:tabular-nums; font-family:var(--font-mono); font-size:var(--fs-xs); }
.mc-card{ border:1px solid var(--border, rgba(127,127,127,.35)); border-radius:6px; padding:8px; margin:0 0 8px; }
.mc-head{ display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
.mc-del{ border:0; background:transparent; color:var(--dim); font-size:var(--fs-md, 16px); cursor:pointer; line-height:1; }
.mc-del:hover{ color:var(--text); }
.mc-scope{ display:flex; flex-wrap:wrap; gap:4px; margin-bottom:6px; }
.mc-chip{ border:1px solid var(--border, rgba(127,127,127,.35)); background:transparent; color:var(--dim); border-radius:10px; padding:1px 8px; font:inherit; font-size:var(--fs-xs); cursor:pointer; }
.mc-chip[aria-pressed="true"]{ color:var(--text); border-color:var(--focus); }
.mc-del:focus-visible, .mc-chip:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.mp-list{ list-style:none; margin:0 0 4px; padding:0; }
.mp-item{ display:block; width:100%; padding:7px 0 7px 8px; color:var(--dim); border-top:1px solid var(--rule-soft); }
.mp-item[aria-current="true"]{ color:var(--text); }
.mp-comp{ margin:14px 0 8px; padding-top:12px; border-top:1px solid var(--rule); font-family:var(--font-display); font-size:var(--fs-lg); }
.mp-apply{ width:100%; margin-top:8px; min-height:34px; }
.mp-part{ margin-bottom:8px; }
.mp-name{ margin-bottom:4px; }
/* run history + compare (feat 153 C3) */
.rs-wb h4{ margin:10px 0 4px; font-size:var(--fs-sm); color:var(--dim); font-weight:600; }
.rs-list{ list-style:none; margin:0 0 8px; padding:0; }
.rs-row{ display:flex; align-items:center; gap:8px; padding:5px 0; border-top:1px solid var(--rule-soft); }
.rs-row[data-stale="true"]{ opacity:.55; }
.rs-open{ flex:1; min-width:0; text-align:left; border:0; background:transparent; color:var(--text); font:inherit; cursor:pointer; padding:0; }
.rs-open:hover{ text-decoration:underline; }
.rs-open:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.rs-when{ color:var(--dim); font-size:var(--fs-xs); font-variant-numeric:tabular-nums; }
.an-cmp{ stroke-width:1.25; }
.an-cmp1{ stroke:#4c9a5c; } .an-cmp2{ stroke:#4472c4; } .an-cmp3{ stroke:#c0463f; }
/* feat 166 E3: the High-lift curves draw up to five more lines than the clean (orange) one. `.an-line` (below) wins over a bare .an-cmpN, hence the scope. */
:is(.hl-curves, .hl-legend) .an-cmp1{ stroke:#4c9a5c; } :is(.hl-curves, .hl-legend) .an-cmp2{ stroke:#4472c4; } :is(.hl-curves, .hl-legend) .an-cmp3{ stroke:#c0463f; }
:is(.hl-curves, .hl-legend) .an-cmp4{ stroke:#b8862f; } :is(.hl-curves, .hl-legend) .an-cmp5{ stroke:#7b5aa6; }
.hl-figs{ display:grid; gap:6px; margin:6px 0; }
.hl-legend{ list-style:none; margin:4px 0 6px; padding:0; display:flex; flex-wrap:wrap; gap:4px 14px; font-size:var(--fs-xs); color:var(--dim); }
.hl-legend li{ display:flex; align-items:center; gap:6px; }
.hl-legend .an-line{ stroke-width:2; }
/* study bank (feat 153 C2; frames 25/26/27): the Analysis step's sidebar is one column, the footer pinned to its bottom */
body[data-step="Analysis"] .stepside{ display:flex; flex-direction:column; --wt-w:300px; padding-bottom:0; }
body[data-step="Analysis"] .an-sec[data-for="Analysis"]{ display:flex; flex-direction:column; flex:1; min-height:0; margin-bottom:0; }
.an-status:empty{ display:none; }
.st-wb{ flex:1; min-height:0; display:flex; flex-direction:column; }
.st-hint{ margin:0 0 10px; color:var(--dim); font-size:var(--fs-xs); line-height:1.4; }
.st-list{ list-style:none; margin:0; padding:0; flex:1; overflow-y:auto; }
.st-row{ border-top:1px solid var(--rule); padding:11px 0; }
.st-head{ display:flex; align-items:center; gap:10px; }
.st-dot{ flex:none; width:14px; height:14px; padding:0; border-radius:50%; border:1.5px solid var(--dim); background:transparent; cursor:pointer; }
.st-dot[aria-pressed="true"]{ background:var(--primary); border-color:var(--primary); }
.st-row[data-status="done"] .st-dot{ background:var(--ok); border-color:var(--ok); }
.st-row[data-status="done"] .st-dot[aria-pressed="true"]{ box-shadow:0 0 0 2px var(--chrome), 0 0 0 3.5px var(--primary); }
.st-row[data-status="error"] .st-dot{ background:var(--error); border-color:var(--error); }
.st-row[data-status="queued"] .st-dot, .st-row[data-status="running"] .st-dot{ background:transparent; border:1.5px dashed var(--primary); }
.st-row[data-status="queued"] .st-dot{ border-color:var(--faint); }
.st-row[data-status="running"] .st-dot{ animation:stSpin 1.4s linear infinite; }
@keyframes stSpin{ to{ transform:rotate(360deg); } }
.st-dot:focus-visible, .st-x:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.st-main{ flex:1; min-width:0; }
.st-name{ font-weight:600; }
.st-sub{ color:var(--dim); font-size:var(--fs-xs); overflow-wrap:anywhere; }
.st-state{ flex:none; color:var(--dim); font-size:var(--fs-xs); font-variant-numeric:tabular-nums; }
.st-row[data-status="done"] .st-state{ color:var(--ok); }
.st-row[data-stale="true"] .st-state{ color:var(--warn); }
.st-row[data-status="running"] .st-state{ color:var(--primary); }
.st-row[data-status="error"] .st-state, .st-row[data-status="error"] .st-sub{ color:var(--error); }
.st-x{ flex:none; border:0; background:transparent; color:var(--dim); cursor:pointer; min-width:24px; min-height:24px; opacity:0; }
.st-row:hover .st-x, .st-row:focus-within .st-x{ opacity:1; }
.st-rowbar{ height:3px; margin:8px 0 0 24px; background:var(--rule-soft); overflow:hidden; }
.st-rowbar::after{ content:""; display:block; width:40%; height:100%; background:var(--primary); animation:stSlide 1.6s ease-in-out infinite; }
@keyframes stSlide{ from{ transform:translateX(-100%); } to{ transform:translateX(250%); } }
.st-form{ margin:10px 0 0 24px; }
.st-form .an-field{ margin-bottom:8px; }
.st-in{ display:inline-flex; align-items:center; gap:4px; }
.st-in input{ width:56px; text-align:right; }
.st-unit{ color:var(--dim); font-size:var(--fs-xs); }
.st-foot{ flex:none; margin:0 -12px; padding:12px 12px 14px; border-top:1px solid var(--rule); display:flex; flex-direction:column; gap:8px; }
.st-foot .an-btn{ width:100%; min-height:34px; }
.st-msg{ margin:0; color:var(--dim); }
.st-msg.run{ color:var(--primary); } .st-msg.warn{ color:var(--warn); } .st-msg.ok{ color:var(--ok); }
.st-note{ margin:0; color:var(--dim); font-size:var(--fs-xs); }
.st-bar{ height:3px; background:var(--rule-soft); overflow:hidden; }
.st-bar span{ display:block; height:100%; background:var(--primary); transition:width .3s; }
.st-bar.warn span{ background:var(--warn); } .st-bar.ok span{ background:var(--ok); }
.app-tabs-tabs .st-note{ color:var(--dim); font-size:var(--fs-xs); margin-left:2px; }
.app-tabs-tabs .st-note:empty{ display:none; }
.st-badge[data-state="ready"]{ border-color:var(--primary); color:var(--primary); }
.statusline::before{ content:""; width:8px; height:8px; border-radius:50%; background:var(--faint); margin-right:8px; flex:none; }
.statusline[data-tone="run"]::before{ background:var(--primary); } .statusline[data-tone="warn"]::before{ background:var(--warn); } .statusline[data-tone="ok"]::before{ background:var(--ok); }
/* running: the model recedes behind the run (frame 27) */
body[data-working="true"] #view canvas{ opacity:.5; }
@media (prefers-reduced-motion:reduce){ .st-bar span{ transition:none; } .st-row[data-status="running"] .st-dot, .st-rowbar::after{ animation:none; } }
.an-prog{ height:2px; margin:0 0 8px; background:var(--rule-soft); overflow:hidden; }
.an-prog span{ display:block; width:30%; height:100%; background:var(--primary); animation:anProg 1.4s ease-in-out infinite; }
@keyframes anProg{ from{ transform:translateX(-100%); } to{ transform:translateX(340%); } }
@media (prefers-reduced-motion:reduce){ .an-prog span{ animation:none; width:100%; } }
.an-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px; }
.an-fig{ margin:0; padding:6px 6px 4px; background:var(--panel); border-radius:6px; }
.an-svg{ width:100%; height:auto; display:block; }
.an-svg text{ fill:var(--dim); font-family:var(--font-mono); font-weight:500; font-size:11px; font-variant-numeric:tabular-nums; }
.an-axis{ fill:none; stroke:var(--rule); stroke-width:1; }
.an-zero{ fill:none; stroke:var(--rule); stroke-width:.75; stroke-dasharray:3 3; }
.tk-hud{ position:fixed; left:50%; bottom:28px; transform:translateX(-50%); z-index:60; padding:6px 12px; border:1px solid var(--primary); background:var(--chrome); color:var(--text); font-size:12px; pointer-events:none; }   /* feat 171: V/R/S modal transform status line */
.tk-ov{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:3; }   /* feat 171: scale's pivot, rubber band and locked axis */
.tk-band{ stroke:var(--primary); stroke-width:1.5; stroke-dasharray:5 4; }
.tk-axis{ stroke-width:1.5; opacity:.85; }
.tk-pivot{ fill:var(--primary); stroke:var(--chrome); stroke-width:1.5; }


.an-band{ fill:var(--dim); opacity:.2; }   /* feat 163 6b: the propeller disk's span footprint on the Slipstream chart */
.an-line{ fill:none; stroke:#ff7a1a; stroke-width:1.5; }   /* the plot curves are orange, like the section cut and the probe */
.an-dot{ fill:#ff7a1a; stroke:none; pointer-events:none; }
.an-dot.sel{ fill:#fff; stroke:var(--primary); stroke-width:2; }
.an-pt{ fill:transparent; stroke:none; cursor:pointer; }
.an-pt:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.andock.stale .an-pt{ cursor:default; }
.an-figcap{ text-align:center; font-size:var(--fs-xs); color:var(--dim); }
.an-stats{ display:grid; grid-template-columns:1fr auto; gap:2px 12px; margin:12px 0 6px; }
.an-stats dt{ color:var(--dim); }
.an-stats dd{ margin:0; text-align:right; white-space:nowrap; font-family:var(--font-mono); font-weight:500; font-variant-numeric:tabular-nums; }
.an-note{ margin:6px 0; color:var(--dim); font-size:var(--fs-xs); overflow-wrap:anywhere; }
.sc-float{ position:absolute; top:64px; right:12px; width:330px; padding:10px 12px 6px; background:var(--chrome); border:1px solid var(--rule-strong); border-radius:6px; z-index:4; }
.sc-bar{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:4px; cursor:grab; touch-action:none; user-select:none; }
.sc-bar:active{ cursor:grabbing; }
.sc-title{ font-weight:600; font-size:var(--fs-sm); }
.sc-x{ border:0; background:transparent; color:var(--dim); font-size:18px; line-height:1; cursor:pointer; min-width:24px; min-height:24px; border-radius:4px; }
.sc-x:hover{ background:var(--hover); color:var(--text); }
.sc-x:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.sc-svg{ width:100%; height:auto; display:block; }
.sc-svg text{ fill:var(--dim); font-size:9px; font-family:var(--font-mono); }
.sc-svg .sc-num{ font-size:8px; }
.sc-axis{ stroke:var(--rule-strong); stroke-width:1; } .sc-zero{ stroke:var(--rule-strong); stroke-width:1; stroke-dasharray:3 3; }
.sc-line{ fill:none; stroke-width:1.5; } .sc-up{ stroke:var(--primary); } .sc-lo{ stroke:#4c8bf5; }
.sc-dot.sc-up{ fill:var(--primary); stroke:none; } .sc-dot.sc-lo{ fill:#4c8bf5; stroke:none; }
.sc-svg text.sc-up{ fill:var(--primary); stroke:none; } .sc-svg text.sc-lo{ fill:#4c8bf5; stroke:none; }
.sc-outline{ fill:var(--rule-soft); stroke:var(--dim); stroke-width:.6; }
.rs-layers{ display:flex; flex-direction:column; }
.rs-layer{ display:flex; align-items:center; gap:8px; padding:4px 0; color:var(--text); cursor:pointer; }
.rs-layer:has(input:disabled){ color:var(--faint); cursor:default; }
.rs-layer input{ accent-color:var(--primary); }
.rs-subh{ font-weight:600; margin:2px 0 4px; }
/* Examples page (feat 153, Examples frame): search + categories | tiles | the picked model */
.examplespage{ display:none; }
body[data-step="Examples"] .examplespage{ display:grid; grid-template-columns:260px minmax(0,1fr) 340px; position:absolute; inset:0; z-index:7; background:var(--bg, var(--panel)); overflow:hidden; }
.xp-side{ padding:24px 20px; border-right:1px solid var(--rule); overflow-y:auto; }
.xp-search{ width:100%; padding:9px 14px; border:1px solid var(--rule-strong); border-radius:999px; background:var(--sunken); color:var(--text); font:inherit; }
.xp-search:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.xp-cats{ list-style:none; margin:22px 0; padding:0; }
.xp-cats button{ display:flex; align-items:center; gap:12px; width:100%; padding:9px 0; border:0; background:transparent; color:var(--dim); font:inherit; font-size:var(--fs-lg); text-align:left; cursor:pointer; }
.xp-cats button[aria-pressed="true"]{ color:var(--text); font-weight:600; }
.xp-dot{ width:8px; height:8px; box-sizing:border-box; border:1.5px solid var(--dim); border-radius:50%; flex:none; }
.xp-cats button[aria-pressed="true"] .xp-dot{ background:var(--primary); border-color:var(--primary); }
.xp-cats button:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.xp-lab{ display:flex; align-items:center; gap:10px; color:var(--dim); cursor:pointer; }
.xp-lab input{ accent-color:var(--primary); }
.xp-centre{ padding:28px 32px; overflow-y:auto; min-width:0; }
.xp-centre h1, .xp-ea h1{ margin:0; font-family:var(--font-display); font-size:34px; font-weight:700; }
.xp-lead{ margin:6px 0 24px; color:var(--dim); font-size:var(--fs-lg); }
.xp-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(170px, 1fr)); gap:18px 18px; }
.xp-grid[data-view="list"]{ grid-template-columns:1fr; gap:6px; }
.xp-tile{ display:flex; flex-direction:column; gap:10px; padding:0; border:0; background:transparent; color:var(--text); font:inherit; text-align:left; cursor:pointer; }
.xp-grid[data-view="list"] .xp-tile{ flex-direction:row; align-items:center; gap:14px; padding:6px; border-radius:8px; }
.xp-grid[data-view="list"] .xp-thumb{ width:96px; flex:none; aspect-ratio:16/10; }
.xp-thumb{ aspect-ratio:16/10; border-radius:8px; background:var(--sunken); border:2px solid transparent; display:flex; align-items:center; justify-content:center; color:var(--dim); overflow:hidden; }
.xp-thumb svg{ width:70%; height:70%; } .xp-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
.xp-tile[aria-pressed="true"] .xp-thumb{ border-color:var(--primary); }
.xp-tile:focus-visible .xp-thumb{ outline:2px solid var(--focus); outline-offset:2px; }
.xp-name{ font-size:var(--fs-lg); font-weight:700; } .xp-sub{ margin-top:3px; color:var(--dim); font-size:var(--fs-sm); line-height:1.4; }
.xp-empty{ color:var(--faint); }
.xp-sec{ margin:14px 0 2px; font-size:var(--fs-sm, 11px); letter-spacing:.06em; text-transform:uppercase; color:var(--faint); }   /* Help list: a step's block */

.xp-detail{ padding:24px 28px; border-left:1px solid var(--rule); overflow-y:auto; display:flex; flex-direction:column; }
.xp-big{ aspect-ratio:16/8; }
.xp-dname{ margin:18px 0 6px; font-family:var(--font-display); font-size:28px; font-weight:700; }
.xp-desc{ margin:0 0 18px; color:var(--dim); font-size:var(--fs-lg); line-height:1.5; }
.xp-stats{ border-top:1px solid var(--rule-soft); margin-bottom:24px; }
.xp-stat{ display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--rule-soft); color:var(--dim); }
.xp-v{ font-family:var(--font-mono); color:var(--text); }
.xp-open, .xp-studies{ width:100%; min-height:44px; margin-bottom:10px; }
.xp-ea{ grid-column:1 / -1; padding:48px; }
@media (max-width:1100px){ body[data-step="Examples"] .examplespage{ grid-template-columns:200px minmax(0,1fr); } .xp-detail{ display:none; } }

.ea-menu-head{ display:flex; align-items:center; gap:12px; padding:12px 14px; border-bottom:1px solid var(--rule); }
.ea-menu-who{ display:flex; flex-direction:column; min-width:0; } .ea-menu-who strong{ font-size:var(--fs-md); } .ea-menu-who span{ color:var(--dim); font-size:var(--fs-sm); overflow:hidden; text-overflow:ellipsis; }
/* Result panels: tools on each chart, floating windows, the tile grid */
.an-fig{ position:relative; }
.an-ptools{ position:absolute; top:2px; right:2px; display:flex; gap:2px; opacity:0; transition:opacity .12s; z-index:2; }
.an-fig:hover > .an-ptools, .an-ptools:focus-within, .an-pwin-head .an-ptools, .tl-head .an-ptools{ opacity:1; }
.an-pwin-head .an-ptools, .tl-head .an-ptools{ position:static; }
.an-ptool{ min-width:22px; min-height:22px; padding:0 4px; border:0; border-radius:4px; background:transparent; color:var(--dim); font-size:13px; line-height:1; cursor:pointer; }
.an-ptool:hover{ background:var(--hover); color:var(--text); }
.an-ptool:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.an-floatlayer{ display:none; position:absolute; inset:0; pointer-events:none; z-index:5; }
body[data-step="Results"] .an-floatlayer{ display:block; }
body[data-rview="tile"] .an-floatlayer{ display:none; }
.an-pwin{ position:absolute; width:300px; pointer-events:auto; background:var(--chrome); border:1px solid var(--rule-strong); border-radius:6px; box-shadow:0 6px 24px rgba(0,0,0,.35); }
.an-pwin-head{ display:flex; align-items:center; justify-content:space-between; padding:4px 6px 4px 10px; border-bottom:1px solid var(--rule); cursor:grab; touch-action:none; user-select:none; }
.an-pwin-title, .tl-title{ font-weight:600; font-size:var(--fs-sm); }
.an-pwin .an-fig{ margin:0; padding:6px; }
.tl-grid{ display:grid; grid-template-columns:repeat(2, minmax(280px, 1fr)); gap:24px; max-width:1000px; }
.tl-cell{ border-top:1px solid var(--rule); }
.tl-head{ display:flex; align-items:center; justify-content:space-between; padding:8px 0; }
.tl-cell .an-fig{ margin:0; background:var(--sunken); border-radius:4px; padding:6px; max-width:420px; }
.rs-tilenote{ margin-top:14px; }
/* Help page (feat 153, Help frame): topics | article | shortcuts. The tutorials and the help sheet (help.js) still open
   over the viewport, so the page steps aside while one is open. */
.helppage{ display:none; }
body[data-step="Help"] .helppage{ display:grid; grid-template-columns:280px minmax(0,1fr) 380px; position:absolute; inset:0; z-index:7; background:var(--bg, var(--panel)); overflow:hidden; }
body[data-step="Help"]:has(.help-panel:not([hidden])) .helppage{ display:none; }
.hv-side{ padding:24px 20px; border-right:1px solid var(--rule); overflow-y:auto; }
.hv-article{ padding:36px 48px; overflow-y:auto; min-width:0; max-width:900px; }
.hv-article h1{ margin:0 0 12px; font-family:var(--font-display); font-size:38px; font-weight:700; }
.hv-article h2{ font-family:var(--font-display); }
.hv-article p, .hv-article li{ line-height:1.6; }
.hp-lead{ margin:0 0 14px; color:var(--dim); font-size:var(--fs-lg); line-height:1.5; }
.hv-spine{ list-style:none; margin:28px 0 0; padding:0; }
.hv-spine li{ position:relative; display:flex; gap:20px; padding-bottom:26px; }
.hv-spine li:not(:last-child)::before{ content:""; position:absolute; left:11px; top:26px; bottom:-2px; width:2px; background:var(--step-rust); }
.hv-node{ flex:none; width:24px; height:24px; border-radius:50%; background:var(--primary); position:relative; z-index:1; margin-top:2px; }
.hv-spine h2{ display:flex; align-items:center; gap:12px; margin:0 0 4px; font-size:26px; }
.hv-title{ border:0; background:transparent; padding:0; font:inherit; color:inherit; cursor:pointer; text-decoration:underline; text-decoration-color:var(--rule-strong, currentColor); text-underline-offset:5px; }   /* a step title that opens its tutorials */
.hv-title:hover{ color:var(--primary); }
.hv-title:focus-visible{ outline:2px solid var(--focus); outline-offset:3px; }
.hp-sec{ margin:26px 0 8px; }
/* A ribbon name in a Help article: underlined, and a card opens under its paragraph (helppage.js toggleCard). */
.hv-article [data-ribbon]{ cursor:pointer; text-decoration:underline; text-decoration-color:var(--primary); text-underline-offset:3px; }
.hv-article .hp-inl{ width:1.15em; height:1.15em; display:inline-block; vertical-align:-.25em; margin-right:.35em; }
.hp-card{ list-style:none; margin:4px 0 14px; padding:10px 14px; border:1px solid var(--rule); border-left:3px solid var(--primary); border-radius:6px; background:var(--panel); }
.hp-card-head{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.hp-card-head .ico{ width:22px; height:22px; }
.hp-card-where{ color:var(--dim); font-size:var(--fs-sm); }
.hp-card p{ margin:8px 0 0; }
.hp-card-sub{ color:var(--dim); font-size:var(--fs-sm); }
.hp-card-set{ margin:4px 0 0; padding-left:18px; columns:2; column-gap:24px; }
.hp-card-set li{ margin:0 0 2px; }

.hp-tut{ display:grid; grid-template-columns:1fr auto; gap:2px 14px; width:100%; text-align:left; padding:12px 14px; margin:0 0 8px; border:1px solid var(--rule-strong, rgba(127,127,127,.35)); border-radius:6px; background:transparent; color:var(--text); font:inherit; cursor:pointer; }
.hp-tut:hover{ background:var(--hover); border-color:var(--primary); }
.hp-tut:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.hp-tut .hp-min{ color:var(--dim); }
.hp-tut .hp-desc{ grid-column:1 / -1; color:var(--dim); }

.hv-spine p{ margin:0; color:var(--dim); font-size:var(--fs-lg); }
.hv-ico{ width:24px; height:24px; color:var(--dim); }
.hv-right{ padding:36px 40px; overflow-y:auto; }
.hv-right h2{ margin:0 0 14px; font-family:var(--font-display); font-size:24px; font-weight:700; }
.hv-key{ display:flex; align-items:center; gap:22px; padding:12px 0; border-top:1px solid var(--rule-soft); }
.hv-key kbd{ flex:none; min-width:64px; font-family:var(--font-mono); font-size:var(--fs-md); color:var(--text); background:transparent; border:0; padding:0; }
.hv-key span{ color:var(--dim); }
@media (max-width:1100px){ body[data-step="Help"] .helppage{ grid-template-columns:200px minmax(0,1fr); } .hv-right{ display:none; } }

/* Home page (feat 153 B1, Home frame) */
.homepage{ display:none; }
body[data-step="File"] .homepage{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); position:absolute; inset:0; z-index:7; background:var(--bg, var(--panel)); overflow:auto; }
.hp-left{ padding:34px 64px 40px; } .hp-right{ padding:36px 40px 40px; border-left:1px solid var(--rule); }
.hp-name{ margin:0; font-family:var(--font-display); font-size:34px; font-weight:700; }
.hp-sub{ margin:6px 0 22px; color:var(--dim); font-size:var(--fs-md); }
.hp-spine{ list-style:none; margin:0; padding:0; }
.hp-step{ position:relative; display:flex; gap:20px; padding-bottom:26px; }
.hp-step:not(:last-child)::before{ content:""; position:absolute; left:11px; top:26px; bottom:-2px; width:2px; background:var(--step-rust); }
.hp-node{ flex:none; width:24px; height:24px; border-radius:50%; border:2px solid var(--dim); box-sizing:border-box; margin-top:2px; background:var(--bg, var(--panel)); position:relative; z-index:1; }
.hp-step[data-state="done"] .hp-node{ background:var(--ok); border-color:var(--ok); }
.hp-step[data-state="stale"] .hp-node{ background:var(--warn); border-color:var(--warn); }
.hp-step[data-state="next"] .hp-node{ background:var(--primary); border-color:var(--primary); box-shadow:0 0 0 4px var(--bg, var(--panel)), 0 0 0 6px var(--primary); }
.hp-stitle{ display:flex; align-items:baseline; gap:12px; font-family:var(--font-display); font-size:22px; font-weight:700; }
.hp-chip{ font-family:var(--font-text, inherit); font-size:var(--fs-sm); font-weight:500; color:var(--step-accent); }
.hp-ssum{ margin-top:6px; color:var(--dim); font-size:var(--fs-md); }
.hp-link{ margin-top:8px; padding:0; border:0; background:transparent; color:var(--step-accent); font:inherit; cursor:pointer; }
.hp-link:hover{ text-decoration:underline; } .hp-link:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.hp-right h2{ margin:0; font-family:var(--font-display); font-size:24px; font-weight:700; }
.hp-right .hp-sub{ margin:6px 0 18px; }
.hp-req{ width:100%; max-width:640px; border-collapse:collapse; font-size:var(--fs-md); }
.hp-req th{ padding:6px 0; text-align:left; font-weight:400; font-size:var(--fs-sm); color:var(--dim); border-bottom:1px solid var(--rule); }
.hp-req td{ padding:11px 0; border-bottom:1px solid var(--rule-soft); }
.hp-req td:first-child{ position:relative; padding-right:22px; }
.hp-rm{ position:absolute; right:0; top:50%; transform:translateY(-50%); border:0; background:transparent; color:var(--faint); cursor:pointer; opacity:0; }
.hp-req tr:hover .hp-rm, .hp-rm:focus-visible{ opacity:1; }
.hp-mono{ font-family:var(--font-mono); font-variant-numeric:tabular-nums; }
.hp-req tr[data-state="fail"] .hp-cur, .hp-req tr[data-state="fail"] .hp-status{ color:var(--step-accent); font-weight:600; }
.hp-req tr[data-state="unknown"] td:not(:first-child){ color:var(--faint); }
.hp-status{ white-space:nowrap; }
.hp-empty{ color:var(--faint); }
.hp-addbox{ margin:14px 0 26px; }
.hp-notes-h{ margin-top:8px !important; font-size:18px !important; }
.hp-notes{ width:100%; max-width:640px; margin-top:8px; padding:6px 0; border:0; background:transparent; color:var(--dim); font:inherit; line-height:1.9; resize:vertical; }
.hp-notes:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; color:var(--text); }
@media (max-width:1000px){ body[data-step="File"] .homepage{ grid-template-columns:1fr; } .hp-right{ border-left:0; border-top:1px solid var(--rule); } }
.an-legend{ position:absolute; top:12px; left:12px; width:auto; padding:8px 10px; background:var(--chrome); border:1px solid var(--rule-strong); border-radius:4px; z-index:3; }
/* Results step (feat 153 Results frames): sidebar per view + the table workspace over the viewport */
.rswork{ position:absolute; inset:0; z-index:6; background:var(--bg, var(--panel)); overflow:auto; padding:18px 20px; }
.rswork[hidden]{ display:none; }
.rs-title{ margin:0; font-family:var(--font-display); font-size:22px; font-weight:700; }
.rs-sub2{ margin:2px 0 14px; font-size:19px; font-weight:600; color:var(--dim); }
.rs-big{ border-collapse:separate; border-spacing:0; border:1px solid var(--rule-strong); border-radius:6px; overflow:hidden; min-width:560px; font-family:inherit; font-variant-numeric:tabular-nums; }
.rs-big th, .rs-big td{ padding:5px 12px; text-align:left; border-top:1px solid var(--rule-soft); }
.rs-big th{ background:var(--sunken); border-top:0; font-weight:600; }
.rs-big .rs-r{ text-align:right; font-family:var(--font-mono); }
.rs-big th.rs-r{ font-family:inherit; }
.rs-big .warn{ color:var(--warn); }
.rs-h{ margin:16px 0 6px; font-family:var(--font-display); font-size:var(--fs-md); font-weight:700; color:var(--text); }
.rs-meta{ margin:2px 0 10px; color:var(--dim); font-size:var(--fs-xs); }
.rs-val{ display:flex; justify-content:space-between; gap:8px; padding:4px 0; color:var(--dim); }
.rs-v{ font-family:var(--font-mono); color:var(--text); font-variant-numeric:tabular-nums; }
.rs-v.stable{ color:var(--ok); } .rs-v.unstable{ color:var(--error); }
.rs-views{ display:flex; flex-direction:column; gap:2px; }
.rs-views button{ text-align:left; padding:7px 8px; border:0; border-radius:4px; background:transparent; color:var(--dim); font:inherit; cursor:pointer; }
.rs-views button[aria-current="true"]{ background:var(--hover); color:var(--text); }
.rs-views button:focus-visible, .rs-pill:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.rs-pills{ display:flex; flex-wrap:wrap; gap:6px 12px; }
.rs-pill{ border:0; border-bottom:2px solid transparent; background:transparent; color:var(--dim); font-family:var(--font-mono); cursor:pointer; padding:2px 0; }
.rs-pill[aria-pressed="true"]{ color:var(--text); border-bottom-color:var(--primary); }
.rs-pill:disabled{ cursor:default; }
.rs-floor{ margin:10px 0; color:var(--warn); }
.rs-stale{ margin-bottom:12px; padding-bottom:12px; border-bottom:1px solid var(--rule); }
.rs-stale strong{ color:var(--warn); }
.rs-stale p{ margin:4px 0 12px; }
.rs-stale .an-btn{ width:100%; min-height:34px; }
.rs-row input{ accent-color:var(--primary); }
.rs-main{ text-align:left; } .rs-name{ font-weight:600; } .rs-sub{ color:var(--dim); font-size:var(--fs-xs); }
.an-lg-title{ margin-bottom:4px; color:var(--dim); }
.an-lg-body{ display:flex; gap:8px; height:150px; }
.an-lg-bar{ width:12px; height:100%; border:0; border-radius:4px; flex:none; }
.an-lg-ends{ position:relative; min-width:42px; font-family:var(--font-mono); font-weight:500;
  font-size:var(--fs-xs); color:var(--dim); font-variant-numeric:tabular-nums; }
.an-lg-ends span{ position:absolute; left:0; white-space:nowrap; }
.an-lg-ends .hi{ top:0; }
.an-lg-ends .lo{ bottom:0; }
.an-lg-ends .zero{ transform:translateY(50%); }
/* Cp colour-scheme menu (schememenu.js): gradient swatches, opened by right-clicking the colour bar */
.an-lg-bar{ cursor:context-menu; }
.an-lg-body{ position:relative; }
/* the arrows on the Cp colour bar: ends change the limits, the middle one moves the mid colour (analysis.js dragLegend) */
.an-lg-h{ position:absolute; left:-10px; width:9px; height:11px; transform:translateY(50%); background:var(--text); clip-path:polygon(0 0, 100% 50%, 0 100%); cursor:ns-resize; touch-action:none; opacity:.75; }
.an-lg-h:hover{ opacity:1; }
.an-lg-h.mid{ background:var(--dim); }
.sm-num{ width:78px; min-height:24px; }
/* the aspect-ratio histogram in Mesh > Checks (aspecthist.js) */
.mh-box{ margin:10px 0 6px; }
.mh-svg{ display:block; width:100%; height:auto; overflow:visible; }
.mh-title{ fill:var(--text); font-size:10.5px; font-weight:600; }
.mh-t{ fill:var(--dim); font-size:9px; font-family:var(--font-mono); } .mh-t.on{ fill:var(--text); font-weight:600; }
.mh-ax{ fill:var(--dim); font-size:9.5px; letter-spacing:.04em; }
.mh-axis{ stroke:var(--rule-strong); stroke-width:1; } .mh-grid{ stroke:var(--rule); stroke-width:.5; stroke-dasharray:2 2; }
.mh-hit{ fill:transparent; } .mh-bin{ cursor:pointer; outline:none; }
.mh-bin:hover .mh-bar{ stroke:#ff7a1a; stroke-width:1.2; } .mh-bin.on .mh-bar, .mh-bin:focus-visible .mh-bar{ stroke:#ff7a1a; stroke-width:2; }
.mh-sel{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:4px; font-size:var(--fs-xs); color:var(--dim); }
.mh-clear{ min-height:26px; padding:0 10px; }
/* the probe card: Cp of the clicked panel (analysis.js paintProbe) */
.an-probe{ position:absolute; z-index:4; width:176px; padding:8px 10px; background:var(--chrome); border:1px solid var(--rule-strong); border-radius:4px; font-size:var(--fs-sm); box-shadow:0 4px 14px rgba(0,0,0,.25); }
.an-probe-h{ display:flex; justify-content:space-between; align-items:center; margin-bottom:4px; font-family:var(--font-mono); }
.an-probe-x{ border:0; background:transparent; color:var(--dim); cursor:pointer; font-size:16px; line-height:1; }
.an-probe-x:hover{ color:var(--text); }
.sm-reset{ width:100%; margin-top:6px; }
.sm-menu{ position:fixed; z-index:60; width:236px; padding:8px; background:var(--chrome); border:1px solid var(--rule-strong); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.28); font-size:var(--fs-sm); color:var(--text); }
.sm-grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-bottom:8px; }
.sm-swatch{ height:22px; border:2px solid transparent; border-radius:4px; outline:1px solid var(--rule-strong); cursor:pointer; padding:0; }
.sm-swatch[aria-pressed="true"]{ border-color:var(--focus); }
.sm-swatch:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.sm-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; padding:3px 0; }

/* aspect-ratio plate: a histogram of the panels per tenth of the range, level with the colour bar (feat 153) */
.an-lg-hist{ display:flex; flex-direction:column-reverse; justify-content:space-between; width:52px; height:100%; flex:none; }
.an-lg-hist span{ display:block; height:calc(10% - 2px); min-width:1px; background:var(--dim); opacity:.7; border-radius:1px; align-self:flex-end; }
.an-lg-floor{ margin-top:4px; color:var(--dim); font-size:var(--fs-xs); }
.an-pop{
  position:fixed; z-index:40; width:240px; padding:10px 12px;
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:8px; color:var(--text);
}
.an-pop h3{ margin:0 0 8px; font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; }
.an-pop-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px;
  font-size:var(--fs-sm); color:var(--dim); }
.an-pop-row input{ width:90px; min-height:28px; background:var(--panel); color:var(--text);
  border:1px solid var(--rule-strong); border-radius:4px; padding:2px 6px; font:inherit; font-family:var(--font-mono); font-weight:500; }
.an-pop-row input.bad{ border-color:var(--error); }

/* ===== Mesh / Analysis / Results polish: the Geometry tab's elements (2026-10-27) =====
   One app, not three styles: the same 16px gutter, uppercase section labels (worktree h2 / .params h3 / .tree-head),
   .t-row selection, .param inputs, input.switch toggles and .seg pressed state. Later than the rules it refines,
   so it wins at equal specificity. Tokens only, plus the owner's #ff7a1a plot/probe accent. */
.stepside, .stepopts{ padding:14px 16px 10px; }
/* title bars (feat 153): the Mesh/Analysis/Results panes get Geometry's header row, left = the step's name (the section's h3),
   right = an Options bar (#stepopts > .o-head); both span the pane edge to edge with a rule underneath */
.stepside > .an-sec > h3, .stepopts > .o-head h2{ margin:0; font-family:inherit; font-size:var(--fs-xs); font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--dim); }
.stepside > .an-sec > h3{ margin:-14px -16px 12px; padding:14px 16px 8px; border-bottom:1px solid var(--rule); }
.stepopts{ padding:0; }
.stepopts > .o-head{ padding:14px 16px 8px; border-bottom:1px solid var(--rule); margin-bottom:12px; }
.stepopts > .o-head h2{ padding:0; }
.stepopts > .so-pane{ padding:0 16px 10px; }
.st-foot{ margin:0 -16px; padding:12px 16px 14px; }
.an-sec > h3{ margin:0 0 10px; font-family:inherit; font-size:var(--fs-xs); font-weight:400; letter-spacing:.08em; text-transform:uppercase; color:var(--dim); }
.mp-comp, .rs-h{ margin:18px 0 8px; padding-top:12px; border-top:1px solid var(--rule); font-family:inherit; font-size:var(--fs-xs);
  font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--dim); }
.rs-body > .rs-h:first-child, .so-pane > .rs-h:first-child{ margin-top:0; padding-top:0; border-top:0; }
.mp-grp{ margin:16px 0 4px; padding:0 2px; letter-spacing:.06em; }
.rs-subh{ margin:0 0 4px; font-size:var(--fs-sm); color:var(--text); }
/* tree rows and the Results view list = the Geometry tree's .t-row */
.mp-node, .rs-views button{ padding:6px 8px; font-size:var(--fs-md); border-left:2px solid transparent; }
.mp-node:hover, .rs-views button:hover{ background:var(--hover); color:var(--text); }
.mp-node[aria-current="true"], .rs-views button[aria-current="true"]{ background:color-mix(in srgb, var(--primary) 16%, transparent);
  color:var(--text); font-weight:600; border-left-color:var(--primary); box-shadow:inset 0 0 0 1px var(--primary); }
.mp-nr{ color:var(--dim); font-weight:400; }
/* segmented control = Geometry's .seg */
.mp-seg button{ min-height:30px; }
.mp-seg button[aria-pressed="true"]{ background:var(--primary); color:var(--on-primary); font-weight:600; }
.mp-seg button:not([aria-pressed="true"]):hover{ background:var(--hover); color:var(--text); }
/* hairline value rows (Home's .hm-row) */
.mp-check, .rs-val{ padding:6px 0; border-top:1px solid var(--rule-soft); font-size:var(--fs-sm); }
.mp-val{ font-family:var(--font-mono); color:var(--text); }
.so-pane[data-for="Mesh"] .an-field{ margin:0; padding:4px 0; border-top:1px solid var(--rule-soft); font-size:var(--fs-sm); }
/* inputs = .param input */
.an-field input, .an-pop-row input, .sm-num{ min-height:30px; box-sizing:border-box; background:var(--sunken); border:1px solid var(--rule-strong);
  border-radius:4px; padding:4px 8px; color:var(--text); font-family:inherit; font-size:var(--fs-md); font-weight:400; text-align:right; font-variant-numeric:tabular-nums; }
.an-field input::placeholder, .sm-num::placeholder{ color:var(--faint); }
.sm-num:focus-visible{ outline:2px solid var(--focus); outline-offset:-1px; }
.an-field select{ min-height:30px; max-width:150px; background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px;
  padding:4px 6px; color:var(--text); font:inherit; font-size:var(--fs-sm); }
.an-field select:disabled{ opacity:.55; }
/* on/off rows = Geometry's switch row: label left, input.switch right */
.an-chk, .rs-layer, .sm-row:has(> .switch){ justify-content:space-between; gap:10px; min-height:30px; font-size:var(--fs-sm); color:var(--text); cursor:pointer; }
.an-chk > .switch, .rs-layer > .switch, .sm-row > .switch{ order:2; }
/* mesh control cards (meshcontrols.js) = the Geometry derived-value card */
.mc-card{ border:1px solid var(--rule-strong); border-radius:6px; padding:10px 12px; background:var(--panel); }
.mc-chip{ border-color:var(--rule-strong); padding:2px 10px; }
.mc-chip:hover{ color:var(--text); background:var(--hover); }
.mc-chip[aria-pressed="true"]{ border-color:var(--primary); background:color-mix(in srgb, var(--primary) 16%, transparent); }
.mc-del{ min-width:24px; min-height:24px; border-radius:4px; }
.mc-del:hover{ background:var(--hover); }
/* viewport plates: the Cp / aspect legend, the probe card, the scheme menu -- one card look (radius 6, soft shadow) */
.an-legend{ padding:10px 12px 10px 22px; border-radius:6px; font-size:var(--fs-sm); color:var(--text); box-shadow:0 4px 14px rgba(0,0,0,.18); }
.an-legend:has(.an-lg-hist){ padding-left:12px; }
.an-conv-head{ display:flex; align-items:center; gap:12px; padding:0 2px 6px; font-size:var(--fs-sm); color:var(--text); }
.an-conv-head select{ font:inherit; }
.an-lg-title{ margin-bottom:8px; color:var(--text); font-weight:600; }
.an-lg-bar{ box-shadow:inset 0 0 0 1px rgba(0,0,0,.12); }
.an-lg-ends{ color:var(--text); }
.an-lg-h:hover, .an-lg-h:active{ background:#ff7a1a; opacity:1; }
.an-lg-hist span{ opacity:.55; }
.an-probe{ width:220px; padding:8px 12px 10px; border-radius:6px; border-top:3px solid #ff7a1a; box-shadow:0 4px 14px rgba(0,0,0,.18); }
.an-probe-h{ margin-bottom:4px; }
.an-probe-h strong{ font-size:var(--fs-md); }
.an-probe-h{ cursor:grab; touch-action:none; user-select:none; } .an-probe.dragging .an-probe-h{ cursor:grabbing; }
.an-probe .rs-v{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; }
.an-probe-x{ min-width:24px; min-height:24px; border-radius:4px; }
.an-probe-x:hover{ background:var(--hover); }
.an-probe-x:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.sm-menu{ padding:10px 12px 12px; }
.sm-h{ margin:0 0 8px; font-size:var(--fs-xs); font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--dim); }
.sm-h:not(:first-child){ margin-top:8px; padding-top:10px; border-top:1px solid var(--rule); }
.sm-row{ min-height:30px; color:var(--dim); }
.sm-swatch:hover{ outline-color:var(--text); }
.sm-swatch[aria-pressed="true"]{ border-color:var(--primary); }
.sm-row input[type="color"]{ width:52px; height:26px; padding:2px; background:var(--sunken); border:1px solid var(--rule-strong); border-radius:4px; cursor:pointer; }
.sm-reset{ margin-top:10px; }


/* ===== app toast (clipboard.js toast(): "Copied …" / "Pasted …") ===== */
.app-toast{
  position:fixed; left:50%; bottom:28px; transform:translateX(-50%); z-index:50;
  padding:8px 14px; border:1px solid var(--primary); border-radius:6px;
  background:var(--panel); color:var(--text); font-size:var(--fs-sm);
  pointer-events:none; max-width:80vw; box-shadow:0 6px 20px rgba(0,0,0,.25);
}
.app-toast[hidden]{ display:none; }
#view > .app-toast{ position:absolute; top:64px; bottom:auto; }   /* below the view toolbar */

/* wing front pane: the dihedral link toggle (wingpanes.js linkButton), over the picture's top-right */
.profile-btn.wing-link{ position:absolute; top:50%; right:6px; transform:translateY(-50%); z-index:4; }   /* the plot keeps its column clear (wingpanes.js linkRoom) */
.wing-row .profile-btn.wing-link{ top:4px; transform:none; }   /* a fin's front host is a narrow sliver: back to the corner */
.profile-btn.wing-link[aria-pressed="true"]{ border-color:var(--primary); }
/* the planform's Fit to image: a text button (the bare .profile-btn is the 26px icon square), pane's top-right (feat 133 F1) */
.profile-btn.wing-fit{ position:absolute; top:4px; right:6px; z-index:4; width:auto; padding:2px 8px; white-space:nowrap; }

/* ===== feat 114: the handle layer (.hd-*) -- every handle style for feats 114-117 lives HERE ===== */
.hd-layer{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; overflow:hidden; z-index:4; }
.hd-layer{ --axis-x:#c0463f; --axis-y:#4c9a5c; --axis-z:#4472c4; }   /* the TransformControls rotate-ring colours (viewport.js VSP_AXIS_COLOR) */
.hd-off{ display:none; }
.hd-grip{ outline:none; }
.hd-hit{ fill:transparent; stroke:none; pointer-events:all; touch-action:none; }
.hd-hit-ring{ fill:transparent; stroke:transparent; stroke-width:24; pointer-events:all; touch-action:none; }
.hd-dot{ fill:var(--chrome); stroke:var(--primary); stroke-width:2; }
.hd-corner{ stroke-width:2.5; }
.hd-pick{ stroke-width:1.5; }   /* feat 119: a value-less joint dot, smaller than a grip */
.hd-grip.is-active .hd-pick{ fill:var(--primary); stroke:var(--focus); stroke-width:3; }
.hd-ring{ fill:none; stroke:var(--primary); stroke-width:2; opacity:.85; }
.hd-grip.is-hover .hd-dot, .hd-grip.is-group .hd-dot{ fill:var(--primary); }
.hd-grip.is-hover .hd-ring{ stroke-width:3.5; opacity:1; }
.hd-focus{ r:16; fill:none; stroke:var(--focus); stroke-width:2; visibility:hidden; }
.hd-grip:focus-visible .hd-focus{ visibility:visible; }
.hd-outline{ fill:none; stroke:var(--primary); stroke-width:1.5; opacity:.8; }
.hd-outline[data-axis=x]{ stroke:var(--axis-x); } .hd-outline[data-axis=y]{ stroke:var(--axis-y); } .hd-outline[data-axis=z]{ stroke:var(--axis-z); }
.hd-outline[data-axis=x], .hd-outline[data-axis=y], .hd-outline[data-axis=z]{ stroke-width:3; opacity:.95; stroke-linecap:round; stroke-linejoin:round; }   /* feat 120: an occupant rotation arc */
.hd-ghost{ fill:none; stroke:var(--primary); stroke-width:1.5; stroke-dasharray:5 4; opacity:.7; }
.hd-limit{ fill:none; stroke:var(--error); stroke-width:3; stroke-linecap:round; }
.hd-grip.hd-linked .hd-dot{ fill:var(--primary); stroke:var(--focus); stroke-width:3; }   /* feat 117: its panel field is hovered or focused */
.hd-grip.hd-linked .hd-ring{ stroke-width:3.5; opacity:1; }
#params .hd-linked{ box-shadow:inset 3px 0 0 var(--primary); background:var(--hover, color-mix(in srgb, var(--primary) 10%, transparent)); }   /* the row whose grip is hovered or focused */
.hd-label{ position:absolute; z-index:6; pointer-events:none; padding:1px 7px; white-space:nowrap;
  font-size:var(--fs-xs); font-variant-numeric:tabular-nums; color:var(--text);
  background:var(--chrome); border:1px solid var(--rule-strong); border-radius:999px; }
.hd-field{ position:absolute; z-index:6; width:80px; box-sizing:border-box; padding:2px 6px; font:inherit;
  font-size:var(--fs-xs); font-variant-numeric:tabular-nums; font-family:var(--font-mono);
  color:var(--text); background:var(--chrome); border:1px solid var(--primary); border-radius:4px; }
.hd-field:focus-visible{ outline:2px solid var(--focus); outline-offset:1px; }
.hd-field[aria-invalid="true"]{ border-color:var(--error); }

/* ===== slider field (feat 150 row 2, widgets.js sliderField) =====
   The mockup's style: label above, then [range, flex:1][value, fixed width, dotted underline]. Tokens only.
   NO OVERFLOW: the row and the range take min-width:0, the value column is a fixed width with tabular numerals (a long
   value shrinks its font, then elides, and never pushes the row), and the label wraps anywhere. `.param input` (above)
   is a text-box rule; the selectors here are one class heavier so the range and the typing box keep their own look. */
.param.slider-field{ min-width:0; }
.param.slider-field > label{ overflow-wrap:anywhere; }
.sl-row{ display:flex; align-items:center; gap:10px; height:28px; min-width:0; }
.param.slider-field input[type="range"]{
  flex:1 1 0; min-width:0; width:100%; height:18px; margin:0; padding:0; border:0; border-radius:0;
  background:transparent; cursor:pointer; appearance:none; -webkit-appearance:none;
}
.param.slider-field input[type="range"]:focus-visible{ outline:none; }
.param.slider-field input[type="range"]:disabled{ opacity:.5; cursor:default; }
/* the fill stops at the thumb's centre: the thumb travels 7px in from each end of the track */
.param.slider-field input[type="range"]::-webkit-slider-runnable-track{
  height:4px; border-radius:2px;
  background:linear-gradient(var(--primary), var(--primary)) 0 0 / calc(7px + (100% - 14px) * var(--f, 0)) 100% no-repeat, var(--rule);
}
.param.slider-field input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none; width:14px; height:14px; margin-top:-5px; border:0; border-radius:50%;
  background:var(--primary); transition:transform .1s, box-shadow .1s;
}
.param.slider-field input[type="range"]::-moz-range-track{ height:4px; border-radius:2px; background:var(--rule); }
.param.slider-field input[type="range"]::-moz-range-progress{ height:4px; border-radius:2px; background:var(--primary); }
.param.slider-field input[type="range"]::-moz-range-thumb{ width:14px; height:14px; border:0; border-radius:50%; background:var(--primary); }
.slider-field.is-dragging input[type="range"]::-webkit-slider-thumb{
  transform:scale(1.286); box-shadow:0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent);
}
.slider-field.is-dragging input[type="range"]::-moz-range-thumb{ box-shadow:0 0 0 3px color-mix(in srgb, var(--primary) 20%, transparent); }
.param.slider-field input[type="range"]:focus-visible::-webkit-slider-thumb{ box-shadow:0 0 0 2px var(--panel), 0 0 0 4px var(--focus); }
.param.slider-field input[type="range"]:focus-visible::-moz-range-thumb{ box-shadow:0 0 0 2px var(--panel), 0 0 0 4px var(--focus); }
@media (prefers-reduced-motion:reduce){ .param.slider-field input[type="range"]::-webkit-slider-thumb{ transition:none; } }

.sl-val{
  flex:0 0 76px; box-sizing:border-box; min-width:0; margin:0; padding:2px 3px; background:none; border:0; border-radius:3px;
  color:var(--text); font:inherit; font-size:var(--fs-md); text-align:right; font-variant-numeric:tabular-nums;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:text;
  text-decoration:underline dotted var(--faint); text-underline-offset:3px;
}
.sl-val[data-long]{ font-size:var(--fs-xs); }
.sl-val:hover{ background:var(--hover); text-decoration-color:var(--text); }
.sl-val:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }
.slider-field.is-dragging .sl-val{ color:var(--primary); text-decoration-color:var(--primary); }
.sl-val:disabled{ color:var(--faint); cursor:default; }
.sl-val:disabled:hover{ background:none; text-decoration-color:var(--faint); }
/* feat 162a: a read-only value (the Controls tab's Preview deflection): not a click-to-type button */
span.sl-val, span.sl-val:hover{ cursor:default; text-decoration:none; background:none; }
/* feat 162a: an icon-only .seg (Controls tab Flap type): the label is a box so its .ico span takes its size */
.seg.seg-icons label{ display:flex; justify-content:center; align-items:center; padding:4px 6px; }
.seg.seg-icons .ico{ width:22px; height:22px; }
.param .sl-row input.sl-edit{
  flex:0 0 76px; width:76px; box-sizing:border-box; padding:4px 6px; font-size:var(--fs-md); text-align:right;
  border-color:var(--primary);
}
.param .sl-row input.sl-edit.bad{ border-color:var(--error); }

/* the inline Min / Max editor under the row (right-click, Shift+F10 or the Menu key, "Set limits...") */
.sl-limits{ display:flex; flex-wrap:wrap; align-items:center; gap:4px 12px; min-width:0; }
.sl-limits > label{ display:flex; align-items:center; gap:6px; }
.param .sl-limits input{ flex:none; width:68px; box-sizing:border-box; padding:4px 6px; font-size:var(--fs-md); text-align:right; }
.sl-limits .sl-note{ flex-basis:100%; margin:0; font-size:var(--fs-xs); line-height:1.4; color:var(--dim); overflow-wrap:anywhere; }
.sl-limits .sl-note[data-bad]{ color:var(--text); font-weight:600; }

/* ===== feat 153 B2: Examples, Help and Preferences restyle (CSS only) =====
   Hairline rows, no boxes, no lift, pill buttons. Later than the rules it refines, so it wins at equal specificity. */
/* Preferences: a panel of hairline sections */
.prefs-panel{ width:300px; padding:6px 16px 14px; border-radius:12px; border-color:var(--rule-strong, var(--rule)); }
.prefs-panel h3{ margin:0; padding:14px 0 4px; border-top:1px solid var(--rule); font-size:var(--fs-sm); font-weight:600; color:var(--dim); letter-spacing:.02em; }
.prefs-panel > h3:first-child{ border-top:0; }
#prefs-layout > h3{ border-top:1px solid var(--rule); }
.prefs-panel .prefs-sub{ margin:0 0 8px; }
.prefs-panel .seg{ border-color:var(--rule-strong, var(--rule)); border-radius:999px; margin-bottom:10px; }
.prefs-panel .hdr-select{ margin:0 0 10px; width:100%; border-radius:999px; background:transparent; padding:6px 12px; }
.prefs-row{ border-radius:0; border-top:1px solid var(--rule-soft); padding:8px 0; margin:0; }
.prefs-row:hover{ background:transparent; }
.prefs-row:hover .pr-name{ color:var(--step-accent); }
/* Examples: flat tiles on hairlines, a pill Open */
.gallery{ padding:28px clamp(20px,4vw,56px) 8px; }
.gal-top h2{ font-family:var(--font-display); font-size:var(--fs-xl); font-weight:700; }
.gal-search{ background:transparent; border-color:var(--rule-strong, var(--rule)); border-radius:999px; padding:8px 16px; }
.gal-grid{ gap:0 24px; }
.gal-grid .p-tile{ background:transparent; border:0; border-top:1px solid var(--rule); border-radius:0; padding-bottom:8px; transition:none; }
.gal-grid .p-tile:hover:not(:disabled){ border-color:var(--rule); transform:none; background:var(--hover); }
.gal-grid .p-tile:focus-visible{ outline:2px solid var(--focus); outline-offset:-2px; }
.gal-grid .thumb{ background:transparent; justify-content:flex-start; padding-left:2px; }
.gal-grid .meta{ padding:6px 2px 8px; }
.gal-grid .p-tile[data-status="no"] .thumb, .gal-grid .p-tile[data-status="partial"] .thumb, .gal-grid .p-tile[data-status="yes"] .thumb{ background:transparent; }
.ex-open{ width:auto; align-self:flex-start; border-radius:999px; border-color:var(--rule-strong, var(--rule)); background:transparent; padding:5px 14px; }
.ex-open:hover:not(:disabled){ border-color:var(--rule-strong, var(--rule)); }
.p-step{ border-radius:999px; }
.gal-banner{ border-radius:8px; }
/* Help: one ~760 px reading column */
.help-panel[data-mode="doc"]{ width:min(var(--help-w, 760px), 100%); box-shadow:none; border-left-color:var(--rule-strong, var(--rule)); }
.help-head{ padding:10px 14px; }
.help-title{ font-family:var(--font-display); font-size:var(--fs-lg); font-weight:700; }
.help-body{ padding:8px 32px 28px; }
.help-body > *{ max-width:68ch; }
.help-body h1{ font-family:var(--font-display); font-weight:700; }
.help-body h2{ padding-top:14px; border-top:1px solid var(--rule); }
.help-nav, .help-x, .help-topics{ border-radius:999px; }

/* ===== feat 153 A1: pills + flat sidebars (dark theme only; light parity is the final pass) ===== */
.hdr-select, .wt-clear, .blend-remove, .sync-err-more, .viewstyle, .part-toggle-btn, .item-add, .ex-open,
.c-form button[type="submit"]{ border-radius:999px; }
.c-form button[type="submit"]{ padding:8px 18px; }   /* the one filled-orange primary of the chat region */
.worktree, .options, .stepside, .stepopts{ background:transparent; border-left:0; border-right:0; border-top:1px solid var(--rule); }
