
/* ============================================================
   BOOT SPLASH — full-screen brand cover shown on every load /
   refresh, faded out by init.js once the dashboard renders.
   Declared early so it paints before the rest of the app.
   ============================================================ */
#formlab-splash {
  position: fixed;
  inset: 0;
  z-index: 99999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 38%, #1e293b 0%, #0f172a 72%);
  opacity: 1;
  transition: opacity 0.55s ease;
}
#formlab-splash.splash-hiding {
  opacity: 0;
  pointer-events: none;
}
#formlab-splash .splash-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  animation: splashPop 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
}
#formlab-splash .splash-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 84px;
  height: 84px;
  border-radius: 50%;
  box-shadow: 0 10px 44px rgba(37, 99, 235, 0.38);
  animation: splashPulse 1.7s ease-in-out infinite;
}
#formlab-splash .splash-word {
  font-size: 30px;
  font-weight: 700;
  letter-spacing: 0.3px;
  color: #ffffff;
}
#formlab-splash .splash-word span { color: #60a5fa; }
@keyframes splashPop {
  from { transform: scale(0.9); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}
@keyframes splashPulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  #formlab-splash .splash-logo,
  #formlab-splash .splash-inner { animation: none; }
}

/* ============================================================
   ANIMATED FLASK LOADER — shared by the boot splash and the
   view-switch loader.

   The liquid wave is an HTML <div> transform animation (.flw). It is
   deliberately NOT SMIL and NOT a CSS transform on an SVG <path>:
   both of those run on the MAIN THREAD, which is blocked precisely
   when this logo is on screen as a loading indicator, so the wave
   froze. Only HTML-element transform/opacity animations get promoted
   to the COMPOSITOR thread, which keeps ticking through a main-thread
   block (same reason the loader's own scale-pulse keeps going).
   Verified in-browser: the wave scrolls during a 6s synchronous block.

   Base coordinate space is 64x64 (matches the SVG paths); size it with
   --fl-scale (target px / 64). The flask body/outline are static; the
   wide white base rect + 4-wavelength wave scroll one wavelength
   (22.938) and loop seamlessly, clipped to the flask body.
   ============================================================ */
.fl-flask {
  position: relative;
  display: block;
  width: 64px;
  height: 64px;
  transform: scale(var(--fl-scale, 1));
  transform-origin: center center;
}
.fl-flask .flc { position: absolute; inset: 0; border-radius: 50%; background: #2563eb; }
.fl-flask .fll {
  position: absolute; inset: 0;
  clip-path: path('M 27.085171,28.914341 16.435214,46.118123 c -1.844844,3.195368 -0.408839,5.734597 3.276915,5.734597 h 24.576824 c 3.684277,0.0102 5.119442,-2.543238 3.276912,-5.734597 L 36.915911,28.914341 Z');
}
.fl-flask .flw {
  position: absolute; inset: 0;
  animation: flFlaskWave 1.6s linear infinite;
  will-change: transform;
}
.fl-flask .flw svg,
.fl-flask .flo {
  position: absolute; inset: 0;
  width: 64px; height: 64px;
  display: block;
  overflow: visible;
}
@keyframes flFlaskWave {
  from { transform: translateX(0); }
  to   { transform: translateX(22.938px); }
}
@media (prefers-reduced-motion: reduce) {
  .fl-flask .flw { animation: none; }
}

/* Workspace-switch overlay — a prominent, screen-blocking "working" state so
   the switch (wipe local + re-pull from cloud) doesn't look like nothing
   happened, and so it can't be rage-clicked while in flight. */
#pro-switch-overlay {
  position: fixed; inset: 0; z-index: 10000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(15, 23, 42, 0.55);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
#pro-switch-overlay .fl-switch-card {
  background: var(--surface, #fff);
  border: 1px solid var(--border, #e2e8f0); border-radius: 14px;
  padding: 26px 34px; text-align: center; min-width: 240px;
  box-shadow: 0 14px 44px rgba(0, 0, 0, 0.28);
}
/* Global button-context spinner — small, inline, inherits text color */
.fl-spinner {
  display: inline-block;
  width: 13px; height: 13px;
  border: 2px solid rgba(128,128,128,0.25);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: fl-spin 0.75s linear infinite;
  vertical-align: middle;
  flex-shrink: 0;
  margin-right: 6px;
}
#pro-switch-overlay .fl-spinner {
  width: 34px; height: 34px; margin: 0 auto 14px;
  border: 3px solid var(--border, #e2e8f0);
  border-top-color: var(--primary, #2563eb);
  border-radius: 50%; animation: fl-spin 0.8s linear infinite;
}
#pro-switch-overlay .fl-switch-title { font-size: 15px; font-weight: 600; color: var(--text, #0f172a); }
#pro-switch-overlay .fl-switch-sub   { font-size: 12.5px; color: var(--text-muted, #64748b); margin-top: 4px; }
@keyframes fl-spin { to { transform: rotate(360deg); } }

/* New-record flash — briefly tints a freshly-added list row/card so the user sees
   where it landed (driven by flashNewRow() in utils.js). The td rule covers table
   rows (tr background can be unreliable); the element rule covers cards. */
@keyframes fl-row-flash { from { background-color: rgba(37,99,235,0.20); } to { background-color: rgba(37,99,235,0); } }
.fl-row-flash { animation: fl-row-flash 2s ease-out 1; }
.fl-row-flash > td { animation: fl-row-flash 2s ease-out 1; }
/* Amber "return" flash — shown when you come back to a row you navigated away from
   (e.g. a lot's Used-in batch link), distinct from the blue new-row flash above. */
@keyframes fl-row-flash-amber { from { background-color: rgba(234,179,8,0.30); } to { background-color: rgba(234,179,8,0); } }
.fl-row-flash-amber { animation: fl-row-flash-amber 2s ease-out 1; }
.fl-row-flash-amber > td { animation: fl-row-flash-amber 2s ease-out 1; }
.fl-spinner-lg {
  width: 34px; height: 34px; margin: 0 0 14px;
  border: 3px solid var(--border, #e2e8f0);
  border-top-color: var(--primary, #2563eb);
}
/* Full-screen busy overlay (busyOverlay) */
#fl-busy-overlay {
  position: fixed; inset: 0; z-index: 9000;
  display: none; align-items: center; justify-content: center;
  background: rgba(15, 23, 42, 0.32);
  backdrop-filter: blur(2px);
}
#fl-busy-overlay .fl-busy-card {
  display: flex; flex-direction: column; align-items: center;
  background: var(--surface, #fff); color: var(--text, #0f172a);
  padding: 22px 30px; border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.22);
}
#fl-busy-overlay .fl-busy-label { font-size: 13px; font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
  #pro-switch-overlay .fl-spinner,
  #fl-busy-overlay .fl-spinner { animation-duration: 1.6s; }
}

/* Sidebar "PRO" badge — outlined blue box matching the AMS-style
   product-line badge reference. Splash deliberately stays a clean
   "FormLab" with no PRO; the badge only lives in the sidebar to
   mark the signed-in state once the app is mounted. */
#sidebar .logo .logo-text .logo-pro {
  /* Sits ON the wordmark's baseline, not centred on its x-height. The trio
     `inline-block` + `overflow:hidden` + `vertical-align:baseline` is what
     does it: for an inline-block with non-visible overflow the CSS baseline
     is synthesised as the box's BOTTOM margin edge, so the pill's underside
     lands exactly where the letters stop. Font-independent by construction —
     the alternative (vertical-align:bottom plus a negative nudge) has to
     cancel the font's descender by hand, which is 0.216em on -apple-system
     and something else on Segoe UI or Roboto. */
  display: inline-block;
  overflow: hidden;
  /* Filled, not outlined. An outline reads as a form control; a fill reads as
     a mark — and it is the same call made everywhere else in the app (selected
     states filled, moving away from borders). --fl-pro-fill is what the
     wordmark-gradient preference swaps; the flat primary is the default. */
  background: var(--fl-pro-fill, #2563eb);
  color: #fff;
  border: 0;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* The pill spans the "b" ascender to the baseline, overshooting 1px at each
     end so it reads as bracketing the word rather than floating inside it.
     --fl-pro-ascender is that ascender expressed in the PILL's own em: "b"
     measures 12.68px against a 17px/800 wordmark, and the pill's font-size is
     .55em of it, so 12.68 / 9.35 = 1.356em. Padding is derived rather than
     typed, because the figure is 2.665px and rounding to 2 or 3 visibly
     misses the tip at this size. */
  --fl-pro-ascender: 1.356em;
  padding: calc((var(--fl-pro-ascender) - 1em) / 2 + 1px) 6px;
  border-radius: 999px;
  line-height: 1;
  vertical-align: baseline;
  /* Optical nudge. The maths is already exact — the box's bottom margin edge
     sits ON the baseline (see above) — but a bordered box next to letter ink
     reads as sitting slightly high, so it gets pushed 1px past level to look
     level. A negative bottom margin moves the border edge below the
     synthesised baseline without disturbing the alignment mechanism itself.
     Paired with the top overshoot above: 1px past the baseline at the bottom,
     1px past the ascender at the top, so the badge brackets the word evenly. */
  margin-bottom: -1px;
  margin-left: 0.42em;
  font-size: 0.55em;
  text-align: center;
  /* Hidden until body.signed-in; fades + slides in 8px from the
     right when the class flips on. */
  opacity: 0;
  transform: translateX(8px);
  transition:
    opacity 0.45s ease 0.2s,
    transform 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.2s;
}
/* Inner span cancels the letter-spacing tail so the glyph block
   sits visually centered between the borders, not 1px left. */
#sidebar .logo .logo-text .logo-pro > span {
  display: inline-block;
  margin-right: -0.14em;
}
body.signed-in #sidebar .logo .logo-text .logo-pro {
  opacity: 1;
  transform: translateX(0);
}

/* ============================================================
   Wordmark preference control.
   ------------------------------------------------------------
   The lockup rendered at a size you can actually judge, with the
   two changeable parts wearing a stepper: a triangle above and
   below, so the affordance says "cycle me" rather than "read
   me". The control doubles as the preview -- each segment paints
   itself from the same live state the sidebar reads, so the two
   cannot drift apart.
   ============================================================ */
.fl-wm-lockup {
  /* Inline flow on ONE baseline, exactly like #sidebar .logo-text -- not a flex
     row. The parts of a wordmark share a baseline; laying them out as
     independent boxes stops it being a preview of the lockup at all. The
     vertical padding is room for the steppers, which sit out of flow. */
  display: block;
  font-size: 34px;
  font-weight: 500;
  letter-spacing: -0.34px;
  line-height: 1;
  color: var(--text);
  user-select: none;
  padding: 26px 0 30px;
  white-space: nowrap;
}
.fl-wm-static { font-weight: 500; }

.fl-wm-seg {
  position: relative;
  display: inline-block;
  vertical-align: baseline;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  cursor: pointer;
  border-radius: 6px;
}
.fl-wm-seg:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

/* Stepper marks -- out of flow, centred over and under the segment. Dimmed at
   rest so they hint rather than compete with the wordmark they bracket. */
.fl-wm-tri {
  position: absolute;
  left: 50%;
  width: 0; height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  opacity: .3;
  transition: opacity .14s ease;
  pointer-events: none;
}
.fl-wm-tri-up   { bottom: calc(100% + 7px); transform: translateX(-50%); border-bottom: 5px solid currentColor; }
.fl-wm-tri-down { top: calc(100% + 7px);    transform: translateX(-50%); border-top: 5px solid currentColor; }
.fl-wm-seg:hover .fl-wm-tri,
.fl-wm-seg:focus-visible .fl-wm-tri { opacity: .85; }

.fl-wm-seg-body { font-weight: 800; color: var(--primary); }
.fl-wm-seg.is-grad .fl-wm-seg-body {
  background: var(--fl-wordmark-ramp);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The real badge at preview scale -- same derivation as .logo-pro, so what you
   toggle is what the sidebar renders. */
.fl-wm-pill {
  display: inline-block;
  overflow: hidden;
  vertical-align: baseline;
  text-align: center;
  background: #2563eb;
  color: #fff;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  --fl-pro-ascender: 1.356em;
  padding: calc((var(--fl-pro-ascender) - 1em) / 2 + 1px) 6px;
  border-radius: 999px;
  font-size: 0.55em;
  line-height: 1;
  margin-left: 0.42em;
  margin-bottom: -1px;
}
.fl-wm-seg.is-grad .fl-wm-pill { background: var(--fl-wordmark-ramp); color: #fff; }

.fl-wm-state {
  position: absolute;
  top: calc(100% + 20px);
  left: 50%;
  transform: translateX(-50%);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 400;
  pointer-events: none;
}


@media (prefers-reduced-motion: reduce) {
  .fl-wm-seg, .fl-wm-tri { transition: none; }
}

/* ============================================================
   Wordmark gradient (Preferences -> Appearance).
   ------------------------------------------------------------
   Two independent switches, set on <html> by the pre-paint block
   in index.html and by setWordmarkGradient():
     .fl-grad-pro  the PRO badge carries the ramp
     .fl-grad-lab  "Lab" carries the same ramp, clipped to text
   Off by default -- a flat #2563eb badge is the baseline. One
   ramp, single-hue: only lightness moves across it, never the
   hue, which is what separates this from the gradients that
   read cheap.
   ============================================================ */
:root { --fl-wordmark-ramp: linear-gradient(135deg, #60a5fa, #2563eb 60%, #1e40af); }

html.fl-grad-pro #sidebar .logo .logo-text .logo-pro { --fl-pro-fill: var(--fl-wordmark-ramp); }

/* "Lab" is the un-classed <span> inside .logo-text -- the pills carry their
   own classes, so :not() keeps the ramp off them. Clipping a gradient to text
   needs color:transparent, which is why this overrides the colour outright
   rather than layering on top of it. */
/* The :not() chain is not decoration — it is specificity. The glass rail's
   accent rule carries five class-level terms, and clipping a gradient to text
   only works while `color` stays transparent, so this has to outrank it or the
   ramp is hidden behind a solid colour on exactly one theme. */
html.fl-grad-lab #sidebar .logo .logo-text > span:not([class]):not(.logo-pro):not(.logo-free) {
  background: var(--fl-wordmark-ramp);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

@media (prefers-reduced-motion: reduce) {
  #sidebar .logo .logo-text .logo-pro {
    transition: opacity 0.2s ease;
    transform: none;
  }
}
/* Free pill — the same object as the Pro pill, same slot, same slide-in, so
   the sidebar always states which tier you are on rather than saying nothing
   until you pay. Muted rather than accent-blue: it marks the state, it does
   not advertise it. Both pills are decided by the pre-paint cookie branch in
   index.html, so exactly one is ever in the DOM's "shown" state — a body with
   .signed-in never has .tier-free. */
#sidebar .logo .logo-text span.logo-free {
  /* Sits ON the wordmark's baseline, not centred on its x-height. The trio
     `inline-block` + `overflow:hidden` + `vertical-align:baseline` is what
     does it: for an inline-block with non-visible overflow the CSS baseline
     is synthesised as the box's BOTTOM margin edge, so the pill's underside
     lands exactly where the letters stop. Font-independent by construction —
     the alternative (vertical-align:bottom plus a negative nudge) has to
     cancel the font's descender by hand, which is 0.216em on -apple-system
     and something else on Segoe UI or Roboto. */
  display: inline-block;
  overflow: hidden;
  background: transparent;
  /* Themed through variables, not literals: the sidebar can be dark, light or
     glass, and a white-alpha pill vanishes on a light rail. The Pro pill gets
     away with a literal because #60a5fa reads on every one of them. */
  color: var(--logo-free-fg, rgba(255, 255, 255, 0.55));
  /* Inset stroke, NOT a border: a border grows an auto-sized box whatever
     box-sizing says, which made this pill 2px taller and 2px wider than the
     filled Pro badge it is supposed to be interchangeable with. */
  border: 0;
  box-shadow: inset 0 0 0 1px var(--logo-free-bd, rgba(255, 255, 255, 0.34));
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* Identical geometry to the Pro badge — see .logo-pro for the derivation.
     The two are one object in two states; a Free user switching to Pro should
     see the label change, not the shape. */
  --fl-pro-ascender: 1.356em;
  padding: calc((var(--fl-pro-ascender) - 1em) / 2 + 1px) 6px;
  border-radius: 999px;
  line-height: 1;
  vertical-align: baseline;
  /* Optical nudge. The maths is already exact — the box's bottom margin edge
     sits ON the baseline (see above) — but a bordered box next to letter ink
     reads as sitting slightly high, so it gets pushed 1px past level to look
     level. A negative bottom margin moves the border edge below the
     synthesised baseline without disturbing the alignment mechanism itself.
     Paired with the top overshoot: 1px past the baseline, 1px past the "b"
     ascender, so the badge brackets the word evenly. */
  margin-bottom: -1px;
  margin-left: 0.42em;
  font-size: 0.55em;
  text-align: center;
  opacity: 0;
  transform: translateX(8px);
  transition:
    opacity 0.45s ease 0.2s,
    transform 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.2s;
}
/* Same letter-spacing tail cancel as the Pro pill — plus an explicit colour
   reset. `#sidebar .logo .logo-text span` paints every span in the wordmark
   accent-blue (it exists for the "Lab" in FormLab), and that rule reaches
   inside this pill too, so without inherit the Free pill renders blue and
   reads as a second Pro badge. */
#sidebar .logo .logo-text span.logo-free > span {
  display: inline-block;
  margin-right: -0.14em;
  color: inherit;
}

/* Exactly one pill is ever on show, and the other must not hold a slot: both
   sit inside `.logo-text`, which is `nowrap` + `overflow:hidden`, so an
   invisible 44px pill silently eats the wordmark's room on a narrow rail.
   The pre-paint branch in index.html always sets one of these two classes, so
   one pill is display:none from the very first paint. */
body.signed-in #sidebar .logo .logo-text span.logo-free { display: none; }
body.tier-free #sidebar .logo .logo-text .logo-pro { display: none; }
body.tier-free #sidebar .logo .logo-text span.logo-free {
  opacity: 1;
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  #sidebar .logo .logo-text span.logo-free {
    transition: opacity 0.2s ease;
    transform: none;
  }
}


:root {
  /* Flat 2D theme — white surface everywhere, no drop shadows on
     page-level elements. Floating elements (modals, popovers, FAB)
     keep their own inline shadows defined on each rule. */
  --bg: #ffffff;
  --surface: #ffffff;
  --bg-soft: #f1f5f9;   /* light grey fill: session bubbles, account card, hover states */
  --primary: #2563eb;
  --primary-dark: #1d4ed8;
  --primary-soft: #dbeafe;   /* light-blue tint: selection fill + input-focus glow */
  --secondary: #64748b;
  --success: #16a34a;
  --warning: #d97706;
  --danger: #dc2626;
  --border: #cbd5e1;
  /* Softened hairline for edges that genuinely must stay a line (data-bearing
   * containers, table frames). --border at #cbd5e1 is strong enough to pull
   * the eye to the frame instead of the content; this sits just above the
   * threshold of visible. Prefer fill contrast or spacing over either. */
  --hairline: rgba(15, 23, 42, .07);
  --text: #1e293b;
  --text-muted: #64748b;
  --radius: 10px;   /* tile corner: .card / .stat-card / dash-panel affordances */
  /* Sidebar background — user-themeable (Preferences → Appearance). Always a
     DARK tone: the rail's text + SVG icons are light/white, so a light value
     would make them illegible. Set per-device on <html> before paint. */
  --sidebar-bg: #111827;
  --shadow: none;
  --shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);

  /* ── Tinted surface layers ─────────────────────────────────────────
   * The 8 most-used hardcoded background / text colors across JS view
   * inline styles. New code should reference these variables; existing
   * inline styles migrate incrementally toward them.
   * When dark mode ships, redefine these in a
   *   @media (prefers-color-scheme: dark) { :root { ... } }
   * block (or body.dark { ... }) — all sites that already use the
   * variable pick up the change for free.
   *
   *                          light value   → suggested dark value
   * ───────────────────────────────────────────────────────────────── */
  --surface-subtle:  #f8fafc;   /* slate-50   → #1e293b  (107 JS uses) */
  --surface-raised:  #f1f5f9;   /* slate-100  → #334155  ( 81 JS uses) */
  --surface-blue:    #eff6ff;   /* blue-50    → #1e3a5f  ( 44 JS uses) */
  --surface-amber:   #fffbeb;   /* amber-50   → #2d1f00  ( 37 JS uses) */
  --surface-danger:  #fee2e2;   /* red-100    → #3b0f0f  ( 24 JS uses) */
  --surface-success: #f0fdf4;   /* green-50   → #052e16  ( 11 JS uses) */
  --surface-sub:     #fafbff;   /* blue tint  → #1a2234  ( 12 JS uses) */
  --text-faint:      #94a3b8;   /* slate-400  → #64748b  (many uses)   */
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
  /* Lock the page to the visual viewport so iOS Safari's address bar
   * doesn't cause the whole HTML to scroll/inset. The inner panes
   * (#content, sidebar nav, etc.) own their own scrolling. */
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-overflow-scrolling: touch;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; }

/* Layout */
/* Use dynamic viewport units so the app fits exactly the visible area
 * on mobile browsers where the toolbar grows/shrinks. Falls back to
 * 100vh on browsers that don't support dvh. */
#app { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }
#sidebar { width: var(--sidebar-width, 220px); background: var(--sidebar-bg, #111827); color: rgba(255,255,255,0.82); display: flex; flex-direction: column; flex-shrink: 0; transition: width 0.2s ease; overflow: hidden; position: relative; height: 100vh; height: 100dvh; }
/* Themed sidebar chrome — dividers, footer-button hovers, footer icon colours,
   and the account popup (bg / borders / hovers / section labels) all use
   translucent-white or theme-derived tints so they harmonize on ANY sidebar
   colour. FormLab Navy keeps its exact original values via the data-sb-navy
   flag, so its look is unchanged. */
#sidebar {
  --sidebar-divider: rgba(255,255,255,0.10);
  --sb-icon: rgba(255,255,255,0.62);
  --sb-hover: rgba(255,255,255,0.10);
  --sb-pop-bg: color-mix(in srgb, var(--sidebar-bg, #111827) 86%, #fff 14%);
  --sb-pop-border: rgba(255,255,255,0.16);
  --sb-pop-hover: rgba(255,255,255,0.10);
  --sb-pop-item-hover: rgba(255,255,255,0.14);
  --sb-pop-item-border: rgba(255,255,255,0.22);
  --sb-pop-item-border-hover: rgba(255,255,255,0.40);
  --sb-pop-section: rgba(255,255,255,0.50);
  --sb-muted: rgba(255,255,255,0.52);
  --sb-chip-bg: rgba(255,255,255,0.12);
  --sb-presence: rgba(255,255,255,0.82);
}
html[data-sb-navy] #sidebar {
  --sidebar-divider: #1f2937;
  --sb-icon: #94a3b8;
  --sb-hover: #1f2937;
  --sb-pop-bg: #1f2937;
  --sb-pop-border: #374151;
  --sb-pop-hover: #2d3748;
  --sb-pop-item-hover: #374151;
  --sb-pop-item-border: #4b5563;
  --sb-pop-item-border-hover: #6b7280;
  --sb-pop-section: #64748b;
  --sb-muted: #64748b;
  --sb-chip-bg: rgba(148,163,184,0.12);
  --sb-presence: #16a34a;
}
/* Light sidebar colour (e.g. Gold) — flip the rail to DARK text + icons/tints so
   it stays legible. data-sb-light is set from the colour's perceived luminance. */
html[data-sb-light] #sidebar {
  color: rgba(0,0,0,0.82);
  --sidebar-divider: rgba(0,0,0,0.18);
  --sb-icon: rgba(0,0,0,0.6);
  --sb-hover: rgba(0,0,0,0.08);
  --sb-pop-bg: color-mix(in srgb, var(--sidebar-bg, #111827) 90%, #000 10%);
  --sb-pop-border: rgba(0,0,0,0.20);
  --sb-pop-hover: rgba(0,0,0,0.08);
  --sb-pop-item-hover: rgba(0,0,0,0.10);
  --sb-pop-item-border: rgba(0,0,0,0.20);
  --sb-pop-item-border-hover: rgba(0,0,0,0.40);
  --sb-pop-section: rgba(0,0,0,0.55);
  --sb-muted: rgba(0,0,0,0.52);
  --sb-chip-bg: rgba(0,0,0,0.10);
  --sb-presence: rgba(0,0,0,0.80);
}
html[data-sb-light] #sidebar { --logo-free-fg: rgba(0,0,0,0.55); --logo-free-bd: rgba(0,0,0,0.26); }
html[data-sb-light] #sidebar .logo { color: rgba(0,0,0,0.9); }
html[data-sb-light] #sidebar .logo .logo-version { color: rgba(0,0,0,0.5); }
html[data-sb-light] .nav-section { color: rgba(0,0,0,0.55); }
html[data-sb-light] .nav-section:hover { color: rgba(0,0,0,0.85); }
html[data-sb-light] .nav-item:hover { background: rgba(0,0,0,0.08); color: #000; }
html[data-sb-light] .nav-item.active { background: color-mix(in srgb, var(--sidebar-bg, #111827) 80%, #000 20%); color: #000; }
html[data-sb-light] .nav-item:hover .nav-count,
html[data-sb-light] .nav-item.active .nav-count { color: rgba(0,0,0,0.75); background: rgba(0,0,0,0.12); }
/* Resting state, not just hover: these only had :hover/.active rules, so on a
 * light rail the collapse button sat at rgba(255,255,255,.55) — white on white,
 * invisible until hovered. Affects the Gold theme as well as glass. */
html[data-sb-light] #sidebar .sidebar-footer .sidebar-toggle,
html[data-sb-light] #sidebar .sidebar-footer .sidebar-scan { color: rgba(0,0,0,0.6); }
html[data-sb-light] #sidebar .sidebar-footer .sidebar-toggle:hover,
html[data-sb-light] #sidebar .sidebar-footer .sidebar-ai:hover,
html[data-sb-light] #sidebar .sidebar-footer .sidebar-scan:hover,
html[data-sb-light] #sidebar .sidebar-footer .sidebar-ai.active { color: #000; }
/* Flip the white-filtered nav + popup icons to black (overrides the inline filter) */
html[data-sb-light] #sidebar .nav-item .icon img,
html[data-sb-light] .sidebar-account-popup-action .spa-ico img { filter: brightness(0) !important; }
html[data-sb-light] .sidebar-account-popup,
html[data-sb-light] .sidebar-account-popup-action,
html[data-sb-light] .sidebar-account-popup-item { color: rgba(0,0,0,0.9); }
#sidebar.collapsed { width: 48px; }
#sidebar.resizing { transition: none; }
#sidebar-resizer {
  position: absolute; right: 0; top: 0; height: 100%; width: 5px;
  cursor: col-resize; z-index: 20; background: transparent;
}
#sidebar-resizer:hover, #sidebar-resizer.dragging { background: rgba(255,255,255,0.15); }
#sidebar .logo { padding: 10px 14px; min-height: 56px;   /* == #topbar height, so the two dividers line up across the shell; padding trimmed from 12px so the logo+version content can't push past it */ font-size: 17px; font-weight: 700; color: #fff; border-bottom: 1px solid var(--sidebar-divider, #1f2937); display: flex; align-items: center; gap: 10px; flex-shrink: 0; cursor: pointer; transition: background 0.15s; }
#sidebar .logo:hover { background: rgba(255,255,255,0.06); }
#sidebar .logo .logo-mark { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
#sidebar .logo .logo-mark svg { display: block; }
#sidebar .logo .logo-text { white-space: nowrap; overflow: hidden; letter-spacing: -0.01em; }
#sidebar .logo .logo-text > span:not([class]) { color: #2563eb; font-weight: 800; }
/* "Form" steps back so "Lab" and the badge read as the pair. Applied to the
   container, since "Form" is a bare text node with no element of its own. */
#sidebar .logo .logo-text { font-weight: 500; }
#sidebar .logo .logo-textwrap { display: flex; flex-direction: column; justify-content: center; min-width: 0; overflow: hidden; }
#sidebar .logo .logo-version { font-size: 10px; font-weight: 500; color: rgba(255,255,255,0.5); letter-spacing: 0; line-height: 1; margin-top: 2px; }
#sidebar.collapsed .logo .logo-text { display: none; }
#sidebar.collapsed .logo .logo-version { display: none; }
/* Hide the whole text wrapper (not just its children) so the empty flex
   child + the .logo gap don't push the mark off-center in the 48px rail. */
#sidebar.collapsed .logo .logo-textwrap { display: none; }
/* Collapsed + Pro: the PRO pill survives the wordmark and sits UNDER the mark,
   overlapping it slightly — an image with its title, not a corner sticker. The
   whole point of the badge is that it is visible at a glance, and the collapsed
   rail was the one place it disappeared entirely.
   .logo-pro is normally an inline run inside .logo-text (display:none here), so
   it has to be pulled out of that flow and re-anchored to the logo itself. */
#sidebar.collapsed .logo { position: relative; padding-bottom: 18px; }
/* .logo-textwrap is display:none when collapsed, and a child of a display:none
   parent cannot render no matter how it is positioned — the pill measured 0x0
   until this. The wrap comes back as an ABSOLUTE box covering .logo, so it
   contributes nothing to the flex row (a zero-size static box still perturbs
   the centring) while giving the pill something to anchor to. */
body.signed-in #sidebar.collapsed .logo .logo-textwrap {
  display: block; position: absolute; inset: 0; pointer-events: none;
}
body.signed-in #sidebar.collapsed .logo .logo-text { display: block; font-size: 0; }
body.signed-in #sidebar.collapsed .logo .logo-version { display: none; }
body.signed-in #sidebar.collapsed .logo .logo-pro {
  display: block; position: absolute;
  left: 50%; transform: translateX(-50%);
  bottom: 2px;                     /* rides just below the 26px mark */
  /* margin:0 is load-bearing. The wordmark badge carries margin-left: .42em to
     space it off "Lab" — inside a wordmark that is correct, but here it pushed
     the pill 3.36px right of the mark it is captioning (0.42 x 8px), which
     measured as an obvious miscentring. margin-bottom: -1px goes with it. */
  margin: 0;
  font-size: 8px; font-weight: 800; letter-spacing: .08em;
  line-height: 1; padding: 2px 5px;
  border-radius: 999px;
  background: var(--fl-pro-fill, #2563eb); color: #fff;
  white-space: nowrap;
}
/* ?hideversion screenshot helper — hide the version label for clean captures */
html.fl-hide-version #app-version { display: none !important; }

/* Free-only "Upgrade to Pro" CTA in the account popup — hidden once signed in as
   Pro (body.signed-in is set from the fv_tier=pro cookie in the head bootstrap). */
body.signed-in #account-upgrade-btn { display: none !important; }
/* "Switch account" is Pro-only — hidden on Free, shown once signed in. */
#account-switch-btn { display: none; }
body.signed-in #account-switch-btn { display: flex !important; }
body.signed-in #account-shared-note { display: block !important; }
#sidebar nav { padding: 8px; flex: 1; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; scrollbar-color: #374151 transparent; }
#sidebar nav::-webkit-scrollbar { width: 6px; }
#sidebar nav::-webkit-scrollbar-track { background: transparent; }
#sidebar nav::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
#sidebar nav::-webkit-scrollbar-thumb:hover { background: #4b5563; }
.nav-section { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255,255,255,0.5); padding: 8px 8px 4px; white-space: nowrap; overflow: hidden; display: flex; align-items: center; justify-content: space-between; gap: 6px; cursor: pointer; user-select: none; border-radius: 5px; transition: color 0.15s; }
.nav-section:hover { color: rgba(255,255,255,0.8); }
.nav-section::after { content: '\25BE'; font-size: 9px; opacity: 0.55; transition: transform 0.15s; }
.nav-section.collapsed::after { transform: rotate(-90deg); }
.nav-item.nav-hidden { display: none !important; }
/* Collapsed: the section header BECOMES the divider. It already carried a
   border-top, but `opacity: 0` faded the border out along with the label, so
   the rule was there and invisible — collapsed mode ran as one undivided
   column of icons. Hiding the text by size/colour instead keeps the 1px line,
   which is what expanded mode's grouping looks like once the words are gone. */
#sidebar.collapsed .nav-section {
  opacity: 1; color: transparent; font-size: 0;
  height: 0; padding: 0; margin: 8px 12px; overflow: hidden;
  border-top: 1px solid var(--sidebar-divider, #1f2937);
}
#sidebar.collapsed .nav-section::after { display: none; }
#sidebar.collapsed .nav-section:first-of-type { display: none; }
#sidebar.collapsed .logo { padding: 12px 0; justify-content: center; }
#sidebar.collapsed nav { padding: 12px 0; }
#sidebar.collapsed .nav-item { padding: 8px 14px; gap: 0; font-size: 0; }
#sidebar.collapsed .nav-item .icon { width: 20px; margin: 0; font-size: 16px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 10px; cursor: pointer; font-size: 13.5px; margin-bottom: 2px; transition: background 0.15s; white-space: nowrap; }
.nav-item:hover { background: rgba(255,255,255,0.07); color: #f1f5f9; }
/* Selected nav item: a lighter shade of the current sidebar background (so it
 * tracks the chosen theme) rather than a fixed blue. Fallback for browsers
 * without color-mix is a translucent-white overlay — also a lighter shade. */
.nav-item.active { background: rgba(255,255,255,0.14); background: color-mix(in srgb, var(--sidebar-bg, #111827) 80%, #fff 20%); color: #fff; font-weight: 600; }
/* Exception: the FormLab Navy (brand default) theme keeps the signature blue. */
html[data-sb-navy] .nav-item.active { background: #2563eb; }
/* Sidebar-color preset swatches (Preferences → Appearance) */
.sb-swatch { width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); cursor: pointer; padding: 0; transition: transform .1s ease, box-shadow .1s ease; }
.sb-swatch:hover { transform: scale(1.08); }
.sb-swatch.active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary); }
/* Divider separating the brand-default swatch from the rest of the palette */
.sb-divider { width: 1px; align-self: stretch; min-height: 22px; background: var(--border); margin: 0 3px; flex-shrink: 0; }
/* Entity-count chip on the right side of nav items. At-a-glance "how
   much do I have of each thing" without opening every page. Updated
   on each navigate() so the count stays fresh. Hidden on collapsed
   sidebar (no room) and when the count is 0 (would just be noise). */
.nav-item .nav-count {
  margin-left: auto;
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--sb-muted);
  background: var(--sb-chip-bg);
  border-radius: 9999px;
  padding: 1px 7px;
  min-width: 18px;
  text-align: center;
  flex-shrink: 0;
  transition: color 0.15s, background 0.15s;
}
.nav-item:hover .nav-count { color: #cbd5e1; background: rgba(148, 163, 184, 0.2); }
.nav-item.active .nav-count { color: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.18); }
#sidebar.collapsed .nav-count { display: none; }
/* Low-stock alert badge on the Inventory nav item — a count that means "act", so
   it reads red rather than as a neutral tally. */
.nav-item .nav-count.nav-count-alert { color: #fecaca; background: rgba(220, 38, 38, 0.28); }
.nav-item:hover .nav-count.nav-count-alert,
.nav-item.active .nav-count.nav-count-alert { color: #fff; background: rgba(220, 38, 38, 0.5); }
.nav-item .icon { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
.nav-item .icon img { width: 18px; height: 18px; object-fit: contain; }
.btn .print-icon { width: 14px; height: 14px; vertical-align: -2px; object-fit: contain; display: inline-block; padding: 0; }
.btn .dl-icon { width: 14px; height: 14px; vertical-align: -2px; object-fit: contain; display: inline-block; padding: 0; }
.btn .search-icon { width: 13px; height: 13px; vertical-align: -2px; object-fit: contain; display: inline-block; padding: 0; }
/* Base size so .search-icon is constrained even outside a .btn — e.g. the
   per-row "Find similar" button and the "Similar to:" chip in the DOE Matrix
   formula filter, which are plain buttons/spans (not .btn). Without this the
   SVG renders at its full intrinsic size — a giant magnifying glass. */
.search-icon { width: 13px; height: 13px; vertical-align: -2px; object-fit: contain; display: inline-block; }

/* Compare-table resize handles. The column handle (.col-rh) and the header-row
   height handle (.row-rh) are appended into <th> cells (which are position:
   relative). Without these rules the handles have zero size and can't be
   grabbed — the resize JS exists but is invisible. */
.col-rh { position: absolute; top: 0; right: -3px; width: 7px; height: 100%; cursor: col-resize; z-index: 3; user-select: none; }
.row-rh { position: absolute; left: 0; bottom: -3px; width: 100%; height: 7px; cursor: row-resize; z-index: 2; user-select: none; }
.col-rh:hover, .row-rh:hover { background: var(--primary); opacity: 0.3; }
.col-drag-over { box-shadow: inset 3px 0 0 var(--primary); }

/* Compare formula-picker chips. Uniform height with ellipsis-truncated names
   (long formula names no longer balloon into multi-line cards), clear
   selected/hover states, whole chip is the click target. */
.cmp-chip { display: inline-flex; align-items: center; gap: 7px; max-width: 240px; padding: 5px 12px; border: 1px solid var(--border); border-radius: 18px; background: #fff; cursor: pointer; font-size: 12px; line-height: 1.3; user-select: none; transition: background .12s, border-color .12s, box-shadow .12s; }
.cmp-chip:hover { border-color: var(--primary); background: #f5f8ff; }
.cmp-chip.on { border-color: var(--primary); background: #eff6ff; box-shadow: inset 0 0 0 1px var(--primary); }
.cmp-chip input[type="checkbox"] { accent-color: var(--primary); width: 14px; height: 14px; margin: 0; flex-shrink: 0; cursor: pointer; }
.cmp-chip-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cmp-chip.on .cmp-chip-name { color: var(--primary); font-weight: 600; }
.cmp-chip-v { color: #94a3b8; font-size: 11px; flex-shrink: 0; }
/* Sort icon — pair of up/down arrows on the toolbar Sort button.
   Slightly taller than .dl-icon because the glyph is vertical. */
.btn .sort-icon { width: 12px; height: 14px; vertical-align: -2px; object-fit: contain; display: inline-block; padding: 0; }
/* Formula reference inside a list cell — reads as text by default,
   only adopts link styling on hover. Reduces "every cell looks
   clickable" visual noise in dense tables while keeping one-click
   drill-in for users who deliberately want it. */
.formula-cell-link {
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  border-bottom: 1px dotted transparent;
  transition: color 0.12s, border-color 0.12s;
}
.formula-cell-link:hover {
  color: var(--primary);
  border-bottom-color: var(--primary);
}

/* Hover-popover preview cards — single global element re-used for
   every formula-cell-link / sample-cell-link. Same visual treatment
   for both; positioned via JS, fades in/out.
   Selectors are comma-grouped so all rules apply to either popover
   class without duplication. */
.formula-popover, .sample-popover {
  position: fixed;
  z-index: 200;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: 12px 14px;
  width: 280px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}
.formula-popover.on, .sample-popover.on { opacity: 1; pointer-events: auto; }
.formula-popover-title, .sample-popover-title {
  font-weight: 600;
  font-size: 13.5px;
  margin-bottom: 4px;
  color: #0f172a;
  word-break: break-word;
}
.formula-popover-meta, .sample-popover-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 6px 0;
}
.formula-popover-stats, .sample-popover-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 12px;
  font-size: 11.5px;
  color: #64748b;
  margin: 8px 0 4px;
  padding-top: 8px;
  border-top: 1px solid #f1f5f9;
}
.formula-popover-stats strong, .sample-popover-stats strong { color: var(--text); font-weight: 600; }
.formula-popover-footer, .sample-popover-footer {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid #f1f5f9;
  display: flex;
  justify-content: flex-end;
  font-size: 12px;
}
.formula-popover-footer a, .sample-popover-footer a {
  color: var(--primary);
  text-decoration: none;
  cursor: pointer;
}
.formula-popover-footer a:hover, .sample-popover-footer a:hover { text-decoration: underline; }

/* Click-to-convert amount value in the composition table. Subtle
   dotted underline signals it's interactive without adding visible
   clutter at rest. */
.amt-convert {
  cursor: pointer;
  border-bottom: 1px dotted #94a3b8;
  transition: color 0.12s, border-color 0.12s;
}
.amt-convert:hover { color: var(--primary); border-bottom-color: var(--primary); }
.amt-conv-popover {
  position: fixed;
  z-index: 200;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: 10px 12px;
  min-width: 150px;
  max-width: 240px;
  font-size: 12.5px;
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s;
}
.amt-conv-popover.on { opacity: 1; pointer-events: auto; }
.amt-conv-title {
  font-weight: 600;
  font-size: 13px;
  color: #0f172a;
  padding-bottom: 6px;
  margin-bottom: 6px;
  border-bottom: 1px solid #f1f5f9;
}
.amt-conv-grid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 3px 12px;
  align-items: baseline;
}
.amt-conv-row { display: contents; }
.amt-conv-val { font-variant-numeric: tabular-nums; font-weight: 500; text-align: right; }
.amt-conv-unit { color: var(--text-muted); }
.amt-conv-cross-label {
  margin: 8px 0 4px;
  padding-top: 6px;
  border-top: 1px dashed #e2e8f0;
  font-size: 11px;
  color: var(--text-muted);
}
.amt-conv-note {
  margin-top: 6px;
  font-size: 11px;
  color: var(--text-muted);
  line-height: 1.4;
  font-style: italic;
}

/* Inline notes editor in the composition table — looks like plain
   text at rest, reveals an editable affordance on hover/focus.
   Saves on blur / Enter via setCompositionNote. */
.comp-note-input {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid transparent;
  background: transparent;
  font-size: 12px;
  color: var(--text-muted);
  padding: 3px 6px;
  border-radius: 4px;
  font-family: inherit;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.comp-note-input::placeholder { color: #cbd5e1; }
.comp-note-input:hover { background: #f8fafc; border-color: var(--border); }
.comp-note-input:focus {
  background: #fff;
  border-color: var(--primary);
  color: var(--text);
  outline: none;
}
/* Image thumbnail injected at the top of a hover popover when the
   entity has a photo attachment (_popoverInjectImage). Full-width
   within the card, capped height, cover-fit so portrait + landscape
   both look tidy. Negative margins pull it flush to the card edges
   above the text content (which keeps its 12-14px padding). */
.popover-thumb {
  margin: -12px -14px 8px;
  border-bottom: 1px solid #f1f5f9;
  background: #f1f5f9;          /* letterbox fill behind portrait images */
  border-radius: 8px 8px 0 0;
  overflow: hidden;
  /* Center the (already-downscaled) thumbnail so portrait images
     don't hug a corner. The JS canvas step caps it at 256×150, so
     the image arrives pre-sized — no CSS scaling needed. */
  display: flex;
  align-items: center;
  justify-content: center;
  max-height: 150px;
}
.popover-thumb img {
  max-width: 100%;
  max-height: 150px;
  display: block;
}

/* Inline replacement for an emoji glyph in body text — same visual
   weight as a 13-14px emoji. Used wherever we swap ⚗ / ▦ / ∿ etc.
   for a proper PNG (card-view counts, section headers, workspace
   buttons). The smaller .emoji-icon-sm version matches 11.5-12px
   text contexts (card-view badge rows). */
.emoji-icon    { width: 14px; height: 14px; vertical-align: -3px; object-fit: contain; display: inline-block; }
.emoji-icon-sm { width: 12px; height: 12px; vertical-align: -2px; object-fit: contain; display: inline-block; }
/* The h2 section headers (16px) deserve a slightly larger glyph */
.emoji-icon-lg { width: 18px; height: 18px; vertical-align: -4px; object-fit: contain; display: inline-block; margin-right: 4px; }
/* Column-header sort indicators — three states (none/up/down). Sized
   to match the height of the header text so they sit on the baseline. */
.th-sort-icon { width: 12px; height: 14px; vertical-align: -3px; object-fit: contain; display: inline-block; margin-left: 4px; flex-shrink: 0; }
/* .btn-print used to override to dark-navy bg with white icons; that
   made the PDF export button stick out from its sibling CSV/Sort
   buttons. Now styled identically to .btn-secondary (white bg, black
   outline, black icons/text) — the .btn-print class is kept as an
   identifier (used by print stylesheets at body.printing-* to hide
   chrome) but is visually a no-op. */
/* Dark primary buttons (e.g. data.js "Export JSON") use the same dark-on-  */
/* transparent export.svg as the light secondary buttons; invert it to     */
/* white so it reads on the primary background (replaces exportwhite.png).  */
.btn-primary .dl-icon { filter: brightness(0) invert(1); }
/* Icon-only square CTA — the top-bar "create" buttons (Add Ingredient, New
   Formula, …) render as a compact blue square with just the "+" glyph, no
   label. Fixed 32×32 to sit at the same height as the sibling text buttons;
   the add.svg artwork is black, so flip it white on the blue primary fill. */
.btn-icon-only { padding: 0; width: 32px; height: 32px; justify-content: center; gap: 0; }
.btn-icon-only .svg-icon { width: 17px; height: 17px; vertical-align: 0; }
.btn-primary.btn-icon-only .svg-icon { filter: brightness(0) invert(1); }
/* Inline SVG icons — uses stroke:currentColor so they adapt to text */
/* color (dark-mode aware for free). 14px matches the PNG icon size. */
.svg-icon { width: 14px; height: 14px; vertical-align: -3px; display: inline-block; flex-shrink: 0; }
.btn .svg-icon { width: 14px; height: 14px; vertical-align: -3px; }
/* Sidebar nav uses a larger icon to match the 18px PNG nav glyphs. */
.nav-item .icon .svg-icon { width: 18px; height: 18px; vertical-align: middle; }
/* Card kebab menu items have a touch more breathing room. */
.ing-card-kebab-menu .svg-icon { margin-right: 4px; }
#sidebar .sidebar-footer { padding: 8px; border-top: 1px solid var(--sidebar-divider, #1f2937); flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 6px; }
#sidebar.collapsed .sidebar-footer { justify-content: center; padding: 8px 0; flex-direction: column; gap: 6px; }
/* Collapsed footer = account + toggle only. AI and Scan move into the account
   popup (markup in index.html), so the rail holds two targets instead of four
   and each one has room to breathe. */
#sidebar.collapsed .sidebar-footer .sidebar-scan,
#sidebar.collapsed .sidebar-footer .sidebar-ai { display: none; }
/* ...and the popup entries are the mirror image: only present while collapsed,
   since expanded mode already has them in the footer. */
.spa-collapsed-only { display: none !important; }
#sidebar.collapsed .sidebar-account-popup .spa-collapsed-only { display: flex !important; }
#sidebar.collapsed .sidebar-account-popup .spa-divider.spa-collapsed-only { display: block !important; }
/* Sync, in the account popup header, ONLY while it is actually working.
   Collapsed to zero the rest of the time — an idle "Synced" row is a permanent
   label for a non-event, and it was one of the two things crowding the 48px
   rail. `display:none` rather than opacity so it takes no space and is not
   announced; aria-live on the element means the state change is spoken when it
   does appear. Inherits colour, so it reads correctly on the dark tint AND on
   glass without a second rule. */
.sidebar-account-popup-header .spa-sync { display: none; margin-left: auto; }
.sidebar-account-popup-header .spa-sync.syncing {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .02em;
  opacity: .75; color: inherit;
}
.sidebar-account-popup-header .spa-sync svg { width: 11px; height: 11px; }
.sidebar-account-popup-header { display: flex; align-items: center; gap: 7px; }

.sidebar-account-popup .spa-divider {
  height: 1px; margin: 6px 8px;
  background: var(--border, #e2e8f0);
}

/* ── Collapsed-rail label bubble ────────────────────────────────────────────
 * One shared bubble (see _flRailShow in js/db.js), not a tooltip per icon.
 * position:fixed and parented to <body> on purpose: the sidebar is a stacking
 * context with its own overflow, so a child would be clipped by the rail it is
 * trying to point out of.
 * Gradient rather than a flat chip because it reads as part of the app's own
 * chrome instead of an OS tooltip — and a gradient keeps its edge against both
 * a light workspace background and a dark one. */
#fl-rail-tip {
  position: fixed; z-index: 1400;
  pointer-events: none;                      /* never eat a click meant for the rail */
  padding: 6px 11px; border-radius: 9px;
  font-size: 12.5px; font-weight: 600; line-height: 1.2; white-space: nowrap;
  color: #fff;
  background: linear-gradient(135deg, #2563eb 0%, #4f46e5 55%, #6d28d9 100%);
  box-shadow: 0 6px 18px -6px rgba(15, 23, 42, .45), 0 1px 2px rgba(15, 23, 42, .2);
  opacity: 0; transform: translateX(-4px);
  transition: opacity .12s ease, transform .12s ease;
}
#fl-rail-tip.on { opacity: 1; transform: translateX(0); }
/* Translucent material: the bubble becomes glass like every other floating
   surface — same rule as the topbar frames, tinted rather than white so a
   label still reads as a label. Blur direct on the element is fine here: it
   contains nothing, opens nothing, and is pointer-events:none, so it cannot
   trap a menu the way the header frames could. */
/* Translucent: real glass — it TINTS WHAT IS BEHIND IT. The first cut kept the
   blue-purple gradient and merely made it 82% opaque, which is not glass: a
   translucent brand colour still imposes its own hue on every background it
   crosses. Glass means the background comes through and the surface only
   lightens and blurs it, so this drops to the same neutral white/blur the
   account popup and topbar frames use, with dark text. The gradient stays as
   the OPAQUE look, where a background is not showing through anyway. */
html[data-sb-float][data-sb-translucent] #fl-rail-tip {
  background: rgba(255, 255, 255, .72);
  color: var(--text, #0f172a);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  backdrop-filter: saturate(180%) blur(18px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6), 0 8px 22px -8px rgba(15, 23, 42, .35);
}
/* No backdrop-filter → nothing to tint, so fall back to an opaque surface
   rather than a see-through white that would sit unreadably on the background. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] #fl-rail-tip {
    background: #f4f6f9; color: var(--text, #0f172a);
  }
}
@media (prefers-reduced-motion: reduce) {
  #fl-rail-tip { transition: none; transform: none; }
  #fl-rail-tip.on { transform: none; }
}
#sidebar .sidebar-footer .sidebar-toggle { background: none; border: none; color: rgba(255,255,255,0.55); cursor: pointer; font-size: 16px; width: 36px; height: 32px; display: flex; align-items: center; justify-content: center; line-height: 1; border-radius: 6px; transition: color 0.15s, background 0.15s; flex-shrink: 0; }
#sidebar .sidebar-footer .sidebar-toggle:hover { color: #fff; background: var(--sb-hover); }
#sidebar .sidebar-footer .sidebar-account {
  background: none; border: none; color: #cbd5e1; cursor: pointer;
  display: flex; align-items: center; gap: 0; padding: 2px;
  border-radius: 6px; font-family: inherit; font-size: 13px;
  min-width: 0; flex: 0 0 auto; transition: background 0.15s, color 0.15s;
}
#sidebar .sidebar-footer .sidebar-account:hover { background: transparent; }
.sidebar-account-avatar {
  width: 28px; height: 28px; flex-shrink: 0;
  /* Free tier: match the other footer icons (themed --sb-icon tint) */
  color: var(--sb-icon);
  transition: color 0.15s ease, opacity 0.15s ease;
}
.sidebar-account:hover .sidebar-account-avatar { opacity: 0.8; }
/* Account label retired — the avatar is the whole control now. Keep the
   selector defined-but-hidden so any historical markup still parses. */
.sidebar-account-label { display: none; }

/* Signed-in (Pro / Supabase session) — recolor the profile icon to
   --primary (blue) so the sidebar at a glance reads "this device has
   an account behind it." Set on <body> by the auth bootstrap module
   in index.html after Supabase confirms a session. */
body.signed-in .sidebar-account-avatar {
  color: var(--primary);
}
body.signed-in .sidebar-account-popup-dot { background: var(--primary); }
/* A signed-in but never-paid Free account (chose to stay signed in on the Free
   app via prowall's never-paid route). Recognized avatar/dot like Pro, but NO
   "Pro" pill and the "Upgrade to Pro" CTA stays visible (we never add
   body.signed-in for these accounts, so the hide rule above doesn't apply). */
body.free-account .sidebar-account-avatar { color: var(--primary); }
body.free-account .sidebar-account-popup-dot { background: var(--primary); }

/* ============================================================
 * DOE Matrix toolbar — grouped, workflow-ordered controls
 * ============================================================ */
/* Top-of-page tab bar for the DOE Matrix view — switches between
 * the Analyze (matrix table) and Generate (design generator) modes.
 * Two big peer buttons with icon, label, and a one-line subtitle. */
.asm-matrix-tabs {
  display: flex; gap: 6px;
  position: relative;             /* containing block for the sliding highlight */
  /* Self-contained segmented control — no accent strip below anymore, so it's
   * fully rounded on all four corners and carries its own bottom margin. The
   * 14px backdrop radius = 10px button radius + 4px padding, so the corners sit
   * concentric with the buttons inside. */
  margin-bottom: 12px;
  padding: 4px;
  background: #f1f5f9;
  border: 1px solid var(--border);
  border-radius: 14px;
}
/* The travelling selected-tab highlight — SHARED by the DOE Matrix
 * Analyze/Generate bar and the Analyze workspace view tabs. Sized + positioned
 * entirely by _asmGlideSync (inline width/transform); the stylesheet owns only
 * the look and the timing. top/height match each bar's 4px padding so it sits
 * exactly on the button. Sits BEHIND the buttons (z-index 0 vs 1) so their text
 * stays readable as it passes underneath. */
.asm-glider {
  position: absolute; left: 0; top: 4px; height: calc(100% - 8px); width: 0;
  border-radius: 10px;
  background: var(--primary);
  z-index: 0;
  pointer-events: none;
  opacity: 0;                     /* revealed only once JS has measured + placed it */
  transition: transform .26s cubic-bezier(.4, 0, .2, 1),
              width     .26s cubic-bezier(.4, 0, .2, 1),
              background-color .26s cubic-bezier(.4, 0, .2, 1),
              box-shadow .26s cubic-bezier(.4, 0, .2, 1);
}
.has-glider > .asm-glider { opacity: 1; }
/* Per-tab colours. The DOE Matrix bar is blue/emerald per mode; the view tabs
 * carry no colour class, so they fall through to the blue default above. */
.asm-glider.analyze  { background: var(--primary); box-shadow: 0 1px 3px rgba(37, 99, 235, 0.28); }
.asm-glider.generate { background: #059669;        box-shadow: 0 1px 3px rgba(5, 150, 105, 0.30); }
/* With the glider live, the active button hands its solid fill over to it —
 * otherwise an opaque background would sit on top and hide the travelling one.
 * Scoped to .has-glider so the no-JS fallback keeps the plain solid look. */
.has-glider [data-tab].on {
  background: transparent; border-color: transparent; box-shadow: none;
}
.asm-matrix-tab {
  flex: 1;
  position: relative; z-index: 1; /* above the glider */
  display: flex; flex-direction: row; align-items: center; gap: 10px;
  padding: 10px 16px;
  background: transparent; color: #475569;
  border: 1px solid transparent;
  border-radius: 10px;
  font-family: inherit; font-size: 13.5px; font-weight: 600;
  cursor: pointer;
  text-align: left;
  /* Colors ease over the same .26s the glider travels, so the text/icon flip
   * rides along with the highlight instead of snapping ahead of it.
   * background is deliberately NOT transitioned: the fallback fill handing over
   * to the glider must be instant, or it fades out over the destination and
   * leaves a ghost trailing behind the travelling highlight. */
  transition: color 0.26s cubic-bezier(.4, 0, .2, 1), box-shadow 0.12s;
}
/* No hover state by design. The sliding highlight IS the feedback here, and a
 * hover fill both competed with it and painted over it mid-slide. Cursor:
 * pointer (above) still carries the affordance. */
/* Per-tab active colors — blue for Analyze, emerald for Generate.
 * Mirrors the headline-CTA palette so the same visual identity
 * follows each mode whether it's an active button or an active tab. */
.asm-matrix-tab-analyze.on {
  background: var(--primary); color: #fff;
  border-color: var(--primary);
  box-shadow: 0 1px 3px rgba(37, 99, 235, 0.28);
}
.asm-matrix-tab-generate.on {
  background: #059669; color: #fff;
  border-color: #059669;
  box-shadow: 0 1px 3px rgba(5, 150, 105, 0.30);
}
.asm-matrix-tab-icon  { font-size: 16px; line-height: 1; }
/* Icons are dark SVGs, dimmed at rest on the light backdrop. When a tab is
 * selected its backdrop turns solid blue/emerald, so flip the icon to pure
 * white (brightness(0) invert(1)) — the same treatment the sidebar uses. */
/* Upscaled from the global .emoji-icon 14px — these sit on a tall two-line
 * button, where 14px read as an afterthought. 20px balances the 14px label +
 * 11px subtitle stacked beside them. */
.asm-matrix-tab-icon .emoji-icon {
  width: 20px; height: 20px; vertical-align: 0;
  opacity: .5;
  transition: filter .26s cubic-bezier(.4, 0, .2, 1), opacity .26s cubic-bezier(.4, 0, .2, 1);
}
.asm-matrix-tab.on .asm-matrix-tab-icon .emoji-icon { filter: brightness(0) invert(1); opacity: 1; }
/* Respect a reduced-motion preference: the highlight still moves to the right
 * tab, it just gets there instantly. */
@media (prefers-reduced-motion: reduce) {
  .asm-glider,
  .asm-matrix-tab,
  .asm-analyze-tabs .asm-tab,
  .asm-matrix-tab-icon .emoji-icon { transition: none; }
}
.asm-matrix-tab-label { font-size: 14px; font-weight: 700; }
.asm-matrix-tab-sub   { font-size: 11px; font-weight: 400; color: #94a3b8; }
.asm-matrix-tab.on .asm-matrix-tab-sub { color: rgba(255, 255, 255, 0.82); }

/* Dismiss button on the DOE Matrix "HOW TO BUILD" guide banner.       */
/* Made more discoverable than a plain text × with no affordance: rest */
/* state shows a subtle 1px border so it reads as a button, hover      */
/* brightens it to make the click target obvious. Recovery is via the  */
/* "? Help" button that appears in the topbar after dismiss.           */
.asm-guide-dismiss {
  position: absolute; top: 8px; right: 8px;
  width: 26px; height: 26px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid #c7d2fe;
  color: #4338ca;
  border-radius: 6px;
  cursor: pointer;
  font-size: 18px; line-height: 1;
  padding: 0;
  transition: background 0.12s, border-color 0.12s, color 0.12s, transform 0.12s;
}
.asm-guide-dismiss:hover {
  background: #fff;
  border-color: #6366f1;
  color: #312e81;
  transform: translateY(-1px);
}

.asm-toolbar-card { margin-bottom: 12px; padding: 12px 16px; }
.asm-toolbar {
  display: flex; align-items: flex-start; justify-content: space-between;
  flex-wrap: wrap; gap: 12px;
}
.asm-toolbar-left {
  display: flex; flex-wrap: wrap;
  align-items: stretch;
  gap: 0; row-gap: 10px;
  flex: 1; min-width: 0;
}
.asm-toolbar-right { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
/* Each workflow step row: [step] [LABEL] [controls]
 * Step + label have fixed widths so the controls start at the same X
 * position across every row when the toolbar wraps vertically. */
.asm-toolbar-group {
  display: flex; align-items: center; gap: 8px;
  padding: 0 14px;
  min-height: 34px;
}
.asm-toolbar-group:first-child { padding-left: 0; }
.asm-toolbar-divider {
  width: 1px; background: var(--border);
  margin: 4px 0; align-self: stretch; flex-shrink: 0;
}
.asm-toolbar-step {
  font-size: 12px; font-weight: 700;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  background: #0f172a; color: #fff;
  border-radius: 50%;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.asm-toolbar-grouplbl {
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: #0f172a;
  flex-shrink: 0;
}
.asm-toolbar-controls {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}
/* Beat the global "input, select, textarea { width: 100% }" rule */
.asm-toolbar select.asm-toolbar-select,
.asm-toolbar-controls select.asm-toolbar-select {
  width: auto; max-width: 280px;
  padding: 5px 8px; border: 1px solid var(--border);
  border-radius: 5px; font-size: 12.5px; background: #fff;
  font-family: inherit; color: var(--text);
  text-transform: none; letter-spacing: normal;
  min-height: 28px;
}
/* When toolbar wraps to stacked vertical rows, align every row's
 * controls at the same X by giving step+label a fixed reserved width. */
/* Structured numbered-row layout: [step] [LABEL] [controls], one step per
 * row, controls aligned at the same X. Held down to small widths (not just
 * desktop) so the toolbar keeps its labelled structure instead of collapsing
 * to a bare stack — only true phones (≤519px) fall back to the compact layout
 * below. The controls column (1fr) wraps internally as it narrows. */
@media (min-width: 520px) {
  .asm-toolbar-left { flex-direction: column; align-items: stretch; }
  .asm-toolbar-group {
    width: 100%; padding: 0;
    display: grid;
    grid-template-columns: 26px 88px 1fr;
    gap: 8px;
    align-items: center;
  }
  .asm-toolbar-divider { display: none; }
}
.asm-toolbar-toggle.on {
  background: #dbeafe !important;
  border-color: #3b82f6 !important;
  color: #1d4ed8 !important;
}
/* "Send to DOE" selection-scope chip — a distinct primary-tinted pill so it's
   clear the matrix is narrowed to a handed-off selection (and how to clear it). */
.asm-scope-chip {
  background: var(--primary-soft); border: 1px solid #93c5fd; color: #1d4ed8; font-weight: 600;
  white-space: nowrap;
}
.asm-scope-chip:hover { background: #dbeafe; border-color: #3b82f6; }

/* The headline Analyze CTA — primary-colored, with subtitle, prominent */
.btn-doe-cta {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 16px;
  background: var(--primary); color: #fff;
  border: none; border-radius: 7px;
  font-family: inherit; font-size: 13.5px; font-weight: 600;
  cursor: pointer;
  box-shadow: 0 1px 3px rgba(37, 99, 235, 0.25), 0 0 0 0 rgba(37, 99, 235, 0);
  transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
  position: relative;
}
.btn-doe-cta:hover {
  background: var(--primary-dark);
  box-shadow: 0 3px 8px rgba(37, 99, 235, 0.35), 0 0 0 4px rgba(37, 99, 235, 0.10);
  transform: translateY(-1px);
}
.btn-doe-cta:active { transform: translateY(0); }
.btn-doe-cta-icon { font-size: 16px; line-height: 1; }
.btn-doe-cta-sub {
  font-size: 10px; font-weight: 500; opacity: 0.85;
  margin-left: 4px; padding-left: 8px;
  border-left: 1px solid rgba(255, 255, 255, 0.3);
}
/* Alt variant — paired with the primary Analyze CTA. Same shape +
 * elevation so it reads as a peer action, but emerald-green to
 * differentiate "generate a new experiment" from "analyze results". */
.btn-doe-cta-alt {
  background: #059669;   /* emerald-600 */
  box-shadow: 0 1px 3px rgba(5, 150, 105, 0.28), 0 0 0 0 rgba(5, 150, 105, 0);
}
.btn-doe-cta-alt:hover {
  background: #047857;   /* emerald-700 */
  box-shadow: 0 3px 8px rgba(5, 150, 105, 0.38), 0 0 0 4px rgba(5, 150, 105, 0.12);
}

/* Zoom pill */
.asm-zoom-pill {
  display: inline-flex; align-items: center; gap: 4px;
  background: #fff; border: 1px solid var(--border);
  border-radius: 6px; padding: 2px 6px;
}
.asm-zoom-btn {
  background: none; border: none; cursor: pointer;
  padding: 2px 6px; font-size: 14px; line-height: 1;
  color: #64748b; font-weight: 600; font-family: inherit;
}
.asm-zoom-btn:hover { color: var(--primary); }
.asm-zoom-range { width: 90px; cursor: pointer; }
.asm-zoom-reset {
  background: none; border: none; cursor: pointer;
  padding: 2px 6px; font-size: 11px; line-height: 1;
  color: #64748b; border-left: 1px solid var(--border);
  font-family: inherit; min-width: 42px;
}
.asm-zoom-reset:hover { color: var(--primary); }

@media (max-width: 1100px) {
  /* On medium screens, hide the CTA subtitle to keep it compact. The step
   * labels (ROWS/COLUMNS/…) stay — they have their own grid column now. */
  .btn-doe-cta-sub { display: none; }
}
/* True-phone fallback: drop the numbered/labelled structure and let the
 * control groups stack full-width with their buttons wrapping. */
@media (max-width: 519px) {
  .asm-toolbar-divider { display: none; }
  .asm-toolbar-group { padding: 0; width: 100%; flex-wrap: wrap; }
  .asm-toolbar-step { display: none; }
  .asm-toolbar-grouplbl { display: none; }
}

/* ============================================================
 * DOE Matrix status bar — bottom strip with counts + zoom + export
 * ============================================================ */
.asm-statusbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding: 8px 14px;
  background: #f8fafc;
  border: 1px solid var(--border);
  border-top: 1px solid #e2e8f0;
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
  font-size: 11.5px; color: #475569;
}
.asm-statusbar-left {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-variant-numeric: tabular-nums;
}
.asm-statusbar-left strong { color: #0f172a; font-weight: 600; }
.asm-statusbar-sep { color: #cbd5e1; }
.asm-statusbar-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* Mini dot-plot strip rendered inside the column header band when the
 * "Distributions" toolbar toggle is on. One dot per row at its value's
 * relative position along the column's min–max range. */
.asm-col-dist {
  margin-top: 6px;
  padding-top: 4px;
  border-top: 1px dashed rgba(0, 0, 0, 0.08);
  user-select: none;
}
.asm-col-dist-empty {
  height: 24px;
}
.asm-col-dist-track {
  position: relative;
  height: 12px;
  background: rgba(0, 0, 0, 0.03);
  border-radius: 2px;
}
.asm-col-dist-dot {
  position: absolute;
  top: 50%;
  width: 5px; height: 5px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  opacity: 0.7;
}
.asm-col-dist-dot:hover { opacity: 1; outline: 1.5px solid #0f172a; }
.asm-col-dist-range {
  display: flex; justify-content: space-between;
  font-size: 9.5px; color: rgba(0, 0, 0, 0.45);
  font-variant-numeric: tabular-nums; font-weight: 500;
  margin-top: 2px; padding: 0 1px;
  text-transform: none; letter-spacing: normal;
}
.asm-col-dist-nodata {
  font-size: 9.5px; color: #cbd5e1; font-weight: 500;
  font-style: italic;
}

/* ============================================================
 * DOE Matrix table — user-resizable column widths
 * ============================================================ */
.asm-col-resizer {
  position: absolute;
  top: 0; bottom: 0;
  right: -3px;
  width: 6px;
  cursor: col-resize;
  z-index: 4;
  background: transparent;
  transition: background 0.12s ease;
}
.asm-col-resizer:hover,
.asm-col-resizer:active {
  background: rgba(37, 99, 235, 0.35);
}
/* While a drag is in progress, lock everyone to the resize cursor */
body.asm-resizing, body.asm-resizing * { cursor: col-resize !important; user-select: none !important; }

/* ============================================================
 * DOE Filter-by-Formula modal — same workflow numbering as the
 * toolbar: ① Find → ② Filter → ③ Select
 * ============================================================ */
.asm-filter-grid {
  display: grid;
  grid-template-columns: 26px 88px 1fr;
  gap: 10px 12px;
  align-items: start;
}
.asm-filter-step {
  font-size: 12px; font-weight: 700;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  background: #0f172a; color: #fff;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 2px;
}
.asm-filter-title {
  font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: #475569;
  padding-top: 4px;
}
.asm-filter-body {
  display: flex; flex-direction: column; gap: 8px;
  min-width: 0;
}
/* Step ① row: search input + group dropdown side-by-side */
.asm-filter-body .asm-filter-search,
.asm-filter-grid input.asm-filter-search {
  flex: 1; min-width: 200px;
  padding: 7px 10px;
  border: 1px solid var(--border); border-radius: 6px;
  font-size: 13px; font-family: inherit;
  width: auto;
}
.asm-filter-body > .asm-filter-search { flex: 0 0 auto; }
.asm-filter-grid .asm-filter-body:first-of-type {
  /* The Find row is single-line: search input + group label inline */
  flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap;
}
.asm-filter-inline-lbl {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: #64748b;
  white-space: nowrap;
}
.asm-filter-grid select.asm-filter-mini-select {
  width: auto;
  padding: 5px 8px; border: 1px solid var(--border);
  border-radius: 5px; font-size: 12.5px; background: #fff;
  font-family: inherit; color: var(--text);
  text-transform: none; letter-spacing: normal;
}
/* Step ② sub-rows: PROJECT / TYPE / DOE FACTOR / RANGE labels in a sub-grid */
.asm-filter-subrow {
  display: grid;
  grid-template-columns: 90px 1fr;
  gap: 8px;
  align-items: start;
}
.asm-filter-subrow-range {
  padding-top: 8px;
  border-top: 1px dashed var(--border);
}
.asm-filter-sublbl {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: #64748b;
  padding-top: 5px;
}
.asm-filter-subchips {
  display: flex; flex-wrap: wrap; gap: 6px;
  align-items: center;
  min-width: 0;
}
.asm-filter-grid select.asm-filter-range-col {
  width: auto; max-width: 240px;
  padding: 4px 8px; border: 1px solid var(--border);
  border-radius: 4px; font-size: 12px;
  font-family: inherit; background: #fff;
  text-transform: none; letter-spacing: normal;
}
.asm-filter-grid input.asm-filter-range-num {
  width: 70px;
  padding: 4px 6px; border: 1px solid var(--border);
  border-radius: 4px; font-size: 12px;
  font-family: inherit;
  text-transform: none; letter-spacing: normal;
}
.asm-filter-similar-banner {
  display: flex; align-items: center; gap: 8px;
  background: #fffbeb;
  border: 1px solid #fde68a;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12px;
  grid-column: 1 / -1;  /* span all columns when inside the subrows body */
}
/* Step ③: bulk action row + list */
.asm-filter-bulk-row {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.asm-filter-count {
  font-size: 11.5px; color: #64748b;
  margin-left: auto;
}
/* Recessed by fill, not outlined. The sidebar is white now, so #f1f5f9 reads
 * clearly on its own — the old border existed because #fafafa was nearly
 * invisible against the pane it sat on. */
.asm-filter-list {
  height: 440px; overflow-y: auto;
  border: none; border-radius: 8px;
  background: #f1f5f9;
  padding: 6px;
}
@media (max-width: 600px) {
  .asm-filter-grid { grid-template-columns: 22px 1fr; }
  .asm-filter-title { grid-column: 2; }
  .asm-filter-body  { grid-column: 1 / -1; }
  .asm-filter-subrow { grid-template-columns: 1fr; }
  .asm-filter-sublbl { padding-top: 0; }
}

/* ============================================================
 * DOE Analyze workspace — full-screen overlay with chart + stats
 * ============================================================ */
/* ── Scrollbars inside the Analyze workspace ────────────────────────────────
 * The platform default here is the classic macOS bar — an opaque grey trough
 * ~15px wide that visually cuts the panel in half. Scoped to this overlay only,
 * so the rest of the app keeps whatever the OS does.
 * The thumb is drawn with a transparent 3px border plus background-clip:
 * content-box, which is what insets it: a 10px hit area (comfortable to grab)
 * carrying a 4px visible bar. Track is transparent, so nothing shows until
 * there is something to scroll.
 * NOTE: styling ::-webkit-scrollbar opts an element out of macOS's floating
 * overlay bars — but these panes already reserve a 15–17px gutter, so this is a
 * net GAIN of ~5px, not a cost. */
#asm-analyze-overlay,
#asm-analyze-overlay * {
  scrollbar-width: thin;                                   /* Firefox */
  scrollbar-color: rgba(100, 116, 139, .35) transparent;
}
#asm-analyze-overlay ::-webkit-scrollbar { width: 10px; height: 10px; }
#asm-analyze-overlay ::-webkit-scrollbar-track,
#asm-analyze-overlay ::-webkit-scrollbar-corner { background: transparent; }
#asm-analyze-overlay ::-webkit-scrollbar-thumb {
  background: rgba(100, 116, 139, .35);
  background-clip: content-box;
  border: 3px solid transparent;
  border-radius: 999px;
}
#asm-analyze-overlay ::-webkit-scrollbar-thumb:hover {
  background: rgba(71, 85, 105, .6);
  background-clip: content-box;
}
/* Dark plots: same shape, lifted off the dark ground instead of pressed into it. */
body.asm-darkplots #asm-analyze-overlay,
body.asm-darkplots #asm-analyze-overlay * {
  scrollbar-color: rgba(148, 163, 184, .35) transparent;
}
body.asm-darkplots #asm-analyze-overlay ::-webkit-scrollbar-thumb {
  background: rgba(148, 163, 184, .35);
  background-clip: content-box;
}
body.asm-darkplots #asm-analyze-overlay ::-webkit-scrollbar-thumb:hover {
  background: rgba(203, 213, 225, .6);
  background-clip: content-box;
}
.asm-analyze-overlay {
  position: fixed; inset: 0;
  background: rgba(15, 23, 42, 0.55);
  display: flex; align-items: center; justify-content: center;
  z-index: 200;
  padding: 0;            /* full-screen workspace — no surrounding gutter */
}
.asm-analyze-panel {
  background: #fff;
  /* Full screen at every size: the DOE Matrix Analyze workspace needs the
     room for wide grids + plots, and a full-bleed window guarantees the
     header (incl. "Analyze · DOE Matrix") always has space to lay out. */
  width: 100vw; max-width: none; height: 100vh; height: 100dvh;
  border-radius: 0; overflow: hidden;
  display: flex; flex-direction: column;
  border: none;
  transition: width .22s ease;
}
.asm-analyze-header {
  background: #f8fafc; color: #0f172a;
  display: flex; flex-direction: column; align-items: stretch;
  flex-shrink: 0;
  /* No bottom border: the header's #f8fafc against the body's #eef2f6 ground
   * already marks the edge, and a line on top of that contrast was the loudest
   * thing in the workspace. */
}
/* Row 1: title on the left, action buttons on the right. */
.asm-analyze-titlebar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 14px 6px;
}
/* Row 2: full-width tab strip with its own bottom padding. Aligns on flex-end
 * because the tabs block is now two stacked rows (category labels + strip) —
 * anything beside it should line up with the STRIP, not with the block's centre. */
.asm-analyze-tabbar {
  display: flex; align-items: flex-end;
  padding: 0 12px 8px;
  gap: 4px;
}
/* AI actions sit immediately after the tab strip (8px of air), not pushed to the
 * far right — they read as a continuation of the row rather than a detached
 * corner cluster. padding-bottom centres them against the 34.5px strip:
 * (34.5 − 26.8) / 2 ≈ 4px. */
.asm-tabbar-actions {
  margin-left: 8px;
  display: flex; align-items: center; gap: 6px;
  padding-bottom: 4px;
}
.asm-tabbar-actions .btn { border-radius: 10px; }
/* Solid violet instead of the outline treatment. Scoped to this row on purpose:
 * .btn-ai is used in ~17 places app-wide and the outline version is correct
 * elsewhere. The border stays at 1px TRANSPARENT rather than being removed, so
 * the metrics (and the 4px centring above) hold; with background-clip at its
 * default the fill runs under it, so no edge shows. .fl-lock draws from
 * currentColor, so the Pro lock turns white along with the label. */
.asm-tabbar-actions .btn-ai {
  background: #6d28d9; color: #fff; border-color: transparent;
}
.asm-tabbar-actions .btn-ai:hover {
  background: #5b21b6; color: #fff; border-color: transparent;
}
/* Sized against the 34px control cluster opposite it — at 15px the title read as
 * secondary to its own header. */
.asm-analyze-header strong { color: #0f172a; white-space: nowrap; flex-shrink: 0; font-size: 18px; }
.asm-analyze-tabs { display: flex; flex-direction: column; align-items: stretch; gap: 3px; }
/* ONE backdrop for both categories — Explore and Model & Optimize share a
   single segmented control rather than sitting on separate cards, so the whole
   view switcher reads as one control. No border anywhere: the fill alone
   defines the surface, and the buttons carry a transparent border purely to
   keep their metrics identical to the bordered version. 14px radius stays
   concentric with the 10px buttons inside the 4px padding. */
.asm-tab-strip {
  position: relative;                          /* containing block for the glider */
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(152px, 1fr);
  gap: 4px;
  /* One step darker than the #f1f5f9 used elsewhere: with no border to define
   * the edge, the fill alone has to carry the surface, and slate-100 was too
   * close to the header behind it to read as a control. */
  background: #e2e8f0;
  border: none;
  border-radius: 14px;
  padding: 4px;
}
/* The headings sit OUTSIDE the backdrop, on a grid whose columns mirror the
   strip's exactly (same template, same gap, same 4px side inset) so each label
   still lines up over the first button of its category. */
.asm-tab-lblrow {
  display: grid; grid-template-columns: repeat(6, minmax(152px, 1fr));
  gap: 4px; padding: 0 4px;
}
.asm-analyze-tabs .asm-tab-grouplbl { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: #94a3b8; white-space: nowrap; }
body.asm-darkplots .asm-analyze-tabs .asm-tab-grouplbl { color: #64748b; }
.asm-analyze-tabs .asm-tab {
  background: transparent; color: #475569;
  /* Transparent rather than removed: keeps the button's metrics identical to
   * the bordered version so nothing shifts, while showing no outline. */
  border: 1px solid transparent; border-radius: 10px;
  padding: 5px 12px; font-size: 12.5px; font-weight: 500;
  cursor: pointer;
  /* Colour eases over the same .26s the glider travels, so the label flip rides
   * along with the highlight instead of snapping ahead of it. No background
   * transition — see the note on the DOE Matrix tabs. */
  transition: color 0.26s cubic-bezier(.4, 0, .2, 1);
  font-family: inherit;
  position: relative; z-index: 1;              /* above the glider */
}
/* No hover state: the sliding highlight IS the feedback, and a hover fill both
 * competed with it and painted over it mid-slide. cursor: pointer carries the
 * affordance. */
/* Selected = primary blue. Only used as the pre-glider fallback (and the
 * no-JS path) — once `has-glider` lands, the travelling glider provides the
 * fill and this goes transparent. */
.asm-analyze-tabs .asm-tab.on { background: var(--primary); color: #fff; border-color: var(--primary); }
/* Hand the fill to the glider. The generic `.has-glider [data-tab].on` rule ties
 * the line above on specificity (0,3,0) and loses on source order, so the
 * handover has to be restated here, scoped to the strip — otherwise the active
 * button keeps painting its own blue at the destination while the glider is
 * still travelling toward it. */
.asm-tab-strip.has-glider .asm-tab.on { background: transparent; border-color: transparent; box-shadow: none; }
/* Compact dropdown equivalent of the tab strip. Rendered alongside the tab
   buttons but hidden on wide screens; on narrow screens (where the full strip
   plus "Analyze · DOE Matrix" can't share one row) the buttons hide and this
   shows instead, keeping the title on one line. See the media query below. */
.asm-analyze-tab-select {
  display: none;
  background: #fff; color: #0f172a;
  border: 1px solid var(--border); border-radius: 6px;
  padding: 5px 26px 5px 10px; font-size: 12.5px; font-weight: 600;
  font-family: inherit; cursor: pointer; max-width: 200px;
}
body.asm-darkplots .asm-analyze-tab-select { background: #111827; color: #f1f5f9; border-color: #374151; }
/* _asmEnhanceSelects wraps the mobile view-switcher <select> in a .fl-dd custom
   dropdown whose trigger is always visible. The native select's display:none
   above can't reach that wrapper, so mirror the mobile-only rule here — without
   it the enhanced dropdown shows on wide screens beside the full tab strip. */
.fl-dd-viewswitch { display: none; }
@media (max-width: 1024px) {
  .asm-analyze-tabs { display: none; }
  .asm-analyze-tab-select { display: inline-block; }
  .fl-dd-viewswitch { display: block; }
}
/* The titlebar cluster shares a corner radius, but NOT a height. ✦ AI and the
 * two toggles stay compact (~27px, the same weight as the view tabs below) so
 * five controls still fit on one row in the Designer view; the close button
 * keeps its larger 34px footprint deliberately — it's the only way out of a
 * full-screen workspace and should stay the most prominent thing here. They
 * sit on a shared centre line, so different heights read as hierarchy rather
 * than as the ragged mismatch of three different radii. */
.asm-analyze-titlebar .btn,
.asm-header-toggle,
.asm-analyze-close {
  border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
}
.asm-analyze-close { height: 34px; min-height: 34px; }
/* Borderless, oversized glyph. The only way out of a full-screen workspace
 * should be the most legible control in the header, and the box outline was
 * doing the opposite — it made the X read as just another 12px toggle. Dropping
 * the border lets the mark itself carry the weight at 24px (up from 18px), and
 * keeps the 34px footprint so the titlebar cluster stays aligned. */
.asm-analyze-close {
  background: transparent; color: #475569; border: none;
  font-size: 24px; font-weight: 500; cursor: pointer; line-height: 1; font-family: inherit;
  width: 34px;
  transition: background 0.15s, color 0.15s;
}
.asm-analyze-close:hover { background: #fee2e2; color: #b91c1c; }
/* The border was also the keyboard-focus cue; with it gone, focus needs its own
 * ring or the exit becomes untrackable when tabbing. */
.asm-analyze-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* Presentation-mode toggles in the workspace header (top-right).
 * Available from any tab — dark mode and color-blind palette. */
/* Soft chip, not an outlined pill: a faint fill marks it as a control without
 * drawing an edge, and the border stays only as a transparent 1px so the
 * metrics (and the shared centre line with the ✕) don't shift. */
.asm-header-toggle {
  background: rgba(15, 23, 42, .05); color: #475569;
  border: 1px solid transparent;
  padding: 4px 10px; font-size: 12px; font-weight: 500; line-height: 1.4;
  cursor: pointer; font-family: inherit;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s;
}
/* On = filled, matching the filled-not-outlined rule used across the workspace. */
.asm-header-toggle.on {
  background: #0f172a; color: #fff; border-color: transparent;
}
.asm-header-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* macOS-style floating panes: the sidebar and the plot area are separate cards
 * resting on a slightly darker ground, so the gap between them — not a border —
 * is what separates them. position: relative anchors the sidebar's folder tab
 * (an absolutely-positioned sibling of the aside) to this row. */
.asm-analyze-body {
  flex: 1; display: flex; min-height: 0; position: relative;
  /* Tiled, not floating: the panes fill the window and the darker ground shows
   * only through the gutter between them. The gutter is 34px — wide enough to
   * hold the 26px folder tab with 8px of air after it, so the tab lives IN the
   * gap instead of overlapping the plot pane. */
  /* Falls back to the stock slate ground when no background has been chosen in
   * Preferences → Appearance; otherwise --ground is a 7% deeper tone of it. */
  background: var(--ground, #e2e8f0);
  /* Left is 0 so the Filters pane meets the window edge; the other three keep
   * their inset so both panes still sit on visible ground. The gutter is a plain
   * 10px gap — the tab is inset into the sidebar's own padding rather than
   * claiming a 34px column of its own, which hands 24px back to the plots. */
  padding: 10px 10px 10px 0; gap: 10px;
}
.asm-analyze-sidebar {
  position: relative;
  width: 260px; flex-shrink: 0;
  /* Flush to the window's left edge, so only the inner corners are rounded —
   * the pane reads as tiled into the corner rather than floating in it. White,
   * matching the plot pane; the darker ground in the gutter is what separates
   * them, not a border. */
  background: #fff;
  border-radius: 0 12px 12px 0;
  box-shadow: 0 1px 3px rgba(15, 23, 42, .06), 0 1px 2px rgba(15, 23, 42, .04);
  /* Extra bottom padding so the last section (currently Diagnostics)
     doesn't butt against the scroll edge — gives a comfortable
     breathing room at the end of the scrollable area. */
  /* overflow-x hidden as a backstop: mid-tween the contents are deliberately
   * wider than the shrinking box, and without this that briefly shows as a
   * horizontal scrollbar. */
  padding: 12px 14px 32px 14px; overflow-y: auto; overflow-x: hidden;
  /* Same .26s curve the tab highlights use, so the whole workspace shares one
   * motion language. The card's fill and shadow are in the transition because
   * they fade out entirely when collapsed (see below). */
  transition: width .26s cubic-bezier(.4, 0, .2, 1),
              padding .26s cubic-bezier(.4, 0, .2, 1),
              background-color .26s cubic-bezier(.4, 0, .2, 1),
              box-shadow .26s cubic-bezier(.4, 0, .2, 1);
}
/* Collapsed = a folder tab, nothing else. The grey strip and its right border
 * fade away completely, so no empty column is left allocated; the width shrinks
 * to just the tab, which is absolutely positioned and so doesn't fight the
 * padding tween. Contents stay in the DOM and fade (rather than display:none)
 * so they clip smoothly during the slide instead of vanishing a frame early. */
.asm-analyze-sidebar.asm-sidebar-collapsed {
  width: 0;                 /* zero, not a strip — the tab lives outside the aside */
  padding: 10px 0;
  overflow: hidden;
  background: transparent;
  box-shadow: none;         /* the card dissolves; only the tab is left */
}
/* A FLOOR, not a fixed width, so the contents don't reflow (and re-wrap every
 * label) while the sidebar animates — they just get clipped by the shrinking
 * box. This was `width: 232px` (260 − 28px padding), which ignored the ~11px the
 * vertical scrollbar takes out of the content box: the contents were then 11px
 * wider than the space available and the sidebar grew a permanent horizontal
 * scrollbar. 220px sits just under the real content width in both cases —
 * with a scrollbar (221px) and without (232px) — so nothing overflows at rest,
 * while still holding the layout steady mid-tween. */
.asm-sidebar-contents { min-width: 205px; transition: opacity .16s ease; }
.asm-analyze-sidebar.asm-sidebar-collapsed .asm-sidebar-contents {
  opacity: 0; pointer-events: none;
}
/* The folder tab. Permanent in both states — it rides the panel's right edge,
 * with `left` tweening on the same curve as the panel's width so the two move as
 * one piece. Rounded on the RIGHT and missing its LEFT border: the tab belongs
 * to the sidebar (which lives to its left), so it faces outward, away from what
 * it's attached to — the same way a folder's tab points away from the folder. */
.asm-sidebar-toggle {
  position: absolute;
  top: 24px;
  /* Sits in the body's 34px gutter at the sidebar's right edge — the body has no
   * padding now, so this is just the pane width. It never overlaps the plot
   * pane, and when collapsed it rests flush against the window's left edge
   * rather than floating in from it. */
  left: 0;                                         /* only ever shown collapsed */
  width: 22px; height: auto;
  padding: 12px 0;
  display: flex; align-items: center; justify-content: center;
  /* Curves away from the hidden pane it opens. */
  border-radius: 0 10px 10px 0;
  border: none;
  /* Tinted rather than white: inset into a white pane, a white tab would be
   * invisible. Reads as a grip on the pane's edge. */
  background: #f1f5f9;
  box-shadow: 2px 1px 3px rgba(15, 23, 42, .06);
  color: #64748b;
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  line-height: 1;
  cursor: pointer;
  z-index: 3;
  font-family: inherit;
  transition: opacity .2s ease, background .15s, color .15s;
}
/* No hover state — the tab now shares the pane's white fill, so a grey hover
 * flash would read as the tab detaching from it. cursor: pointer carries the
 * affordance. */
/* Expanded, the panel closes from its OWN ✕ inside the frame, so the edge tab
 * has no job and is hidden entirely — nothing insets into the panel any more. */
.asm-analyze-sidebar:not(.asm-sidebar-collapsed) + .asm-sidebar-toggle {
  opacity: 0; pointer-events: none;
}
/* writing-mode turns the label sideways; it reads top-to-bottom down the tab. */
.asm-sidebar-tablbl { writing-mode: vertical-rl; text-orientation: mixed; line-height: 1; }
/* Close control INSIDE the panel — part of the scrollable content rather than a
 * tab hanging off the edge, so nothing overlaps the filters. Floated right so it
 * shares the first row with the section beneath it instead of claiming one. */
.asm-sidebar-close {
  float: right; margin: -2px -2px 0 6px;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 7px;
  background: transparent; color: #64748b;
  font-size: 12px; line-height: 1; cursor: pointer; font-family: inherit;
}
.asm-sidebar-close:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
body.asm-darkplots .asm-sidebar-close { color: #cbd5e1; }
body.asm-darkplots .asm-sidebar-toggle {
  background: #1f2937; border-color: #374151; color: #cbd5e1;
}
body.asm-darkplots .asm-sidebar-toggle { border-left: none; }
.asm-analyze-main {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column;
  padding: 14px;
  background: #fff;
  /* Inset on all four sides, so all four corners round. The folder tab now sits
   * in the body's gutter rather than overlapping this pane, so the old 34px left
   * padding that kept it off the content is no longer needed. */
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(15, 23, 42, .06), 0 1px 2px rgba(15, 23, 42, .04);
  overflow: hidden;
}
/* When the heatmap view is showing, allow the main pane to scroll so the
 * correlations list below the grid is reachable. (Scatter view stays
 * non-scrolling because its chart sizes to flex available space.) */
.asm-analyze-main:has(#asm-heatmap-wrap),
.asm-analyze-main:has(.asm-heatmap-wrap) {
  overflow-y: auto;
}
/* No divider rule: a dashed line was the noisiest thing in the sidebar, and the
 * gap alone already groups these. Spacing bumped 12→18px to carry the
 * separation the line used to. */
/* Grouping is by proximity now that the dividers are gone, so the gap BETWEEN
 * sections (24px) has to be clearly larger than the gaps WITHIN one (6–8px);
 * at 18px the ratio was too shallow to read as separate groups. */
.asm-side-section { padding-bottom: 0; margin-bottom: 24px; }
.asm-side-section:last-child { margin-bottom: 8px; }
.asm-side-section:last-child { border-bottom: none; }
.asm-side-title {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: #64748b; margin-bottom: 10px;
}
/* Field labels: keep uppercase styling that matches the global label rule,
 * but lock down the size + spacing so they fit our narrow sidebar. */
.asm-analyze-sidebar label.asm-side-label {
  display: block; font-size: 11px; color: #475569; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-bottom: 8px;
}
/* Select inside a labelled field — beat the global input/select rule */
.asm-analyze-sidebar .asm-side-label select,
.asm-analyze-sidebar select.asm-side-select {
  width: 100%; margin-top: 4px;
  padding: 6px 8px; border: 1px solid transparent;
  border-radius: 6px; font-size: 12.5px; font-family: inherit;
  font-weight: 400; letter-spacing: normal; text-transform: none;
  color: var(--text); background: #f1f5f9;
}
/* Numeric inputs (X min, X max, …) — also beat the global rule */
.asm-analyze-sidebar input.asm-side-input {
  width: 100%; padding: 6px 8px; border: 1px solid transparent;
  border-radius: 6px; font-size: 12px; font-family: inherit;
  font-weight: 400; letter-spacing: normal; text-transform: none;
  background: #f1f5f9;
}
.asm-analyze-sidebar input.asm-side-input::placeholder {
  text-transform: none; letter-spacing: normal; color: #94a3b8;
}
/* Inline number inputs sitting inside a labelled field (mixture Total,
 * Contour levels / Grid resolution) — compact sizing so they don't fall
 * through to the global input rule's larger font/padding. (Spinners are
 * hidden globally.) */
.asm-analyze-sidebar .asm-side-label input[type="number"] {
  padding: 5px 7px; border: 1px solid transparent; border-radius: 6px;
  font-size: 12px; font-family: inherit; font-weight: 400;
  letter-spacing: normal; text-transform: none; color: var(--text); background: #f1f5f9;
}
/* The fields actually rendered in this sidebar carry .asm-toolbar-select or no
 * class at all, so they miss the .asm-side-input rules above — catch them by
 * type. Checkboxes are deliberately excluded: their outline IS the control. */
/* Scoped by the overlay id: a class-level selector here loses its border-color
 * to a later declaration even though it wins on background, so the id settles
 * the cascade outright rather than leaving one field outlined. */
#asm-analyze-overlay .asm-analyze-sidebar input.asm-toolbar-select,
#asm-analyze-overlay .asm-analyze-sidebar textarea.asm-toolbar-select,
#asm-analyze-overlay .asm-analyze-sidebar input[type="text"],
#asm-analyze-overlay .asm-analyze-sidebar input[type="search"],
#asm-analyze-overlay .asm-analyze-sidebar input[type="number"],
#asm-analyze-overlay .asm-analyze-sidebar select,
#asm-analyze-overlay .asm-analyze-sidebar textarea {
  background: #f1f5f9; border: 1px solid transparent; border-radius: 6px;
}
/* The border was the only thing marking these as fields, so the fill takes that
 * job — and focus, which the border also used to carry, gets an explicit ring.
 * The field lightens to white on focus so the active one is unmistakable. */
#asm-analyze-overlay .asm-analyze-sidebar input.asm-toolbar-select:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar textarea.asm-toolbar-select:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar input[type="text"]:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar input[type="search"]:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar input[type="number"]:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar textarea:focus-visible,
#asm-analyze-overlay .asm-analyze-sidebar select:focus-visible,
.asm-analyze-sidebar .asm-side-label select:focus-visible,
.asm-analyze-sidebar select.asm-side-select:focus-visible,
.asm-analyze-sidebar input.asm-side-input:focus-visible,
.asm-analyze-sidebar .asm-side-label input[type="number"]:focus-visible {
  outline: none;
  background: #fff;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
body.asm-darkplots .asm-analyze-sidebar .asm-side-label input[type="number"] {
  background: #1f2937; border-color: transparent; color: #f1f5f9;
}
/* Inline checkbox labels: keep them sentence case so the checkbox reads
 * naturally next to its text. Also constrain the checkbox to its native
 * size — the global input rule was stretching it to 100% width. */
.asm-analyze-sidebar label.asm-side-check {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: #475569; cursor: pointer; margin-top: 6px;
  font-weight: 500;
  text-transform: none; letter-spacing: normal;
  white-space: nowrap;
}
/* Custom checkbox: a filled square rather than an outlined one, so it matches
 * the fill-over-border treatment used everywhere else in the workspace. The
 * tick is a background SVG because pseudo-elements aren't reliable on inputs. */
.asm-analyze-sidebar label.asm-side-check input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 15px; height: 15px; padding: 0; margin: 0; flex-shrink: 0;
  border: none; border-radius: 5px;
  background: #e2e8f0;
  cursor: pointer;
  transition: background .15s;
}
.asm-analyze-sidebar label.asm-side-check input[type="checkbox"]:checked {
  background: var(--primary)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-6'/%3E%3C/svg%3E")
    center / 13px 13px no-repeat;
}
.asm-analyze-sidebar label.asm-side-check input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
/* Range-input grids (X min/max, Y min/max) — make sure the two columns
 * actually let inputs shrink to fit instead of overflowing. */
.asm-analyze-sidebar .asm-side-grid-2 {
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 6px;
}
/* Help text boxes — same recessed-card design as the 3D stats panel
 * (same bg, border, radius, padding) so the sidebar reads consistently. */
.asm-side-help {
  font-size: 11.5px; color: #475569; line-height: 1.45;
  /* Fill-only, same reasoning as .asm-filter-list. */
  background: #f1f5f9; border: none;
  border-radius: 8px; padding: 10px 12px;
}
/* Give the footnote help box room from the checkboxes / labels directly above
 * it (Mixture · Diagnostics sections) so each group doesn't read cramped. */
.asm-analyze-sidebar label.asm-side-check + .asm-side-help,
.asm-analyze-sidebar label.asm-side-label + .asm-side-help { margin-top: 11px; }
/* A little more rhythm between an option checkbox and the help text or title
 * that follows it. Targets only a section's own stacked option checkboxes —
 * NOT the dense Inputs/Outputs picker lists inside .asm-designer-collist. */
.asm-analyze-sidebar .asm-side-section > label.asm-side-check { margin-top: 8px; }
.asm-analyze-sidebar .asm-side-section > label.asm-side-check:first-of-type { margin-top: 4px; }
/* Even out the rhythm where a section mixes checkboxes and labelled fields
 * (e.g. Points: the "95% data ellipse" checkbox directly above "Size by").
 * A .asm-side-label carries no top margin, so without this it jams against the
 * checkbox above it — the cramped-looking stretch toward the bottom of the
 * panel. Give both mixed transitions the same breathing room. */
.asm-analyze-sidebar label.asm-side-check + label.asm-side-label,
.asm-analyze-sidebar label.asm-side-label + label.asm-side-check { margin-top: 14px; }
/* Segmented control, same construction as the DOE Matrix Analyze/Generate bar
 * and the Analyze view tabs: a filled backdrop instead of an outline, and a
 * radius kept concentric with the buttons inside (9px button + 3px padding =
 * 12px). #f8fafc was near-invisible against the now-white sidebar, which is
 * precisely why it needed a border to be seen. */
.asm-toggle-group {
  display: flex; gap: 4px; margin-bottom: 8px;
  border: none; background: #f1f5f9;
  border-radius: 12px; padding: 3px;
  position: relative;                 /* containing block for the sliding highlight */
}
/* The shared .asm-glider, sized to THIS control's 3px padding. */
.asm-toggle-group > .asm-glider { top: 3px; height: calc(100% - 6px); border-radius: 9px; }
/* Highlight live → the selected button hands its fill over, exactly as on the
 * tab bars. Specificity has to beat `.asm-toggle.on`, hence the group prefix. */
.asm-toggle-group.has-glider .asm-toggle.on { background: transparent; }
.asm-toggle {
  flex: 1; background: transparent; border: none;
  padding: 5px 8px; font-size: 11.5px; font-weight: 500;
  cursor: pointer; border-radius: 9px; color: #475569; font-family: inherit;
  position: relative; z-index: 1;     /* above the glider */
  transition: color .26s cubic-bezier(.4, 0, .2, 1);
}
/* No hover fill — the selected state is the feedback, matching every other
 * segmented control in the workspace. */
.asm-toggle.on { background: var(--primary); color: #fff; }

/* Chart + stats */
.asm-chart-wrap {
  flex: 1; min-height: 0;
  position: relative; background: #fff;
  /* Must stay a line (it frames a data canvas), so it's softened rather than
   * removed — --hairline instead of the much stronger --border. */
  border: 1px solid var(--hairline); border-radius: 8px;
  padding: 8px;
}
/* Return-from-drilldown flash — a ring that pulses out from the point you came
   back to, three times, then fades. Shared by the Design Space map and the
   Scatter plot (both drop it, centred on the point, into their position:relative
   canvas wrapper). Ignores the pointer; honors reduced-motion. */
.asm-plot-flash {
  position: absolute; width: 12px; height: 12px; border-radius: 50%;
  transform: translate(-50%, -50%);
  border: 2.5px solid var(--primary, #6366f1);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55);
  pointer-events: none; z-index: 6;
  animation: asmPlotFlash 0.55s ease-out 3;
}
@keyframes asmPlotFlash {
  0%   { transform: translate(-50%, -50%) scale(0.7); opacity: 0.95; }
  70%  { opacity: 0.55; }
  100% { transform: translate(-50%, -50%) scale(3.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .asm-plot-flash { animation: asmPlotFlashRM 1.2s ease-out 1; }
  @keyframes asmPlotFlashRM { 0%, 60% { opacity: 0.9; } 100% { opacity: 0; } }
}
.asm-stats-panel {
  flex-shrink: 0; max-height: 38%; overflow-y: auto;
  margin-top: 10px; padding: 10px 12px;
  background: #f8fafc; border: 1px solid var(--hairline); border-radius: 8px;
  font-size: 12px; color: #1e293b;
}
.asm-stats-title {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: #64748b; margin-bottom: 6px;
}
.asm-stat-line {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px; padding: 3px 0;
}
.asm-stat-chip {
  background: #fff; border: 1px solid var(--border);
  border-radius: 4px; padding: 2px 7px; font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.asm-series-dot {
  display: inline-block; width: 10px; height: 10px;
  border-radius: 50%; flex-shrink: 0;
}

/* 3D-surface canvas fills the wrap exactly via absolute inset. The
 * absolute positioning decouples the canvas display-size from the
 * wrap's own layout so the canvas can't push the wrap taller (the
 * hand-rolled 3D canvas hit this feedback loop; Chart.js handles
 * its own sizing for the scatter view). */
.asm-3d-wrap { overflow: hidden; }
.asm-3d-canvas {
  position: absolute;
  top: 8px; left: 8px;
  /* width/height in pixels minus the 8px padding on each side. Canvas
   * elements don't stretch with width/height: auto + inset because their
   * intrinsic 300x150 size acts as the default. Explicit calc() wins. */
  width: calc(100% - 16px) !important;
  height: calc(100% - 16px) !important;
  display: block;
}

/* Axis-range sliders (X / Y in the scatter sidebar) */
.asm-range-group { margin-top: 8px; padding-top: 6px; border-top: 1px dashed var(--border); }
.asm-range-label {
  font-size: 11px; font-weight: 600; color: #475569;
  text-transform: uppercase; letter-spacing: 0.05em;
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 4px;
}
.asm-range-reset {
  background: none; border: none; cursor: pointer;
  font-size: 14px; color: #94a3b8; padding: 0 4px;
  font-family: inherit;
}
.asm-range-reset:hover { color: var(--primary); }
.asm-range-row {
  display: grid;
  grid-template-columns: 28px 1fr 56px;
  align-items: center; gap: 6px;
  margin-bottom: 4px;
}
.asm-range-handle-label {
  font-size: 10.5px; color: #94a3b8; text-transform: uppercase;
  letter-spacing: 0.04em; font-weight: 600;
}
.asm-range-slider { width: 100%; padding: 0; min-height: 0; cursor: pointer; }
.asm-range-num {
  width: 56px; padding: 4px 5px; border: 1px solid var(--border);
  border-radius: 4px; font-size: 11px; font-family: inherit;
  text-transform: none; letter-spacing: normal;
}

/* Sidebar slider rows (3D rotation/tilt/zoom) */
.asm-side-slider-row {
  display: grid;
  grid-template-columns: 56px 1fr 44px;
  align-items: center; gap: 6px;
  margin-bottom: 6px;
  font-size: 11.5px; color: #475569;
}
.asm-side-slider-label {
  font-size: 10.5px; font-weight: 600; color: #475569;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.asm-side-slider { width: 100%; padding: 0; min-height: 0; cursor: pointer; }
.asm-side-slider-val {
  font-size: 11px; color: #0f172a; font-variant-numeric: tabular-nums;
  text-align: right;
}

/* Dark mode for plots — toggled via body.asm-darkplots class. Applies to
 * the analyze workspace only so the rest of the app stays light.
 * Window/header/sidebar all match FormLab's main sidebar (#111827).
 * Inner cards/panels step up one shade (#1f2937) so they pop without
 * fighting the window. Text uses lighter slate-200 / slate-300. */
body.asm-darkplots .asm-analyze-panel,
body.asm-darkplots .asm-analyze-header,
body.asm-darkplots .asm-analyze-sidebar,
body.asm-darkplots .asm-analyze-main,
body.asm-darkplots .asm-chart-wrap,
body.asm-darkplots .asm-hm-scroll,
body.asm-darkplots .asm-hm-grid,
body.asm-darkplots .asm-3d-wrap { background: #111827 !important; color: #f1f5f9; }
body.asm-darkplots .asm-chart-wrap,
body.asm-darkplots .asm-hm-scroll { border-color: #374151 !important; }
body.asm-darkplots .asm-hm-corner,
body.asm-darkplots .asm-hm-rowhdr,
body.asm-darkplots .asm-hm-colhdr-bot { background: #1f2937 !important; color: #e5e7eb !important; border-color: #374151 !important; }
body.asm-darkplots .asm-hm-title,
body.asm-darkplots .asm-hm-subtitle,
body.asm-darkplots .asm-hm-notes { color: #f1f5f9; }
body.asm-darkplots .asm-hm-titlebar { border-bottom-color: #374151; }
body.asm-darkplots .asm-stats-panel { background: #1f2937 !important; color: #e5e7eb; border-color: #374151 !important; }
body.asm-darkplots .asm-stats-panel strong { color: #fff; }
body.asm-darkplots .asm-stat-chip { background: #111827 !important; border-color: #374151 !important; color: #e5e7eb; }

/* Workspace header — dark-mode tabs / close / toggles. Header itself is
 * the same dark as the panel; tab styles flip back to dark-on-fill. */
body.asm-darkplots .asm-analyze-header { border-bottom-color: #1f2937; color: #f1f5f9; }
body.asm-darkplots .asm-analyze-header strong { color: #fff; }
/* The cluster backdrop needs its own dark value — left light it would be a
 * glaring grey slab on the dark shell. Sits one step above the panel so the
 * group still reads as a raised control, matching the light-mode relationship. */
body.asm-darkplots .asm-tab-strip { background: #1f2937; }
body.asm-darkplots .asm-analyze-tabs .asm-tab { color: #e5e7eb; border-color: transparent; }
/* Blue in dark mode too, so the selected tab keeps one identity across themes.
 * White on --primary is 4.6:1, so the label still passes AA on the dark shell. */
body.asm-darkplots .asm-analyze-tabs .asm-tab.on { background: var(--primary); color: #fff; border-color: var(--primary); }
/* Same handover in dark mode — the rule above is (0,4,1), so it needs an even
 * more specific restatement to let the glider own the fill. */
body.asm-darkplots .asm-tab-strip.has-glider .asm-tab.on { background: transparent; border-color: transparent; box-shadow: none; }
body.asm-darkplots .asm-analyze-close { color: #e5e7eb; }
body.asm-darkplots .asm-analyze-close:hover { background: #1f2937; color: #fff; }
body.asm-darkplots .asm-header-toggle { color: #e5e7eb; background: rgba(255, 255, 255, .07); border-color: transparent; }
/* On = primary fill, same as every other toggle. A white fill here made the
 * active Light/Dark button the brightest thing on a dark screen. */
body.asm-darkplots .asm-header-toggle.on { background: var(--primary); color: #fff; border-color: transparent; }

/* Analyze sidebar — gets dark theme too. Same window color, separated
 * from main by a subtle border; controls inside get dark-mode fills. */
/* Dark: the ground drops BELOW the cards (#0b1220 vs #111827) so the panes still
 * float, and the shadow is deepened since a soft light shadow is invisible here. */
body.asm-darkplots .asm-analyze-body { background: #0b1220; }
body.asm-darkplots .asm-analyze-sidebar,
body.asm-darkplots .asm-analyze-main { box-shadow: 0 1px 3px rgba(0, 0, 0, .5); }
body.asm-darkplots .asm-analyze-sidebar.asm-sidebar-collapsed { box-shadow: none; }
body.asm-darkplots .asm-sidebar-toggle { background: #1f2937; box-shadow: 2px 1px 3px rgba(0, 0, 0, .5); }
body.asm-darkplots .asm-analyze-sidebar { color: #e5e7eb; }
/* The de-bordered boxes need a dark fill that still reads as recessed. */
body.asm-darkplots .asm-side-help,
body.asm-darkplots .asm-filter-list { background: #1f2937; border: none; }
body.asm-darkplots .asm-side-title { color: #cbd5e1; }
/* Scoped to the analyze sidebar so this dark fill doesn't bleed into
 * modals (DOE generator etc.) that reuse .asm-side-help but render
 * outside the analyze workspace context. */
body.asm-darkplots .asm-analyze-sidebar .asm-side-help {
  background: #1f2937; border-color: #374151; color: #cbd5e1;
}
body.asm-darkplots .asm-analyze-sidebar .asm-side-help strong { color: #f1f5f9; }
body.asm-darkplots .asm-toggle-group {
  background: #1f2937; border: none;
}
body.asm-darkplots .asm-analyze-sidebar label.asm-side-label { color: #e5e7eb; }
body.asm-darkplots .asm-analyze-sidebar label.asm-side-check { color: #e5e7eb; }
/* background-COLOR, not the `background` shorthand: the shorthand also resets
 * background-image, and this rule outranks the light :checked rule that supplies
 * the tick SVG — so a checked box in dark mode lost its checkmark entirely. */
body.asm-darkplots .asm-analyze-sidebar label.asm-side-check input[type="checkbox"] { background-color: #374151; }
body.asm-darkplots .asm-analyze-sidebar label.asm-side-check input[type="checkbox"]:checked { background-color: var(--primary); }
body.asm-darkplots .asm-analyze-sidebar select,
body.asm-darkplots .asm-analyze-sidebar input[type="text"],
body.asm-darkplots .asm-analyze-sidebar input[type="number"],
body.asm-darkplots .asm-analyze-sidebar input.asm-side-input,
body.asm-darkplots .asm-analyze-sidebar input.asm-toolbar-select,
body.asm-darkplots .asm-analyze-sidebar textarea {
  background: #1f2937 !important;
  border-color: transparent !important;
  color: #f1f5f9 !important;
}
body.asm-darkplots .asm-analyze-sidebar input::placeholder,
body.asm-darkplots .asm-analyze-sidebar textarea::placeholder { color: #94a3b8; }
body.asm-darkplots .asm-analyze-sidebar .asm-toggle {
  background: transparent; color: #e5e7eb; border-color: transparent;
}
body.asm-darkplots .asm-analyze-sidebar .asm-toggle.on { background: var(--primary); color: #fff; border-color: var(--primary); }
/* Slider rows in sidebar (3D rotation, etc.) */
body.asm-darkplots .asm-side-slider-label { color: #cbd5e1; }
body.asm-darkplots .asm-side-slider-val   { color: #cbd5e1; }
/* Buttons inside the analyze sidebar — dark theme so they blend */
/* Sidebar buttons (Copy matrix as TSV, …): filled chip instead of a white
 * outlined button, matching the toggles and fields beside them. */
#asm-analyze-overlay .asm-analyze-sidebar .btn-secondary {
  background: #f1f5f9; border: 1px solid transparent; color: var(--text);
}
#asm-analyze-overlay .asm-analyze-sidebar .btn-secondary:hover { background: #e2e8f0; }
#asm-analyze-overlay .asm-analyze-sidebar .btn-secondary:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
/* Must carry the overlay id too: the light rule above is id-scoped (1,3,0) and
 * would otherwise outrank this (0,3,1), leaving the button light on dark. */
body.asm-darkplots #asm-analyze-overlay .asm-analyze-sidebar .btn-secondary {
  background: #1f2937; color: #e5e7eb; border-color: transparent;
}
body.asm-darkplots #asm-analyze-overlay .asm-analyze-sidebar .btn-secondary:hover {
  background: #374151; color: #fff;
}
/* Dark treatment for the analyze HEADER action buttons (Add note, Save image,
   Print PDF) so they blend with the dark page instead of popping as bright
   white chips. The old rule carved out .btn-print on the grounds that it had its
   own dark navy fill — that stopped being true (see the note at .btn-print), so
   the carve-out just left Print PDF white on dark. */
body.asm-darkplots #asm-analyze-overlay .btn-secondary {
  background: #1f2937; color: #e5e7eb; border-color: #374151;
}
body.asm-darkplots #asm-analyze-overlay .btn-secondary:hover {
  background: #374151; color: #fff;
}

/* Equation pill overlay on the scatter canvas — replaces the
 * canvas-drawn version so it can have proper padding and be dragged. */
.asm-eq-overlay {
  position: absolute;
  z-index: 6;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(15, 23, 42, 0.18);
  border-radius: 6px;
  padding: 8px 12px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  cursor: grab;
  font-size: 12px;
  line-height: 1.4;
  user-select: none;
  max-width: 360px;
  display: flex;
  align-items: flex-start;
  gap: 6px;
}
.asm-eq-overlay.asm-eq-dragging { cursor: grabbing; box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
.asm-eq-body { flex: 1; min-width: 0; }
.asm-eq-line {
  display: flex; align-items: center; gap: 8px;
  padding: 2px 0;
  font-variant-numeric: tabular-nums;
}
.asm-eq-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.asm-eq-text { color: #0f172a; }
/* Minimize / expand toggle (top-right of the equation pill). */
.asm-eq-toggle {
  background: rgba(0, 0, 0, 0.06);
  border: none;
  border-radius: 4px;
  padding: 0 6px;
  font-size: 14px;
  font-family: inherit; color: #475569;
  cursor: pointer;
  line-height: 1.4;
  margin-left: 2px;
  flex-shrink: 0;
}
.asm-eq-toggle:hover { background: rgba(0, 0, 0, 0.12); color: #0f172a; }
.asm-eq-toggle-mini {
  font-weight: 700; font-style: italic;
  font-size: 13px;
  padding: 2px 8px;
  background: transparent;
}
.asm-eq-overlay.asm-eq-minimized {
  padding: 2px 4px;
  cursor: grab;
}
body.asm-darkplots .asm-eq-overlay {
  background: rgba(15, 23, 42, 0.85);
  border-color: rgba(226, 232, 240, 0.25);
}
body.asm-darkplots .asm-eq-text { color: #e2e8f0; }
body.asm-darkplots .asm-eq-toggle { background: rgba(255,255,255,0.10); color: #cbd5e1; }
body.asm-darkplots .asm-eq-toggle:hover { background: rgba(255,255,255,0.18); color: #fff; }

/* Annotation overlay on top of the scatter canvas / 3D wrap. The layer
 * itself is absolutely positioned to fill the chart-wrap; each note is
 * an absolutely-positioned div inside. */
.asm-annot-layer {
  position: absolute; inset: 8px;   /* matches chart-wrap padding */
  pointer-events: none;
  z-index: 7;
}
.asm-annot {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: 12px; color: #0f172a; line-height: 1.35;
  font-family: inherit;
  max-width: 260px;
  max-height: 200px;
  overflow: hidden;
  padding: 4px 8px;
  border-radius: 4px;
  pointer-events: auto;
  cursor: grab;
  user-select: none;
  word-break: break-word;
  overflow-wrap: anywhere;
}
/* Render rich text consistently with the editor — bullets stay inside
   the box, paragraphs/lists get sensible spacing, inline color and
   font-size from execCommand are preserved. */
.asm-annot p { margin: 0 0 4px; }
.asm-annot p:last-child { margin-bottom: 0; }
.asm-annot ul, .asm-annot ol { margin: 2px 0; padding-left: 18px; }
.asm-annot li { margin: 1px 0; }
.asm-annot b, .asm-annot strong { font-weight: 700; }
.asm-annot i, .asm-annot em       { font-style: italic; }
.asm-annot u                      { text-decoration: underline; }
.asm-annot:hover { outline: 1.5px dashed var(--primary); outline-offset: 2px; }
.asm-annot.asm-annot-dragging { cursor: grabbing; opacity: 0.9; box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
.asm-annot-bg { background: rgba(255, 255, 255, 0.92); border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
.asm-annot-transparent { background: transparent; }
body.asm-darkplots .asm-annot { color: #e2e8f0; }
body.asm-darkplots .asm-annot-bg { background: rgba(15, 23, 42, 0.85); border-color: #334155; }
/* Placement mode — show crosshair on hovered chart */
.asm-chart-wrap.asm-annot-placing { cursor: crosshair !important; }
.asm-chart-wrap.asm-annot-placing canvas { cursor: crosshair !important; }
.asm-annot-editor {
  font-family: inherit;
}
.asm-annot-editor:focus { outline: 2px solid var(--primary); outline-offset: -2px; }
/* Keep bullets / numbered lists inside the editor's padding box —
   default <ul>/<ol> padding-inline-start of 40px puts the marker
   to the left of the editor border when the editor itself only has
   10px of left padding. */
.asm-annot-editor ul,
.asm-annot-editor ol {
  margin: 4px 0;
  padding-left: 22px;
}
.asm-annot-editor li { margin: 2px 0; }

/* Brush-selection rectangle drawn on the scatter canvas */
.asm-brush-rect {
  position: absolute;
  pointer-events: none;
  border: 1.5px dashed var(--primary);
  background: rgba(37, 99, 235, 0.10);
  z-index: 6;
}

/* ============================================================
   Designer (Predictor) — Phase 1: per-output regression with
   live input sliders. Two-column main pane: inputs (sliders) on
   the left, predicted outputs table on the right.
   ============================================================ */
.asm-designer-wrap {
  padding: 10px 18px 20px;
  overflow: auto;
  background: #f8fafc;
  flex: 1 1 auto;
  min-height: 0;
}
body.asm-darkplots .asm-designer-wrap { background: #111827; color: #f1f5f9; }
/* asm-darkplots is a scoped SKIN, not a full theme swap, so the shared --text /
   --text-muted tokens keep their light-mode (dark-grey) values and go unreadable on
   the dark analyze surfaces. Re-scope the tokens across the whole dark overlay so
   every var()-driven label flips to a legible light-on-dark value in one place;
   elements with their own explicit darkplots color still win. */
body.asm-darkplots .asm-analyze-overlay { --text: #e2e8f0; --text-muted: #94a3b8; color-scheme: dark; }
/* color-scheme:dark above themes the NATIVE controls (select drop-downs, option
   lists, checkboxes, scrollbars) — otherwise a <select>'s option popup renders on
   the OS's white background while inheriting the now-light text = invisible.
   Below: the few hardcoded (non-token) slate/indigo colors the rescope can't reach. */
body.asm-darkplots .asm-side-help { color: #cbd5e1; }
body.asm-darkplots .asm-toggle { color: #94a3b8; }
body.asm-darkplots .asm-nr-badge,
body.asm-darkplots .asm-nr-collapsed-rec { color: #a5b4fc; }
/* Inverse-design role captions — inline colors moved to classes so dark mode can
   lift the (too-dark-on-dark) blue; the emerald stays bright enough but is paired. */
.asm-role-cap-use { color: #2563eb; font-weight: 600; }
.asm-role-cap-improve { color: #10b981; font-weight: 600; }
body.asm-darkplots .asm-role-cap-use { color: #60a5fa; }
body.asm-darkplots .asm-role-cap-improve { color: #34d399; }

.asm-designer-empty {
  max-width: 520px;
  margin: 48px auto;
  padding: 24px 28px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 10px;
  text-align: left;
}
.asm-designer-empty h3 {
  margin: 0 0 10px;
  font-size: 16px;
  color: #0f172a;
}
body.asm-darkplots .asm-designer-empty {
  background: #1f2937;
  border-color: #374151;
}
body.asm-darkplots .asm-designer-empty h3 { color: #f1f5f9; }

.asm-designer-grid {
  display: grid;
  /* Outputs carry a 5-column table (Output · Predicted · Target · Observed
     range · Model), so give that side more room and let the inputs (just
     sliders) run narrower. */
  grid-template-columns: minmax(240px, 0.82fr) minmax(430px, 1.45fr);
  gap: 20px;
  align-items: start;
}
@media (max-width: 900px) {
  .asm-designer-grid { grid-template-columns: 1fr; }
}

.asm-designer-inputs,
.asm-designer-outputs {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px 16px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.03);
  /* min-width:0 lets the grid item shrink to its column track so the
   * prediction table can never push its panel past the right edge of
   * the workspace; overflow-x:auto then scrolls the table inside the
   * card if it still needs more width on narrow viewports. */
  min-width: 0;
  overflow-x: auto;
}
body.asm-darkplots .asm-designer-inputs,
body.asm-darkplots .asm-designer-outputs {
  background: #1f2937;
  border-color: #374151;
}

.asm-designer-section-title {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #334155;
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
body.asm-darkplots .asm-designer-section-title {
  color: #e5e7eb;
  border-bottom-color: #374151;
}

/* Slider row: label on top, range + numeric + observed-range below */
.asm-designer-slider-row {
  margin-bottom: 9px;
}
.asm-designer-slider-row:last-child { margin-bottom: 4px; }

.asm-designer-slider-label {
  display: block;
  font-size: 12.5px;
  font-weight: 500;
  color: #0f172a;
  margin-bottom: 3px;
  text-transform: none;
  letter-spacing: 0;
}
body.asm-darkplots .asm-designer-slider-label { color: #f1f5f9; }

/* The value now rides with the label, so the track (and its response curve)
   gets the full column width. */
.asm-designer-slider-head {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 3px;
}
.asm-designer-slider-head .asm-designer-slider-label { flex: 1; margin-bottom: 0; min-width: 0; }
.asm-designer-slider-bar {
  display: block;
}
.asm-designer-slider-track { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.asm-designer-slider-bar input[type="range"] {
  width: 100%;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--primary);
}
input.asm-designer-slider-num {
  flex: none;
  width: 76px;
  padding: 2px 6px;
  font-size: 12px;
  font-family: inherit;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #fff;
  text-align: right;
}
input.asm-designer-slider-num:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 2px rgba(37,99,235,0.18);
}
body.asm-darkplots input.asm-designer-slider-num {
  background: #111827;
  border-color: #374151;
  color: #f1f5f9;
}

.asm-designer-slider-ends {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: #94a3b8;
  font-variant-numeric: tabular-nums;
  position: absolute; left: 3px; right: 3px; bottom: 1px;
  pointer-events: none;
  text-shadow: 0 0 3px #fff, 0 0 2px #fff;
  padding: 0 2px;
}
/* Dark mode: lighten the min/max labels AND drop the white glow — on a dark track
   that halo makes the small numbers read as blurry. */
body.asm-darkplots .asm-designer-slider-ends { color: #cbd5e1; text-shadow: none; }

.asm-designer-help {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  font-size: 11px;
  color: #64748b;
  line-height: 1.5;
}
.asm-designer-role-cap { font-size: 10.5px; color: #64748b; margin: 4px 2px 0; line-height: 1.35; }
body.asm-darkplots .asm-designer-role-cap { color: #94a3b8; }
body.asm-darkplots .asm-designer-help {
  color: #cbd5e1;
  border-top-color: #374151;
}

/* --- Designer dark-mode surfaces: recipe card, badges, buttons, blocks --- */
/* Recommended recipe card */
.asm-designer-recipe { background: #f0fdf4; border: 1px solid #bbf7d0; }
.asm-designer-recipe-title { color: #065f46; }
.asm-designer-recipe-score { color: #16a34a; }
.asm-designer-recipe-sub { color: #64748b; }
.asm-designer-recipe-chip { font-size: 12px; background: #fff; border: 1px solid #d1fae5; border-radius: 6px; padding: 2px 8px; white-space: nowrap; color: #0f172a; }
.asm-designer-recipe-chip-lbl { color: #64748b; }
body.asm-darkplots .asm-designer-recipe { background: #0f2417; border-color: #14532d; }
body.asm-darkplots .asm-designer-recipe-title { color: #6ee7b7; }
body.asm-darkplots .asm-designer-recipe-score { color: #4ade80; }
body.asm-darkplots .asm-designer-recipe-sub { color: #94a3b8; }
body.asm-darkplots .asm-designer-recipe-chip { background: #1f2937; border-color: #14532d; color: #e5e7eb; }
body.asm-darkplots .asm-designer-recipe-chip-lbl { color: #94a3b8; }
/* Apply / Find-optimal flash — pulses a green ring around the Recommended-recipe
   card so the user sees where Apply landed (the card renders up in the left
   column, easy to miss when the candidates panel is scrolled). Honors
   reduced-motion. */
.asm-designer-recipe-flash { animation: asmRecipeFlash 0.7s ease-out 2; }
@keyframes asmRecipeFlash {
  0%   { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.55); }
  60%  { box-shadow: 0 0 0 7px rgba(22, 163, 74, 0.12); }
  100% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .asm-designer-recipe-flash { animation: asmRecipeFlashRM 1s ease-out 1; }
  @keyframes asmRecipeFlashRM { 0%, 60% { box-shadow: 0 0 0 4px rgba(22, 163, 74, 0.28); } 100% { box-shadow: 0 0 0 0 rgba(22, 163, 74, 0); } }
}

/* PRO badge + optimizer lock */
.asm-designer-pro-badge { color: #7c3aed; background: #f3e8ff; border: 1px solid #e9d5ff; }
.asm-designer-prolock { border: 1px dashed #cbd5e1; background: #f8fafc; }
.asm-designer-prolock-title { color: #334155; }
body.asm-darkplots .asm-designer-pro-badge { color: #c4b5fd; background: #2e1065; border-color: #5b21b6; }
body.asm-darkplots .asm-designer-prolock { border-color: #374151; background: #111827; }
body.asm-darkplots .asm-designer-prolock-title { color: #e5e7eb; }

/* Colored action buttons (.btn.* for specificity over .btn-secondary) */
.btn.asm-btn-explain { background: #f5f3ff; border-color: #ddd6fe; color: #6d28d9; }
.btn.asm-btn-suggest { background: #ecfdf5; border-color: #a7f3d0; color: #065f46; }
body.asm-darkplots .btn.asm-btn-explain { background: #2e1065; border-color: #5b21b6; color: #c4b5fd; }
body.asm-darkplots .btn.asm-btn-suggest { background: #064e3b; border-color: #065f46; color: #6ee7b7; }

/* Inline number input (Top N) */
.asm-designer-num-input { background: #fff; }
body.asm-darkplots .asm-designer-num-input { background: #111827; border-color: #374151; color: #f1f5f9; }

/* Model-quality badge */
.asm-mq { font-size: 12px; font-weight: 600; border-radius: 999px; padding: 3px 11px; border: 1px solid; }
.asm-mq-none { color: #64748b; background: #f1f5f9; border-color: #64748b55; }
.asm-mq-good { color: #16a34a; background: #f0fdf4; border-color: #16a34a55; }
.asm-mq-fair { color: #b45309; background: #fffbeb; border-color: #b4530955; }
.asm-mq-weak { color: #dc2626; background: #fef2f2; border-color: #dc262655; }
body.asm-darkplots .asm-mq-none { color: #cbd5e1; background: #1f2937; border-color: #475569; }
body.asm-darkplots .asm-mq-good { color: #4ade80; background: #0f2417; border-color: #166534; }
body.asm-darkplots .asm-mq-fair { color: #fbbf24; background: #2a2010; border-color: #92400e; }
body.asm-darkplots .asm-mq-weak { color: #f87171; background: #2a1414; border-color: #991b1b; }
.asm-designer-flowstrip { font-size: 11.5px; color: #64748b; line-height: 1.4; }
body.asm-darkplots .asm-designer-flowstrip { color: #94a3b8; }
/* Footer variant — the 1→2→3→4 workflow reference now sits below the diagnostic
   cards, set off by a light rule so it reads as a footnote, not a banner. */
.asm-designer-flowstrip-foot { display: block; margin: 0 2px 14px; padding-top: 7px; border-top: 1px dashed var(--border); }
/* Collapsed advisor → the flowstrip is the first thing in the pane, so its dashed
   separator would be an orphan line right under the title bar. Drop it there. */
.asm-designer-wrap > .asm-designer-flowstrip-foot:first-child { border-top: none; padding-top: 0; }

/* Model health read — consolidated "can I trust this fit?" chips. */
/* Model advisor — one collapse over the three advisory cards (Model health, What
   to run next, Progress over rounds). Its toggle + collapsed rollup live in the
   pinned Designer TITLE BAR (.asm-adv-hdr), so collapsing frees the whole pane and
   the summary stays visible above the predictions without a separate line. */
.asm-adv-hdr { display: flex; align-items: center; align-self: center; min-width: 0; }
.asm-hm-titlebar .asm-hm-actions { margin-left: auto; }   /* keep Save/Print hard right */
.asm-adv-hdr-btn {
  display: inline-flex; align-items: center; gap: 7px; max-width: 100%;
  padding: 3px 8px; margin: 0; background: none; border: 1px solid transparent;
  border-radius: 7px; cursor: pointer; text-align: left; font: inherit; color: inherit;
}
.asm-adv-hdr-btn:hover { background: var(--surface-2, #f1f5f9); border-color: var(--border); }
.asm-adv-hdr-label { font-size: 12px; font-weight: 700; letter-spacing: .01em; color: var(--text-muted, #64748b); }
.asm-advisor-caret {
  width: 6px; height: 6px; flex: none;
  border-right: 1.5px solid var(--text-muted, #94a3b8);
  border-bottom: 1.5px solid var(--text-muted, #94a3b8);
  transform: rotate(45deg); transition: transform .15s; margin-right: 1px;
}
.asm-adv-hdr-btn.collapsed .asm-advisor-caret { transform: rotate(-45deg); }
.asm-advisor-cards { padding-top: 0; }
.asm-advisor-cards > :last-child { margin-bottom: 6px; }   /* sit closer to the flow-strip separator */
/* Rollup: verdict pill · next-strategy chip · trend sparkline · Pro nudge. */
.asm-advisor-rollup { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; min-width: 0; }
.asm-advisor-roll-verdict { display: inline-flex; align-items: center; font-size: 11px; font-weight: 600; border-radius: 999px; padding: 1px 9px; border: 1px solid; white-space: nowrap; }
.asm-advisor-roll-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; display: inline-block; margin-right: 5px; }
.asm-advisor-roll-dot.warn { background: #d97706; }
.asm-advisor-roll-dot.bad  { background: #dc2626; }
.asm-advisor-roll-strat { font-size: 11px; color: var(--text-muted, #64748b); white-space: nowrap; }
.asm-advisor-roll-strat strong { color: var(--text); font-weight: 600; }
.asm-advisor-roll-trend { display: inline-flex; align-items: center; }
/* Dark-plots is a scoped body class, NOT a full theme swap, so var(--text) /
   var(--text-muted) keep their light (dark-grey) values — unreadable on the dark
   title bar. Restore contrast for the title-row advisor text explicitly. */
body.asm-darkplots .asm-adv-hdr-label,
body.asm-darkplots .asm-advisor-roll-strat { color: #94a3b8; }
body.asm-darkplots .asm-advisor-roll-strat strong { color: #e2e8f0; }
/* Free nudge in the collapsed rollup — verdict is theirs, next steps are Pro. */
.asm-advisor-roll-pro {
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  font-size: 10.5px; font-weight: 700; letter-spacing: .02em;
  color: #7c3aed; background: #f5f3ff; border: 1px solid #c4b5fd;
  border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
body.asm-darkplots .asm-advisor-roll-pro { color: #c4b5fd; background: #241b3a; border-color: #6d28d9; }
/* Compact locked teaser standing in for a Pro advisor card (Free users). */
.asm-adv-teaser {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  margin: 0 0 8px; padding: 9px 12px; border: 1px dashed #c4b5fd;
  border-radius: 9px; background: #faf9ff;
}
.asm-adv-teaser:hover { border-color: #a78bfa; background: #f5f3ff; }
.asm-adv-teaser-lock { color: #7c3aed; flex: none; }
.asm-adv-teaser-title { font-size: 12px; font-weight: 700; letter-spacing: .02em; color: var(--text); }
.asm-adv-teaser-sub { font-size: 11px; color: var(--text-muted, #64748b); flex: 1 1 auto; min-width: 0; }
.asm-adv-teaser-sub strong { color: #7c3aed; font-weight: 700; }
.asm-adv-teaser-cta { font-size: 11px; font-weight: 700; color: #7c3aed; white-space: nowrap; flex: none; }
body.asm-darkplots .asm-adv-teaser { border-color: #6d28d9; background: #1c1630; }
body.asm-darkplots .asm-adv-teaser:hover { background: #241b3a; }
body.asm-darkplots .asm-adv-teaser-lock,
body.asm-darkplots .asm-adv-teaser-sub strong,
body.asm-darkplots .asm-adv-teaser-cta { color: #c4b5fd; }
body.asm-darkplots .asm-adv-hdr-btn:hover { background: #1f2937; border-color: #334155; }
.asm-mh-card { margin: 0 0 14px; padding: 11px 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface, #fff); }
.asm-mh-verdict { align-self: center; font-size: 11px; font-weight: 600; border-radius: 999px; padding: 1px 9px; border-width: 1px; border-style: solid; cursor: pointer; white-space: nowrap; }
/* "Can't build a model yet" callout — shown when nothing fits, in place of the
   (empty) health/next-runs cards. Amber, actionable. */
.asm-cantfit { border-color: #fcd34d; background: #fffbeb; }
body.asm-darkplots .asm-cantfit { border-color: #92400e; background: #2a2010; }
.asm-cantfit-head { font-size: 12px; font-weight: 700; letter-spacing: .02em; color: #92400e; display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
body.asm-darkplots .asm-cantfit-head { color: #fbbf24; }
.asm-cantfit-icon { color: #d97706; font-size: 12px; }
body.asm-darkplots .asm-cantfit-icon { color: #fbbf24; }
.asm-cantfit-reason { font-size: 12px; color: var(--text); line-height: 1.45; margin-bottom: 3px; }
.asm-cantfit-fix { font-size: 11.5px; color: var(--text-muted, #64748b); line-height: 1.5; }
.asm-cantfit-btn { margin-left: 4px; vertical-align: baseline; }
body.asm-darkplots .asm-mh-card { background: #1a2231; border-color: #334155; }
/* Header is a full-width toggle button (collapse/expand the checks). */
.asm-mh-head { display: flex; align-items: baseline; gap: 8px; margin: 0 0 7px; width: 100%; padding: 0; background: none; border: none; cursor: pointer; text-align: left; font: inherit; color: inherit; }
.asm-mh-caret { align-self: center; width: 6px; height: 6px; flex: none; border-right: 1.5px solid var(--text-muted, #94a3b8); border-bottom: 1.5px solid var(--text-muted, #94a3b8); transform: rotate(45deg); transition: transform .15s; margin-right: 1px; }
.asm-mh-card.collapsed .asm-mh-caret { transform: rotate(-45deg); }
.asm-mh-title { font-size: 12px; font-weight: 700; letter-spacing: .02em; color: var(--text); }
.asm-mh-sub { font-size: 11px; font-weight: 400; color: var(--text-muted, #94a3b8); }
.asm-mh-dot.asm-mh-summary { width: 8px; height: 8px; align-self: center; margin-left: auto; background: currentColor; border: none; }
.asm-mh-card:not(.collapsed) .asm-mh-dot.asm-mh-summary { display: none; }
.asm-mh-card.collapsed .asm-mh-head { margin-bottom: 0; }
.asm-mh-card.collapsed .asm-mh-chips { display: none; }
.asm-mh-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.asm-mh-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 500; border-radius: 999px;
  padding: 3px 10px 3px 8px; border: 1px solid;
  /* Each chip carries a plain-English explanation (custom data-tip tooltip) — a
     help cursor signals it's hoverable so users discover them. */
  cursor: help;
}

/* Custom tooltip (data-tip) — styled, instant, readable for long text.
   Named -dtip to avoid the pre-existing .fl-tip (a small nowrap chip tip). */
.fl-dtip {
  position: fixed; z-index: 5000; max-width: 320px;
  background: #0f172a; color: #e2e8f0;
  font-size: 12px; line-height: 1.5; font-weight: 400;
  text-transform: none; letter-spacing: normal; text-align: left; white-space: normal;
  padding: 8px 11px; border-radius: 8px;
  box-shadow: 0 8px 28px rgba(15, 23, 42, .3);
  pointer-events: none; opacity: 0; transform: translateY(3px);
  transition: opacity .12s ease, transform .12s ease;
}
.fl-dtip.fl-dtip-on { opacity: 1; transform: none; }
.fl-dtip.fl-dtip-below { transform: translateY(-3px); }
.fl-dtip.fl-dtip-below.fl-dtip-on { transform: none; }
.fl-dtip-p { display: block; }
.fl-dtip-p + .fl-dtip-p { margin-top: 7px; }
.asm-mh-chip strong { font-weight: 700; }
.asm-mh-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: currentColor; }
.asm-mh-good    { color: #16a34a; background: #f0fdf4; border-color: #16a34a55; }
.asm-mh-warn    { color: #b45309; background: #fffbeb; border-color: #b4530955; }
.asm-mh-bad     { color: #dc2626; background: #fef2f2; border-color: #dc262655; }
.asm-mh-neutral { color: #64748b; background: #f1f5f9; border-color: #64748b55; }
/* Text stays dark for legibility, but the DOT carries the status colour so the
   card reads at a glance (the tinted bg/border above reinforce it). */
.asm-mh-chip strong, .asm-mh-chip { color: var(--text, #1e293b); }
.asm-mh-good    .asm-mh-dot { background: #16a34a; }
.asm-mh-warn    .asm-mh-dot { background: #d97706; }
.asm-mh-bad     .asm-mh-dot { background: #dc2626; }
.asm-mh-neutral .asm-mh-dot { background: #94a3b8; }
body.asm-darkplots .asm-mh-good    { color: #4ade80; background: #0f2417; border-color: #166534; }
body.asm-darkplots .asm-mh-warn    { color: #fbbf24; background: #2a2010; border-color: #92400e; }
body.asm-darkplots .asm-mh-bad     { color: #f87171; background: #2a1414; border-color: #991b1b; }
body.asm-darkplots .asm-mh-neutral { color: #cbd5e1; background: #1f2937; border-color: #475569; }
body.asm-darkplots .asm-mh-good    .asm-mh-dot { background: #4ade80; }
body.asm-darkplots .asm-mh-warn    .asm-mh-dot { background: #fbbf24; }
body.asm-darkplots .asm-mh-bad     .asm-mh-dot { background: #f87171; }
body.asm-darkplots .asm-mh-neutral .asm-mh-dot { background: #94a3b8; }

/* "What to run next" — recommends among the three augmentation strategies. */
.asm-nr-card { margin: 0 0 14px; padding: 11px 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface, #fff); }
body.asm-darkplots .asm-nr-card { background: #1a2231; border-color: #334155; }
.asm-nr-title { font-size: 12px; font-weight: 700; letter-spacing: .02em; color: var(--text); margin-bottom: 9px; }
.asm-nr-sub { font-size: 11px; font-weight: 400; color: var(--text-muted, #94a3b8); margin-left: 6px; }
.asm-nr-info { align-self: center; margin-left: 5px; font-size: 11px; font-weight: 400; color: var(--text-muted, #94a3b8); cursor: help; opacity: .75; flex: none; }
.asm-nr-info:hover, .asm-nr-info:focus { opacity: 1; color: var(--primary); outline: none; }
/* Collapsible header (button) */
/* Explicit font (not `font: inherit`) so the title text matches Model health /
   Progress at 12px/700 — the shorthand reset was clobbering .asm-nr-title and
   leaving the button text at the inherited 14px/400. Sub-spans override below. */
.asm-nr-head-btn { display: flex; align-items: baseline; gap: 6px; width: 100%; padding: 0; background: none; border: none; cursor: pointer; text-align: left; font-family: inherit; font-size: 12px; font-weight: 700; letter-spacing: .02em; color: inherit; }
.asm-nr-caret { align-self: center; width: 6px; height: 6px; flex: none; border-right: 1.5px solid var(--text-muted, #94a3b8); border-bottom: 1.5px solid var(--text-muted, #94a3b8); transform: rotate(45deg); transition: transform .15s; margin-right: 3px; }
.asm-nr-card.collapsed .asm-nr-caret { transform: rotate(-45deg); }
.asm-nr-card.collapsed .asm-nr-title { margin-bottom: 0; }
.asm-nr-card.collapsed .asm-nr-rows { display: none; }
.asm-nr-collapsed-rec { margin-left: auto; font-size: 10.5px; font-weight: 700; color: var(--primary); background: rgba(99,102,241,.12); border: 1px solid rgba(99,102,241,.3); border-radius: 4px; padding: 1px 7px; }
.asm-nr-row { border: 1px solid var(--border); border-radius: 8px; padding: 9px 11px; margin-bottom: 7px; }
.asm-nr-row:last-child { margin-bottom: 0; }
.asm-nr-rec { border-color: var(--primary); background: rgba(99, 102, 241, .05); }
body.asm-darkplots .asm-nr-rec { background: rgba(99, 102, 241, .12); }
.asm-nr-head { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
.asm-nr-name { font-size: 13px; font-weight: 700; color: var(--text); }
.asm-nr-tag { font-size: 10.5px; color: var(--text-muted, #94a3b8); }
.asm-nr-badge { margin-left: auto; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--primary); background: rgba(99, 102, 241, .12); border: 1px solid rgba(99, 102, 241, .3); border-radius: 4px; padding: 1px 6px; }
.asm-nr-desc { font-size: 12px; color: var(--text); line-height: 1.45; margin-bottom: 3px; }
.asm-nr-why { font-size: 11.5px; color: var(--text-muted, #64748b); line-height: 1.45; margin-bottom: 8px; }
body.asm-darkplots .asm-nr-why { color: #94a3b8; }

/* Progress over rounds — the retrospective learning curve (Phase 3a) */
.asm-pc-card { margin: 0 0 14px; padding: 11px 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface, #fff); }
body.asm-darkplots .asm-pc-card { background: #1a2231; border-color: #334155; }
.asm-pc-head { display: flex; align-items: baseline; gap: 6px; width: 100%; padding: 0; background: none; border: none; cursor: pointer; text-align: left; font: inherit; color: inherit; }
.asm-pc-head .asm-pc-caret { align-self: center; width: 6px; height: 6px; flex: none; border-right: 1.5px solid var(--text-muted, #94a3b8); border-bottom: 1.5px solid var(--text-muted, #94a3b8); transform: rotate(45deg); transition: transform .15s; margin-right: 3px; }
.asm-pc-card.collapsed .asm-pc-caret { transform: rotate(-45deg); }
.asm-pc-head { font-size: 12px; font-weight: 700; letter-spacing: .02em; color: var(--text); }
.asm-pc-sub { font-size: 11px; font-weight: 400; color: var(--text-muted, #94a3b8); margin-left: 6px; }
.asm-pc-body { margin-top: 10px; }
.asm-pc-card.collapsed .asm-pc-body { display: none; }
.asm-pc-empty { font-size: 11.5px; color: var(--text-muted, #64748b); padding: 4px 2px; }
.asm-pc-svg { width: 100%; height: auto; max-height: 170px; display: block; }
.asm-pc-grid { stroke: var(--border); stroke-width: 1; opacity: .5; }
.asm-pc-guide { stroke: var(--text-muted, #94a3b8); stroke-width: 1; stroke-dasharray: 2 3; opacity: .45; }
.asm-pc-line { stroke: var(--primary, #6366f1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.asm-pc-dot { fill: var(--primary, #6366f1); cursor: help; }
.asm-pc-axlbl { fill: var(--text-muted, #94a3b8); font-size: 9px; }
.asm-pc-trend { font-size: 11.5px; color: var(--text); line-height: 1.45; margin-top: 6px; }
.asm-pc-rounds { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 8px; }
.asm-pc-round { font-size: 10.5px; color: var(--text); background: var(--ground, #f1f5f9); border: 1px solid var(--border); border-radius: 4px; padding: 1px 7px; white-space: nowrap; cursor: help; }
body.asm-darkplots .asm-pc-round { background: #1f2937; border-color: #334155; }
.asm-pc-round-arr { color: var(--text-muted, #94a3b8); font-size: 11px; }
.asm-pc-spark { margin-left: auto; align-self: center; display: inline-flex; }
.asm-pc-spark-line { stroke: var(--primary, #6366f1); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
.asm-pc-spark-dot { fill: var(--primary, #6366f1); }
.asm-pc-caveat { font-size: 10.5px; color: var(--text-muted, #94a3b8); line-height: 1.4; margin-top: 6px; font-style: italic; }

.asm-designer-help-link {
  background: transparent; border: none; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 500;
  color: var(--primary); padding: 1px 2px; text-transform: none; letter-spacing: 0;
}
.asm-designer-help-link:hover { text-decoration: underline; }
body.asm-darkplots .asm-designer-help-link { color: #93c5fd; }
/* Step badge — ties the 1-2-3-4 flow strip to the matching section headers */
.asm-step-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--primary); color: #fff;
  font-size: 10px; font-weight: 700; line-height: 1;
  vertical-align: middle; flex-shrink: 0;
}
.asm-step-badge-done { background: #16a34a; }
body.asm-darkplots .asm-step-badge { background: #3b82f6; }
body.asm-darkplots .asm-step-badge-done { background: #16a34a; }
/* Sidebar "what goes in the model" badges: A/B in slate, against the workflow's
   1-4 in blue. Letters rather than numbers because the two sequences are
   unrelated — picking Outputs (B) and setting a Target on a prediction (2) are
   different actions, and sharing a glyph invited reading them as one sequence. */
.asm-step-badge-pick {
  background: #334155; color: #fff; border: none;
  width: 16px; height: 16px; font-size: 9.5px;
}
body.asm-darkplots .asm-step-badge-pick { background: #64748b; color: #fff; }

/* Diagnostics + coefficients sub-panels */
.asm-designer-diag, .asm-designer-coeffs { background: #f8fafc; }
body.asm-darkplots .asm-designer-diag, body.asm-darkplots .asm-designer-coeffs { background: #111827; }

/* Prediction table */
.asm-designer-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
.asm-designer-table thead th {
  text-align: left;
  padding: 6px 10px 8px;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #64748b;
  border-bottom: 1px solid var(--border);
  background: transparent;
}
.asm-designer-table tbody td {
  padding: 10px;
  border-bottom: 1px solid #f1f5f9;
  vertical-align: middle;
}
/* Kill the global `tbody tr` border (darker --border) that stacked a second,
   different-shade line under each row on top of the intended light td border. */
.asm-designer-table tbody tr { border-bottom: none; }
.asm-designer-table tbody tr:last-child td { border-bottom: none; }
body.asm-darkplots .asm-designer-table thead th { color: #cbd5e1; border-bottom-color: #374151; }
body.asm-darkplots .asm-designer-table tbody td { border-bottom-color: #374151; }

.asm-designer-out-name {
  font-weight: 500;
  color: #0f172a;
}
body.asm-darkplots .asm-designer-out-name { color: #f1f5f9; }

.asm-designer-out-pred {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.asm-designer-out-pred strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--primary);
}
body.asm-darkplots .asm-designer-out-pred strong { color: #60a5fa; }
.asm-designer-pi {
  font-size: 11px;
  color: #94a3b8;
  margin-left: 4px;
}

.asm-designer-out-range {
  font-variant-numeric: tabular-nums;
  color: #64748b;
  font-size: 11.5px;
  white-space: nowrap;
}
body.asm-darkplots .asm-designer-out-range { color: #cbd5e1; }
body.asm-darkplots .asm-designer-pi { color: #cbd5e1; }
body.asm-darkplots .asm-designer-target-dash { color: #cbd5e1; }

/* Stays a real <td> (inherits vertical-align:middle from tbody td) so its bottom
   border lines up with the sibling cells on tall/wrapped rows. `display:flex` on
   the td itself dropped its table-cell height, leaving its border ~5px too high —
   the misalignment. The flex lives on an inner wrapper instead. */
.asm-designer-out-r2-inner {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
}
.asm-designer-r2 {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: help;
}
.asm-designer-r2.r2-strong { background: #dcfce7; color: #166534; }
.asm-designer-r2.r2-medium { background: #fef9c3; color: #854d0e; }
.asm-designer-r2.r2-weak   { background: #fee2e2; color: #991b1b; }
body.asm-darkplots .asm-designer-r2.r2-strong { background: rgba(34,197,94,0.18);  color: #86efac; }
body.asm-darkplots .asm-designer-r2.r2-medium { background: rgba(234,179,8,0.18);  color: #fde68a; }
body.asm-darkplots .asm-designer-r2.r2-weak   { background: rgba(239,68,68,0.18);  color: #fca5a5; }
.asm-designer-r2.r2-neutral { background: #f1f5f9; color: #475569; }
body.asm-darkplots .asm-designer-r2.r2-neutral { background: rgba(148,163,184,0.18); color: #cbd5e1; }

.asm-designer-r2.asm-designer-loo {
  font-weight: 500;
  font-size: 10.5px;
  opacity: 0.92;
}

/* Sidebar column-picker list — scrollable when many columns */
.asm-designer-collist {
  max-height: 220px;
  overflow-y: auto;
  padding: 4px 4px 4px 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #fafbfc;
}
.asm-designer-collist label.asm-side-check {
  padding: 3px 8px !important;
  font-size: 11.5px;
}
body.asm-darkplots .asm-designer-collist {
  background: #111827;
  border-color: #374151;
}

/* Inverse-design controls — sit below the input sliders */
.asm-designer-invdesign {
  border-top: 1px dashed var(--border);
  margin-top: 14px;
  padding-top: 0;
}
body.asm-darkplots .asm-designer-invdesign { border-top-color: #374151; }

/* Target column: dropdown + 1-2 small numeric inputs inline */
.asm-designer-out-target {
  white-space: nowrap;
}
.asm-designer-target-row {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.asm-designer-target-mode {
  font-size: 11px;
  font-family: inherit;
  padding: 3px 4px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #fff;
  cursor: pointer;
  text-transform: none;
  letter-spacing: 0;
  width: auto;
}
.asm-designer-target-num {
  width: 58px !important;
  padding: 3px 5px;
  font-size: 11px;
  font-family: inherit;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #fff;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.asm-designer-target-num:focus,
.asm-designer-target-mode:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 2px rgba(37,99,235,0.18);
}
.asm-designer-target-dash {
  font-size: 11px;
  color: #94a3b8;
}
body.asm-darkplots .asm-designer-target-mode,
body.asm-darkplots .asm-designer-target-num {
  background: #111827;
  border-color: #374151;
  color: #f1f5f9;
}

/* Hit indicator inside the predicted cell */
.asm-designer-hit {
  display: inline-block;
  font-weight: 700;
  font-size: 12px;
  width: 16px;
  height: 16px;
  line-height: 16px;
  text-align: center;
  border-radius: 50%;
  margin-left: 4px;
  vertical-align: middle;
  cursor: help;
}
.asm-designer-hit-yes  { background: #dcfce7; color: #166534; }
.asm-designer-hit-near { background: #fef9c3; color: #854d0e; }
.asm-designer-hit-no   { background: #fee2e2; color: #991b1b; }
body.asm-darkplots .asm-designer-hit-yes  { background: rgba(34,197,94,0.22); color: #86efac; }
body.asm-darkplots .asm-designer-hit-near { background: rgba(234,179,8,0.22); color: #fde68a; }
body.asm-darkplots .asm-designer-hit-no   { background: rgba(239,68,68,0.22); color: #fca5a5; }

/* Candidates panel — top-K results from grid search */
.asm-designer-candidates {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
body.asm-darkplots .asm-designer-candidates { border-top-color: #374151; }

.asm-designer-cand-scroll {
  overflow-x: auto;
  max-height: 360px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #fafbfc;
}
body.asm-darkplots .asm-designer-cand-scroll {
  background: #111827;
  border-color: #374151;
}
/* Candidates table runs many columns (one per input + output) — keep it
   dense by default so more fits before horizontal scroll. A per-table
   zoom control (in the panel header) scales this up/down on demand. */
.asm-designer-candzoom { display: inline-flex; align-items: center; gap: 1px; }
.asm-designer-candzoom span { min-width: 38px; text-align: center; font-size: 11px; color: #64748b; }

/* Full-screen candidates: promote the in-place panel to a fixed overlay.
   Pure CSS (toggled by a body class) so it survives the designer's
   re-renders and Apply/zoom keep working on the same live element. */
body.asm-cand-fullscreen .asm-designer-candidates {
  position: fixed; inset: 16px; z-index: 300; margin: 0;
  background: #fff; border: 1px solid var(--border); border-top: 1px solid var(--border);
  border-radius: 10px; box-shadow: 0 20px 60px rgba(15,23,42,0.35);
  padding: 16px 18px; overflow: auto; padding-top: 14px;
}
body.asm-cand-fullscreen .asm-designer-cand-scroll { max-height: calc(100vh - 130px); }
body.asm-cand-fullscreen.asm-darkplots .asm-designer-candidates { background: #0f172a; border-color: #374151; }

.asm-designer-cand-table {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  min-width: 100%;
  width: max-content;
}
.asm-designer-cand-table thead th {
  position: sticky;
  top: 0;
  background: #f1f5f9;
  z-index: 1;
  white-space: normal;       /* wrap long names → narrower columns */
  max-width: 70px;
  word-break: break-word;
  vertical-align: bottom;
  line-height: 1.15;
  padding: 4px 7px 5px;
}
.asm-designer-cand-table tbody td {
  padding: 4px 8px;
  white-space: nowrap;
}
.asm-designer-cand-table tbody tr:hover {
  background: rgba(37,99,235,0.05);
}
body.asm-darkplots .asm-designer-cand-table thead th { background: #1f2937; color: #e5e7eb; }
body.asm-darkplots .asm-designer-cand-table tbody tr:hover { background: rgba(96,165,250,0.08); }
body.asm-darkplots .asm-designer-cand-table tbody td { color: #e5e7eb; }

.asm-designer-cand-rank {
  font-weight: 600;
  color: #334155;
}
body.asm-darkplots .asm-designer-cand-rank { color: #e5e7eb; }
.asm-designer-cand-score {
  font-weight: 600;
  color: var(--primary);
}
body.asm-darkplots .asm-designer-cand-score { color: #60a5fa; }

/* Pareto-optimal badge on candidate rank */
.asm-designer-pareto-badge {
  display: inline-block;
  margin-left: 3px;
  padding: 1px 5px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  border-radius: 8px;
  background: #ede9fe;
  color: #5b21b6;
  vertical-align: middle;
  cursor: help;
}
body.asm-darkplots .asm-designer-pareto-badge {
  background: rgba(139,92,246,0.22);
  color: #c4b5fd;
}

/* Per-point color picker popup */
.asm-color-menu {
  position: fixed; z-index: 300;
  background: #fff; border: 1px solid var(--border); border-radius: 6px;
  padding: 8px; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
  min-width: 180px;
}
.asm-color-menu-title {
  font-size: 11px; font-weight: 600; color: #475569;
  text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px;
}
.asm-color-swatches {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin-bottom: 8px;
}
.asm-color-swatch {
  width: 26px; height: 22px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.1);
  cursor: pointer; padding: 0;
}
.asm-color-swatch:hover { transform: scale(1.08); }
.asm-color-clear {
  width: 100%; background: #f1f5f9; border: 1px solid var(--border);
  border-radius: 4px; padding: 5px 8px; font-size: 11.5px;
  cursor: pointer; font-family: inherit;
}

/* Correlation heatmap */
.asm-hm-titlebar {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding-bottom: 10px;
  border-bottom: 2px solid var(--border);
  margin-bottom: 14px;
}
/* Designer: the title row also carries the advisor toggle/rollup — bottom-align so
   "Designer" sits on the same baseline as the rollup, and tighten the gap to the
   pane below (the advisor cards sit right under it when expanded). */
.asm-hm-titlebar--designer { align-items: flex-end; margin-bottom: 8px; padding-bottom: 7px; }
.asm-hm-title { font-size: 17px; font-weight: 700; color: #0f172a; line-height: 1.2; }
.asm-hm-subtitle { font-size: 11.5px; color: #64748b; margin-top: 3px; font-variant-numeric: tabular-nums; }
.asm-hm-notes {
  font-size: 11.5px; color: #334155; margin-top: 6px;
  line-height: 1.45; font-style: italic;
  max-width: 70ch;
  white-space: pre-wrap;
}
.asm-hm-actions { display: flex; gap: 6px; flex-shrink: 0; }
.asm-hm-legend {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 11.5px; color: #475569;
  margin-bottom: 10px;
}
.asm-hm-legend-text {
  font-variant-numeric: tabular-nums; font-weight: 600;
  color: #334155;
}
.asm-hm-bar {
  flex: 0 0 180px; height: 12px; border-radius: 3px;
  background: linear-gradient(to right, rgb(220, 38, 38), #ffffff, rgb(37, 99, 235));
  border: 1px solid var(--border);
}
.asm-hm-sigkey { color: #94a3b8; }
.asm-hm-rangetxt {
  color: #475569; font-style: italic;
  padding-left: 8px; margin-left: 4px;
  border-left: 1px solid var(--border);
}
.asm-hm-scroll {
  flex: 1; min-height: 0; overflow: auto;
  /* Frames the correlation grid — a real container edge, so softened not cut. */
  border: 1px solid var(--hairline); border-radius: 8px;
  background: #fff;
}
.asm-hm-grid {
  display: grid;
  gap: 0;
  background: #fff;
}
.asm-hm-cell {
  font-size: 11px; padding: 6px 8px;
  border-right: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
  display: flex; align-items: center; justify-content: center;
  text-align: center; min-height: 32px;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  transition: background 0.08s ease, box-shadow 0.08s ease;
}
.asm-hm-corner {
  background: #f8fafc;
  border-right: 1px solid #cbd5e1;
}
/* Row labels — left side. Allow wrapping to 2 lines for long names. */
.asm-hm-rowhdr {
  background: #f8fafc;
  font-weight: 600; color: #334155;
  font-size: 11px; padding: 6px 10px;
  border-right: 1px solid #cbd5e1;
  text-align: right; justify-content: flex-end; align-items: center;
  min-width: 160px; max-width: 220px;
  white-space: normal;
  line-height: 1.25;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
/* Column labels — bottom. Wrap horizontally as needed, no fixed height
 * (the row sizes to its tallest label, so short labels don't waste space). */
.asm-hm-colhdr-bot {
  background: #f8fafc;
  font-weight: 600; color: #334155;
  font-size: 10.5px; padding: 6px 4px;
  border-top: 1px solid #cbd5e1;
  border-right: 1px solid #e2e8f0;
  min-height: auto;
  align-items: flex-start;
  justify-content: center;
  white-space: normal;
  line-height: 1.25;
  text-align: center;
  word-break: break-word;
  overflow: hidden;
}
.asm-hm-data {
  background: #fff; border: none; cursor: pointer;
  font-weight: 600; font-family: inherit;
  border-right: 1px solid rgba(0,0,0,0.05);
  border-bottom: 1px solid rgba(0,0,0,0.05);
  display: flex; flex-direction: column; gap: 0;
  padding: 4px 6px; min-height: 36px;
}
.asm-hm-data:hover:not(.asm-hm-diag) {
  outline: 2px solid #0f172a; outline-offset: -2px;
  z-index: 2; position: relative;
}
.asm-hm-diag { cursor: default; opacity: 0.55; }
/* Fade out cells where p ≥ 0.05 so significant pairs stand out. */
.asm-hm-insig { opacity: 0.22; }
.asm-hm-insig:hover { opacity: 0.85; }
.asm-hm-val { font-size: 11.5px; line-height: 1.2; }
.asm-hm-stars { font-size: 9px; opacity: 0.85; line-height: 1; }
/* Hover highlight for whole row + column */
.asm-hm-hl-row, .asm-hm-hl-col {
  box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.35);
}
.asm-hm-rowhdr.asm-hm-hl-row,
.asm-hm-colhdr-bot.asm-hm-hl-col {
  background: #dbeafe; color: #1e3a8a;
}
/* Cross-highlight between the grid and the correlations list (same pair). */
/* Clickable trend sparkline in the Test Performance card — opens the full
   Stability View. Subtle hover/focus affordance so it reads as interactive. */
.perf-trend-open:hover { background: #eff6ff; box-shadow: inset 0 0 0 1px #dbeafe; }
.perf-trend-open:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }

.asm-hm-list-row.asm-hm-linked { background: #dbeafe; }
.asm-hm-list-row.asm-hm-linked td { color: #0f172a; }
.asm-hm-data.asm-hm-linked {
  outline: 2px solid #2563eb; outline-offset: -2px;
  box-shadow: inset 0 0 0 2px rgba(37, 99, 235, 0.85); position: relative; z-index: 1;
}
/* Briefly flashes when the user returns to the heatmap from a scatter
 * drill-down — shows them exactly which cell the scatter came from.
 *
 * Pattern: three quick on/off pulses (≈900 ms each) with a brief rest
 * between, ≈2.7 s total. Catches the eye like a notification badge
 * instead of one slow fade. */
@keyframes asmHmFlash {
  0%   { box-shadow: 0 0 0 0  rgba(251, 191, 36, 0),    inset 0 0 0 0 rgba(180, 83, 9, 0); }
  20%  { box-shadow: 0 0 0 6px rgba(251, 191, 36, 0.85), inset 0 0 0 3px rgba(180, 83, 9, 0.90); }
  55%  { box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.55), inset 0 0 0 2px rgba(180, 83, 9, 0.55); }
  85%  { box-shadow: 0 0 0 0  rgba(251, 191, 36, 0),    inset 0 0 0 0 rgba(180, 83, 9, 0); }
  100% { box-shadow: 0 0 0 0  rgba(251, 191, 36, 0),    inset 0 0 0 0 rgba(180, 83, 9, 0); }
}
.asm-hm-flash {
  animation: asmHmFlash 0.9s ease-out 3;
  z-index: 5; position: relative;
}

/* Drill-down navigation strip on the Scatter view when the user opens it
 * from a heatmap cell or list row. */
.asm-scatter-nav {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 6px 10px;
  margin-bottom: 8px;
  background: #fffbeb;
  border: 1px solid #fde68a;
  border-radius: 6px;
  font-size: 12px;
}
.asm-scatter-nav-crumb { color: #475569; flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.asm-scatter-nav-crumb strong { color: #0f172a; font-weight: 600; }
.asm-scatter-nav-spacer { flex: 0 1 8px; }
.asm-scatter-nav-pos {
  font-variant-numeric: tabular-nums;
  font-size: 11px; color: #64748b;
  min-width: 50px; text-align: center;
}
.asm-scatter-nav-arrow:disabled { opacity: 0.45; cursor: not-allowed; }
body.asm-darkplots .asm-scatter-nav { background: #1f2937; border-color: #374151; }
body.asm-darkplots .asm-scatter-nav-crumb { color: #cbd5e1; }
body.asm-darkplots .asm-scatter-nav-crumb strong { color: #fff; }
body.asm-darkplots .asm-scatter-nav-pos { color: #cbd5e1; }

/* Correlations list table — sits below the heatmap grid */
.asm-hm-list-section {
  margin-top: 16px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.asm-hm-list-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding: 10px 14px;
  background: #f8fafc;
  border-bottom: 1px solid var(--border);
}
.asm-hm-list-title {
  font-size: 13px; font-weight: 600; color: #0f172a;
}
.asm-hm-list-count {
  font-size: 11.5px; font-weight: 500; color: #64748b;
  margin-left: 8px;
}
.asm-hm-list-print-title {
  font-size: 13px; font-weight: 600; color: #0f172a;
  padding: 10px 14px;
  background: #f8fafc;
  border-bottom: 1px solid var(--border);
}
.asm-hm-list-filter-label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; color: #475569;
  text-transform: none; letter-spacing: normal; font-weight: 500;
}
.asm-hm-list-filter-input {
  width: 70px; padding: 4px 6px;
  border: 1px solid var(--border); border-radius: 4px;
  font-size: 12px; font-family: inherit;
}
.asm-hm-list-tablewrap { overflow-x: auto; }
.asm-hm-list-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  table-layout: fixed;
}
.asm-hm-list-table thead th {
  background: #f1f5f9; color: #475569;
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  padding: 8px 10px;
  border-bottom: 1px solid #cbd5e1;
  white-space: nowrap;
  overflow: hidden;
}
.asm-hm-list-table thead th:hover { background: #e2e8f0; }
.asm-hm-list-sort-arrow { color: #cbd5e1; font-size: 9px; margin-left: 4px; }
.asm-hm-list-sort-arrow.on { color: var(--primary); }
.asm-hm-list-resizer {
  position: absolute; top: 0; bottom: 0; right: -3px;
  width: 6px; cursor: col-resize; z-index: 4; background: transparent;
  transition: background 0.12s;
}
.asm-hm-list-resizer:hover,
.asm-hm-list-resizer:active { background: rgba(37, 99, 235, 0.35); }
.asm-hm-list-table tbody td {
  padding: 6px 10px;
  border-bottom: 1px solid #f1f5f9;
  /* Inherit alignment from the <col> via the th's text-align won't cascade
   * to td. Instead apply per-column classes below. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.asm-hm-list-row { cursor: pointer; transition: background 0.1s; }
.asm-hm-list-row:hover { background: #eff6ff; }
.asm-hm-list-rank {
  text-align: right; color: #94a3b8; font-weight: 600;
}
.asm-hm-list-num { text-align: right; white-space: nowrap; }
.asm-hm-list-r.pos { color: #1d4ed8; font-weight: 600; }
.asm-hm-list-r.neg { color: #dc2626; font-weight: 600; }
.asm-hm-list-sig { text-align: center; font-weight: 700; color: #64748b; }
.asm-hm-list-sig.sig-1 { color: #d97706; }
.asm-hm-list-sig.sig-2 { color: #d97706; }
.asm-hm-list-sig.sig-3 { color: #b45309; }

/* Correlations list — dark mode coverage */
body.asm-darkplots .asm-hm-list-section { background: #1f2937 !important; border-color: #374151; }
body.asm-darkplots .asm-hm-list-toolbar { background: #111827; border-bottom-color: #374151; }
body.asm-darkplots .asm-hm-list-title { color: #f1f5f9; }
body.asm-darkplots .asm-hm-list-count { color: #cbd5e1; }
body.asm-darkplots .asm-hm-list-print-title { background: #111827; border-bottom-color: #374151; color: #f1f5f9; }
body.asm-darkplots .asm-hm-list-filter-label { color: #cbd5e1; }
body.asm-darkplots .asm-hm-list-filter-input { background: #111827; border-color: #374151; color: #f1f5f9; }
body.asm-darkplots .asm-hm-list-table thead th { background: #111827; color: #e5e7eb; border-bottom-color: #374151; }
body.asm-darkplots .asm-hm-list-table thead th:hover { background: #1f2937; }
body.asm-darkplots .asm-hm-list-table tbody td { color: #e5e7eb; border-bottom-color: #1f2937; }
body.asm-darkplots .asm-hm-list-row:hover { background: rgba(96,165,250,0.10); }
body.asm-darkplots .asm-hm-list-rank { color: #cbd5e1; }
body.asm-darkplots .asm-hm-list-r.pos { color: #60a5fa; }
body.asm-darkplots .asm-hm-list-r.neg { color: #fca5a5; }
body.asm-darkplots .asm-hm-list-sig { color: #cbd5e1; }
body.asm-darkplots .asm-hm-list-sort-arrow { color: #94a3b8; }

/* Print-only / screen-only helpers */
.asm-hm-print-only { display: none !important; }
@media print {
  .asm-hm-screen-only { display: none !important; }
  .asm-hm-print-only { display: block !important; }
}
/* When the user clicks Save image, hide on-screen-only controls (action
 * buttons, list toolbar) so they don't appear in the captured PNG.
 * Also reveal the print-only list-title so the table has a header in
 * the saved image. */
body.asm-exporting-image .asm-hm-screen-only { display: none !important; }
body.asm-exporting-image .asm-hm-print-only { display: block !important; }
/* Scatter / 3D image export: lay the chart + FULL statistics out as a
 * normal stacked block so the chart canvas can't overlap the (live-view
 * height-capped, scrollable) stats panel. The chart-wrap height is frozen
 * in JS before capture so Chart.js doesn't reflow when flex is dropped. */
/* Break the flex chain above the capture target so the main pane can grow to its
   full content height (a flex item is otherwise stretched to the viewport, which
   clipped tall Designer reports at the fold). */
body.asm-exporting-image .asm-analyze-body { display: block !important; height: auto !important; overflow: visible !important; }
body.asm-exporting-image .asm-analyze-main { display: block !important; height: auto !important; overflow: visible !important; }
body.asm-exporting-image .asm-chart-wrap { flex: none !important; }
body.asm-exporting-image .asm-stats-panel { flex: none !important; max-height: none !important; overflow: visible !important; }
/* The Designer's output is its own fixed-height scroll box; unclamp it so the
   WHOLE report is captured, not just the on-screen portion. */
body.asm-exporting-image .asm-designer-wrap { flex: none !important; height: auto !important; overflow: visible !important; }

/* When the user clicks Print PDF, body gets .asm-printing-heatmap. The
 * rules below override the iOS viewport-fix height locks, hide every
 * chrome element, and let the analyze overlay flow as page content.
 *
 * Critical: we DON'T use `body > *:not(#app)` here — the :not() id bumps
 * specificity above the overlay-restore rule and would keep the overlay
 * hidden. Explicit ID list instead. */
@media print {
  html:has(body.asm-printing-heatmap),
  body.asm-printing-heatmap {
    height: auto !important;
    overflow: visible !important;
    background: #fff !important;
  }
  body.asm-printing-heatmap #sidebar,
  body.asm-printing-heatmap #sidebar-backdrop,
  body.asm-printing-heatmap #mobile-menu-btn,
  body.asm-printing-heatmap #topbar,
  body.asm-printing-heatmap #content,
  body.asm-printing-heatmap #feedback-fab,
  body.asm-printing-heatmap #modal-backdrop,
  body.asm-printing-heatmap #account-popup,
  body.asm-printing-heatmap .asm-hm-screen-only { display: none !important; }
  body.asm-printing-heatmap #app,
  body.asm-printing-heatmap #main {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }
  body.asm-printing-heatmap .asm-analyze-overlay {
    display: block !important;
    position: static !important;
    inset: auto !important;
    background: #fff !important;
    padding: 0 !important;
    z-index: auto !important;
  }
  body.asm-printing-heatmap .asm-analyze-panel {
    display: flex !important;
    flex-direction: column !important;
    height: auto !important; max-width: none !important;
    width: 100% !important;
    box-shadow: none !important; border: none !important;
    border-radius: 0 !important;
    overflow: visible !important;
  }
  body.asm-printing-heatmap .asm-analyze-header,
  body.asm-printing-heatmap .asm-analyze-sidebar { display: none !important; }
  body.asm-printing-heatmap .asm-analyze-body {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }
  body.asm-printing-heatmap .asm-analyze-main {
    display: block !important;
    overflow: visible !important;
    height: auto !important;
    padding: 8mm !important;
    background: #fff !important;
  }
  /* Designer output is a fixed-height scroll box — unclamp so the whole report
     flows across print pages instead of clipping at the viewport. */
  body.asm-printing-heatmap .asm-designer-wrap {
    flex: none !important;
    height: auto !important;
    overflow: visible !important;
  }
  body.asm-printing-heatmap .asm-hm-scroll {
    overflow: visible !important;
    max-height: none !important;
    border: 1px solid #cbd5e1 !important;
    page-break-inside: avoid;
  }
  body.asm-printing-heatmap .asm-hm-list-section {
    page-break-before: auto;
    margin-top: 14px;
  }
  body.asm-printing-heatmap .asm-hm-list-tablewrap { overflow: visible !important; }
  body.asm-printing-heatmap .asm-hm-list-table { font-size: 10px; }
  body.asm-printing-heatmap .asm-hm-list-row { cursor: default; }
  body.asm-printing-heatmap .asm-hm-data { cursor: default; }
  body.asm-printing-heatmap .asm-hm-print-only { display: block !important; }

  /* ---- Print DOE matrix mode ---- */
  html:has(body.asm-printing-matrix),
  body.asm-printing-matrix {
    height: auto !important;
    overflow: visible !important;
    background: #fff !important;
  }
  body.asm-printing-matrix #sidebar,
  body.asm-printing-matrix #sidebar-backdrop,
  body.asm-printing-matrix #mobile-menu-btn,
  body.asm-printing-matrix #topbar,
  body.asm-printing-matrix #feedback-fab,
  body.asm-printing-matrix #modal-backdrop,
  body.asm-printing-matrix #account-popup,
  body.asm-printing-matrix .asm-toolbar,
  body.asm-printing-matrix .asm-toolbar-card { display: none !important; }
  body.asm-printing-matrix #app,
  body.asm-printing-matrix #main,
  body.asm-printing-matrix #content {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }
  /* The matrix table itself — let it flow at full width with small page margins */
  body.asm-printing-matrix #asm-table-wrap,
  body.asm-printing-matrix .asm-table-wrap {
    overflow: visible !important;
    max-height: none !important;
    page-break-inside: auto;
  }
  body.asm-printing-matrix table { font-size: 9px !important; }

  /* Don't force an orientation — let the user pick portrait/landscape
   * in the print dialog. A wide rectangular matrix often prints best in
   * landscape, but tall matrices and reports work fine in portrait. */
  @page { margin: 10mm; }
}

@media (max-width: 768px) {
  /* Overlay/panel are full-screen at all sizes now (see base rule) — only the
     internal sidebar→stacked layout differs on phones. */
  .asm-analyze-body { flex-direction: column; }
  .asm-analyze-sidebar,
  .asm-analyze-sidebar.asm-sidebar-collapsed {
    width: 100%; max-height: 35%; padding: 12px 14px;
    border-right: none; border-bottom: 1px solid var(--border);
    overflow-y: auto;
  }
  /* No collapse toggle on mobile — the panel already stacks above main. */
  .asm-sidebar-toggle { display: none; }
  .asm-analyze-sidebar.asm-sidebar-collapsed .asm-sidebar-contents { display: block; }
}

/* Account popup — floats above the account button when toggled.
   Uses position: fixed so it escapes the sidebar's overflow: hidden
   clip (which is there for the collapse animation). The exact
   left/bottom values are written by toggleAccountMenu() in db.js
   from the button's bounding rect; the defaults below are just so
   the popup lands somewhere sensible if JS hasn't run yet. */
.sidebar-account-wrap { position: relative; flex: 0 0 auto; min-width: 0; display: flex; }
.sidebar-account-popup {
  position: fixed;
  bottom: 56px;
  left: 8px;
  min-width: 220px;
  max-width: min(320px, calc(100vw - 16px));
  background: var(--sb-pop-bg);
  color: #f1f5f9;
  border: 1px solid var(--sb-pop-border);
  border-radius: 10px;
  padding: 0 0 5px;   /* breathing room below the last action (Sign In / Logout) */
  font-size: 12px;
  font-weight: 500;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  display: none;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
  z-index: 60;
}
/* Long emails should wrap inside the popup, not stretch it past the
   viewport edge. */
.sidebar-account-popup-sub { word-break: break-word; overflow-wrap: anywhere; }
.sidebar-account-popup.open { display: inline-flex; }
.sidebar-account-popup::after {
  /* Arrow pointing down at the account button */
  content: "";
  position: absolute;
  top: 100%;
  left: 14px;
  border: 6px solid transparent;
  border-top-color: var(--sb-pop-bg);
}
.sidebar-account-popup-header {
  display: flex; align-items: center; gap: 8px;
  font-weight: 600; font-size: 13px;
  padding: 10px 12px 6px;
}
.sidebar-account-popup-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--success);
  flex-shrink: 0;
}
.sidebar-account-popup-sub {
  font-size: 11px;
  color: var(--sb-icon);
  font-weight: 400;
  padding: 0 12px 4px;   /* tighter gap down into the first action (Settings) */
  margin: 0;
}
.sidebar-account-popup-action {
  background: transparent;
  color: #f1f5f9;
  border: none;
  border-radius: 10px;
  margin: 1px 6px;
  width: auto;
  padding: 9px 10px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  transition: background 0.15s ease;
  /* Flex row + fixed icon slot so every label starts at the same x, whether the
     icon is an SVG or a glyph. */
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Fixed-width icon slot — keeps labels aligned across mixed SVG/emoji icons. */
.sidebar-account-popup-action .spa-ico {
  width: 16px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px; line-height: 1;
}
.sidebar-account-popup-action .spa-ico img {
  width: 14px; height: 14px; object-fit: contain;
  filter: brightness(0) invert(1); opacity: 0.85;   /* monochrome white on the dark popup */
}
.sidebar-account-popup-action:hover { background: var(--sb-pop-hover); }
.sidebar-account-popup-action:focus-visible {
  outline: 2px solid #60a5fa; outline-offset: -2px;
}

/* ---- Account switcher dialog (proOpenAccountSwitcher) ---- */
/* Docks above the sidebar account button (positioned in JS) and is roomier than
   the default float-pop — this is the primary account-switching surface. */
#pro-acct-switch-pop { width: 340px; border-radius: 10px; }
#pro-acct-switch-pop .pro-float-pop-head { font-size: 14px; padding: 13px 14px; }
#pro-acct-switch-pop .pro-float-pop-body { padding: 10px; }
.acct-switch-list { display: flex; flex-direction: column; gap: 2px; }
.acct-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px; border-radius: 10px;
}
.acct-row:hover { background: var(--bg-soft); }
.acct-row.acct-row-cur { background: var(--bg-soft); }
.acct-row-main { flex: 1 1 auto; min-width: 0; }
.acct-row-name {
  font-size: 13px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Sub-line under the display name: "email" or "email · Session expired".
   Flex so the email truncates but "Session expired" is never clipped off. */
.acct-row-email {
  display: flex; align-items: baseline; min-width: 0;
  font-size: 12px; color: var(--text-muted);
}
.acct-row-email .acct-em { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-expired { flex: 0 0 auto; white-space: nowrap; color: #dc2626; font-weight: 600; }
.acct-cur { color: #16a34a; font-weight: 600; font-size: 11px; }
.acct-row-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* Compact ✕ so a long email keeps its room (the primary Switch/Sign-in stays a button). */
.acct-forget {
  flex-shrink: 0; width: 26px; height: 26px; padding: 0; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; color: var(--text-muted);
  background: transparent; border: 1px solid var(--border); cursor: pointer;
}
.acct-forget:hover { background: var(--bg-soft); color: var(--text); border-color: var(--border-strong); }
.acct-switch-foot { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
.sidebar-account-popup-section {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--sb-pop-section);
  padding: 4px 2px 2px;
}
.sidebar-account-popup-item {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  color: #f1f5f9;
  border: 1px solid var(--sb-pop-item-border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  text-align: left;
  width: 100%;
  font-family: inherit;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.sidebar-account-popup-item:hover { background: var(--sb-pop-item-hover); border-color: var(--sb-pop-item-border-hover); }
.sidebar-account-popup-item:focus-visible { outline: 2px solid #60a5fa; outline-offset: 1px; }
.sidebar-account-popup-item-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; flex-shrink: 0; opacity: 0.75;
}
.sidebar-account-popup-divider {
  height: 1px;
  background: #374151;
  margin: 4px 0;
}

/* ---- Pro workspace selector widget (account popup) ---- */
#pro-widget { display: flex; flex-direction: column; gap: 6px; }
/* Trigger is the "currently selected" chip — a LIGHTER tint of the active-nav
   sidebar background (which is sidebar-bg mixed 20% white; this goes lighter) so
   it belongs in the dark sidebar. The dropdown overlays it seamlessly: the
   current row lands right on top with the same look. Shared → orange text. */
.pro-ws-selector {
  display: flex; align-items: center; justify-content: space-between;
  /* Sits ON the dark sidebar — a hair lighter than the sidebar bg (not a bright
     light chip), so it reads as an inset control rather than a sticker. */
  background: color-mix(in srgb, var(--sidebar-bg, #111827) 90%, #fff 10%);
  border: none; border-radius: 10px; padding: 6px 9px;
  min-height: 34px; box-sizing: border-box;
  /* line-height matches the .current dropdown row that overlays it on open —
     otherwise the label/checkmark jump ~1px when the menu opens. */
  line-height: 1.2;
  font-size: 12.5px; color: inherit; cursor: pointer; user-select: none;
  transition: background 0.15s; gap: 8px; outline: none;
}
.pro-ws-selector:hover { background: color-mix(in srgb, var(--sidebar-bg, #111827) 84%, #fff 16%); }
.pro-ws-selector:focus-visible { outline: 2px solid #60a5fa; outline-offset: 1px; }
/* Checkmark = a noticeable brand tint that pops on the dark control. */
.pro-ws-selector .pro-ws-check { color: #60a5fa; font-weight: 700; }
.pro-ws-selector .pro-ws-chev { color: currentColor; }
/* Shared (a teammate's) workspace: amber left stripe + amber check + amber label
   so it's unmistakable you're in someone else's data, not your own. */
.pro-ws-selector.shared { box-shadow: inset 3px 0 0 #fbbf24; }
.pro-ws-selector.shared .pro-ws-check { color: #fbbf24; }
.pro-ws-selector.shared .pro-ws-selector-label { color: #fbbf24; }
.pro-ws-selector-label { display: flex; align-items: center; gap: 8px; font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pro-ws-selector-right { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.pro-ws-chev { font-size: 11px; opacity: 0.75; display: inline-block; transition: transform 0.2s; line-height: 1; }
.pro-ws-chev.open { transform: rotate(180deg); }
.pro-ws-cog-btn { background: none; border: none; color: #64748b; cursor: pointer; padding: 2px 4px; border-radius: 10px; line-height: 1; display: flex; align-items: center; transition: background 0.15s; }
.pro-ws-cog-btn:hover { background: rgba(255,255,255,0.1); }
/* Workspace dropdown — white "action modal" surface. Rows are compact and
   background-less until hovered or current. */
.pro-ws-dropdown {
  background: #fff; border: 1px solid var(--border); border-radius: 10px;
  display: flex; flex-direction: column; gap: 4px; padding: 7px;
  box-shadow: 0 12px 32px rgba(10,18,40,0.22);
}
/* Open: the modal spawns at the trigger's size (only the current row visible,
   sitting over the button) and unfolds DOWNWARD into the full menu — a clip
   reveal from the top, so width never changes. */
@keyframes proWsOpen {
  from { clip-path: inset(0 0 calc(100% - 48px) 0 round 10px); opacity: 0.9; }
  to   { clip-path: inset(0 0 0 0 round 10px); opacity: 1; }
}
.pro-ws-dropdown.pro-ws-opening { animation: proWsOpen 0.17s ease-out; }
/* Close: fade out (JS adds this, then hides after the transition). */
.pro-ws-dropdown.pro-ws-closing { opacity: 0; transition: opacity 0.13s ease-in; pointer-events: none; }
/* Every row (workspace buttons + cog) shares this height + left inset so they
   line up in a column and the flex `gap` gives even spacing everywhere. */
.pro-ws-drop-item {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  min-height: 34px; box-sizing: border-box;
  background: transparent; border: none; border-radius: 10px;
  color: var(--text); font-size: 12.5px; padding: 6px 9px; cursor: pointer;
  font-family: inherit; transition: background 0.12s;
  white-space: nowrap; overflow: hidden; line-height: 1.2;
}
.pro-ws-drop-item .pro-ws-item-label { overflow: hidden; text-overflow: ellipsis; }
.pro-ws-drop-item:hover:not([disabled]) { background: #eef2f7; }
/* Current selection — the top row: a light-grey chip that is EXACTLY the size of
   the sidebar trigger (full-bleed to the modal's top edge via negative margins —
   no modal padding around it), so it lands right on the button. Unchanged on
   hover; the up-caret on the right closes the menu. */
.pro-ws-drop-item.current,
.pro-ws-drop-item.current:hover {
  background: #e8edf3; color: var(--text); font-weight: 600;
  margin: -8px -8px 0; width: calc(100% + 16px); border-radius: 10px 10px 0 0;
}
.pro-ws-drop-item.current .pro-ws-check { color: #2563eb; font-weight: 700; }
/* Match the sidebar trigger's chevron size (.pro-ws-chev = .fl-tri 0.72em @ 11px ≈ 8px). */
.pro-ws-drop-item.current .pro-ws-cur-chev { margin-left: auto; width: 8px; height: 8px; background: var(--text-muted); opacity: 0.75; flex-shrink: 0; }
.pro-ws-drop-item[disabled] { cursor: default; }
/* Section divider — spacing comes from the dropdown's flex gap, so it's even
   above and below (margin:0 keeps top/bottom symmetric). */
.pro-ws-divider { height: 1px; background: var(--border); margin: 0; flex-shrink: 0; }
/* Foot row: cogwheel button, left-aligned so its glyph sits under the workspace
   icons (same 9px inset), and matching the row height. */
.pro-ws-foot { display: flex; align-items: center; gap: 8px; padding: 0; }
/* Settings cog — the ONLY row with a permanent background fill. */
.pro-ws-cog-btn {
  display: inline-flex; align-items: center; justify-content: flex-start;
  width: 34px; height: 34px; flex-shrink: 0; box-sizing: border-box; padding: 0 0 0 9px;
  background: #eef2f7; border: none; border-radius: 10px; cursor: pointer;
  color: #475569; transition: background 0.12s;
}
.pro-ws-cog-btn:hover { background: #e2e8f0; }
.pro-ws-cog {
  display: inline-block; width: 15px; height: 15px; background: currentColor; opacity: 0.9;
  flex-shrink: 0; -webkit-mask: url(/icns-svg/actions/cogwheel.svg?v=647f3cf745) center/contain no-repeat;
  mask: url(/icns-svg/actions/cogwheel.svg?v=647f3cf745) center/contain no-repeat;
}
/* Desktop: sync shows in its own row below the dropdown, so hide the in-foot copy.
   Mobile: show it to the right of the cogwheel and drop the standalone row. */
/* Permanent, at every width: this is the one place sync is always shown, sitting
   to the right of the workspace settings button. It used to be display:none
   with a mobile-only override, which left desktop relying on the rail glyph. */
.pro-ws-foot-sync { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-muted); min-width: 0; margin-left: auto; }
.pro-ws-foot-sync span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pro-ws-foot-sync.local-mode { color: #d97706; }
@media (max-width: 768px) {
  /* .pro-ws-foot-sync is permanent now — only the standalone row still needs
     hiding here, so the two don't say the same thing twice on a phone. */
  .pro-ws-dropdown ~ .pro-sync-status { display: none; }
}
/* Diverged-draft banner — a bulk load (demo/import) is local-only, sync frozen. */
.pro-ws-draft { background: rgba(217,119,6,0.14); border: 1px solid rgba(217,119,6,0.45); border-radius: 7px; padding: 8px 9px; margin: 0 4px 7px; }
.pro-ws-draft-msg { font-size: 11px; color: #fbbf24; line-height: 1.35; margin-bottom: 7px; }
.pro-ws-draft-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.pro-ws-draft-actions .btn { font-size: 11px; padding: 3px 9px; }
.pro-ws-draft-note { font-size: 10.5px; color: #94a3b8; margin-top: 6px; line-height: 1.35; }

/* Subscription cancellation grace banner — top of #main (account-level). */
#fl-sub-banner { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; background: #fffbeb; border-bottom: 1px solid #fde68a; color: #92400e; padding: 8px 16px; font-size: 13px; line-height: 1.4; }
#fl-sub-banner .fl-sub-banner-msg { display: flex; align-items: center; gap: 7px; min-width: 0; }
#fl-sub-banner .fl-sub-banner-msg .fl-warn { background-color: #b45309; }
#fl-sub-banner .fl-sub-banner-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
#fl-sub-banner .fl-sub-banner-x { background: none; border: none; color: #92400e; font-size: 19px; line-height: 1; cursor: pointer; padding: 0 4px; opacity: 0.7; }
#fl-sub-banner .fl-sub-banner-x:hover { opacity: 1; }

/* Offline banner — live connection status for Cloud-mode Pro. Orange, not
   dismissible; auto-removes when back online. */
#fl-offline-banner { display: flex; align-items: center; gap: 12px; background: #fff7ed; border-bottom: 1px solid #fed7aa; color: #9a3412; padding: 8px 16px; font-size: 13px; line-height: 1.4; }
#fl-offline-banner .fl-offline-banner-msg { display: flex; align-items: center; gap: 8px; min-width: 0; }
#fl-offline-banner .fl-offline-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: 50%; background: #f97316; box-shadow: 0 0 0 0 rgba(249,115,22,0.5); animation: flOfflinePulse 1.8s ease-out infinite; }
@keyframes flOfflinePulse { 0% { box-shadow: 0 0 0 0 rgba(249,115,22,0.5); } 70% { box-shadow: 0 0 0 6px rgba(249,115,22,0); } 100% { box-shadow: 0 0 0 0 rgba(249,115,22,0); } }

/* "Working…" indicator — bottom-right banner with a smoothly rotating beaker
   (180°/second). Shown for load/save actions that can run long. */
.fl-working { position: fixed; right: 18px; bottom: 18px; z-index: 9000; display: flex; align-items: center; gap: 9px; background: #fff; border: 1px solid var(--border); box-shadow: 0 8px 28px rgba(10,18,40,0.18); border-radius: 10px; padding: 9px 14px 9px 11px; font-size: 13px; font-weight: 600; color: #0f172a; }
.fl-working[hidden] { display: none; }
.fl-working .fl-working-beaker { width: 20px; height: 20px; transform-origin: 50% 50%; animation: flWorkingTip 2s ease-in-out infinite; }
.fl-working .fl-working-text { line-height: 1; }
/* Tip 180°, pause, tip another 180°, pause — repeat (a "pouring beaker" beat),
   rather than a continuous spin. Each turn is smooth (ease-in-out); the flat
   keyframe spans are the pauses. */
@keyframes flWorkingTip {
  0%,  12%  { transform: rotate(0deg); }
  44%, 56%  { transform: rotate(180deg); }
  88%, 100% { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) { .fl-working .fl-working-beaker { animation-duration: 4s; } }

/* Remote publish-lock banner — a teammate is rewriting the workspace. */
.pro-ws-lock { background: rgba(59,130,246,0.14); border: 1px solid rgba(59,130,246,0.45); border-radius: 7px; padding: 7px 9px; margin: 0 4px 7px; font-size: 11px; color: #93c5fd; line-height: 1.35; }
.pro-ws-viewer { background: rgba(100,116,139,0.18); border: 1px solid rgba(148,163,184,0.45); border-radius: 7px; padding: 7px 9px; margin: 0 4px 7px; font-size: 11px; color: #cbd5e1; line-height: 1.35; }

/* Viewer (read-only) gate: disable mutation buttons in the main data views +
   topbar while keeping navigation, search/filter/sort, and export usable. RLS
   is the real guarantee; this prevents confusing local-only edits. */
body.fv-viewer #content .btn-primary,
body.fv-viewer #content .btn-danger,
body.fv-viewer #content .settings-btn-danger,
body.fv-viewer #topbar-actions .btn-primary {
  opacity: 0.4 !important; pointer-events: none !important; cursor: not-allowed !important;
}

/* Live co-editors (presence) — between the workspace name and the sync row. */
.pro-ws-presence { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; font-size: 11px; color: var(--sb-presence); padding: 2px 8px 0; line-height: 1.4; }
.pro-ws-presence-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; flex-shrink: 0; box-shadow: 0 0 0 0 rgba(34,197,94,0.6); animation: proPresencePulse 2s ease-out infinite; }
@keyframes proPresencePulse { 0% { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); } 70% { box-shadow: 0 0 0 5px rgba(34,197,94,0); } 100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); } }
.pro-sync-status { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--sb-muted); padding: 2px 8px 0; }
.pro-sync-main { display: flex; align-items: center; gap: 5px; min-width: 0; }
.pro-sync-status.has-presence .pro-sync-main { flex: 1; overflow: hidden; }
.pro-sync-status.has-presence #pro-sync-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pro-sync-divider { width: 1px; align-self: stretch; min-height: 14px; background: rgba(148,163,184,0.3); margin: 0 8px; flex-shrink: 0; }
.pro-sync-presence { display: flex; align-items: center; flex: 1; min-width: 0; }
.pro-sync-presence .pro-ws-presence-mini { margin-right: 0 !important; }
.pro-sync-status.syncing { color: #93c5fd; }
.pro-sync-status.local-mode { color: #fbbf24; }
.pro-sync-svg { flex-shrink: 0; overflow: visible; }
.pro-sync-status.syncing .pro-sync-svg { animation: proSyncSpin 1s linear infinite; transform-origin: center; }
/* Counter-clockwise to match the undo/refresh arrow's arrowhead — a clockwise
   spin on this glyph reads as running "backwards". */
@keyframes proSyncSpin { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }

/* Floating workspace dropdown — overlays the page instead of pushing nav down. */
.pro-ws-dropdown { box-shadow: 0 12px 32px rgba(0,0,0,0.4); }

/* Presence area is a button → opens the People popup. Two looks: an avatar
   stack when the sidebar is expanded, a dot+count (or single avatar) badge
   when collapsed. */
.pro-sync-presence { cursor: pointer; }
.pro-presence-badge { display: none; align-items: center; gap: 3px; }
.pro-presence-count {
  font-size: 11px; font-weight: 700; color: #fff; background: #16a34a;
  border-radius: 9999px; min-width: 16px; height: 16px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
}

/* Collapsed sidebar: workspace = icon only (floating dropdown on click); sync
   row = icon on the left, presence badge on the right, no divider between. */
/* The right slot is hidden again. It was un-hidden in v1.16.112 to carry the
   sync icon; sync has since moved to the workspace dropdown, so the slot is
   empty — but an empty flex item still consumes the row's GAP, and that gap
   dragged the centred group 4px left, which read as an off-centre icon. Hiding
   the slot (rather than only its chevron) removes the item and the gap with it. */
#sidebar.collapsed #pro-ws-name,
#sidebar.collapsed .pro-ws-selector-right,
#sidebar.collapsed #pro-sync-label,
#sidebar.collapsed .pro-sync-divider,
#sidebar.collapsed .pro-presence-stack { display: none !important; }
#sidebar.collapsed .pro-ws-selector { justify-content: center; padding: 6px 4px; gap: 0; }
#sidebar.collapsed .pro-ws-selector-label { gap: 0; max-width: 100%; }
#sidebar.collapsed .pro-sync-status { justify-content: center; gap: 6px; padding: 4px 0; }
/* The workspace glyph is an icon now, not a ✓. */
.pro-ws-check img { display: block; width: 15px; height: 15px; opacity: .9; }
#sidebar.collapsed .pro-ws-check img { width: 17px; height: 17px; }
/* Sync is NOT on the workspace button. It briefly was (v1.16.112) and that put a
   second glyph on a control whose whole job, once collapsed, is to say which
   workspace you are in. It lives in the workspace dropdown's footer instead,
   beside the settings cog — see .pro-ws-foot-sync. The standalone rail glyph
   still goes, which leaves the people badge that row to itself. */
#sidebar.collapsed .pro-sync-main { display: none; }
/* The presence wrapper stretched to the full row width with the badge pinned to
   its left edge, so the row's justify-content: center was centring a full-width
   box and moving nothing — the badge measured 8.5px left of the rail's centre.
   Shrink it to its content and the centring applies to what you can actually
   see. (Vertical was already right: dot and icon share a centre line to 0.00px.) */
#sidebar.collapsed .pro-sync-presence {
  flex: 0 0 auto; justify-content: center; margin-right: 0;
}
#sidebar.collapsed .pro-presence-badge { display: inline-flex; }
#sidebar.collapsed #pro-widget { padding: 0 2px; }

/* Floating sidebar popups (People list, invite window) — light app surface so
   they read over the main content, not the dark sidebar. */
.pro-float-pop {
  position: fixed; z-index: 1200; width: 264px; max-width: calc(100vw - 24px);
  background: var(--surface, #fff); color: var(--text, #0f172a);
  border: 1px solid var(--border, #e2e8f0); border-radius: 10px;
  box-shadow: 0 14px 40px rgba(0,0,0,0.22); overflow: hidden; display: none;
}
.pro-float-pop-head { font-size: 12.5px; font-weight: 700; padding: 10px 12px; border-bottom: 1px solid var(--border); }
.pro-float-pop-body { padding: 8px; max-height: 62vh; overflow-y: auto; }
.pro-pop-muted { font-size: 12px; color: var(--text-muted); margin: 4px 2px; line-height: 1.4; }
.pro-pop-section-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 7px 4px 3px; }
.pro-pop-row { display: flex; align-items: center; gap: 9px; padding: 5px 4px; }
.pro-pop-row-main { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.pro-pop-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pro-pop-sub { font-size: 11px; color: var(--text-muted); }
.pro-pop-avatar { position: relative; display: inline-flex; flex-shrink: 0; }
.pro-pop-avatar-pending { width: 24px; height: 24px; border-radius: 50%; align-items: center; justify-content: center; background: var(--surface-2, #f1f5f9); color: var(--text-muted); font-size: 12px; }
.pro-pop-presence-dot { position: absolute; right: -1px; bottom: -1px; width: 9px; height: 9px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 0 2px var(--surface, #fff); }
.pro-pop-x { background: none; border: 1px solid var(--border); color: var(--text-muted); font-size: 11px; padding: 3px 8px; border-radius: 6px; cursor: pointer; flex-shrink: 0; font-family: inherit; }
.pro-pop-x:hover { color: #ef4444; border-color: #ef4444; }
.pro-pop-add { display: block; width: 100%; text-align: center; margin-top: 8px; padding: 8px; border: 1px dashed var(--border); background: none; color: var(--primary); font-size: 12.5px; font-weight: 600; border-radius: 8px; cursor: pointer; font-family: inherit; }
.pro-pop-add:hover { background: color-mix(in srgb, var(--primary) 8%, transparent); }
.pro-pop-input { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 7px; font-size: 13px; font-family: inherit; box-sizing: border-box; }
.pro-pop-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 8px; }

#main { flex: 1; display: flex; flex-direction: column; overflow: hidden; transition: margin-right .22s ease; position: relative; }
/* position + z-index so the bar and everything it opens sit above page content.
 * Needed because glass mode puts a backdrop-filter on #topbar, and that makes
 * it a stacking context — which traps its dropdowns inside it, so #content
 * (later in DOM order) painted over them no matter how high the menu's own
 * z-index was. Elevating the bar itself lifts the whole subtree instead.
 * 30 clears the sticky detail header (20) and stays under modals (300). */
#topbar { position: relative; z-index: 30; background: transparent; border-bottom: none; padding: 0 10px; height: 56px; display: flex; align-items: center; justify-content: space-between; box-shadow: none; }
#topbar h1 { font-size: 17px; font-weight: 600; min-width: 0; max-width: 52vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Top-bar lead group: [ ◀ | ▶ ] + title, in one frame ────────────────────
 * The frame is inset 10px from the top-bar edges so it lands on the same 10px
 * rhythm as the floating rail's margin — the two read as one row of tiles
 * rather than a tile beside loose text. 56px bar - 10px top - 10px bottom
 * leaves the 36px frame height below. */
.tb-lead { display: flex; align-items: center; gap: 10px; min-width: 0; flex-shrink: 0; }
/* When the rail floats it already carries a 10px margin, so the bar's own left
 * padding would double the gap to 20px and the frame would stop lining up with
 * the rail's inset. Docked has no margin, so it keeps the 10px. */
/* Desktop only: the rail's own 10px margin supplies the left gap there. On
 * mobile the rail is an off-canvas drawer and isn't in flow at all, so zeroing
 * this jammed the hamburger against the window edge. */
@media (min-width: 769px) { html[data-sb-float] #topbar { padding-left: 0; } }
/* The lead group is flex-shrink: 0 so flexbox can't squeeze it on desktop. On a
 * phone that's what pushed the actions frame past the right edge — the title
 * already ellipsizes, so let the group give way instead. */
@media (max-width: 768px) {
  /* The lead group deliberately does NOT shrink here. js/topbar-overflow.js
   * measures it to decide what fits ("Both the left group and the actions bar
   * are flex-shrink:0, so the left group's measured width is stable") — letting
   * it shrink made the controller read an already-squeezed lead, conclude the
   * buttons fit, and squeeze the title instead of overflowing them into ⋯.
   * Capping the title is what frees the room: 38vw ≈ 142px on a 375px screen. */
  #topbar h1 { max-width: 38vw; }
  /* Forward is hidden on NARROW phones only. With it shown the lead needs
   * 142px + 38vw, so the bar only fits from ~440px up — under that the actions
   * frame ran off the edge (measured: 388 against a 375 viewport). Back stays
   * at every width; forward comes back on large phones and tablets. */
  /* A "+ Thing" CTA collapses to a square "+" — the word is the first thing
   * worth losing when the bar is 375px wide, and the tooltip / aria-label still
   * carry it. Generalises the per-view folds Ingredients and Formulas already had. */
  #topbar-actions .tb-cta-label { display: none; }
  #topbar-actions .btn-primary:has(.tb-cta-label) { width: 34px; padding: 0; }
}
@media (max-width: 519px) {
  #fwd-btn { display: none; }
}
.tb-frame {
  display: flex; align-items: center; gap: 8px;
  /* One spacing value everywhere: the padding at the ends of the row equals the
   * gap between the controls, so nothing sits tighter to the edge than it does
   * to its neighbour. 34px control + 8 + 8 = 50. */
  height: 50px; padding: 8px;
  /* Concentric with the 10px controls it holds: 10 + 8px of padding = 18. An
   * equal 10px would make the outer curve look tighter than the inner one. */
  border-radius: 18px;
  min-width: 0;
}
/* The title sits between the two triangles, so it takes the padding the frame's
 * uniform 3px doesn't give it. */
.tb-frame h1 { padding: 0 6px; }
/* The trough is a FLOATING-only surface. Docked has no 10px inset for it to sit
 * on and no background behind the shell, so a fill there would just be a grey
 * slab around the title. Floating also gets the frame pinned to 10px rather
 * than centered: the bar's 1px bottom border eats into the content box, so
 * centering lands it at 9.5px and its top edge stops matching the rail. */
/* Floating: the bar grows to 66px so the frames can run down to the sidebar's
 * logo divider (rail top 10 + logo 56 = 66). The frames themselves are the only
 * surface in the bar — #topbar's own fill is dropped below — so the left group
 * and the right actions read as two tiles on the background, level with the
 * rail's logo tile. */
html[data-sb-float] #topbar { height: 66px; }
/* The frames hug their controls on one spacing value: 8px of padding all round,
 * matching the 8px gap between the controls themselves. 34px control + 8 + 8 =
 * 50px. The bar stays 66px and the frames keep their centre line (13 + 25 = 38),
 * so nothing else on the row shifts. */
html[data-sb-float] .tb-lead,
html[data-sb-float] #topbar-actions { align-self: flex-start; margin-top: 13px; height: 50px; }
html[data-sb-float] .tb-frame { height: 50px; }
/* The right-hand actions get the same holder. It's a plain flex row, so it
 * already sizes to however many buttons a view puts in it. */
html[data-sb-float] #topbar-actions {
  padding: 8px; border-radius: 18px;   /* 10px buttons + 8px padding */
  align-items: center;
}
html[data-sb-float] .tb-frame,
html[data-sb-float] #topbar-actions {
  /* OPAQUE: a solid white holder. It was a 5% tint, which is nearly nothing —
   * and since the bar itself has no fill when floating, that left the title and
   * controls sitting straight on the workspace background. Over a dark photo
   * the dark title went invisible. White is also what the material setting
   * means here: opaque = you see a surface, not the background. */
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
/* Glass: the frames become the glass surface the bar used to provide. On a
 * ::before because backdrop-filter would make each frame the containing block
 * for the position:fixed / absolute menus opened from inside it. */
html[data-sb-float][data-sb-translucent] .tb-frame,
html[data-sb-float][data-sb-translucent] #topbar-actions {
  background: transparent; position: relative; isolation: isolate;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5),
              0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
html[data-sb-float][data-sb-translucent] .tb-frame::before,
html[data-sb-float][data-sb-translucent] #topbar-actions::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] .tb-frame::before,
  html[data-sb-float][data-sb-translucent] #topbar-actions::before { background: #f4f6f9; }
}
/* Controls sit a step LIGHTER than the glass they're on, plus a drop shadow —
 * on a translucent surface, value alone doesn't separate a control from its
 * holder; the lift is what makes it read as a button. */
html[data-sb-float][data-sb-translucent] #mobile-menu-btn,
html[data-sb-float][data-sb-translucent] .tb-navbtn,
html[data-sb-float][data-sb-translucent] #topbar-actions .btn {
  background: rgba(255, 255, 255, .82);
  border-color: transparent;
  color: #0f172a;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .14), 0 2px 6px rgba(15, 23, 42, .08);
}
/* :not(.btn-primary) — the generic glass hover out-specifies .btn-primary's own
 * background, so hovering the blue CTA turned it white. Danger has its own
 * hover rule further down and is already safe. */
html[data-sb-float][data-sb-translucent] #topbar-actions .btn:not(.btn-primary):hover { background: rgba(255, 255, 255, .95); }
html[data-sb-float][data-sb-translucent] #topbar-actions .btn-primary:hover { background: var(--primary-hover, #1d4ed8); color: #fff; }
/* The glass colour rule above is (0,3,1) and would out-specify .tb-navbtn:disabled
 * (0,2,0), so a dead direction would stop greying out on glass. Restate it here
 * at (0,4,1) — the grey-out is the whole signal that a direction is unavailable. */
html[data-sb-float][data-sb-translucent] .tb-navbtn:disabled { color: rgba(15, 23, 42, .28); }
/* Buttons that carry their own meaning keep their hue, tinted onto the glass
 * rather than flattened to white — a Delete that reads as a neutral chip is a
 * worse control than a loud one. */
html[data-sb-float][data-sb-translucent] #topbar-actions .btn-danger {
  background: rgba(254, 226, 226, .88); color: #b91c1c;
}
html[data-sb-float][data-sb-translucent] #topbar-actions .btn-danger:hover { background: rgba(254, 202, 202, .95); }
/* The primary CTA stays solid: it's the one control that should not look like
 * it's made of the same material as everything around it. */
html[data-sb-float][data-sb-translucent] #topbar-actions .btn-primary {
  background: var(--primary); color: #fff;
}
/* Back and forward bracket the title — one on each side, each its own control.
 * They match the action buttons across the bar (both are the control layer, so
 * they share a 10px corner) and the frames round to 13 around them. */
.tb-navbtn {
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  /* Square: the glyph is symmetric, so anything else reads as a lopsided box. */
  height: 100%; aspect-ratio: 1; width: auto; padding: 0;
  border: none; border-radius: 10px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .10);
  cursor: pointer; color: var(--text-muted);
  font-size: 15px; line-height: 1;
  transition: background .12s ease, color .12s ease;
}
.tb-navbtn:hover:not(:disabled) { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); }
.tb-navbtn:active:not(:disabled) { background: color-mix(in srgb, var(--text) 12%, transparent); }
/* Greyed, not hidden — the control keeps its shape when a direction is dead. */
.tb-navbtn:disabled { color: color-mix(in srgb, var(--text) 28%, transparent); cursor: default; }
.tb-frame h1 { margin: 0; }
/* Actions bar wins space over the (truncatable) title; the overflow controller
   (js/topbar-overflow.js) moves buttons that don't fit into the ⋯ menu.
   flex-shrink:0 so flexbox doesn't squeeze it to nothing; overflow stays visible
   so the ⋯ dropdown isn't clipped. */
#topbar .actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
/* Top-bar action buttons get a softer 10px corner (vs the global 6px .btn) and
   a uniform height so every button + dropdown trigger lines up. Scoped to the
   header so the rest of the app's buttons are unchanged. */
#topbar-actions .btn { border-radius: 10px; height: 34px; min-height: 34px; justify-content: center; }
/* Square icon-only top-bar button (the ⋯ Chemistry menu): width == height. */
#topbar-actions .btn-icon-sq { width: 34px; padding: 0; gap: 0; }
/* ⋯ overflow trigger + dropdown */
.topbar-of { position: relative; flex-shrink: 0; display: flex; align-items: center; }
.topbar-of-btn { padding: 6px 8px; line-height: 0; }
.topbar-of-btn svg { fill: currentColor; display: block; }
.topbar-of-menu {
  position: absolute; right: 0; top: 100%; margin-top: 6px; z-index: 250;
  display: none; flex-direction: column; gap: 2px; min-width: 190px; max-width: 80vw;
  background: var(--surface); border: 1px solid var(--border); border-radius: 9px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.18); padding: 5px;
}
.topbar-of-menu.open { display: flex; }
/* Buttons rehomed into the menu render as full-width left-aligned list items,
   shedding their toolbar pill look (override inline styles with !important). */
.topbar-of-menu > .btn,
.topbar-of-menu > button {
  width: 100% !important; justify-content: flex-start !important; text-align: left;
  border: none !important; background: none !important; box-shadow: none !important;
  color: var(--text) !important; font-weight: 500 !important; border-radius: 6px !important;
  padding: 8px 10px !important; margin: 0 !important; min-width: 0 !important;
}
.topbar-of-menu > .btn:hover,
.topbar-of-menu > button:hover { background: var(--surface-2, #f1f5f9) !important; }
/* Originally-blue (primary) actions stay blue when rehomed into the ⋯ menu, so
   the main action is still visually obvious. Placed after the reset above so it
   wins. */
.topbar-of-menu > .btn-primary {
  background: var(--primary) !important; color: #fff !important; font-weight: 600 !important;
}
.topbar-of-menu > .btn-primary:hover { background: var(--primary-hover, #1d4ed8) !important; }
/* Destructive actions (Delete) are pushed to the bottom of the menu, under a
   thin divider, and keep their danger-red text so they read as distinct. */
.topbar-of-sep { height: 1px; margin: 4px 6px; background: var(--border); flex-shrink: 0; }
.topbar-of-menu > .btn-danger { color: var(--danger, #dc2626) !important; }
.topbar-of-menu > .btn-danger:hover { background: var(--danger-soft, #fef2f2) !important; }
/* Icon-only buttons (e.g. the ⓘ "About" button) carry a hidden text label that
   reveals itself only once folded into the ⋯ menu — a lone glyph is ambiguous
   in a vertical list. In the toolbar the span stays hidden (icon-only). */
.topbar-of-label { display: none; }
.topbar-of-menu .topbar-of-label { display: inline; }
/* Export ▾ folds into the ⋯ as FLAT rows, not a nested dropdown-in-a-dropdown
   (tapping the inner trigger used to close the whole ⋯, so it "disappeared").
   Hide the Export trigger and surface its CSV / Excel / PDF options directly. */
.topbar-of-menu .card-kebab-wrap { width: 100%; display: block !important; position: static !important; }
.topbar-of-menu .card-kebab-wrap > .btn { display: none !important; }
.topbar-of-menu .card-kebab-wrap > .card-kebab-menu {
  display: flex !important; position: static; top: auto; right: auto;
  min-width: 0; border: none; box-shadow: none; padding: 0; background: none;
}
.topbar-of-menu .card-kebab-wrap > .card-kebab-menu button {
  width: 100%; justify-content: flex-start; text-align: left;
  border-radius: 6px; padding: 8px 10px; font-size: 13px; color: var(--text); font-weight: 500;
}
.topbar-of-menu .card-kebab-wrap > .card-kebab-menu button:hover { background: var(--surface-2, #f1f5f9); }
#content { flex: 1; padding: 24px; overflow-y: auto; }

/* Cards / panels */
.card { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); border: 1px solid var(--border); }
.card-header { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.card-header h2 { font-size: 15px; font-weight: 600; }
.card-body { padding: 18px; }
/* Bottom-corner fix: a card rounds its own border, but an opaque full-bleed
   last child — a .table-wrap (opaque since v1.15.383) or an inline footer bar —
   paints square corners over the card's rounded bottom. Round the flush last
   child's OWN bottom corners so the card's radius shows through, rather than
   clipping the card (which would crop focus rings / edge popovers). Covers the
   last child whether it sits directly in the card or in a padding-0 .card-body. */
.card > *:last-child,
.card > .card-body:last-child > *:last-child {
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
}

/* Ingredient filter row */
.ing-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 20px;
  align-items: flex-start;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
  background: #f8fafc;
}
.ing-filter-group { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.ing-filter-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.ing-filter-chips { display: flex; flex-wrap: wrap; gap: 4px; max-width: 320px; }

/* Compact filter row: each filter group is one horizontal line
 * (label on the left, chips inline to the right) so the panel
 * doesn't grow tall with many chip values. */
.test-filter-row-compact { flex-direction: column; gap: 8px; }
.ing-filter-group-h {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  width: 100%;
}
.ing-filter-group-h .ing-filter-label {
  flex: 0 0 100px;
  text-align: right;
}
.ing-filter-group-h .ing-filter-chips {
  flex: 1 1 auto;
  max-width: none;
  min-width: 0;
}
.ing-filter-row-clear { align-self: flex-end; }
@media (max-width: 768px) {
  .ing-filter-group-h { flex-wrap: wrap; }
  .ing-filter-group-h .ing-filter-label { flex-basis: auto; text-align: left; }
}
.ing-filter-chip {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  color: #1e293b;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}
.ing-filter-chip:hover:not(.on) { background: #f1f5f9; border-color: #94a3b8; }
.ing-filter-chip.on { background: var(--primary); color: #fff; border-color: var(--primary); }
.ing-filter-chip-picto { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px 2px 4px; }
.ing-filter-chip-picto .picto-mini { display: inline-flex; align-items: center; justify-content: center; }
.ing-filter-chip-picto .picto-mini svg { display: block; }
.ing-filter-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--primary);
  color: #fff;
  border-radius: 999px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  font-size: 11px;
  font-weight: 600;
  margin-left: 4px;
}

/* View mode toggle (Table / Cards / Group) */
.view-toggle { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; flex-shrink: 0; }
.view-toggle-btn { background: #fff; border: none; padding: 5px 12px; font-size: 12px; color: var(--text-muted); cursor: pointer; transition: background 0.15s, color 0.15s; border-right: 1px solid var(--border); white-space: nowrap; }
.view-toggle-btn:last-child { border-right: none; }
.view-toggle-btn:hover:not(.on) { background: #f1f5f9; color: var(--text); }
.view-toggle-btn.on { background: var(--primary); color: #fff; }

/* Ingredients sub-toolbar: normalize every control to the same 34px height +
   10px corner as the top action bar, so Search / Filter / view switcher / Sort
   / Card-fields all line up. Scoped to .ing-toolbar so other list views keep
   their current sizing. !important beats the inline border-radius:6px that the
   search box and the group <select>s ship with. */
.ing-toolbar .btn,
.ing-toolbar .btn-sm,
.ing-toolbar .ing-search-wrap input,
.ing-toolbar .view-toggle,
.ing-toolbar .view-toggle-select,
.ing-toolbar select {
  height: 34px;
  box-sizing: border-box;
  border-radius: 10px !important;
}
.ing-toolbar .btn-sm { display: inline-flex; align-items: center; }
.ing-toolbar .view-toggle { padding: 0; }
.ing-toolbar .view-toggle-btn { height: 100%; display: inline-flex; align-items: center; }
/* Filter and Sort are 1:1 squares (34×34) — icon centered, never stretched. */
.ing-toolbar .ing-filter-btn,
.ing-toolbar [data-sort-panel-btn] {
  flex: 0 0 34px; width: 34px; padding: 0; justify-content: center;
}
/* The Columns/Card-fields cogwheel button (right of Sort) — square like the
   other icon buttons; hidden on phones (it folds into the ⋯ menu there). */
.ing-toolbar .ing-cols-btn { display: inline-flex; }
.ing-toolbar .ing-cols-btn .btn { width: 34px; padding: 0; justify-content: center; }
/* Keep the filter button square even with an active-count badge: float the
   badge into the top-right corner instead of widening the button. */
.ing-toolbar .ing-filter-btn { position: relative; }
.ing-toolbar .ing-filter-btn .ing-filter-badge {
  position: absolute; top: -5px; right: -5px; margin: 0;
}

/* Search box: fills the left side when there's room (flex:1). The wrapper holds
   a masked search glyph inset on the left; the input is borderless-styled here
   since its inline styles were removed. */
.ing-search-wrap { position: relative; display: inline-flex; flex: 1 1 320px; min-width: 0; }
.ing-search-wrap::before {
  content: ""; position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; background: var(--text-muted, #94a3b8); pointer-events: none;
  -webkit-mask: url(/icns-svg/menu-bar/search.svg?v=a511dc8d72) center / contain no-repeat;
          mask: url(/icns-svg/menu-bar/search.svg?v=a511dc8d72) center / contain no-repeat;
}
.ing-toolbar .ing-search-wrap input {
  width: 100%; padding: 6px 10px 6px 32px; font-size: 13px;
  border: 1px solid var(--border); background: #fff; color: var(--text);
}
.ing-toolbar .ing-search-wrap input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); }

/* ── Toolbar reflow (desktop / tablet) ────────────────────────────────────
   Treat the toolbar as a query container so its controls key off the toolbar's
   OWN width, not the viewport (the sidebar + panels change how much room the
   toolbar actually gets at any given screen size). When the toolbar is too
   narrow to hold Search + the controls comfortably on one row, Search claims
   the full first row and the controls drop to a clean second row — view
   switcher pinned left, the buttons clustered right, whitespace in between and
   nothing stretched. Mobile (≤768px) keeps its own compact single-row layout
   further down, so this only runs at ≥769px. */
.ing-toolbar { container-type: inline-size; }
@media (min-width: 769px) {
  @container (max-width: 620px) {
    .ing-toolbar .ing-search-wrap { flex: 1 1 100%; }
    .ing-toolbar .view-toggle { margin-right: auto; }
  }
}

/* Column-order row inside the ⋯ menu (group mode). A labelled <select>, not a
   button — clicking it opens the native picker without closing the menu. */
.card-kebab-menu .kebab-select-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: 13px; color: var(--text); white-space: nowrap; }
.card-kebab-menu .kebab-select-row select { flex: 1; min-width: 0; padding: 5px 8px; border: 1px solid var(--border); border-radius: 8px; font-size: 12px; font-family: inherit; background: #fff; color: var(--text); }

/* Mobile-only Import/Export accordions inside the single ⋯ menu. The headers
   reuse the flat full-width borderless .card-kebab-menu button style; tapping
   one expands its options indented below (inline-expand). Hidden on desktop,
   where the separate Import ▾ / Export ▾ top-bar buttons show instead. */
.ing-more-mobile { display: none; }
/* When the ⋯ menu would carry only mobile-only items (no chemistry ops), hide
   the whole ⋯ trigger on desktop — its desktop content lives in toolbar buttons. */
@media (min-width: 769px) { #topbar-actions .ing-more-mobileonly { display: none !important; } }
.card-kebab-menu .more-acc-caret { margin-left: auto; transition: transform .15s; opacity: .7; }
.card-kebab-menu .more-acc.open .more-acc-caret { transform: rotate(90deg); }
.card-kebab-menu .more-acc-sub { display: none; }
.card-kebab-menu .more-acc.open .more-acc-sub { display: block; }
.card-kebab-menu .more-acc-sub button { padding-left: 32px; font-size: 12px; color: var(--text-muted); }

/* Drop the divider directly under the top action bar on the Ingredients view —
   the title bar flows straight into the toolbar (scoped via body[data-view]). */
body.fl-list-styled #topbar { border-bottom-color: transparent; box-shadow: none; }
/* Line the sub-toolbar up with the top bar: same 24px side inset (was 18px) so
   the rows share left/right edges, and a tighter top inset so the two rows sit
   closer together (was 14px all round).

   Scoped to the LIST CARD's own header. It used to read `.card-header`, which is
   a body-level selector, so the deliberately lopsided 4px/9px padding leaked
   into every other card header in the app — most visibly every card title in
   Settings, which sat 4px from its top edge and 10px from the divider below.
   Floating mode overrides this with 8px further down; docked still uses it. */
body.fl-list-styled #content > .list-card > .card-header { padding: 4px 24px 9px; }

/* Phones: the old single squished row made Search unusably narrow, so mirror the
   desktop clip reflow instead — Search claims the full first row, and the controls
   drop to a second row with the view switcher pinned LEFT and the buttons clustered
   RIGHT in order Filter · Sort · Settings. The group-by select (group mode) wraps
   to its own full-width line below. */
@media (max-width: 768px) {
  body.fl-list-styled .card-header { flex-direction: row; align-items: center; }
  body.fl-list-styled .ing-toolbar { flex-wrap: wrap; gap: 6px; width: 100%; justify-content: flex-start; }
  /* Search: full first row (flex-basis 100% forces the wrap). */
  body.fl-list-styled .ing-search-wrap { order: 1; flex: 1 1 100%; min-width: 0; }
  body.fl-list-styled .ing-toolbar .ing-search-wrap input { width: 100% !important; }
  /* Row 2: view switcher left (margin-right:auto shoves the buttons right), then
     Filter · Sort · Settings clustered at the right edge. */
  body.fl-list-styled .ing-toolbar .view-toggle-select { order: 2; flex: 0 0 auto; width: auto; min-width: 0; margin-right: auto; }
  body.fl-list-styled .ing-toolbar .ing-filter-btn { order: 3; }
  body.fl-list-styled .ing-toolbar [data-sort-panel-btn] { order: 4; }
  body.fl-list-styled .ing-toolbar .ing-cols-btn { order: 5; }
  /* Group controls drop to their own full-width row below the toolbar on phones. */
  body.fl-list-styled .ing-toolbar .ing-group-ctl { order: 6; flex: 1 1 100%; }
  body.fl-list-styled .ing-toolbar .ing-group-ctl select { flex: 1 1 0; min-width: 0; }
  /* Ingredients-only mobile fold: import/export collapse into the single ⋯ and
     + Ingredient shrinks to a "+". The Settings cogwheel now stays in the toolbar
     row (Filter · Sort · Settings) like every other table, so it's no longer
     hidden here. Formulas keeps its toolbar buttons visible on phones too. */
  body[data-view="ingredients"] .ing-toolbar .ing-cols-btn { display: inline-flex; order: 5; }
  body[data-view="ingredients"] #topbar-actions .ing-topbar-dd { display: none !important; }
  body[data-view="ingredients"] .ing-more-mobile { display: block; }
  body[data-view="ingredients"] .ing-add-label { display: none; }
  body[data-view="ingredients"] #topbar-actions .btn-primary { order: -1; width: 34px; padding: 0; }
  /* Formulas: not enough room on a phone for "+ Formula" — collapse it to a
     square "+" (label hidden). Stays on the right (the ⋯ overflow controller
     pins the primary far-right), so no order override here. */
  body[data-view="formulations"] .ing-add-label { display: none; }
  body[data-view="formulations"] #topbar-actions .btn-primary { width: 34px; padding: 0; }
}
/* Mobile alternative to the segmented control: a compact dropdown (lsViewToggleHtml
   emits both; CSS swaps them). On phones the segmented buttons hide and this shows. */
.view-toggle-select { display: none; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; font-family: inherit; background: #fff; color: var(--text); flex-shrink: 0; }
@media (max-width: 768px) {
  .view-toggle { display: none; }
  .view-toggle-select { display: inline-block; }
  /* Stack the header so the title sits above a full-width toolbar (instead of the
     title and a squished, overflowing toolbar sharing one row). */
  .card-header { flex-direction: column; align-items: stretch; gap: 8px; }
  .card-header > div { justify-content: flex-start !important; }
  /* The fixed-width search box is the main thing that runs a table off-screen on
     a phone — let it take the full row instead. */
  .card-header input[type="text"],
  .card-header input[type="search"] { width: 100% !important; min-width: 0; flex: 1 1 100%; }
}

/* Cards view */
.ing-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; padding: 14px; }
.ing-card { background: #fff; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; display: flex; flex-direction: column; position: relative; }
.ing-card:hover { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
/* Always-visible kebab menu used by every list card variant
 * (Projects / Ingredients / Formulations / Batches / Samples).
 * Tucks into the top-right corner; opens a popup menu on click. */
.card-kebab-wrap {
  position: absolute;
  /* Moved from left:6 → right:6 so the kebab tucks into the
     top-right corner (matching the long-standing CSS comment
     "Tucks into the top-right corner"). The previous top-LEFT
     position required a 28px padding-left reservation on every
     card's first child to keep the title from sliding under the
     button — which left a permanent 28px dead band on the left
     of every card. With the kebab on the right, that reservation
     is gone and titles flow from the actual left edge. */
  top: 6px; right: 6px;
  z-index: 3;
}
/* In LIST TABLES the kebab wrap is re-homed into an actions cell (its position
   is overridden to relative inline). The card z-index:3 above would otherwise
   paint the ⋮ ABOVE the sticky header row (z-index:2) when a row scrolls under
   it — pin the resting table kebab below the header. The OPEN menu still floats
   on top: _toggleCardKebab sets an inline z-index:4000 while open. */
.list-card [id$="-table-region"] td .card-kebab-wrap { z-index: 0; }
.card-kebab {
  width: 22px; height: 22px;
  padding: 0;
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid transparent;
  border-radius: 4px;
  color: var(--text-muted);
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: color 0.12s, background 0.12s, border-color 0.12s;
}
.card-kebab:hover,
.card-kebab.open { color: var(--text); background: #fff; border-color: var(--border); }
/* Card multi-select checkbox. Sits in the BOTTOM-right corner so it clears the
   favorite star (top-left) and the kebab (top-right). Kept out of the way —
   revealed on card hover, or whenever a selection is already active
   (body.has-bulk-selection is set by the bulk engine), or when the box itself
   is checked — so cards stay clean but selection is one click away. A white
   chip lets it sit cleanly over whatever content is beneath it. */
.card-select {
  position: absolute; bottom: 6px; right: 6px; z-index: 3;
  display: inline-flex; padding: 2px; border-radius: 4px;
  background: rgba(255, 255, 255, 0.9); cursor: pointer;
  opacity: 0; transition: opacity 0.12s;
}
/* Both card shapes must be listed: Group view renders .ing-group-card, so a
   rule naming only .ing-card left its checkbox stuck at opacity 0 — present and
   clickable, but invisible, which is worse than absent. */
.ing-card:hover .card-select,
.ing-group-card:hover .card-select,
body.has-bulk-selection .card-select,
.card-select:has(.bulk-row-cb:checked) { opacity: 1; }
.card-select .bulk-row-cb { width: 16px; height: 16px; margin: 0; cursor: pointer; }
/* Group-view select-all checkbox. Unlike .card-select it does NOT appear on any
   active selection (that would light up all 30+ group headers when you tick one
   item) — you invoke a group select-all by going TO the group, so it reveals only
   on hovering that group, or when the group already holds a pick (so its
   indeterminate/checked state stays visible). Bottom-aligned to echo the cards. */
.fl-group-all { flex: none; display: flex; align-items: center; cursor: pointer; opacity: 0; transition: opacity 0.12s; }
.ing-group-col:hover .fl-group-all,
.ing-group-col:has(.bulk-row-cb:checked) .fl-group-all { opacity: 1; }
.fl-group-all-cb { width: 16px; height: 16px; margin: 0; cursor: pointer; accent-color: #38bdf8; }
.ing-card:has(.bulk-row-cb:checked),
.ing-group-card:has(.bulk-row-cb:checked) {
  border-color: var(--primary);
  box-shadow: 0 0 0 1px var(--primary);
  background: #f8faff;
}
/* Reserves a strip at the bottom of a selectable card so the bottom-right
   .card-select checkbox (revealed on hover) floats over empty space instead of
   covering the last content line (e.g. a panel's "N tests"). */
/* Tall enough to clear the checkbox band (bottom:6px + ~20px control = 26px)
   even on the COMPACT group cards, which use 8px bottom padding rather than 12
   — at 18px their cost line landed exactly on the checkbox. */
.card-select-gutter { height: 22px; }
.card-kebab-menu {
  display: none;
  position: absolute;
  /* Right-aligned with the kebab button (now on the top-right of
     the card). Opens dropping left so a card hugging the right
     edge of the viewport doesn't push the menu off-screen. */
  top: calc(100% + 4px); right: 0;
  min-width: 140px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  padding: 4px;
  flex-direction: column;
  /* Menus sit on the top layer. 10 put this BELOW app chrome that legitimately
   * elevates itself — #ing-detail-head is sticky at 20 — so a kebab opened near
   * a sticky header clipped behind it. _toggleCardKebab already stamps 4000
   * inline when it portals the menu to <body>; matching that here keeps the
   * in-place (non-portaled) case on the same layer instead of silently lower. */
  z-index: 4000;
}
.card-kebab-menu.open { display: flex; animation: fl-menu-in 0.14s ease-out; }
/* Fade-out on close: keep it laid out + fade opacity over 0.2s, then JS removes it. */
.card-kebab-menu.closing { display: flex !important; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; }
@keyframes fl-menu-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* Modal backdrop shared by every popup menu — makes it modal (nothing else is
   clickable while open) and swallows the dismiss click so closing a menu never
   clears a table's row selection. Transparent; the menu itself sits above it. */
#fl-menu-backdrop { position: fixed; inset: 0; z-index: 3900; display: none; background: transparent; }
#fl-menu-backdrop.show { display: block; }
.card-kebab-menu button {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 6px 10px;
  background: none; border: none;
  font-size: 12.5px; text-align: left;
  color: var(--text);
  cursor: pointer; border-radius: 4px;
  white-space: nowrap;
  font-family: inherit;
}
.card-kebab-menu button:hover { background: #f1f5f9; }
.card-kebab-menu button.danger { color: var(--danger); }
.card-kebab-menu button.danger:hover { background: #fee2e2; }
/* Push the first content row right so it doesn't collide with the
 * kebab in the top-left corner. The kebab-wrap itself is absolute-
 * positioned, so we want the padding on the next sibling. */
/* Kebab now sits in top-right of the card (see .card-kebab-wrap).
   Reserve 22px of right-padding on the first child so the title
   (and the UID/status badge that sits in that title's top-right
   on Formulations / Ingredients / Samples) doesn't slide under
   the kebab. 22px = kebab button width; the kebab itself sits at
   right:6 of the card so the actual visual gap is 22-6 from the
   inner content's right edge — tight, but no overlap. */
.ing-card .ing-card-body > div:first-child,
.ing-group-card > .card-kebab-wrap + div { padding-right: 22px; }
.ing-card-thumb { background: #f8fafc; padding: 4px; height: 120px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid var(--border); }
.ing-card-body { padding: 10px 12px 12px; }
/* Compact cover thumbnail on a Sample card — small square avatar to
   the left of the name/status row, instead of a full-width band.
   Square crop (object-fit:cover) is fine at this size; the whole
   image is still viewable via the attachment preview / hover popover. */
/* Upload menu drop zone — a green-+ drag-and-drop target shown in the
   "Add attachment" modal opened from the ⤴ Upload button. */
.attach-menu-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  padding: 30px 20px; border: 2px dashed var(--border); border-radius: 10px;
  background: #fafbff; cursor: pointer; text-align: center;
  transition: border-color 0.15s, background 0.15s;
}
.attach-menu-drop:hover { border-color: #22c55e; background: #f0fdf4; }
.attach-menu-drop.attach-menu-drop-active { border-color: #16a34a; border-style: solid; background: #dcfce7; }
.attach-menu-plus {
  width: 54px; height: 54px; border-radius: 50%;
  background: #22c55e; color: #fff;
  font-size: 36px; line-height: 1; font-weight: 300;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 14px rgba(34, 197, 94, 0.4);
}
.attach-menu-drop-text { font-size: 14px; font-weight: 600; color: var(--text); }
.attach-menu-drop-sub  { font-size: 11.5px; color: var(--text-muted); }

/* ---- Per-measurement attachment chips (test-detail measurements table) ----
   Compact file pills: thumbnail/icon + filename + delete. Images get a
   hover-zoom preview that reuses the thumbnail's already-resolved blob URL. */
.meas-att-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.meas-att-chip {
  position: relative; display: inline-flex; align-items: center; gap: 3px;
  max-width: 190px; padding: 2px 4px 2px 2px;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface, #fff);
}
.meas-att-open {
  display: inline-flex; align-items: center; gap: 5px; min-width: 0;
  text-decoration: none; color: var(--text, #475569);
}
.meas-att-ico {
  width: 26px; height: 26px; flex: 0 0 auto; border-radius: 4px;
  object-fit: cover; border: 1px solid var(--border); background: #f1f5f9;
}
.meas-att-fileico { display: inline-flex; align-items: center; justify-content: center; }
.meas-att-fileico img { width: 15px; height: 15px; object-fit: contain; opacity: .55; }
.meas-att-name {
  font-size: 11px; line-height: 1.2; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 130px;
}
.meas-att-del {
  flex: 0 0 auto; width: 15px; height: 15px; padding: 0; line-height: 1;
  border: none; background: transparent; color: #94a3b8; cursor: pointer; border-radius: 50%;
}
.meas-att-del:hover { color: #dc2626; background: #fee2e2; }
.meas-att-add {
  padding: 4px 8px; border: 1px dashed #cbd5e1; background: var(--surface, #fff);
  color: #64748b; border-radius: 4px; font-size: 11px; cursor: pointer; white-space: nowrap;
}
.meas-att-add:hover { border-color: #94a3b8; color: #475569; }
/* Hover-zoom preview — images only (SVG is blocked at upload, so this is safe). */
.meas-att-zoom {
  position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 60; display: none;
  padding: 4px; background: var(--surface, #fff); border: 1px solid var(--border);
  border-radius: 6px; box-shadow: 0 8px 24px rgba(15, 23, 42, .18);
}
.meas-att-zoom img {
  display: block; max-width: 240px; max-height: 240px;
  width: auto; height: auto; object-fit: contain; border-radius: 3px;
}
.meas-att-chip:hover .meas-att-zoom { display: block; }

.sample-card-cover-mini {
  width: 52px; height: 52px;
  flex-shrink: 0;
  border-radius: 6px;
  object-fit: cover;
  border: 1px solid var(--border);
  background: #f1f5f9;
  display: block;
}

/* Group / kanban view */
.ing-group-board { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); align-items: start; gap: 12px; padding: 14px; }
.ing-group-col { background: #f8fafc; border: 1px solid var(--border); border-radius: 8px; display: flex; flex-direction: column; max-height: 70vh; min-width: 0; }
.ing-group-head { padding: 10px 12px; border-bottom: 1px solid #1f2937; background: #111827; color: #fff; border-radius: 8px 8px 0 0; }
.ing-group-list { padding: 8px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.ing-group-card { background: #fff; border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; cursor: pointer; transition: border-color 0.15s; position: relative; }
.ing-group-card:hover { border-color: var(--primary); }

/* Data view */
.data-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  max-width: 900px;
}
.data-grid > .card:first-child { grid-column: 1 / -1; }
.data-grid > .data-danger-card {
  grid-column: 1 / -1;
  border: 2px solid var(--danger);
}

/* Chart-type selector in arrange-mode panel headers */
.dash-chart-type {
  display: none;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 6px;
  font-size: 11px;
  font-family: inherit;
  color: var(--text);
  cursor: pointer;
}
.dash-edit .dash-chart-type { display: inline-block; }

/* Cost basis selector — sits inline in the composition table header */
.cost-basis-select {
  font-family: inherit;
  font-size: 12px;
  padding: 1px 4px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #fff;
  color: var(--primary);
  font-weight: 600;
  cursor: pointer;
  width: auto;          /* don't stretch inside flex containers */
  vertical-align: middle;
  margin: 1px 2px;      /* breathing room when the header wraps */
}
.cost-basis-select:focus { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Composition table headers — auto-grow when the column is narrower than
   the multi-element content (% of Total + Cost columns carry inline
   dropdowns, labels, and a sort badge). Setting white-space:normal +
   vertical-align:top lets the header row grow taller so every component
   stays visible no matter how narrow the column gets, instead of getting
   clipped at the column edge. The body rows below stay at their normal
   height — only the <thead> row grows. */
#comp-table thead th {
  vertical-align: top;
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.35;
}
#comp-table thead th .th-sort-icon {
  vertical-align: middle;
}
/* Fixed layout makes the per-column widths (defaults in _COMP_DEFAULT_WIDTHS +
   drag-to-resize, applied via _renderColWidths) authoritative — without it the
   browser auto-sizes columns to content and ignores the widths, so the resize
   handles look dead. width:max-content + min-width:100% lets the table be the
   sum of its columns (so it scrolls horizontally) yet still fill a wider pane. */
#comp-table { table-layout: fixed; width: max-content; min-width: 100%; }
/* Body cells clip so a narrowed column doesn't bleed into its neighbour; the
   ingredient name gets an ellipsis. (Header cells intentionally wrap so the
   cost/% dropdowns stay visible; the notes <input> already fills its cell.) */
#comp-table tbody td { overflow: hidden; }
#comp-table tbody td[data-col="ingredient"] { white-space: nowrap; text-overflow: ellipsis; }
#comp-table thead th .cost-basis-select { max-width: 100%; }
/* Flattened (read-only) composition table — same fixed-layout treatment so its
   4 columns are resizable; body cells clip with an ellipsis on the text ones. */
#flat-comp-table { table-layout: fixed; width: max-content; min-width: 100%; }
#flat-comp-table tbody td { overflow: hidden; }
#flat-comp-table tbody td[data-col="ingredient"],
#flat-comp-table tbody td[data-col="role"],
#flat-comp-table tbody td[data-col="source"] { white-space: nowrap; text-overflow: ellipsis; }

/* Pricing & Margin — single-line basis bar above the input grid */
.pricing-basis-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  margin-bottom: 16px;
  background: #f8fafc;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px;
  flex-wrap: wrap;     /* graceful wrap only if absolutely needed */
}
.pricing-basis-bar .pbb-label  { color: var(--text-muted); }
.pricing-basis-bar .pbb-value  { color: var(--primary); font-weight: 700; }
.pricing-basis-bar .pbb-select {
  flex: 0 0 auto;
  width: auto;
  min-width: 80px;
  padding: 3px 8px;
}
.pricing-basis-bar .pbb-hint {
  margin-left: auto;
  color: var(--text-muted);
  font-size: 11px;
  font-style: italic;
}

/* Cost change indicator */
.cost-change-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
}
.cost-change-chip.up   { background: #fef2f2; color: var(--danger); }
.cost-change-chip.down { background: #f0fdf4; color: #16a34a; }

/* Sort panel — popover anchored to ⇅ Sort button */
.sort-panel {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.14);
  z-index: 90;
  font-family: inherit;
}
.sort-panel-header {
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.sort-panel-close {
  background: none;
  border: none;
  font-size: 14px;
  cursor: pointer;
  color: #94a3b8;
  padding: 0 4px;
  line-height: 1;
}
.sort-panel-close:hover { color: var(--text); }
.sort-panel-body { padding: 6px 14px; }
.sort-panel-footer {
  padding: 10px 14px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 8px;
}
.sort-key-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 0;
}
.sort-key-num {
  font-size: 11px;
  font-weight: 700;
  color: var(--primary);
  background: #eff6ff;
  border-radius: 4px;
  padding: 2px 0;
  width: 22px;
  text-align: center;
  flex-shrink: 0;
}
.sort-key-col {
  flex: 1;
  min-width: 0;
  padding: 5px 8px;
  font-size: 13px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: #fff;
}
.sort-dir-toggle {
  display: inline-flex;
  flex-shrink: 0;
}
.sort-dir-btn {
  border: 1px solid var(--border);
  background: #fff;
  font-size: 11px;
  padding: 4px 8px;
  cursor: pointer;
  line-height: 1.4;
  color: var(--text);
}
.sort-dir-btn:first-child { border-radius: 5px 0 0 5px; }
.sort-dir-btn:last-child  { border-radius: 0 5px 5px 0; margin-left: -1px; }
.sort-dir-btn.on { background: var(--primary); color: #fff; border-color: var(--primary); z-index: 1; position: relative; }
.sort-key-move,
.sort-key-remove {
  width: 24px;
  height: 24px;
  font-size: 11px;
  background: #f1f5f9;
  color: #475569;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  flex-shrink: 0;
}
.sort-key-move:hover:not(:disabled),
.sort-key-remove:hover { background: #e2e8f0; }
.sort-key-move:disabled { opacity: 0.35; cursor: not-allowed; }
.sort-key-remove:hover { background: #fee2e2; color: var(--danger); }
.sort-empty {
  padding: 14px 4px;
  font-size: 13px;
  color: var(--text-muted);
  font-style: italic;
  text-align: center;
}

/* Sidebar Feedback button — lives in the sidebar footer between
   the Account button and the Collapse-sidebar toggle. Same visual
   weight as .sidebar-toggle so the row reads as three equal-rank
   utility buttons. Previously this was a floating FAB pinned to
   the viewport's bottom-right (.feedback-fab), but it floated over
   page content and felt visually loud — moved into the persistent
   sidebar slot so it's always reachable without obstructing the
   work area. */
#sidebar .sidebar-footer .sidebar-feedback,
#sidebar .sidebar-footer .sidebar-ai,
#sidebar .sidebar-footer .sidebar-scan {
  background: none;
  border: none;
  color: var(--sb-icon);
  cursor: pointer;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  flex-shrink: 0;
  transition: color 0.15s, background 0.15s;
  font-size: 16px;
  line-height: 1;
  font-family: inherit;
}
#sidebar .sidebar-footer .sidebar-feedback:hover,
#sidebar .sidebar-footer .sidebar-ai:hover,
#sidebar .sidebar-footer .sidebar-scan:hover {
  color: #fff;
  background: var(--sb-hover);
}
#sidebar .sidebar-footer .sidebar-feedback:focus-visible,
#sidebar .sidebar-footer .sidebar-ai:focus-visible,
#sidebar .sidebar-footer .sidebar-scan:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
/* Active state — the AI drawer is open. Mirrors the muted-glow
   highlight used by an active nav-item, since this is the same
   "you are here" affordance for a feature that used to live in the
   nav list. */
#sidebar .sidebar-footer .sidebar-ai.active {
  color: #fff;
  background: var(--sb-hover);
}

.data-snapshot {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px 24px;
}
.data-section-title {
  grid-column: 1 / -1;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  padding: 6px 4px 0 4px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 6px;
  margin-top: 4px;
}
.data-section-title.danger { color: var(--danger); border-bottom-color: #fecaca; }

/* Backup row actions. The × was padded 2px 7px around a glyph, so it came out
   as a short wide rectangle a couple of px shorter than Restore beside it.
   Both now share one height and the × is a true square via aspect-ratio,
   so the pair lines up whatever the glyph metrics do.

   Specificity matters here: .btn-sm (padding 4px 10px) is declared LATER in this
   file, so a bare .bk-act loses the tie and keeps that padding — which left the
   24px square with 4px of content box and squashed the masked glyph to a 2px
   sliver that read as a dot. Qualifying with .btn wins on specificity instead
   of relying on source order. */
.btn.bk-act { font-size: 11px; height: 24px; padding: 0 9px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.btn.bk-act-x { width: 24px; padding: 0; aspect-ratio: 1; }
/* The glyph is a flex item now, so stop it being shrunk by the centring. */
.btn.bk-act-x > .fl-cross { flex: 0 0 auto; width: 11px; height: 11px; }
/* Delete reads as destructive at a glance: pale red fill, red glyph, no hard
   border (the fill carries it — same reasoning as the Danger zone on glass).
   The glyph is a currentColor mask, so colouring the button colours the X. */
.btn.bk-act-x { background: #fef2f2; border-color: transparent; color: #dc2626; }
.btn.bk-act-x:hover { background: #fee2e2; color: #b91c1c; }
html[data-sb-translucent] .btn.bk-act-x { background: rgba(254, 226, 226, 0.62); }

/* Automatic-backups switch */
.bk-auto { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-size: 12px; color: var(--text-muted); margin: 0 0 12px; user-select: none; }
.bk-auto input { width: 14px; height: 14px; margin: 0; cursor: pointer; flex: 0 0 auto; }
.bk-auto:hover { color: var(--text); }
.bk-auto-off { color: #b45309; font-weight: 600; }

/* ── Foldable category sections (Data tab) ──────────────────────────────────
   The header keeps the look of .data-section-title so the tab is not visually
   restyled — it just becomes a button with a disclosure triangle. The body
   collapses with the grid 0fr→1fr technique used by .sec-fold-body, which
   animates without needing a measured pixel height. Cards inside keep the
   two-column layout via .data-fold-cards, since wrapping them removed them
   from .data-grid's own grid. */
.data-fold { grid-column: 1 / -1; }
/* Locked category (shared workspace): shut, greyed, non-interactive. The chevron
   stays put rather than being removed, so the row still reads as a section that
   would open — it just says why it will not. */
.data-fold-locked .data-fold-head,
.sec-box.data-fold-locked .sec-fold { opacity: 0.5; cursor: not-allowed; }
.data-fold-locked .data-fold-head:hover { color: var(--text-muted); }
.data-fold-lock {
  margin-left: 8px; font-size: 9.5px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
}
.sec-box.data-fold-locked .sec-fold { justify-content: flex-start; gap: 8px; }
.sec-box.data-fold-locked .sec-fold-chev { margin-left: auto; }
.data-fold-head {
  display: flex; align-items: center; gap: 7px; width: 100%;
  background: none; border: none; border-bottom: 1px solid var(--border);
  font-family: inherit; font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-muted); cursor: pointer;
  padding: 6px 4px; margin-top: 4px; text-align: left;
}
.data-fold-head:hover { color: var(--text); }
.data-fold-chev {
  display: inline-block; width: 0; height: 0; flex: 0 0 auto;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  border-left: 5px solid currentColor; transition: transform 0.2s ease;
}
.data-fold-head.open .data-fold-chev { transform: rotate(90deg); }
.data-fold-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.22s ease; }
.data-fold-body.open { grid-template-rows: 1fr; }
/* The overflow:hidden that makes the collapse work also clips the cards' drop
   shadow, which in glass mode reaches ~12px sideways and ~20px below
   (0 8px 24px). Widen the clipping box past the cards with a negative margin
   and push the cards back to their original x with equal padding, so the
   shadow has room INSIDE the clip while the card edges stay aligned with the
   fold header and the Danger zone. Bottom padding is applied only when open —
   the same trick .sec-fold-inner uses — so a closed fold still collapses to
   exactly 0. */
.data-fold-inner { overflow: hidden; min-height: 0; margin-inline: -14px; padding-inline: 14px; }
.data-fold-body.open .data-fold-inner { padding-bottom: 20px; }
.data-fold-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding-top: 14px; }
.data-fold-cards > .data-section-title,
.data-fold-cards > #demo-tabs,
.data-fold-cards > .sec-box { grid-column: 1 / -1; }
@media (prefers-reduced-motion: reduce) {
  .data-fold-body, .data-fold-chev { transition: none; }
}
@media (max-width: 720px) { .data-fold-cards { grid-template-columns: 1fr; } }

/* Translucent theme: the Danger zone's red outline is the loudest border in the
   app, and borders are exactly what glass mode is trying to stop drawing
   attention to. Swap it for a pale red fill — the tint carries the warning, the
   edge does not. border-color stays `transparent` rather than border:none so the
   box keeps its 1px metrics and nothing reflows when the theme changes. */
html[data-sb-translucent] .sec-danger-box {
  border-color: transparent;
  background: rgba(254, 226, 226, 0.62);
}
html[data-sb-translucent] .sec-danger-box .sec-fold { color: #b91c1c; }
.data-grid .card .card-body p:last-of-type { margin-bottom: 14px; }
.data-grid .card .demo-bullets {
  color: var(--text-muted);
  font-size: 12.5px;
  margin: 0 0 12px 18px;
  line-height: 1.65;
}
.data-snapshot > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.data-snapshot .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
.data-snapshot .value { font-size: 16px; font-weight: 600; }

/* Dashboard */
/* auto-fit (not -fill) so the 7 count tiles stretch to fill the row instead of
   leaving empty tracks; the smaller 125px min lets all seven sit on one row at
   normal desktop widths, still wrapping gracefully when the window is narrow. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(125px, 1fr)); gap: 12px; margin-bottom: 24px; }
.stat-card { background: var(--surface); border-radius: var(--radius); padding: 15px 16px; box-shadow: var(--shadow); border: 1px solid var(--border); }

/* ---- Onboarding: first-run "Start here" hero (js/onboarding.js) ---- */
.ob-hero {
  position: relative; margin-bottom: 20px; padding: 20px 22px;
  border: 1px solid var(--border); border-radius: 12px;
  background:
    radial-gradient(120% 140% at 0% 0%, rgba(37,99,235,0.07), transparent 60%),
    linear-gradient(180deg, var(--bg-soft), var(--surface));
}
.ob-hero-x {
  position: absolute; top: 10px; right: 12px; width: 26px; height: 26px;
  border: none; background: transparent; color: var(--text-muted);
  font-size: 20px; line-height: 1; cursor: pointer; border-radius: 6px;
}
.ob-hero-x:hover { background: rgba(100,116,139,0.14); color: var(--text); }
.ob-hero-title { font-size: 17px; font-weight: 700; color: var(--text); }
.ob-hero-sub { margin-top: 4px; font-size: 13px; color: var(--text-muted); line-height: 1.5; max-width: 640px; }
.ob-hero-actions {
  margin-top: 16px; display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.ob-card {
  text-align: left; display: flex; flex-direction: column; gap: 4px;
  padding: 14px 14px 15px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); cursor: pointer; font-family: inherit;
  transition: border-color .15s, box-shadow .15s, transform .05s;
}
.ob-card:hover { border-color: var(--primary); box-shadow: 0 2px 10px rgba(37,99,235,0.10); }
.ob-card:active { transform: translateY(1px); }
.ob-card-primary { border-color: #bfdbfe; background: #eff6ff; }
.ob-card-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 8px; margin-bottom: 4px;
  background: var(--primary); color: #fff; font-size: 16px; font-weight: 700;
}
.ob-card-h { font-size: 13.5px; font-weight: 600; color: var(--text); }
.ob-card-d { font-size: 12px; color: var(--text-muted); line-height: 1.45; }

/* First-run hero: the Showcase demo as the dominant primary CTA, with the two
   "start your own" options quieted to a secondary row below. */
.ob-hero-cta {
  display: flex; align-items: center; gap: 14px; width: 100%;
  text-align: left; margin-top: 16px; padding: 15px 18px;
  background: linear-gradient(135deg, #eff6ff, #dbeafe);
  border: 1.5px solid var(--primary); border-radius: 12px;
  cursor: pointer; font-family: inherit;
  transition: box-shadow .15s, transform .05s;
}
.ob-hero-cta:hover { box-shadow: 0 6px 20px rgba(37,99,235,0.18); }
.ob-hero-cta:active { transform: translateY(1px); }
.ob-hero-cta-ico {
  flex: 0 0 auto; width: 40px; height: 40px; display: flex;
  align-items: center; justify-content: center;
  background: var(--primary); color: #fff; border-radius: 10px; font-size: 20px;
}
.ob-hero-cta-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.ob-hero-cta-h {
  font-size: 15.5px; font-weight: 700; color: var(--text);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.ob-hero-cta-badge {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: #fff; background: var(--primary); padding: 2px 8px; border-radius: 9999px;
}
.ob-hero-cta-d { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
.ob-hero-cta-arrow { flex: 0 0 auto; font-size: 22px; color: var(--primary); font-weight: 700; }
.ob-hero-sec {
  display: grid; gap: 10px; margin-top: 10px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
/* Secondary cards read quieter than the hero: muted (not accent) icon tile. */
.ob-card-ico-sec { background: #eef2f7; color: #64748b; }

/* Once-only feature tips (js/onboarding.js obTipHtml) */
.ob-tip {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 12px; padding: 8px 10px;
  background: #f5f3ff; border: 1px solid #ddd6fe; border-radius: 8px;
  font-size: 12px; line-height: 1.45; color: #4c1d95;
}
.ob-tip-spark { color: #7c3aed; font-weight: 700; line-height: 1.45; }
.ob-tip-text { flex: 1; min-width: 0; }
.ob-tip-text strong { color: #5b21b6; }
.ob-tip-x {
  border: none; background: transparent; color: #7c3aed; cursor: pointer;
  font-size: 16px; line-height: 1; padding: 0 2px; border-radius: 4px; flex: 0 0 auto;
}
.ob-tip-x:hover { background: rgba(124,58,237,0.12); }

.stat-card .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
.stat-card .value { font-size: 28px; font-weight: 700; }
.stat-card.blue .value { color: var(--primary); }
.stat-card.green .value { color: var(--success); }
.stat-card.amber .value { color: var(--warning); }

/* Dashboard draggable grid */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 220px;
  grid-auto-flow: row dense;
  gap: 16px;
  align-items: stretch;
}
.dash-panel { min-width: 0; min-height: 0; position: relative; }
.dash-panel > .card { height: 100%; overflow: hidden; }

/* Drag handle — hidden until Arrange mode */
.dash-drag-handle {
  display: none; cursor: grab; font-size: 18px; color: #94a3b8;
  line-height: 1; flex-shrink: 0; padding: 2px;
  border-radius: 4px; transition: color 0.15s;
}
.dash-drag-handle:active { cursor: grabbing; }
.dash-edit .dash-drag-handle { display: block; }
.dash-edit .dash-drag-handle:hover { color: var(--primary); }

/* Needs Attention triage strip — rolls up the app's alert signals. Scrolls
   inside the fixed-height panel when several signals fire at once. */
.dash-attention { height: 100%; overflow: auto; padding: 10px 14px 12px; display: flex; flex-direction: column; gap: 7px; }
.dash-att-row { border-radius: 8px; border: 1px solid var(--border); border-left-width: 3px; padding: 7px 10px 8px; background: var(--surface); }
.dash-att-row.is-high { border-left-color: #dc2626; background: #fef2f2; }
.dash-att-row.is-med  { border-left-color: #d97706; background: #fffbeb; }
.dash-att-head { display: flex; align-items: baseline; gap: 7px; }
.dash-att-count { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.dash-att-label { font-size: 12.5px; font-weight: 600; color: var(--text); }
.dash-att-jump { margin-left: auto; color: var(--text-muted); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; text-decoration: none; flex-shrink: 0; }
.dash-att-jump:hover { color: var(--primary); }
.dash-att-items { display: flex; flex-wrap: wrap; gap: 5px 6px; margin-top: 5px; }
.dash-att-chip {
  font-size: 11px; color: var(--text); background: rgba(15,23,42,0.05);
  border-radius: 5px; padding: 1px 7px; cursor: pointer; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: none;
}
.dash-att-chip:hover { background: rgba(15,23,42,0.10); color: var(--primary); }
.dash-att-more { font-size: 11px; color: var(--text-muted); align-self: center; }

/* Header blocks (count tiles + Design Space card) — hideable via Arrange like
   grid panels, but they stay full-width bands above the grid. The × shows only
   while arranging (the .editing class is toggled on them in that mode). */
.dash-header-block { position: relative; }
.dash-header-block.editing { outline: 1px dashed var(--border); outline-offset: 4px; border-radius: var(--radius); }
.dash-header-close {
  position: absolute; top: -8px; right: -8px;
  width: 22px; height: 22px; border-radius: 50%;
  background: #fff; border: 1px solid var(--border); color: #64748b;
  font-size: 15px; line-height: 1; cursor: pointer;
  display: none; align-items: center; justify-content: center;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12); transition: all 0.15s; z-index: 3;
}
.dash-header-close:hover { background: var(--danger); color: #fff; border-color: var(--danger); transform: scale(1.05); }
.dash-header-block.editing .dash-header-close { display: inline-flex; }

/* Close button — floats top-right of panel, only in arrange mode */
.dash-close {
  position: absolute;
  top: -8px; right: -8px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid var(--border);
  color: #64748b;
  font-size: 15px; line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center; justify-content: center;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12);
  transition: all 0.15s;
  z-index: 2;
}
.dash-close:hover { background: var(--danger); color: #fff; border-color: var(--danger); transform: scale(1.05); }
.dash-edit .dash-close { display: inline-flex; }

/* Resize handle — bottom-right corner, only in arrange mode */
.dash-resize-handle {
  position: absolute;
  bottom: 2px; right: 2px;
  width: 16px; height: 16px;
  cursor: nwse-resize;
  display: none;
  z-index: 2;
  background:
    linear-gradient(135deg,
      transparent 0%, transparent 55%,
      #94a3b8 55%, #94a3b8 65%,
      transparent 65%, transparent 75%,
      #94a3b8 75%, #94a3b8 85%,
      transparent 85%);
  border-radius: 0 0 var(--radius) 0;
  opacity: 0.7;
  transition: opacity 0.15s;
}
.dash-resize-handle:hover { opacity: 1; }
.dash-edit .dash-resize-handle { display: block; }
/* Enlarge the grab zone well beyond the 16px chevron (without changing its
   look) so the handle is easy to catch even when the Add-Panel tile sits just
   below the bottom row. */
.dash-resize-handle::before { content: ''; position: absolute; inset: -12px; }
.dash-panel.dash-resizing { user-select: none; }
.dash-panel.dash-resizing > .card { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Add-panel tile — sibling below the grid, only in arrange mode */
.dash-add-tile {
  display: none;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  margin-top: 28px;
  padding: 14px 20px;
  background: transparent;
  border: 2px dashed #cbd5e1;
  border-radius: var(--radius);
  color: #64748b;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s;
}
.dash-add-tile:hover { border-color: var(--primary); color: var(--primary); background: #f8fafc; }
.dash-add-plus { font-size: 22px; line-height: 1; font-weight: 300; }
.dash-grid.dash-edit + .dash-add-tile { display: flex; }
/* While a panel is being resized, the Add-Panel tile must not steal the cursor
   or hover — it recedes and ignores pointer events until the drag ends. */
.dash-grid.dash-resizing-live + .dash-add-tile { pointer-events: none; opacity: 0.3; }

.dash-add-menu {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  min-width: 220px;
  padding: 6px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.dash-add-item {
  background: transparent;
  border: none;
  padding: 8px 12px;
  text-align: left;
  font-size: 13px;
  border-radius: 4px;
  cursor: pointer;
  color: #1e293b;
}
.dash-add-item:hover { background: #f1f5f9; color: var(--primary); }
.dash-add-empty { padding: 10px 12px; font-size: 13px; color: #94a3b8; font-style: italic; }
.dash-add-group { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); padding: 8px 12px 3px; }
.dash-add-group:first-child { padding-top: 4px; }

/* Named-layout preset menu (topbar "Layout" button) */
.dash-preset-menu { min-width: 250px; }
.dash-preset-head { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); padding: 4px 12px 6px; }
.dash-preset-item { display: flex; flex-direction: column; gap: 1px; padding: 8px 12px; }
.dash-preset-name { font-size: 13px; font-weight: 600; }
.dash-preset-desc { font-size: 11.5px; color: var(--text-muted); }
.dash-preset-item:hover .dash-preset-desc { color: var(--primary); }
.dash-preset-foot { font-size: 11px; color: var(--text-muted); padding: 6px 12px 3px; border-top: 1px solid var(--border); margin-top: 4px; }
/* Tiny 2×2 grid glyph for the "Layout" button */
.dash-layout-glyph { display: inline-grid; grid-template-columns: 1fr 1fr; gap: 2px; width: 12px; height: 12px; vertical-align: -1px; margin-right: 2px; }
.dash-layout-glyph > span { background: currentColor; border-radius: 1px; opacity: .8; }

/* Drag states */
.dash-panel.dash-dragging { opacity: 0.3; }
.dash-panel.dash-over > .card {
  outline: 2px dashed var(--primary);
  outline-offset: 3px;
  background: #eff6ff;
}

/* Edit mode — highlight panels */
.dash-edit .dash-panel > .card {
  outline: 1px dashed #cbd5e1;
  outline-offset: 2px;
}
.dash-edit .dash-panel:hover > .card { outline-color: #94a3b8; }
.stat-card.red .value { color: var(--danger); }

/* ============================================================
   Full-screen list views — the library lists (Ingredients,
   Formulas, Equipment, Samples, Batches, Test Methods / Test
   Panels, Step Presets) go edge-to-edge: no card box, no 24px
   page gutter, just the data filling the viewport and scrolling
   INSIDE the region. The card-header (search / filters / toolbar)
   stays put and, in table mode, the column-header row pins while
   the rows scroll under it.

   Scoped by body[data-view] (set by the router) so detail views
   and dashboards keep ordinary page flow + their card chrome. The
   list card is always a direct child of #content (siblings: an
   optional intro / analysis panel above it), so it takes the
   leftover height. The 24px #content gutter is dropped — the
   card-header and table cells supply their own inset, so content
   lines up with minimal, consistent padding instead of a wide
   margin on either side. */
body[data-view="ingredients"]      #content,
body[data-view="inventory"]        #content,
body[data-view="materials"]        #content,
body[data-view="finished-goods"]   #content,
body[data-view="formulations"]     #content,
body[data-view="equipment"]        #content,
body[data-view="samples"]          #content,
body[data-view="batches"]          #content,
body[data-view="parameters"]       #content,
body[data-view="templates"]        #content,
body[data-view="testing"]          #content,
body[data-view="procedure-templates"] #content {
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
  padding: 0;
}
/* Formula Analysis panel: when it's open the list-card (#form-table-card) is
   hidden, so the panel — not a .list-card — has to fill the leftover height
   and own its scroll. Without this the flex-column #content (overflow:hidden)
   clips its Compare / Pivot / Ingredient-Map tables with no way to scroll.
   Padding restores the page gutter the list-card mode drops. (Hidden when
   analysis is closed, so list mode is unaffected.) */
body[data-view="formulations"] #content > #formula-analysis-panel {
  /* Thin side gutter (was 24px) so the Compare / Pivot / Ingredient-Map tables
     use nearly the full content width — more columns visible at once.
     Flex column + overflow:hidden: the panel owns the height and the inner
     .fa-scroll table region fills it (see below), so the table extends to the
     bottom of the pane instead of leaving a gap, and scrolls internally. */
  flex: 1 1 auto; min-height: 0; overflow: auto; padding: 14px 8px; margin-bottom: 0;
}
/* Fill-height (Pivot / Ingredient Map only — toggled via .fa-fill in
   _renderAnalysisTab). Compare keeps the scrolling-panel model above because
   its viz chart below the table needs its own room. The chain card →
   analysis-content → .fa-scroll lets the data table grow to the full panel
   height (and scroll past it) instead of floating short in an empty card. */
#formula-analysis-panel.fa-fill { overflow: hidden; display: flex; flex-direction: column; }
#formula-analysis-panel.fa-fill > .card { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#formula-analysis-panel.fa-fill > .card > .card-header { flex: 0 0 auto; }
#formula-analysis-panel.fa-fill #analysis-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* The scroll region fills leftover space; sibling controls keep natural height.
   Both-axes auto so the frozen header (top) pins. */
#formula-analysis-panel.fa-fill .fa-scroll { flex: 1; min-height: 0; overflow: auto; }
/* The list card drops all the card chrome (box / shadow / radius / surface
   fill) and fills the remaining height as a flex column. */
#content > .list-card {
  flex: 1 1 auto; min-height: 0; margin: 0;
  display: flex; flex-direction: column;
  background: transparent; border: none; box-shadow: none; border-radius: 0;
}
/* The one visible data region scrolls; the hidden sibling is display:none
   and drops out of the flex flow. -table-region (table mode) and
   -visual-region / -cards-region (cards & group modes) all qualify. */
#content > .list-card > [id$="-table-region"],
#content > .list-card > [id$="-visual-region"],
#content > .list-card > [id$="-cards-region"] {
  flex: 1 1 auto; min-height: 0; overflow: auto;
}
/* Table mode goes truly full-bleed — the cells' own 14px padding is the only
   side inset, so the grid spans the viewport. Let the region own the scroll
   (both axes) and pin the column headers. */
.list-card > [id$="-table-region"] { padding: 0; }
.list-card [id$="-table-region"] .table-wrap { overflow: visible; }
/* top:-1px (not 0) closes the sub-pixel gap where a scrolling row would
   otherwise bleed through just above the sticky header. */
.list-card [id$="-table-region"] thead th { position: sticky; top: -1px; z-index: 2; }
/* Border-box so a dragged column width (set as `width`) equals the cell's
   rendered border-box width — no per-drag jump by the cell's padding. */
.list-card [id$="-table-region"] table thead th,
.list-card [id$="-table-region"] table tbody td { box-sizing: border-box; }
/* List-table header styling: white (not grey), and a single 1px divider line
   under the sticky header — same thickness/colour as the body row dividers, and
   it belongs to the non-scrolling header (sticky th) so it stays put on scroll.
   Vertical 1px dividers between header columns mark the drag-to-resize boundary
   (the .col-rh grab handle sits over each one). */
.list-card [id$="-table-region"] thead th {
  background: #fff;
  /* Both the bottom line AND the column dividers are inset box-shadows, not
     borders: in a border-collapse table, borders on a sticky <th> get painted
     by the scrolling body cells and slide away on scroll. Shadows stay put, so
     the horizontal underline and the vertical dividers both stick to the header.
     (null the global 2px border-bottom so it isn't doubled.) */
  border-bottom: none; border-right: none;
  box-shadow: inset 0 -1px 0 var(--border), inset -1px 0 0 var(--border);
  /* Compact header row: tighter vertical padding + a smaller, lighter label. */
  padding-top: 3px; padding-bottom: 3px;
  font-size: 10.5px; line-height: 1.25;
}
.list-card [id$="-table-region"] thead th:last-child { box-shadow: inset 0 -1px 0 var(--border); }

/* Grouped inventory: each group header FREEZES just below the sticky thead
   (--inv-group-top = measured thead height) and its label also pins LEFT so the
   section name (name · count · subtotal) stays visible while the table scrolls
   sideways. All groups share ONE tbody, so every sticky-top header sticks at the
   same line and they overlap as you scroll; the incoming header — opaque and later
   in the DOM — masks the ones stacked beneath, which reads as a clean handoff. That
   masking holds only while a header never spills outside its opaque cells, so the
   label sits IN FLOW in a cell that spans a few columns (see the markup) instead of
   overflowing a narrow one — an overflowing label used to bleed the previous group
   through the next. A sticky descendant of a table cell won't pin horizontally in
   Chrome, but a sticky table cell does (same as the frozen first column), so the
   pins live on the cells. --inv-group-left is the checkbox column's width (0 without
   a bulk column) so the label lines up with the first data column; z-index keeps the
   header above the body's sticky-left frozen first column scrolling beneath. */
.list-card [id$="-table-region"] .inv-group-row td.inv-group-label,
.list-card [id$="-table-region"] .inv-group-row td.inv-group-fill {
  position: sticky;
  top: var(--inv-group-top, 44px);
  z-index: 2;
}
.list-card [id$="-table-region"] .inv-group-row td.inv-group-label {
  left: var(--inv-group-left, 0px);
  z-index: 3;
}
/* The group row's empty first cell keeps the grey bar across the checkbox column
   and pins left:0 with it — override the frozen-column's white background. */
.list-card [id$="-table-region"] .inv-group-row td.bulk-check-col {
  position: sticky;
  top: var(--inv-group-top, 44px);
  left: 0;
  background: var(--surface-2, #f1f5f9);
  z-index: 3;
}
/* Vertically centre the row-action kebab in its cell. The inline-block wrap sits
   on the text baseline and rides low in the taller row (vertical-align:middle
   doesn't lift it), so absolutely-centre it instead. The floating menu is
   portaled to <body> off the button's rect, so pulling the wrap out of flow is
   safe. */
.inv-list-table td[data-col="actions"] { position: relative; }
.inv-list-table td[data-col="actions"] .card-kebab-wrap {
  /* !important overrides the wrap's inline position:relative (inline beats a
     plain rule on specificity). */
  position: absolute !important; top: 50%; right: 10px; transform: translateY(-50%);
}

/* Inventory section tabs — Overview / Finished goods (same nav item, own path).
   Sits at the top of #content (flex:0 0 auto so the list-card below scrolls). */
.inv-section-tabs { display: flex; gap: 2px; flex: 0 0 auto; padding: 10px 24px 0; border-bottom: 1px solid var(--border); }
.inv-section-tab { background: none; border: none; padding: 8px 14px; font-size: 13px; font-weight: 600; color: var(--text-muted, #64748b); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.inv-section-tab:hover { color: var(--text); }
.inv-section-tab.on { color: var(--primary); border-bottom-color: var(--primary); }

/* USED-IN inline expand: a full-width, distinctly-tinted panel that animates
   open/closed via a grid-rows 0fr→1fr transition (height-agnostic, no JS
   measuring). The <td> spans every column (large colspan, clamped by the
   browser) so the panel runs the full table width. */
/* The expand row must NOT inherit the uniform 40px row height, or a collapsed
   panel leaves an empty 40px placeholder row. height:auto lets it shrink to its
   (0-height) content when closed. */
/* USED-IN list expands INSIDE its own cell (no full-width underneath row) — the
   ingredient row simply grows taller. Animated open/close via grid-rows 0fr→1fr. */
.usedin-cell-grid { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .25s ease; }
.usedin-cell-list.open .usedin-cell-grid { grid-template-rows: 1fr; }
#ing-table.usedin-no-anim .usedin-cell-grid { transition: none; }
.usedin-cell-clip { overflow: hidden; min-height: 0; }
.usedin-cline {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 11.5px; line-height: 1.5; cursor: pointer; padding-top: 1px;
}
.usedin-cline:first-child { padding-top: 4px; }
.usedin-cname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--primary); font-weight: 600; }
.usedin-cver { color: var(--text-muted); font-weight: 400; }
/* Composition weight (wt%) for this ingredient in the formula — pinned right so
   it stays visible while the name truncates. */
.usedin-cwt { flex: 0 0 auto; color: var(--text-muted); font-weight: 500; font-size: 10.5px; white-space: nowrap; }
/* When a row's used-in list is open, top-align the row's cells so ID/Name sit
   next to the count instead of being centred in the grown row. */
#ing-table tbody tr:has(.usedin-cell-list.open) td { vertical-align: top; }
#ing-table tbody tr:has(.usedin-cell-list.open) td.usedin-cell > div:first-child { margin-bottom: 2px; }
/* Same "everything on top, expanded list on the bottom" treatment for the
   Formulas → Ingredients expandable column (and Test Panels → Parameters): when
   a row's inline detail is open, top-align the whole row so the other cells sit
   at the top instead of floating in the awkward vertical middle. */
#form-table tbody tr:has(.ing-detail.open) td,
#tmpl-table tbody tr:has(.tmpl-params.open) td { vertical-align: top; }
/* Expand-all: a real bordered button in the "Used In" header cell. */
.usedin-expand-all { background: #fff; border: 1px solid var(--border); border-radius: 5px; padding: 0 4px; margin-left: 6px; cursor: pointer; color: var(--text-muted); display: inline-flex; align-items: center; vertical-align: middle; }
.usedin-expand-all:hover { color: var(--text); background: var(--surface-2, #f1f5f9); border-color: #94a3b8; }
/* Smaller sort triangle in list-table headers. */
.list-card [id$="-table-region"] .th-sort-icon { width: 9px; height: 9px; vertical-align: -1px; margin-left: 5px; }
/* Always leave room for the up/down sort arrow on the header's row: reserve a
   fixed right gutter in the header cell (part of the column's width) and pin the
   arrow into it, vertically centred. This way the arrow can never wrap onto its
   own line when a column is narrow — done purely with width/CSS, no JS. */
.list-card [id$="-table-region"] thead th { padding-right: 18px; }
.list-card [id$="-table-region"] thead th .th-sort-icon {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%); margin-left: 0;
}
/* No divider line above the header — the toolbar row flows straight into the
   table (the table's own sticky header line is the only one near the top). */
#content > .list-card > .card-header { border-bottom: none; }

/* Banded (zebra) rows instead of divider lines: white / light-grey / white.
   Striping is applied with a .row-band class on alternating VISIBLE data rows
   (CSS :nth-child can't be used — hidden "used-in-formula" expand rows are
   interleaved and would throw the alternation off). Hover gets a distinct blue
   tint so it reads on both bands. */
.list-card [id$="-table-region"] tbody tr { border-bottom: none; }
.list-card [id$="-table-region"] tbody tr.row-band { background: #eef1f6; }
.list-card [id$="-table-region"] tbody tr:hover { background: #dce8fc; }
/* Documents table isn't a .list-card region (aggregated view), so band it directly. */
#doclib-table tbody tr.row-band { background: #eef1f6; }
#doclib-table tbody tr:hover { background: #dce8fc; }
/* Projects table is a dashboard-panel hybrid (not a .list-card region) — band it directly too. */
#proj-table tbody tr.row-band { background: #eef1f6; }
#proj-table tbody tr:hover { background: #dce8fc; }
/* …and give its body cells the list-table clip-on-resize treatment (nowrap +
   ellipsis), so a narrowed column clips cleanly instead of wrapping/overflowing
   (which made the frozen ID badge wrap and the column border cut into it). This
   also makes the set widths authoritative so resize tracks in both directions. */
#proj-table tbody td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Projects isn't a .list-card, so its headers miss the list-table treatment.
   thSort puts `white-space:normal` inline on each th (to wrap long labels), and
   the sort triangle is an inline-block — so on a tight column the triangle drops
   onto its own line BELOW the label (the reported bug). Keep each header on one
   line (nowrap, over the inline style) so label + triangle stay together; the
   table scrolls horizontally if that widens it. Also use the compact 9px icon. */
#proj-table thead th { white-space: nowrap !important; }
#proj-table thead th .th-sort-icon { width: 9px; height: 9px; vertical-align: -1px; margin-left: 5px; }
/* Freeze the checkbox column (col 1) on horizontal scroll too — same as the
   .list-card views; the first data column (ID) is pinned just right of it by
   _renderFrozenCols. Opaque bg per row state so scrolled cells don't bleed. */
#proj-table th.bulk-check-col,
#proj-table td.bulk-check-col { position: sticky; left: 0; z-index: 1; background: #fff; }
#proj-table thead th.bulk-check-col { z-index: 3; }
#proj-table tbody tr.row-band td.bulk-check-col { background: #eef1f6; }
#proj-table tbody tr:hover td.bulk-check-col { background: #dce8fc; }

/* Frozen columns: the selection checkbox (always col 1, pinned here) + the
   first VISIBLE data column (pinned by JS in _renderFrozenCols, so it follows
   reorder/hide rather than being hard-coded). Sticky cells need an opaque
   background matching the row (white / zebra band / hover) or scrolled cells
   bleed through; the header corner sits above both the sticky header row
   (z-index 2) and the body sticky cells (z-index 1). */
.list-card [id$="-table-region"] th.bulk-check-col,
.list-card [id$="-table-region"] td.bulk-check-col { position: sticky; left: 0; z-index: 1; background: #fff; }
/* Match the row's background transition so the frozen checkbox cell fades to the
   hover colour in sync with the rest of the row (not instantly / "too early"). */
.list-card [id$="-table-region"] tbody td.bulk-check-col { transition: background 0.1s; }
/* Checkbox + first data column are one frozen block, so the header checkbox must
   NOT draw its own right divider (only the bottom underline). Otherwise the header
   shows a 1px line between the checkbox and ID that the seamless body doesn't —
   the "pixel of room from the checkbox to the sticky frame". */
.list-card [id$="-table-region"] thead th.bulk-check-col { z-index: 3; box-shadow: inset 0 -1px 0 var(--border); }
.list-card [id$="-table-region"] tbody tr.row-band td.bulk-check-col { background: #eef1f6; }
.list-card [id$="-table-region"] tbody tr:hover td.bulk-check-col { background: #dce8fc; }
/* Mousedown press feedback on the frozen checkbox cell (its base #fff otherwise
   out-specifies the generic .row-pressed td rule, so it stayed white on press). */
.list-card [id$="-table-region"] tbody tr.row-pressed td.bulk-check-col { background: #f8fafc; }
/* Press + select are INSTANT on the frozen checkbox cell — the 0.1s transition
   above is only for hover-sync; on mousedown it made the sticky cell lag behind. */
.list-card [id$="-table-region"] tbody tr.row-pressed td.bulk-check-col,
.list-card [id$="-table-region"] tbody tr.row-selected td.bulk-check-col { transition: none; }

/* Selected rows (bulk checkbox / row-click) get a blue outline. A contiguous
   run of selected rows shares ONE outline: primary-soft fill on every selected
   row; the top edge only on the run's first row (.sel-run-top) and the bottom
   only on its last (.sel-run-bottom) — so no line appears between rows in the
   middle; left + right edges via a border on the run's first/last cell. The run
   classes are computed in JS (_bulkSyncRowSel) rather than a CSS `+`/`:has()`
   adjacency rule, because Test Reports interleaves a hidden detail <tr> between
   data rows which would break sibling adjacency. Only bulk-selectable tables
   carry .row-selected, so a bare tbody scope is safe; placed after the
   zebra-band / hover rules so it wins ties by source order. */
tbody tr.row-selected td { background: var(--primary-soft); }
tbody tr.row-selected.sel-run-top td { box-shadow: inset 0 1px 0 var(--primary); }
tbody tr.row-selected.sel-run-bottom td { box-shadow: inset 0 -1px 0 var(--primary); }
tbody tr.row-selected.sel-run-top.sel-run-bottom td { box-shadow: inset 0 1px 0 var(--primary), inset 0 -1px 0 var(--primary); }
tbody tr.row-selected td:first-child { border-left: 1px solid var(--primary); }
tbody tr.row-selected td:last-child  { border-right: 1px solid var(--primary); }
/* Keep the frozen checkbox cell inside the selection fill (beats the band rule above). */
.list-card [id$="-table-region"] tbody tr.row-selected td.bulk-check-col,
#doclib-table tbody tr.row-selected td.bulk-check-col,
#proj-table tbody tr.row-selected td.bulk-check-col { background: var(--primary-soft); }

/* Formula Compare table: freeze the leftmost "Field" label column on
   horizontal scroll so each row stays identified across many formula columns
   (the whole point of the compare matrix). The section-header rows are
   full-width colspan cells; pinning them sticky-left keeps the section label
   (General / Composition …) visible too instead of scrolling off. Backgrounds
   re-asserted so scrolled formula cells don't show through the pinned cells. */
#compare-tbl thead th:first-child,
#compare-tbl tbody td:first-child:not([colspan]) {
  position: sticky; left: 0; z-index: 2;
}
/* Header row freeze (vertical): pin the column-header row so the formula
   names stay visible while scrolling down long composition lists. Sticks
   within #compare-table-wrap (a bounded both-axes scroll box). Header cells
   carry an inline position:relative (sort/reorder), so override with
   !important. z-index 3 keeps the header above the body's sticky-left Field
   column (z-index 2) — without it, tbody (later in DOM) would paint over. */
/* border-collapse + a sticky header makes the shared bottom border render in
   broken segments and bleed into the header as rows scroll under it. Draw the
   header's bottom edge with a box-shadow (it travels with the sticky cell and
   paints over the bleed) and drop the collapsed border-bottom. */
#compare-tbl thead th { position: sticky !important; top: 0; z-index: 3; border-bottom: none; box-shadow: inset 0 -1px 0 var(--border); }
/* The Field header is the top-left corner — pinned on BOTH axes, above all.
   It also pins left, so give its right divider a shadow too (collapsed
   border-right would bleed on horizontal scroll). */
#compare-tbl thead th:first-child { background: #f1f5f9; left: 0; z-index: 4; border-right: none; box-shadow: inset 0 -1px 0 var(--border), inset -1px 0 0 var(--border); }
#compare-tbl tbody td:first-child:not([colspan]) { background: #fafafa; box-shadow: inset -1px 0 0 var(--border); }
/* Default cap on the first (label) column so it doesn't auto-balloon to the
   longest ingredient/formula name — leaves more room for the data columns.
   Truncates with an ellipsis (full text on hover via title); short tables stay
   narrower than the cap under auto-layout. A user drag-resize injects an
   explicit width that overrides this (compare-field-w style). Applied to the
   body cells only — the short "Field"/"Formula" header sets no width, and
   keeping overflow off the header preserves the resize-handle hit area. */
#compare-tbl tbody td:first-child:not([colspan]) { max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
/* A full-width colspan banner can't stick itself (it fills the row → no sticky
   travel), so pin its LABEL via the inner sticky span instead. */
#compare-tbl tbody td[colspan] > span { position: sticky; left: 12px; display: inline-block; }

/* Pivot Table + Ingredient Map: same header-row freeze. Their wraps are bounded
   both-axes scroll boxes (see _pivotHtml / _ingredientMapHtml); headers carry an
   opaque background already, so a plain sticky-top suffices (no inline position
   to override, no frozen first column). */
#pivot-table-el thead th,
#ingmap-table-el thead th { position: sticky; top: 0; z-index: 2; }

/* Single-line rows: every cell stays on one line and truncates with an ellipsis
   so row heights are uniform. The long text columns get a max-width so they
   truncate instead of stretching the table; short/numeric columns size to fit. */
.list-card [id$="-table-region"] tbody td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.list-card [id$="-table-region"] th[data-col="name"],
.list-card [id$="-table-region"] td[data-col="name"],
.list-card [id$="-table-region"] th[data-col="chemicalName"],
.list-card [id$="-table-region"] td[data-col="chemicalName"],
.list-card [id$="-table-region"] th[data-col="description"],
.list-card [id$="-table-region"] td[data-col="description"] { max-width: 240px; }
/* Formulas Project column: a wider default so common project names (incl. the
   long auto-generated DOE names) fit without a manual resize. Still resizable —
   a drag sets an inline width that overrides this. */
.list-card [id$="-table-region"] th[data-col="projectName"],
.list-card [id$="-table-region"] td[data-col="projectName"] { min-width: 240px; }
.list-card [id$="-table-region"] th[data-col="cas"],
.list-card [id$="-table-region"] td[data-col="cas"],
.list-card [id$="-table-region"] th[data-col="supplier"],
.list-card [id$="-table-region"] td[data-col="supplier"],
.list-card [id$="-table-region"] th[data-col="catalog"],
.list-card [id$="-table-region"] td[data-col="catalog"],
.list-card [id$="-table-region"] th[data-col="smiles"],
.list-card [id$="-table-region"] td[data-col="smiles"] { max-width: 150px; }

/* Uniform, compact rows. A fixed height + middle alignment means a stray tall
   cell (or a chip row measured before flFitChips collapses it) can't inflate a
   single row — every data row lands on the same baseline height. */
.list-card [id$="-table-region"] tbody tr { height: 40px; }
.list-card [id$="-table-region"] tbody td { padding-top: 3px; padding-bottom: 3px; vertical-align: middle; }

/* Chip-fit cell (FUNCTION tags, HAZARDS pictograms): one no-wrap line, clipped;
   flFitChips() hides the overflow and appends a "+N" pill. */
.chip-fit { display: flex; flex-wrap: nowrap; align-items: center; gap: 3px; overflow: hidden; width: 100%; min-width: 0; }
.chip-fit > * { flex: 0 0 auto; }
.chip-more {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; color: #64748b; background: #eef1f6;
  border: 1px solid var(--border); border-radius: 999px; padding: 1px 6px;
  cursor: default; white-space: nowrap;
}
.chip-more:hover, .chip-more:focus-visible { background: #e2e8f0; color: #334155; outline: none; }

/* USED-IN trigger: count + caret, opens the interactive formula popover. */
.usedin-pill {
  background: none; border: 1px solid #cbd5e1; border-radius: 4px; padding: 2px 8px;
  cursor: pointer; font-size: 11px; color: #64748b; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 4px;
}
.usedin-pill:hover { background: #f1f5f9; color: #334155; border-color: #94a3b8; }

/* NOTES: the clipped span drives the "is it truncated?" check in cell-popovers. */
.notes-cell .cell-clip { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }

/* Shared cell popovers (position:fixed, placed by cell-popovers.js). */
.fl-cell-pop {
  position: fixed; z-index: 1400; background: var(--surface, #fff);
  border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(15,23,42,.18); padding: 8px;
  max-width: min(360px, 92vw); font-size: 12.5px; color: var(--text);
}
.fl-cell-pop-hover { pointer-events: none; }              /* read-only overlay */
.fl-chip-pop-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.fl-cell-pop-text { white-space: normal; word-break: break-word; line-height: 1.4; }

/* Filter overlay — anchored popover (replaces the inline filter row). Caps its
   height and scrolls because the chip groups can get long. */
/* Near-fullscreen filter modal: backdrop + a panel inset ~5% from every edge. */
/* Light backdrop (not a dimming sheet): the filter modal is small and the
   point is to watch the table update live as you toggle chips. */
.ing-filter-backdrop { position: fixed; inset: 0; z-index: 1290; background: rgba(15,23,42,.08); }
/* Content-sized, top-anchored: width/height grow with the number of filter
   groups (capped), so a 1-group view gets a small box and a many-group view a
   wider one — never the old near-fullscreen sheet that hid the table. */
.ing-filter-overlay { position: fixed; inset: auto; top: 64px; left: 50%; transform: translateX(-50%); z-index: 1300;
  width: max-content; min-width: 260px; max-width: min(840px, 94vw); max-height: 72vh;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface, #fff); border: 1px solid var(--border); border-radius: 14px;
  box-shadow: 0 24px 64px rgba(15,23,42,.32); }
.ing-filter-overlay-head { display: flex; align-items: center; justify-content: space-between;
  padding: 14px 18px; border-bottom: 1px solid var(--border); font-size: 15px; font-weight: 600; flex-shrink: 0; }
.ing-filter-overlay-close { border: none; background: none; cursor: pointer; padding: 4px; color: var(--text-muted); line-height: 0; }
.ing-filter-overlay-close:hover { color: var(--text); }
.ing-filter-overlay-body { overflow-y: auto; padding: 18px 20px; }
/* Chip groups flow left-to-right and wrap; each group caps its width so its
   chips wrap into a tidy block. With the overlay at width:max-content this makes
   the modal exactly as wide as the groups need (1 group = narrow), wrapping to
   more rows only once it hits the overlay's max-width. */
.ing-filter-overlay .ing-filter-row { display: flex; flex-wrap: wrap;
  gap: 16px 28px; align-items: flex-start; padding: 0; border: none; }
.ing-filter-overlay .ing-filter-group { display: flex; flex-direction: column; gap: 7px; max-width: 340px; }
.ing-filter-overlay .ing-filter-chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* Smart Search info (i) button — sits inside the search field on the right. */
.ing-search-info { position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
  border: none; background: none; cursor: pointer; padding: 2px; line-height: 0;
  color: var(--text-muted, #94a3b8); }
.ing-search-info:hover { color: var(--primary); }
.ing-toolbar .ing-search-wrap input { padding-right: 30px; }

/* Clear (×) button — appears when the search input has content, replaces the (i) button. */
.ing-search-clear { display: none; position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  border: none; background: none; cursor: pointer; padding: 0; line-height: 1;
  color: var(--text-muted, #94a3b8); font-size: 14px; width: 18px; height: 18px;
  align-items: center; justify-content: center; border-radius: 50%; }
.ing-search-clear:hover { color: var(--text); background: #e2e8f0; }
.ing-search-wrap input:not(:placeholder-shown) ~ .ing-search-clear { display: flex; }
.ing-search-wrap input:not(:placeholder-shown) ~ .ing-search-info { display: none; }
.ing-info-tip { position: fixed; z-index: 1400; max-width: 340px; background: #0f172a; color: #fff;
  padding: 10px 13px; border-radius: 9px; font-size: 12px; line-height: 1.55;
  box-shadow: 0 10px 28px rgba(0,0,0,.28); pointer-events: none; }

/* Inline group controls (Group-by + Column-order) in the toolbar (group mode). */
.ing-group-ctl { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* Cards / Group mode: give the grid a little breathing room so the cards
   aren't glued to the edges, while still filling + scrolling box-free. */
.list-card > [id$="-visual-region"],
.list-card > [id$="-cards-region"] { padding: 4px 18px 18px; }

/* Table */
.table-wrap { overflow-x: auto; }

/* ============================================================
   Row-capped table — shows N rows, scrolls for the rest.
   ------------------------------------------------------------
   The cap height is measured in JS (_capTableAtRows) because
   rows are not a fixed height: a two-line role chip or a wrapped
   ingredient name makes one row taller than its neighbours, so a
   hard px value would cut mid-row. The header sticks so the
   column labels survive the scroll.
   Row pickers open as position:fixed elements appended to body,
   so this scroll container does not clip them.
   ============================================================ */
.fl-rowcap { overflow-y: auto; }
.fl-rowcap thead th { position: sticky; top: 0; z-index: 3; }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
thead th { background: #f8fafc; text-align: left; padding: 10px 14px; font-weight: 400; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); border-bottom: 2px solid var(--border); white-space: nowrap; }
tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: #f8fafc; }
tbody td { padding: 10px 14px; vertical-align: middle; }
.empty-row td { text-align: center; color: var(--text-muted); padding: 32px; font-style: italic; }

/* Collapsible blue info panel that intros a list page (Step Presets,
   Test Panels). Uses native <details>/<summary> for accessibility +
   keyboard support, but hides the browser's default disclosure marker
   so the right-edge chevron is the only one visible. Body shows when
   open (controlled by the [open] attribute on <details>). */
/* ⓘ view-info popover — shown when the topbar info button is clicked */
.view-info-popover {
  position: fixed;
  z-index: 900;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 10px;
  box-shadow: 0 4px 20px rgba(15,23,42,0.12);
  padding: 14px 16px;
  font-size: 13px;
  color: #1e3a8a;
  line-height: 1.55;
}

.descriptor-panel {
  width: 100%;
  margin-bottom: 14px;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 8px;
  overflow: hidden;
}
.descriptor-panel > .descriptor-panel-summary {
  padding: 10px 14px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  line-height: 1.4;
  user-select: none;
  font-size: 12.5px;
  color: #1e3a8a;
}
.descriptor-panel .descriptor-panel-icon {
  font-size: 18px;
  line-height: 1;
  color: #1e40af;
  flex-shrink: 0;
}
.descriptor-panel .descriptor-panel-title {
  color: #1e3a8a;
  flex: 1;
  min-width: 0;
}
.descriptor-panel .descriptor-panel-chev {
  font-size: 11px;
  color: #1e40af;
  font-weight: 500;
  flex-shrink: 0;
}
.descriptor-panel > .descriptor-panel-summary:hover { background: #dbeafe; }
.descriptor-panel .descriptor-panel-body {
  padding: 0 14px 16px 42px;     /* Left padding aligns body w/ title text past the icon */
  font-size: 12.5px;
  color: #1e3a8a;
  line-height: 1.55;
}

/* Entity map widget on the Dashboard. Three-column layout —
   Library → Design → Lab — with chip buttons that navigate to
   each entity's list. Big horizontal arrows between columns show
   the workflow direction; ↓ "vsub" labels link entities WITHIN
   a column (Projects → Formulas, Batches → Samples → Tests).
   Span-3 in the dashboard grid so all three columns fit
   side-by-side at normal widths. */
.emap-wrap {
  display: grid;
  /* Four tiers — Library → Design → Lab → Analyze — separated by three
     arrow gutters. Each tier is a flexible 1fr column; arrow gutters
     are auto-sized to their content. */
  grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr;
  gap: 12px;
  padding: 10px 14px 12px;
  height: 100%;
  align-content: start;
}
.emap-col {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
/* Inner wrapper that holds the actual chips + sub-arrows. Splitting
   it out from .emap-col lets the tier label stay top-anchored while
   only the chip group centers within the remaining vertical space.
   The flex:1 here makes the wrapper claim every pixel below the tier
   label; justify-content:center then floats its children at the
   midpoint — visually aligning short columns (Design's 2 chips) with
   the row-centered forward arrows without dragging the tier label
   along for the ride. */
.emap-col-chips {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  justify-content: center;
}
.emap-tier-label {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  padding-bottom: 4px;
  border-bottom: 1px dashed var(--border);
  margin-bottom: 2px;
}
.emap-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  transition: background 0.12s, border-color 0.12s, transform 0.06s;
  min-width: 0;
}
.emap-chip:hover  { background: #eff6ff; border-color: var(--primary); transform: translateY(-1px); }
.emap-chip:active { transform: translateY(0); }
.emap-chip-icon {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 14px; flex-shrink: 0;
}
.emap-chip-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.emap-chip-name { font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.emap-chip-role { font-size: 10.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.emap-arrow {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 2px;
  color: var(--text-muted);
}
.emap-arrow-glyph { font-size: 20px; color: var(--primary); line-height: 1; }
.emap-arrow-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; margin-top: 4px; white-space: nowrap; }
.emap-vsub {
  font-size: 10.5px;
  color: var(--text-muted);
  padding: 1px 0 1px 30px;        /* Aligns under the chip icon (~22 icon + 8 gap = 30) */
  font-style: italic;
}

/* Stack vertically when the panel is narrow (single column / mobile). */
@media (max-width: 760px) {
  .emap-wrap { grid-template-columns: 1fr; }
  .emap-arrow { flex-direction: row; gap: 6px; padding: 4px 0; }
  .emap-arrow-glyph { transform: rotate(90deg); }
}

/* Lab → Library back-arrow. Communicates the inventory feedback edge
   (batch save → stock decrement on the upstream ingredient) that the
   straight left-to-right pipeline arrows can't draw directly. A single
   dashed primary-color rule spans the row; the ← chevron pins the
   destination side, the two labels float on the line with the card
   background painted behind them so they appear to 'cut' the dash. */
.emap-back-arrow {
  /* Spans the full grid width so the dashed line matches the
     flowchart above visually. Internally uses the same 7-column
     template as .emap-wrap so the children pin to specific tier
     columns: the leading "← consume stock" group anchors to col 1
     (Library), the detail "batch save · …" anchors to col 5 (Lab)
     — the tier where the consume-stock action originates. The
     dashed ::before line still spans the full width; both labels
     paint var(--surface) behind themselves so they appear to cut
     the line at their respective tier columns.
     Sitting inside .emap-wrap means it shares the widget's height
     budget — no overflow:hidden clipping at the dashboard-panel
     boundary. */
  grid-column: 1 / -1;
  position: relative;
  margin-top: 10px;
  padding: 6px 4px 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr;
  align-items: center;
  font-size: 11px;
  color: var(--text-muted);
  z-index: 0;
}
.emap-back-arrow::before {
  content: '';
  position: absolute;
  left: 18px; right: 0; top: 50%;
  border-top: 1.5px dashed var(--primary);
  opacity: 0.5;
  z-index: -1;
}
.emap-back-arrow-leading {
  grid-column: 1;
  justify-self: start;
  display: flex;
  align-items: center;
  gap: 6px;
}
.emap-back-arrow-tip {
  font-size: 18px;
  color: var(--primary);
  line-height: 1;
  font-weight: 700;
  padding: 0 4px;
  background: var(--surface);
}
.emap-back-arrow-label,
.emap-back-arrow-detail {
  background: var(--surface);
  padding: 1px 8px;
  white-space: nowrap;
}
.emap-back-arrow-label {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--primary);
}
.emap-back-arrow-detail {
  /* Pin to col 5 (Lab) so the trigger description sits visually under
     the tier where the action happens. justify-self centers within
     the column. */
  grid-column: 5;
  justify-self: center;
  font-style: italic;
  color: var(--text-muted);
}
/* On narrow viewports the columns stack vertically — the horizontal
   back-arrow doesn't fit that orientation, so swap to a compact "↑
   consume stock" line above the wrap. */
@media (max-width: 760px) {
  .emap-back-arrow { flex-direction: column; gap: 4px; margin: 10px auto 0; padding: 0; }
  .emap-back-arrow::before { display: none; }
  .emap-back-arrow-tip { transform: rotate(90deg); }
  .emap-back-arrow-detail { margin-left: 0; }
}

/* Favorite-star toggle dropped into list rows (favStarHtml() in
   recents-favorites.js). Stroked star by default (greyish), filled
   amber when on.
   inline-block (not inline-flex) so the star flows with text and
   doesn't trigger min-content constraints on a parent flex container
   — wrapping the star in display:flex container was making nearby
   flex:1 siblings stop growing properly and leaving dead space
   beside the title. 18×18 box is still hit-able on touch but loses
   the visual padding the old 22×22 had. */
.fav-star {
  display: inline-block;
  width: 18px; height: 18px;
  text-align: center; line-height: 18px;
  border-radius: 3px;
  color: #cbd5e1; cursor: pointer; user-select: none;
  font-size: 14px; vertical-align: middle;
  transition: color .12s, transform .08s;
}
.fav-star:hover { color: #f59e0b; }
.fav-star:active { transform: scale(0.9); }
.fav-star.fav-on { color: #f59e0b; }
.fav-star.fav-on:hover { color: #d97706; }

/* The "★ Favorites" filter chip on list pages. Mirrors the existing
   filter-toggle visuals but with amber accent when active so it's
   visually distinct from the generic Filter button. */
.fav-filter-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border-radius: 6px;
  font-size: 12px; font-weight: 500; cursor: pointer;
  background: #fff; color: var(--text); border: 1px solid var(--border);
  transition: all .12s;
}
.fav-filter-chip:hover { background: #fef3c7; border-color: #fbbf24; }
.fav-filter-chip.fav-filter-on {
  background: #fef3c7; border-color: #f59e0b; color: #92400e; font-weight: 600;
}
.fav-filter-chip .fav-filter-star { color: #f59e0b; }

/* Buttons */
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 10px; font-size: 13px; font-weight: 500; cursor: pointer; border: none; transition: all 0.15s; white-space: nowrap; font-variant-emoji: text; line-height: 1.4; }
/* line-height + font-variant-emoji together: font-variant-emoji:text
   forces glyphs that have BOTH text + emoji presentations (⚠ ↕ ↗ ☐ etc.)
   to render monochrome — modern Chrome/Safari support it, older ignore.
   The explicit line-height:1.4 is the belt-and-suspenders piece: with
   inline-flex + align-items:center the button's intrinsic height comes
   from the content's line-box. If the glyph happens to render at emoji
   metrics anyway (1.3× cap-height) the line-box stays anchored to 1.4em
   so the button doesn't bloat. Result: every .btn renders at the same
   nominal height regardless of which glyph it contains.
   '⚠ Failure Analysis' rendered 29 px while '▦ Coverage Matrix' rendered
   23 px in the same tab row before this fix. */
.tab, .view-toggle-btn { font-variant-emoji: text; line-height: 1.4; }
.btn-primary { background: var(--primary); color: #fff; border: 1px solid transparent; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-secondary { background: #fff; color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: #f1f5f9; }
/* Ingredient Preview-box source buttons: transparent resting fill so they sit
   lightly under the preview image; keep a subtle hover for feedback. */
.ing-preview-actions .btn-secondary { background: transparent; }
.ing-preview-actions .btn-secondary:hover { background: #f1f5f9; }
/* Toggle-on visual for secondary buttons used as toggles (Formula
   Compare's Transpose/Diff Only/Origin only, etc.). Without this rule
   the buttons swapped state internally but looked identical, leaving
   the user unable to tell if the toggle was active. */
.btn-secondary.btn-active {
  background: #dbeafe;
  color: var(--primary);
  border-color: var(--primary);
  font-weight: 600;
  box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.08);
}
.btn-secondary.btn-active:hover { background: #bfdbfe; }

/* Formula Analysis "Print / PDF" — hide chrome so the printed page
   shows only the analysis content (toolbar + table + viz + picker)
   as it looks on screen. Triggered by printAnalysis() adding the
   body class temporarily; @media print clause ensures the same rules
   apply during the print dialog regardless of how the user invoked
   it (Cmd+P also works). */
body.printing-analysis #sidebar,
body.printing-analysis #topbar,
body.printing-analysis #sidebar-backdrop,
body.printing-analysis #feedback-fab,
body.printing-analysis #form-table-card {
  display: none !important;
}
body.printing-analysis #main { margin-left: 0 !important; padding: 0 !important; }
body.printing-analysis #content { padding: 0 !important; }
/* The fill-height flex chain clips the table to the pane (overflow:hidden) and
   scrolls internally — for print we must let the WHOLE table flow, so unwind
   the flex/scroll constraints back to natural height. */
body.printing-analysis #formula-analysis-panel,
body.printing-analysis #formula-analysis-panel > .card,
body.printing-analysis #formula-analysis-panel #analysis-content,
body.printing-analysis #formula-analysis-panel .fa-scroll {
  display: block !important; overflow: visible !important;
  height: auto !important; max-height: none !important; min-height: 0 !important; flex: none !important;
}
/* Same unwinding for the Projects Portfolio Pivot overlay so its table flows. */
body.printing-analysis #projects-analysis-panel,
body.printing-analysis #projects-analysis-panel > .card,
body.printing-analysis #projects-analysis-panel #proj-pivot-content,
body.printing-analysis #projects-analysis-panel .pp-scroll {
  display: block !important; overflow: visible !important;
  height: auto !important; max-height: none !important; min-height: 0 !important; flex: none !important;
}
/* Hide the Print button itself + tabs in print output — the tab name
   is implicit from the content shown. */
body.printing-analysis .btn-print,
body.printing-analysis .fa-tab { display: none !important; }
@media print {
  /* Mirror the runtime rules so Cmd+P from the Analysis view also
     prints cleanly even without first clicking Print PDF. */
  body[data-view="formulations"] #sidebar,
  body[data-view="formulations"] #topbar,
  body[data-view="formulations"] #feedback-fab {
    display: none !important;
  }
}
/* .btn-print is now visually identical to .btn-secondary — the class
   stays for print-stylesheet targeting (e.g. body.printing-* selectors
   that hide the button when the user actually invokes print). */
/* Transparent 1px border so danger buttons match the box height of
   .btn-secondary (which has a 1px border) when they sit in the same row —
   e.g. Edit / Duplicate / Delete on a detail header. No visual change. */
.btn-danger { background: #fee2e2; color: var(--danger); border: 1px solid transparent; }
.btn-danger:hover { background: #fecaca; }
.btn-success { background: #dcfce7; color: var(--success); }
.btn-success:hover { background: #bbf7d0; }
.btn-sm { padding: 4px 10px; font-size: 12px; }

/* Workspace launcher buttons — used in the Test Results topbar (and
 * eventually any topbar that links into a dedicated workspace).
 * Each variant carries its workspace's identity color so the same
 * visual language follows the user once they land:
 *   stability     → blue   (matches stability.js modeColor)
 *   compare       → violet (neutral; Compare's own mode color flips
 *                            tests/samples/formulations once inside)
 * Sized to sit next to .btn-sm in the topbar without dominating
 * primary actions (which stay on .btn-primary blue). */
.btn-workspace {
  display: inline-flex; align-items: center; gap: 6px;
  /* Match the standard .btn footprint (was 4px 12px / 12px — read smaller than
     the other toolbar action buttons). line-height mirrors .btn so the height
     lines up exactly. */
  padding: 7px 14px; font-size: 13px; font-weight: 600; line-height: 1.4;
  border-radius: 6px; border: none; color: #fff; cursor: pointer;
  transition: background 0.12s, box-shadow 0.12s, transform 0.12s;
}
.btn-workspace:hover { transform: translateY(-1px); }
.btn-workspace-stability {
  background: #2563eb;
  box-shadow: 0 1px 3px rgba(37, 99, 235, 0.28);
}
.btn-workspace-stability:hover { background: #1d4ed8; }
.btn-workspace-compare {
  background: #6366f1;
  box-shadow: 0 1px 3px rgba(99, 102, 241, 0.28);
}
.btn-workspace-compare:hover { background: #4f46e5; }
/* Icon-only action buttons (View, Edit, Duplicate, Delete in row    */
/* action columns). Enforce a consistent box + alignment so all four  */
/* buttons in the row line up flush regardless of:                    */
/*   - content width (narrow ✕ glyph vs wider PNG icons)              */
/*   - parent variant border (.btn-secondary has 1px border,          */
/*     .btn-danger doesn't — would make Delete 2px shorter without    */
/*     min-height + transparent border below)                          */
/*   - inline baseline: button-with-image and button-with-text have    */
/*     different inline baselines, which made the ✕ Delete sit 3px    */
/*     lower than its siblings in the table cell. vertical-align +    */
/*     forcing the same border on btn-danger fixes that.              */
.btn-icon { padding: 5px 8px; min-width: 32px; min-height: 26px; box-sizing: border-box; justify-content: center; vertical-align: middle; }
.btn-icon.btn-danger { border: 1px solid transparent; }

/* Forms */
/* AI Assistant right-docked drawer (copilot-style). z-index sits below the
   modal-backdrop (300) so the key-entry modal stacks above it, and above the
   analyze overlay (200). */
.ai-drawer {
  position: fixed; z-index: 250;
  background: var(--surface, #fff);
  border: none; border-radius: 16px; overflow: hidden;
  box-shadow: 0 18px 50px rgba(15,23,42,0.22);
  display: flex; flex-direction: column;
  transition: transform .22s ease, opacity .18s ease;
}
/* Docked: flush with the right edge (no gap → no shadow peek) + left-only shadow
   so there's no visible grey line on the right side. */
.ai-drawer.docked {
  top: 0; right: 0; bottom: 0; left: auto; height: auto;
  width: var(--ai-drawer-w, 440px); max-width: 92vw;
  border-radius: 16px 0 0 16px;
  box-shadow: -2px 0 12px rgba(15,23,42,0.08);
  transform: translateX(calc(100% + 4px));
}
.ai-drawer.docked.open { transform: translateX(0); }
/* Floating: free-positioned (inline left/top/width/height), dragged by the header,
   resized from the bottom-right corner. Fades + scales in. */
.ai-drawer.floating {
  right: auto; bottom: auto;
  min-width: 320px; min-height: 360px; max-width: 96vw; max-height: 100dvh;
  resize: both; overflow: hidden;
  opacity: 0; transform: scale(.97); pointer-events: none;
}
.ai-drawer.floating.open { opacity: 1; transform: scale(1); pointer-events: auto; }
.ai-drawer.dragging { transition: none; }
/* Drag-to-resize grip on the docked (left) edge — width only; hidden when floating
   (the native bottom-right corner handles that). */
.ai-drawer-resize {
  position: absolute; left: 0; top: 0; width: 7px; height: 100%;
  cursor: ew-resize; z-index: 3;
}
.ai-drawer-resize:hover { background: var(--primary); opacity: .35; }
.ai-drawer.floating .ai-drawer-resize { display: none; }
body.ai-resizing { cursor: ew-resize; user-select: none; }
/* No slide/margin easing while actively dragging — track the cursor 1:1. */
body.ai-resizing .ai-drawer, body.ai-resizing #main,
body.ai-resizing .asm-analyze-panel { transition: none !important; }
/* Reflow: on wide screens the DOCKED drawer pushes the content (margin = drawer
   width + its right inset). Floating never reflows. Below 1100px it overlays. */
@media (min-width: 1100px) {
  body.ai-open:not(.ai-floating) #main { margin-right: var(--ai-drawer-w, 440px); }
  /* The Analyze workspace is a separate full-bleed fixed overlay (not #main),
     so it needs its own reflow: shrink the panel and pin it left so the docked
     drawer sits in the freed gutter instead of covering the grid / plots. */
  body.ai-open:not(.ai-floating) .asm-analyze-overlay { justify-content: flex-start; }
  body.ai-open:not(.ai-floating) .asm-analyze-panel {
    width: calc(100vw - var(--ai-drawer-w, 440px));
  }
}
.ai-drawer-header {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 14px; flex-shrink: 0;
  background: #f8fafc;
  border-radius: 16px 16px 0 0; /* match both floating and docked base radius */
  cursor: grab; touch-action: none;
}
/* Docked has a flat right edge — match that so the grey fill doesn't bleed. */
.ai-drawer.docked .ai-drawer-header { border-radius: 16px 0 0 0; }
.ai-drawer.dragging .ai-drawer-header { cursor: grabbing; }
.ai-drawer-header button { cursor: pointer; }
.ai-dock-btn {
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 4px 7px; color: var(--text-muted); display: inline-flex; align-items: center;
}
.ai-dock-btn:hover { background: #f1f5f9; color: var(--text); }
.ai-dock-btn svg { display: block; }
/* Body is a non-scrolling flex column: the thread scrolls, the composer is
   pinned at the bottom. */
.ai-drawer-body {
  flex: 1; min-height: 0; padding: 0;
  display: flex; flex-direction: column;
}
.ai-drawer-note {
  padding: 10px 12px; background: #fffbeb; border: 1px solid #fcd34d;
  border-radius: 6px; color: #92400e; font-size: 12.5px;
}
/* Scrolling conversation transcript. */
.ai-thread {
  flex: 1; min-height: 0; overflow-y: auto; padding: 14px;
  display: flex; flex-direction: column; gap: 12px;
}
.ai-empty { display: flex; flex-direction: column; gap: 12px; }
.ai-empty-intro { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; }
.ai-suggest-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-muted);
}
.ai-suggest-list { display: flex; flex-direction: column; gap: 6px; }
/* Sample queries are full sentences — let them wrap instead of clipping at the
   button's edge (.btn defaults to white-space:nowrap with a fixed height). */
.ai-suggest-list .btn {
  white-space: normal; height: auto; min-height: 0;
  text-align: left; justify-content: flex-start; line-height: 1.4;
}
/* Chat bubbles: user right (brand), assistant left (dark, for markdown contrast). */
.ai-msg { display: flex; }
.ai-msg-user { justify-content: flex-end; }
.ai-msg-ai { justify-content: flex-start; }
.ai-bubble {
  max-width: 88%; padding: 10px 12px; border-radius: 12px;
  font-size: 13px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
}
.ai-bubble-user { background: var(--primary); color: #fff; border-bottom-right-radius: 4px; }
/* Assistant bubble: light surface (was a dark "black box") with normal text. */
.ai-bubble-ai { background: #f1f5f9; color: var(--text, #0f172a); border-bottom-left-radius: 4px; }
.ai-bubble-ai code { background: rgba(15,23,42,0.06); padding: 1px 4px; border-radius: 4px; }
/* Assistant bubble + its cost hint, stacked (the parent .ai-msg-ai is a flex row). */
.ai-col { display: flex; flex-direction: column; align-items: flex-start; max-width: 88%; }
.ai-col .ai-bubble { max-width: 100%; }
/* BYOK prompt-size hint under an answer — quiet, informational, not alarming. */
.ai-token-foot {
  font-size: 10.5px; color: var(--text-muted, #94a3b8);
  margin: 3px 2px 0; letter-spacing: .01em; cursor: default; user-select: none;
}
.ai-token-foot:hover { color: var(--text-muted, #64748b); }
/* Clickable record UID inside an AI answer (PROJ-001 → that project). */
.ai-link { color: #7c3aed; cursor: pointer; font-weight: 600; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
.ai-link:hover { text-decoration-style: solid; background: rgba(124,58,237,0.08); border-radius: 3px; }
/* GHS hazard pictogram rendered inline in an AI answer (via [ghs:key] shortcode). */
.ai-ghs { display: inline-block; vertical-align: -5px; margin: 0 1px; }
.ai-ghs svg { width: 18px; height: 18px; }
/* Failed-request bubble — distinct red, clearly NOT a normal data answer. */
.ai-bubble-error { background: #fef2f2; border: 1px solid #fecaca; color: #7f1d1d; border-bottom-left-radius: 4px; }
.ai-err-head { font-weight: 700; font-size: 12.5px; color: #b91c1c; }
.ai-err-body { font-size: 12px; margin-top: 3px; color: #991b1b; word-break: break-word; }
.ai-err-hint { font-size: 11.5px; margin-top: 5px; color: #9a3412; }
.ai-err-foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.ai-err-tag { font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: #b91c1c; opacity: .7; flex: 1; }
.ai-thinking { color: var(--primary); display: inline-flex; align-items: baseline; gap: 2px; }
.ai-thinking .ai-spark { animation: pulse 1.2s ease-in-out infinite; }
.ai-think-who { margin-left: 8px; font-size: 10.5px; color: var(--text-muted, #94a3b8); }
/* Animated "…" — each dot fades in turn, the classic typing indicator. */
.ai-dots span { animation: aiDot 1.2s infinite; opacity: 0.2; }
.ai-dots span:nth-child(2) { animation-delay: 0.2s; }
.ai-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes aiDot { 0%, 60%, 100% { opacity: 0.2; } 30% { opacity: 1; } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
/* Blinking cursor at the end of the live (streaming) bubble. */
.ai-caret { display: inline-block; width: 6px; height: 1em; margin-left: 1px; vertical-align: -2px; background: var(--primary); border-radius: 1px; animation: aiCaret 1s steps(2) infinite; }
@keyframes aiCaret { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* New message slides up + fades in — the "send → on screen" transition. */
@keyframes aiBubbleIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.ai-anim-in { animation: aiBubbleIn .26s cubic-bezier(.22,.61,.36,1) both; }
/* AI write-tool confirm card: a proposed create/update awaiting Apply. */
.ai-action-card {
  max-width: 88%; border: 1px solid var(--primary); border-radius: 12px;
  border-bottom-left-radius: 4px; background: var(--surface, #fff);
  box-shadow: 0 1px 3px rgba(15,23,42,0.08); overflow: hidden;
}
.ai-act-title {
  padding: 8px 12px; font-size: 12px; font-weight: 600; color: var(--primary);
  background: rgba(99,102,241,0.08); border-bottom: 1px solid var(--border);
}
.ai-act-body { padding: 8px 12px; display: flex; flex-direction: column; gap: 4px; }
/* AI analysis result card (correlation / model fit) — a compact data table
   rendered inside the thread after an analysis action is applied. */
.ai-analysis-card .ai-act-body { gap: 0; }
.ai-analysis-meta { font-size: 11.5px; color: var(--text-muted); margin-bottom: 6px; }
.ai-hr { border: 0; border-top: 1px solid var(--border); margin: 12px 0; }
.ai-analysis-tbl { width: 100%; border-collapse: collapse; font-size: 12px; }
.ai-analysis-tbl th { text-align: left; color: var(--text-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .03em; padding: 3px 6px; border-bottom: 1px solid var(--border); }
.ai-analysis-tbl td { padding: 3px 6px; border-bottom: 1px solid #f1f5f9; }
.ai-act-row { display: flex; gap: 8px; font-size: 12.5px; line-height: 1.4; }
.ai-act-k { flex-shrink: 0; min-width: 72px; color: var(--text-muted); }
.ai-act-v { color: var(--text); word-break: break-word; }
.ai-act-warn .ai-act-v { color: #b45309; }
.ai-act-btns { display: flex; justify-content: flex-end; gap: 8px; padding: 8px 12px; border-top: 1px solid var(--border); }
/* Navigation card's Open/Show button: shrink within the narrow chat and ellipsize
   a long label (full text is on the button's hover title) instead of overflowing. */
.ai-act-btns .ai-act-open { min-width: 0; flex: 0 1 auto; overflow: hidden; }
.ai-act-btns .ai-act-open .ai-act-open-lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ai-act-blocked { padding: 8px 12px; font-size: 12px; color: #b45309; background: #fffbeb; }
.ai-act-done { padding: 8px 12px; font-size: 12.5px; color: #15803d; }
.ai-act-link { color: var(--primary); text-decoration: underline; cursor: pointer; }
.ai-act-dismissed { padding: 8px 12px; font-size: 12px; color: var(--text-muted); }
/* Composer pinned at the bottom — no top divider; a pill input with the send
   button embedded inside on the right. */
.ai-composer {
  flex-shrink: 0; padding: 10px 12px 12px; background: var(--surface, #fff);
}
.ai-composer-row { display: flex; position: relative; }
/* Honest, persistent privacy line under the composer. */
.ai-privacy-note {
  margin-top: 7px; font-size: 10.5px; line-height: 1.35; color: var(--text-muted, #94a3b8);
  cursor: default;   /* plain wrapping text — NOT flex (flex split the sentence into 3 columns when narrow) */
}
.ai-privacy-note strong { color: var(--text, #334155); font-weight: 600; }
/* AI accent — give the AI affordances a recognisable violet so they read as
   "AI", without going back to a loud floating button. */
#sidebar .sidebar-footer .sidebar-ai { color: #a78bfa; font-size: 17px; }
#sidebar .sidebar-footer .sidebar-ai:hover { color: #c4b5fd; background: rgba(167,139,250,0.16); }
.btn-ai { border: 1px solid #c4b5fd; color: #6d28d9; background: #fff; }
.btn-ai:hover { background: #f5f3ff; border-color: #7c3aed; color: #5b21b6; }
.ai-input {
  flex: 1; width: 100%; box-sizing: border-box;
  padding: 13px 48px 13px 16px;
  border: 1px solid var(--border); border-radius: 999px;
  font-size: 13.5px; font-family: inherit; color: var(--text);
  background: #f8fafc; transition: border-color .15s, box-shadow .15s, background .15s;
}
.ai-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.12); background: #fff; }
.ai-input:disabled { opacity: .6; }
.ai-send {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; border-radius: 50%; border: none; padding: 0;
  background: var(--primary); color: #fff; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s, transform .08s;
}
.ai-send:hover { background: var(--primary-hover, #1d4ed8); }
.ai-send:active { transform: translateY(-50%) scale(.9); }
.ai-send:disabled { opacity: .45; cursor: not-allowed; }
.ai-send svg { display: block; }
/* While a response is generating, the send button becomes a red Stop button. */
.ai-send.ai-send-stop { background: var(--danger, #dc2626); }
.ai-send.ai-send-stop:hover { background: #b91c1c; }
/* "✦ Explain" inline help button — placed next to section headers to pre-fill the AI with an explain question. */
.ai-help-btn { display: inline-flex; align-items: center; gap: 2px; font-size: 10.5px; padding: 1px 5px; border: 1px solid #c4b5fd; border-radius: 4px; background: none; cursor: pointer; color: #6d28d9; opacity: 0.75; font-weight: 500; line-height: 1.5; vertical-align: middle; margin-left: 6px; white-space: nowrap; font-family: inherit; }
.ai-help-btn:hover { opacity: 1; background: #f5f3ff; border-color: #7c3aed; color: #5b21b6; }
/* Full-screen on phones (no inset / rounding there). */
@media (max-width: 640px) {
  .ai-drawer.docked { top: 0; right: 0; bottom: 0; left: 0; width: 100vw; max-width: 100vw; border-radius: 0; }
}

/* Toast notices (non-blocking alert() replacement). Above the modal backdrop
   so save/error confirmations show over open modals. */
.toast-stack { position: fixed; bottom: 76px; right: 20px; z-index: 400; display: flex; flex-direction: column; gap: 8px; width: 360px; max-width: calc(100vw - 40px); pointer-events: none; }
.toast { pointer-events: auto; display: flex; align-items: flex-start; gap: 10px; background: #1e293b; color: #f1f5f9; padding: 10px 12px 10px 14px; border-radius: 8px; font-size: 13px; line-height: 1.45; box-shadow: 0 8px 24px rgba(15,23,42,0.28); opacity: 0; transform: translateY(8px); transition: opacity .2s ease, transform .2s ease; border-left: 3px solid #64748b; }
.toast-msg { flex: 1 1 auto; min-width: 0; white-space: pre-wrap; word-break: break-word; }
.toast-close { flex: 0 0 auto; background: none; border: none; color: #94a3b8; font-size: 18px; line-height: 1; cursor: pointer; padding: 0 2px; margin: -1px -2px 0 0; border-radius: 4px; transition: color .12s ease; }
.toast-close:hover { color: #f1f5f9; }
.toast.toast-in { opacity: 1; transform: translateY(0); }
.toast.toast-out { opacity: 0; transform: translateY(8px); }
.toast.toast-success { border-left-color: #22c55e; }
.toast.toast-error   { border-left-color: #ef4444; }
.toast.toast-info    { border-left-color: #3b82f6; }
@media (max-width: 640px) { .toast-stack { left: 12px; right: 12px; bottom: 12px; width: auto; max-width: none; } }

/* ── "Storage full" sticky banner (persistent — every save fails until freed) ── */
/* Bottom-right notification, not a full-width top bar. It used to span the top
   edge and push the whole app down, which is a lot of chrome for a state you
   might sit in for a while. Docked bottom-right it reads as urgent without
   taking the workspace. Solid red (not the pale tint) so it is unmistakable.
   Above toasts (400) and below modals (300 is the backdrop, but this must clear
   a toast stack) — 600 keeps it over everything except portaled menus. */
.storage-full-banner {
  position: fixed; right: 18px; bottom: 18px; left: auto; top: auto; z-index: 600;
  width: min(380px, calc(100vw - 36px));
  display: grid; grid-template-columns: auto 1fr auto; align-items: start; gap: 4px 10px;
  padding: 13px 14px 14px; border-radius: 12px;
  background: #dc2626; border: 1px solid #b91c1c; color: #fff;
  font-size: 12.5px; line-height: 1.5;
  box-shadow: 0 12px 34px rgba(153,27,27,0.42), 0 2px 8px rgba(15,23,42,0.28);
  animation: sfbIn 0.22s cubic-bezier(0.16,1,0.3,1);
}
@keyframes sfbIn { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .storage-full-banner { animation: none; } }
.storage-full-banner .sfb-icon { grid-column: 1; grid-row: 1; font-size: 15px; line-height: 1.3; }
.storage-full-banner .sfb-msg { grid-column: 2; grid-row: 1; min-width: 0; }
.storage-full-banner .sfb-msg strong { display: block; margin-bottom: 3px; font-size: 13px; }
.storage-full-banner .sfb-actions { grid-column: 2 / -1; grid-row: 2; display: flex; align-items: center; gap: 8px; margin-top: 11px; }
.storage-full-banner .sfb-btn { background: #fff; color: #991b1b; border: none; border-radius: 7px; padding: 6px 12px; font-size: 12.5px; font-weight: 600; font-family: inherit; cursor: pointer; white-space: nowrap; }
.storage-full-banner .sfb-btn:hover { background: #fee2e2; }
.storage-full-banner .sfb-btn-2 { background: rgba(255,255,255,0.14); color: #fff; border: 1px solid rgba(255,255,255,0.45); font-weight: 500; }
.storage-full-banner .sfb-btn-2:hover { background: rgba(255,255,255,0.24); }
/* Close sits top-right of the card, out of the button row. */
.storage-full-banner .sfb-close { grid-column: 3; grid-row: 1; background: none; border: none; color: rgba(255,255,255,0.85); font-size: 18px; line-height: 1; cursor: pointer; padding: 0 0 0 6px; margin-top: -2px; }
.storage-full-banner .sfb-close:hover { color: #fff; }
@media (max-width: 640px) { .storage-full-banner { right: 10px; bottom: 10px; width: calc(100vw - 20px); } }

/* ── Settings modal — fixed-size window, light sidebar + content panel ── */
.modal.settings-modal {
  width: 100%;
  /* Cards inside the panel cap at 900px, so a full-bleed modal on a 16:9
     display left ~780px of empty gutter hard against the right edge — the
     content hugged the left and the dialog looked broken rather than roomy.
     Cap the whole dialog at what its contents actually need: settings nav
     (180) + panel padding (52) + card (900) ~= 1132, plus a little air.
     Below that width it stays full-bleed, so small screens are unchanged. */
  max-width: 1180px;
  height: 100%;
  max-height: 100%;
  overflow: hidden;
  margin-inline: auto;
}
@media (max-width: 1180px) {
  .modal.settings-modal { max-width: 100%; }
}
.settings-layout {
  display: flex;
  flex: 1;
  overflow: hidden;
}
.settings-nav {
  width: 180px;
  flex-shrink: 0;
  padding: 14px 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  background: var(--surface);
  border-right: 1px solid var(--border);
}
/* Account identity card at the top of the settings nav (both tiers). Borderless
   — a soft fill that deepens on hover / when the Account tab is active. */
.settings-account-card {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left; cursor: pointer; font-family: inherit;
  background: var(--surface); border: none;
  border-radius: 10px; padding: 9px 10px; margin-bottom: 9px;
  transition: background 0.12s;
}
.settings-account-card:hover { background: var(--bg-soft); }
/* Selected (Account tab active) — fill with the main FormLab blue, white text,
   and invert the PRO pill (white fill, blue text) so it reads on the blue. */
.settings-account-card.active { background: var(--primary); }
.settings-account-card.active .sac-name { color: #fff; }
.settings-account-card.active .sac-sub { color: rgba(255,255,255,0.85); }
.settings-account-card.active .sac-pro-badge { background: #fff; color: var(--primary); border-color: #fff; }
.settings-account-card.active .sac-av-placeholder { filter: brightness(0) invert(1) opacity(0.9); }
.sac-avatar { width: 34px; height: 34px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; }
.sac-avatar img { width: 34px; height: 34px; border-radius: 50%; }
/* Only the person-in-circle PLACEHOLDER is greyed — never the real avatar photo
   (that filter was blanking the signed-in user's avatar to a dark blob). */
.sac-avatar img.sac-av-placeholder { filter: brightness(0) opacity(0.4); }
.sac-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; gap: 3px; }
.sac-name { font-size: 13px; font-weight: 600; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sac-sub { font-size: 11px; color: var(--text-muted); }
/* "PRO" pill — mirrors the iconic logo badge, in the main FormLab blue. */
.sac-pro-badge {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--primary); border: 1px solid var(--primary);
  font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em;
  padding: 1px 5px; border-radius: 3px; line-height: 1; font-size: 9px;
}
/* Borderless, bigger settings close ✕ */
.settings-close-x {
  background: none; border: none; cursor: pointer;
  font-size: 21px; line-height: 1; color: var(--text-muted);
  padding: 4px 9px; border-radius: 8px; font-family: inherit;
  transition: color 0.12s, background 0.12s;
}
.settings-close-x:hover { color: var(--text); background: var(--bg-soft); }
/* --- Account tab profile header (borderless, title-less) --- */
.pro-profile { display: flex; gap: 18px; align-items: center; padding: 4px 2px 20px; }
.pro-profile-av { position: relative; border: none; background: none; padding: 0; cursor: pointer; border-radius: 50%; line-height: 0; flex-shrink: 0; }
.pro-profile-av .pro-av-edit {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.45); color: #fff; font-size: 12px; font-weight: 600;
  border-radius: 50%; opacity: 0; transition: opacity .12s;
}
.pro-profile-av:hover .pro-av-edit { opacity: 1; }
.pro-profile-main { flex: 1; min-width: 0; }
.pro-profile-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.pro-name-input {
  font-size: 22px; font-weight: 700; color: var(--text); background: transparent;
  border: none; border-bottom: 1px solid var(--border); padding: 2px 0;
  font-family: inherit; min-width: 0; flex: 1; outline: none; transition: border-color .12s;
}
.pro-name-input:hover { border-bottom-color: var(--text-muted); }
/* Edit mode = FormLab-blue underline, but NO light-blue box-shadow glow
   (override just the global input:focus box-shadow). */
.pro-name-input:focus { border-bottom-color: var(--primary); box-shadow: none; }
.pro-profile-email { font-size: 12.5px; color: var(--text-muted); margin-top: 8px; }
/* "· PRO Since YYYY" — inline main-blue text after the email. */
.pro-since { color: var(--primary); font-weight: 700; white-space: nowrap; }
/* Organization name — same inline-underline textbox as the display name, sized
   as the secondary line under the email. Auto-saves like the name (no button). */
.pro-org-input {
  display: block; margin-top: 4px; width: 100%; max-width: 320px;
  font-size: 13px; font-weight: 400; color: var(--text); background: transparent;
  border: none; border-bottom: 1px solid var(--border); padding: 1px 0;
  font-family: inherit; outline: none; transition: border-color .12s;
}
.pro-org-input::placeholder { color: var(--text-muted); font-weight: 400; }
.pro-org-input:hover { border-bottom-color: var(--text-muted); }
.pro-org-input:focus { border-bottom-color: var(--primary); box-shadow: none; }

/* ===== Security tab ===== 10px boxes with the title INSIDE the border. */
.sec-box { border: 1px solid var(--border); border-radius: 10px; margin-bottom: 16px; overflow: hidden; }
.sec-box-head { font-size: 17px; font-weight: 700; color: var(--text); padding: 14px 16px 10px; }
.sec-box-body { padding: 4px 16px 16px; }
/* Sessions bubble list */
.sec-bubbles { display: flex; flex-direction: column; gap: 8px; padding: 4px 16px 14px; }
.sec-bubble {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--bg-soft); border-radius: 10px; padding: 10px 13px;
}
.sec-bubble-main { min-width: 0; }
.sec-bubble-title { font-size: 13px; font-weight: 600; color: var(--text); }
.sec-bubble-cur { color: #16a34a; font-weight: 600; }
.sec-bubble-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.sec-bubble-empty { font-size: 12px; color: var(--text-muted); padding: 8px 0; }
/* Footer row under the bubbles: Close all (left) · idle policy (right) */
.sec-box-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-top: 1px solid var(--border);
}
.sec-idle { font-size: 12.5px; font-weight: 600; color: var(--text-muted); white-space: nowrap; }

/* Kiosk stations. A station is a browser left running at a bench, so the row
   leads with what actually tells two of them apart — browser, platform, screen —
   and treats the address as a hint, not an identity. */
.sec-kiosk-banner {
  display: flex; align-items: center; gap: 10px; margin: 0 16px 12px;
  padding: 10px 13px; border-radius: 10px;
  background: #fef3c7; color: #92400e; font-size: 12.5px; font-weight: 600;
}
.sec-kiosk-banner .skb-dot {
  flex: none; width: 8px; height: 8px; border-radius: 999px; background: #d97706;
}
.sec-bubble.is-pending { background: #fffbeb; box-shadow: inset 0 0 0 1px #fcd34d; }
.sec-bubble.is-revoked { opacity: .6; }
.sec-kiosk-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
.sec-kiosk-tag {
  font-size: 11px; font-weight: 600; color: var(--text-muted);
  background: var(--surface); border-radius: 999px; padding: 2px 8px;
}
.sec-kiosk-tag.ip { font-variant-numeric: tabular-nums; }
.sec-kiosk-acts { display: flex; gap: 6px; flex: none; }
/* Shown only when stations exist in more than one workspace — the cap is per
   workspace, so each group carries its own count. */
.sec-kiosk-wshead {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-muted); padding: 10px 2px 2px;
}
.sec-kiosk-wshead b { font-weight: 700; color: #16a34a; text-transform: none; letter-spacing: normal; }

/* Pending-station indicator. Marks the trail to Settings -> Security: the
   profile button, the Settings item in its popup, and the Security tab. Orange
   rather than the accent blue, because it is asking for an action, and it must
   not read as "selected". */
.has-pending { position: relative; }
.has-pending::after {
  content: ""; position: absolute; width: 8px; height: 8px; border-radius: 999px;
  background: #f97316; box-shadow: 0 0 0 2px var(--bg, #fff), 0 0 6px 1px rgba(249, 115, 22, .9);
  pointer-events: none;
}
/* The avatar is a circle, so the dot rides its top-right shoulder rather than
   the bounding box's corner, where it would float in the gap. */
#account-btn.has-pending::after { top: 2px; right: 2px; }
#account-settings-btn.has-pending::after { top: 50%; right: 10px; margin-top: -4px; }
.settings-nav-item.has-pending::after { top: 50%; right: 10px; margin-top: -4px; }
@keyframes kiosk-pending-pulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--bg, #fff), 0 0 5px 1px rgba(249, 115, 22, .75); }
  50%      { box-shadow: 0 0 0 2px var(--bg, #fff), 0 0 9px 3px rgba(249, 115, 22, 1); }
}
.has-pending::after { animation: kiosk-pending-pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .has-pending::after { animation: none; } }
.sec-dot { display: inline-block; width: 7px; height: 7px; border-radius: 999px;
           margin-right: 6px; vertical-align: baseline; background: var(--text-muted); }
.sec-dot.on { background: #16a34a; }
.sec-dot.pending { background: #d97706; }
/* Password: two side-by-side buttons + reveal-on-click change form */
.sec-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.sec-pw-form { display: flex; flex-direction: column; gap: 8px; max-width: 340px; margin-top: 12px; }
.sec-pw-form[hidden] { display: none; }   /* [hidden] alone loses to display:flex */
.sec-pw-input { padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; font-family: inherit; }
/* Danger zone — folded header button */
.sec-danger-box { border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
.sec-fold {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; background: none; border: none; cursor: pointer; font-family: inherit;
  /* Compact when folded; grows when open (animated). */
  font-size: 13.5px; font-weight: 700; color: var(--danger); padding: 12px 16px;
  transition: font-size 0.22s ease, padding 0.22s ease;
}
.sec-fold.open { font-size: 18px; padding: 14px 16px; }
.sec-fold-chev { display: inline-block; width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 6px solid currentColor; transition: transform 0.22s ease; }
.sec-fold.open .sec-fold-chev { transform: rotate(90deg); }
/* Animated reveal: collapse via grid 0fr→1fr (works with unknown content height). */
.sec-fold-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.22s ease; }
.sec-fold-body.open { grid-template-rows: 1fr; }
.sec-fold-inner { overflow: hidden; min-height: 0; padding: 0 16px; }
.sec-fold-body.open .sec-fold-inner { padding-bottom: 16px; }
.sec-danger-desc { font-size: 12.5px; color: var(--text-muted); line-height: 1.55; margin-bottom: 12px; }

/* ===== Workspaces tab ===== */
.ws-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 2px 0 14px; }  /* big header, no box */
.ws-loading { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 40px 0; }
.ws-loading img { width: 56px; height: 56px; animation: splashPulse 1.7s ease-in-out infinite; }
.ws-loading-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--primary); animation: wsPulseDot 1.2s ease-in-out infinite; }
@keyframes wsPulseDot { 0%,100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.25); } }
.ws-list { display: flex; flex-direction: column; gap: 10px; }
.ws-card { border: 1px solid var(--border); border-radius: 10px; padding: 11px 13px; user-select: none; }
.ws-card-current { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }
.ws-card-head { display: flex; align-items: center; gap: 12px; }
.ws-card-left { flex: 0 0 34px; width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; }
.ws-card-left img, .ws-card-left > span { border-radius: 50%; }
.ws-check { color: var(--primary); font-size: 22px; font-weight: 800; line-height: 1; }
.ws-card-main { flex: 1; min-width: 0; }
.ws-card-name { font-size: 13.5px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; }
.ws-card-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ws-card-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
/* Triangle (borderless, no hover change) + kebab (outlined). */
.ws-tri, .ws-kebab {
  background: none; cursor: pointer; color: var(--text-muted);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: inherit; line-height: 1; border-radius: 10px;
}
.ws-tri { border: none; padding: 6px; }
.ws-kebab { border: 1px solid var(--border); padding: 0; font-size: 17px; width: 28px; height: 28px; flex: 0 0 28px; }
.ws-kebab:hover { background: var(--bg-soft); }
.ws-tri .fl-tri { transition: transform 0.15s; }
/* Click a card → blue border flashes then fades (signals it's double-clickable). */
@keyframes wsClickFlash { 0% { border-color: var(--primary); } 100% { border-color: var(--border); } }
.ws-card.ws-flash { animation: wsClickFlash 0.5s ease-out; }
.ws-card-current.ws-flash { animation: none; }
/* Divider between workspace groups (current / local+own / shared). */
.ws-divider { height: 1px; background: var(--border); margin: 4px 2px; border: none; }
/* Cloud-storage block inside the members dropdown. */
.ws-storage { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
/* Special invitation card — blue-tinted, clearly stands apart. */
.ws-card-invite { border-color: color-mix(in srgb, var(--primary) 55%, var(--border)); background: color-mix(in srgb, var(--primary) 7%, var(--surface)); }
.ws-invite-ico { font-size: 18px; }
.ws-invite-badge {
  font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.1em;
  color: #fff; background: var(--primary); padding: 2px 6px; border-radius: 999px;
}
/* Member/pending dropdown — light-grey 10px cards. */
.ws-members-wrap { padding-top: 10px; }
.ws-members { display: flex; flex-direction: column; gap: 8px; }
.ws-member {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--bg-soft); border-radius: 10px; padding: 8px 11px;
}
.ws-member-main { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ws-member-text { min-width: 0; }
.ws-member-name { font-size: 13px; font-weight: 600; color: var(--text); }
.ws-member-name .ws-member-email { color: var(--text-muted); font-weight: 400; }
.ws-member-role { font-size: 12px; color: var(--text-muted); margin-top: 1px; }
.ws-member-pending { border: 1px dashed color-mix(in srgb, var(--primary) 40%, var(--border)); background: color-mix(in srgb, var(--primary) 4%, var(--surface)); }
.ws-member-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.ws-role-select { font-size: 12.5px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 8px; font-family: inherit; background: var(--surface); }
/* X-icon remove button (replaces the "Remove" text button). */
.ws-x {
  background: none; border: none; cursor: pointer; color: var(--text-muted);
  width: 24px; height: 24px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; transition: background 0.12s, color 0.12s;
}
.ws-x:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); }
/* Robust top gap between the last member and the Invite button (important so a
   parent flex/gap rule can't collapse it). */
.ws-invite-btn { display: inline-flex; margin-top: 14px !important; }
/* Right-click / kebab options menu. */
.ws-menu {
  position: fixed; z-index: 4000; min-width: 172px; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px; padding: 6px;
  box-shadow: 0 12px 32px rgba(10,18,40,0.22); display: flex; flex-direction: column; gap: 2px;
}
.ws-menu-item {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: none; border: none; border-radius: 8px; padding: 8px 10px;
  font-size: 13px; color: var(--text); cursor: pointer; font-family: inherit;
}
.ws-menu-item:hover { background: var(--bg-soft); }
.ws-menu-item.danger { color: var(--danger); }
/* Invite-by-email mini prompt. */
.ws-invite-prompt-backdrop { position: fixed; inset: 0; z-index: 4100; background: rgba(15,23,42,0.35); display: flex; align-items: center; justify-content: center; }
.ws-invite-prompt { background: var(--surface); border-radius: 12px; padding: 18px; width: 340px; max-width: calc(100vw - 32px); box-shadow: 0 20px 50px rgba(10,18,40,0.3); }
.ws-invite-prompt h4 { font-size: 15px; font-weight: 700; margin: 0 0 12px; }
.ws-invite-prompt input[type=email] { width: 100%; box-sizing: border-box; padding: 8px 11px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: inherit; }
.ws-invite-roles { display: flex; gap: 8px; margin: 10px 0 14px; }
.ws-invite-role { flex: 1; text-align: center; padding: 8px; border: 1px solid var(--border); border-radius: 8px; font-size: 12.5px; font-weight: 600; cursor: pointer; color: var(--text-muted); background: var(--surface); }
.ws-invite-role.on { border-color: var(--primary); color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
.ws-invite-prompt-actions { display: flex; justify-content: flex-end; gap: 8px; }
/* Manage Data sub-view */
.md-crumb { font-size: 18px; font-weight: 700; color: var(--text); margin: 2px 0 16px; display: flex; align-items: center; gap: 8px; }
.md-crumb a { color: var(--primary); cursor: pointer; }
.md-crumb a:hover { text-decoration: underline; }
.md-crumb-sep { color: var(--text-muted); font-weight: 400; }
.md-crumb-cur { color: var(--text-muted); }
.md-summary { border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 18px; max-width: 900px; }
.md-counts { font-size: 12.5px; color: var(--text-muted); line-height: 1.6; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; }
.md-counts-label { font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; font-size: 11px; color: var(--text); }
.md-counts-bits { flex: 1; min-width: 0; }
.md-counts-bits strong { color: var(--text); font-weight: 600; }
.md-counts-total { color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.md-storage { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.md-store-line { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--text-muted); }
.md-store-line strong { color: var(--text); font-weight: 600; }
/* Combined snapshot+storage categories (Records / Documents). */
.md-cat + .md-cat { margin-top: 10px; }
.md-cat-head { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text); }
.md-cat-head b { font-weight: 600; }
.md-cat-count { color: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; margin-left: auto; white-space: nowrap; }
.md-cat-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.md-cat-bits { font-size: 12px; color: var(--text-muted); line-height: 1.6; margin-top: 3px; padding-left: 17px; }
.md-cat-bits strong { color: var(--text); font-weight: 600; }
/* Manage Data: 10px rounding on the tool cards (buttons are already 10px). */
#md-tools .card { border-radius: 10px; }
.settings-nav-item {
  background: none;
  border: none;
  border-radius: 10px;
  padding: 9px 11px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  text-align: left;
  width: 100%;
  font-family: inherit;
  transition: background 0.12s, color 0.12s;
}
.settings-nav-item:hover { background: var(--bg-soft); color: var(--text); }
.settings-nav-item.active { background: var(--primary); color: #fff; }
/* Fixed-width icon slot so every label starts at the same x, regardless of
   whether the icon is an SVG <img> or a glyph (⚙ ⇅ ⟲). */
.settings-nav-ico { display: inline-flex; align-items: center; justify-content: center; width: 16px; margin-right: 8px; vertical-align: middle; line-height: 1; flex-shrink: 0; }
/* SVG icons inside settings nav — match text color per state */
.settings-nav-item img { filter: brightness(0) opacity(0.45); vertical-align: middle; }
.settings-nav-item:hover img { filter: brightness(0) opacity(0.65); }
.settings-nav-item.active img { filter: brightness(0) invert(1); }
.settings-panel {
  flex: 1;
  padding: 22px 26px;
  overflow-y: auto;
  background: var(--surface);
}

/* Preference cards used to cap at 560px, which left 373px of gutter hard against
   the right edge of a 933px panel. They now share the Data tab's 900px cap so
   both tabs fill the dialog identically. Rows here flow left (checkbox, then
   label) rather than space-between, so the extra width does not strand a control
   away from its label. margin-inline keeps the column centred in the leftover. */
#settings-prefs-content > .card { margin-inline: auto; }
.settings-tab-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 16px;
}
.settings-tab-section {
  display: flex;
  flex-direction: column;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.settings-tab-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.settings-tab-row:last-child { border-bottom: none; }
.settings-tab-row--danger .settings-tab-row-label { color: #ef4444; }
.settings-tab-row-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--text);
  margin-bottom: 2px;
}
.settings-tab-row-desc {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.45;
}
.settings-btn-danger {
  background: transparent;
  color: #ef4444;
  border: 1px solid #ef4444;
  flex-shrink: 0;
}
.settings-btn-danger:hover { background: #fef2f2; border-color: #dc2626; color: #dc2626; }
@media (max-width: 560px) {
  .settings-layout { flex-direction: column; }
  .settings-nav { width: 100%; flex-direction: row; border-right: none; border-bottom: 1px solid var(--border); }
}

/* ── Main modal backdrop ─────────────────────────────────────── */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 300; display: flex; align-items: center; justify-content: center; padding: 16px; }
.modal { background: var(--surface); border-radius: 10px; width: 100%; max-width: 640px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 20px 25px rgba(0,0,0,0.15); }
.modal.wide { max-width: 1000px; }
/* Extra-wide: data-grid-heavy editors (e.g. Test Result measurements,
   8 columns). Capped by the viewport, so it shrinks on smaller screens. */
.modal.xwide { max-width: 1240px; }

/* DOE generator layout.
   Steps flow ACROSS then down — 1|2, 3|4, 5 — so numbered sections read in the
   order they are numbered. A column-first split put step 1 beside step 4, and
   the eye goes left-to-right first, so it read 1 → 4.
   The design space sits in a STICKY RAIL beside the form, not below it. */
.asm-doegen-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 440px); gap: 0 26px; align-items: start; }
.asm-doegen-form { min-width: 0; }
.asm-doegen-rail { min-width: 0; align-self: stretch; }
.asm-doegen-rail-inner { position: sticky; top: 12px; }
.asm-doegen-full { grid-column: 1 / -1; }
.asm-doegen-step .form-grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 0 22px; }
@media (max-width: 900px) { .asm-doegen-step .form-grid { grid-template-columns: 1fr; } }

/* Expanded: the All-pairs matrix and All-factors views want the whole width, so
   the rail drops below the form and spans it rather than being squeezed. */
.asm-doegen-cols.rail-expanded { grid-template-columns: minmax(0, 1fr); }
.asm-doegen-cols.rail-expanded .asm-doegen-rail-inner { position: static; }

/* One column when the user asks for it — chart below the form. */
.asm-doegen-cols.cols-1 { grid-template-columns: 1fr; }
.asm-doegen-cols.cols-1 .asm-doegen-rail-inner { position: static; }

/* …and automatically when there isn't the width for two, whatever the
   preference says. Two ~300px columns read worse than scrolling.
   The AI drawer is the case a viewport media query cannot see: it shrinks #main
   rather than the window, so a 1400px viewport can leave under 1000px of
   content. Keyed off the body class the drawer already sets. */
@media (max-width: 1180px) {
  .asm-doegen-cols, .asm-doegen-cols.cols-2 { grid-template-columns: 1fr; }
  .asm-doegen-rail-inner { position: static; }
}
@media (max-width: 1500px) {
  body.ai-open:not(.ai-floating) .asm-doegen-cols,
  body.ai-open:not(.ai-floating) .asm-doegen-cols.cols-2 { grid-template-columns: 1fr; }
  body.ai-open:not(.ai-floating) .asm-doegen-rail-inner { position: static; }
}

/* The first section in each column shouldn't carry the divider that separates
   stacked sections. In a two-column grid that means the first TWO. */
.asm-doegen-form > .asm-doegen-step:first-child .asm-doegen-sec { margin-top: 0; padding-top: 0; border-top: none; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.modal-header h3 { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
/* Title icons (e.g. the printer on print-config modals) are SVGs with no
   intrinsic size cap outside .btn — constrain them so they read as a title
   glyph, not a giant hero illustration that bloats the modal. */
.modal-header h3 img { width: 18px; height: 18px; object-fit: contain; flex: 0 0 auto; }
/* Bare ✕ dismiss in a modal header — icon only, no border / background / label. */
.fl-modal-x {
  flex: 0 0 auto; border: none; background: none; cursor: pointer;
  color: var(--text-muted); font-size: 18px; line-height: 1;
  padding: 2px; margin: -2px -4px -2px 8px; border-radius: 6px;
}
.fl-modal-x:hover { color: var(--text); background: var(--bg-soft); }
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group.span-2 { grid-column: span 2; }
/* Modals that stack bare .form-group blocks straight in the body (e.g. the
   print-config / "print selection" modals) get no grid row-gap, so the groups
   run together. Add breathing room between consecutive direct-child groups.
   Grid-wrapped form-groups (inside .form-grid) aren't direct children, so
   they're unaffected. */
.modal-body > .form-group + .form-group { margin-top: 16px; }
.form-group.span-3 { grid-column: span 3; }
/* Form field labels: Title Case (as authored), NOT uppercased. ALL CAPS is
   reserved for short structural eyebrows (nav sections, detail-page field
   eyebrows, table headers, badges) where it reads as hierarchy; a form label
   is often multi-word and carries a parenthetical ("Spec (acceptance)",
   "Default method / standard"), where caps loses word-shape readability and
   reads as shouty. Muted color + weight 500 keep it subordinate to the input. */
label { font-size: 12px; font-weight: 500; color: var(--text-muted); letter-spacing: normal; }
/* Radio/checkbox OPTION CARDS built on <label> (e.g. Register-blend modes):
   reset the global label styling so a card's title + multi-line description
   reads as normal prose; inner elements still set their own weight/size/color. */
label.fl-opt-card { text-transform: none; letter-spacing: normal; font-weight: 400; font-size: 13px; color: var(--text); }
/* 10px matches --radius, the tile corner used by .card and every folder in the
   app, so a field sits in the same visual language as the surface holding it.
   6px read as a legacy form control next to those.
   Controls that set their OWN radius are deliberately left alone: .fl-cell-input
   and the other compact grid inputs are tighter on purpose, and a 10px corner on
   a 26px-tall cell reads pill-ish rather than considered. */
input, select, textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px; font-size: 14px; font-family: inherit; color: var(--text); background: #fff; transition: border-color 0.15s; }
/* Hide native number-input spinner arrows app-wide. They add no value here
   (precise values are typed, step:1 is meaningless for decimals), clutter
   compact/table cells, and are easy to misclick. The wheel-to-change hazard
   is handled by a global guard in init.js. */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
/* Text-style fields get the focus ring; radios/checkboxes must NOT (a 3px
   box-shadow around a 15px control reads as a lingering grey/blue box that
   only clears on blur). They use accent-color for state + a crisp
   focus-visible outline for keyboard users. */
input:not([type="radio"]):not([type="checkbox"]):focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
input[type="radio"], input[type="checkbox"] { accent-color: var(--primary); }
input[type="radio"]:focus, input[type="checkbox"]:focus { outline: none; box-shadow: none; }
input[type="radio"]:focus-visible, input[type="checkbox"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
/* Placeholder/hint text — lighter than real input (--text #1e293b) and muted
   (#64748b) so an "e.g. …" suggestion clearly reads as a hint, not a value. The
   browser default (~#757575) sits too close to entered text. Scoped placeholder
   rules elsewhere (analyze sidebar, testing-dark, comp-note) win by specificity. */
input::placeholder, textarea::placeholder { color: #94a3b8; opacity: 1; }
textarea { resize: vertical; min-height: 70px; }
/* ── Uniform form-control height (enforced, not per-field) ──────────────────
   Browsers render <select> ~2px taller than <input> even with identical
   padding/border/font (intrinsic type difference), so hand-built forms drift
   — e.g. a Purity <input> ended up shorter than its neighbouring Grade
   <select>. Pin ONE min-height on every text-like control inside a .form-group
   so inputs, selects, and the buttons that flex-stretch beside them all line
   up. Any NEW field dropped into a .form-group inherits this automatically —
   no per-field height styling needed. box-sizing:border-box (global) means
   this includes the 8px padding + 1px border. Scoped to .form-group so compact
   table inputs (.fl-cell-input) and toggles keep their own sizing; non-text
   input types (checkbox/radio/range/file/color) and textarea are excluded so
   their bespoke dimensions survive. */
.form-group input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]),
.form-group select { min-height: 38px; }
/* Shared compact "box" for dense table cells (composition UNIT/ROLE, amounts,
   set-price unit/currency) — same border/radius/focus language as the form
   inputs, just table-sized. .fl-unit-input pairs with a <datalist> for
   type-to-filter unit picking. */
.fl-cell-input { width: 100%; padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; font-family: inherit; color: var(--text); background: #fff; transition: border-color 0.15s; }
.fl-cell-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
/* Unit combobox suggestion popup — our replacement for the native <datalist>
   dropdown (which mis-anchors to the top-left inside fixed-position modals).
   Grouped (Mass / Volume / Length …) with a sticky search box. */
.fl-unit-combo-pop {
  position: fixed; z-index: 1400;
  background: var(--surface, #fff); color: var(--text, #0f172a);
  border: 1px solid var(--border, #e2e8f0); border-radius: 8px;
  box-shadow: 0 10px 28px rgba(0,0,0,0.18); padding: 4px; min-width: 150px;
}
.fl-unit-combo-searchwrap { padding: 2px 2px 4px; }
.fl-unit-combo-search {
  width: 100%; padding: 6px 9px; border: 1px solid var(--border); border-radius: 6px;
  font-size: 13px; font-family: inherit; color: var(--text); box-sizing: border-box;
}
.fl-unit-combo-search:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
.fl-unit-combo-list { max-height: 232px; overflow-y: auto; }
.fl-unit-combo-group {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-muted); padding: 6px 10px 3px; font-weight: 600;
}
.fl-unit-combo-item { padding: 6px 10px; font-size: 13px; border-radius: 5px; cursor: pointer; white-space: nowrap; }
.fl-unit-combo-item:hover, .fl-unit-combo-item.active { background: color-mix(in srgb, var(--primary) 12%, transparent); }
.fl-unit-combo-item.current { font-weight: 600; color: var(--primary); }
.fl-unit-combo-item.fl-unit-combo-custom { color: var(--primary); font-style: italic; }
.fl-unit-combo-empty { padding: 8px 10px; font-size: 12px; color: var(--text-muted); }
/* Triangle icons — replace the ▾ ▴ ▶ ◀ ▼ ▲ ▸ unicode glyphs (carets, expand /
   collapse toggles, sort markers, breadcrumbs). em-sized so they scale with the
   surrounding text, currentColor mask so they inherit colour (blue chevrons,
   muted carets, etc.). Root-relative url() works at both / and /free/ and is
   cache-busted by stamp-cache. */
.fl-tri {
  display: inline-block; width: 0.72em; height: 0.72em;
  background: currentColor; vertical-align: -0.04em;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.fl-tri-down  { -webkit-mask-image: url(/icns-svg/arrows/triangle-down.svg?v=75e481e59f);  mask-image: url(/icns-svg/arrows/triangle-down.svg?v=75e481e59f); }
.fl-tri-up    { -webkit-mask-image: url(/icns-svg/arrows/triangle-up.svg?v=6c75f51066);    mask-image: url(/icns-svg/arrows/triangle-up.svg?v=6c75f51066); }
.fl-tri-left  { -webkit-mask-image: url(/icns-svg/arrows/triangle-left.svg?v=c6932a2c92);  mask-image: url(/icns-svg/arrows/triangle-left.svg?v=c6932a2c92); }
.fl-tri-right { -webkit-mask-image: url(/icns-svg/arrows/triangle-right.svg?v=b5b93b215e); mask-image: url(/icns-svg/arrows/triangle-right.svg?v=b5b93b215e); }
/* Directional line arrows — replace → ← ↑ ↓ (+ diagonals) in visible UI text.
   Same em-sized currentColor-mask approach as the triangles. */
.fl-ar {
  display: inline-block; width: 0.85em; height: 0.85em;
  background: currentColor; vertical-align: -0.1em;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.fl-ar-e  { -webkit-mask-image: url(/icns-svg/arrows/arrow-E.svg?v=849ef8ad3d);  mask-image: url(/icns-svg/arrows/arrow-E.svg?v=849ef8ad3d); }
.fl-ar-w  { -webkit-mask-image: url(/icns-svg/arrows/arrow-W.svg?v=55e411252a);  mask-image: url(/icns-svg/arrows/arrow-W.svg?v=55e411252a); }
.fl-ar-n  { -webkit-mask-image: url(/icns-svg/arrows/arrow-N.svg?v=4f6fd96a3d);  mask-image: url(/icns-svg/arrows/arrow-N.svg?v=4f6fd96a3d); }
.fl-ar-s  { -webkit-mask-image: url(/icns-svg/arrows/arrow-S.svg?v=7db319aca4);  mask-image: url(/icns-svg/arrows/arrow-S.svg?v=7db319aca4); }
.fl-ar-ne { -webkit-mask-image: url(/icns-svg/arrows/arrow-NE.svg?v=5597666826); mask-image: url(/icns-svg/arrows/arrow-NE.svg?v=5597666826); }
.fl-ar-nw { -webkit-mask-image: url(/icns-svg/arrows/arrow-NW.svg?v=eda2bde79c); mask-image: url(/icns-svg/arrows/arrow-NW.svg?v=eda2bde79c); }
.fl-ar-se { -webkit-mask-image: url(/icns-svg/arrows/arrow-SE.svg?v=baf6382b9b); mask-image: url(/icns-svg/arrows/arrow-SE.svg?v=baf6382b9b); }
.fl-ar-sw { -webkit-mask-image: url(/icns-svg/arrows/arrow-SW.svg?v=326a4f1c0a); mask-image: url(/icns-svg/arrows/arrow-SW.svg?v=326a4f1c0a); }
/* Rotational refresh arrows — replace ↺ ⟲ (counter-clockwise) and ↻ ⟳ (clockwise)
   on reset / restore / refresh / rotate controls. Reuse the .fl-ar box. */
.fl-undo { -webkit-mask-image: url(/icns-svg/arrows/undo.svg?v=a2e37225c7); mask-image: url(/icns-svg/arrows/undo.svg?v=a2e37225c7); }
.fl-redo { -webkit-mask-image: url(/icns-svg/arrows/redo.svg?v=427bad4c93); mask-image: url(/icns-svg/arrows/redo.svg?v=427bad4c93); }
.fl-bidh { -webkit-mask-image: url(/icns-svg/arrows/bidir-horizontal.svg?v=d02365074b); mask-image: url(/icns-svg/arrows/bidir-horizontal.svg?v=d02365074b); }
.fl-bidv { -webkit-mask-image: url(/icns-svg/arrows/bidir-vertical.svg?v=7e060840ac); mask-image: url(/icns-svg/arrows/bidir-vertical.svg?v=7e060840ac); }
/* Inline action-icon glyph replacements (⚙︎ ✎ 🔍 ⇄ ⊞ etc.) — em-sized
   currentColor masks so they inherit text colour in labels, badges and
   toolbar buttons, matching the .fl-ar / .fl-tri family above. */
.fl-gear, .fl-edit, .fl-search, .fl-print, .fl-compare, .fl-import, .fl-columns, .fl-panels, .fl-pin, .fl-logo, .fl-cloud, .fl-doe, .fl-file, .fl-lock, .fl-cross, .fl-check, .fl-warn {
  display: inline-block; width: 0.95em; height: 0.95em;
  background: currentColor; vertical-align: -0.14em;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.fl-pin     { -webkit-mask-image: url(/icns-svg/actions/pin.svg?v=a58fde889a); mask-image: url(/icns-svg/actions/pin.svg?v=a58fde889a); }
.fl-gear    { -webkit-mask-image: url(/icns-svg/actions/cogwheel.svg?v=647f3cf745);    mask-image: url(/icns-svg/actions/cogwheel.svg?v=647f3cf745); }
.fl-edit    { -webkit-mask-image: url(/icns-svg/actions/edit.svg?v=a518740d86);        mask-image: url(/icns-svg/actions/edit.svg?v=a518740d86); }
.fl-search  { -webkit-mask-image: url(/icns-svg/menu-bar/search.svg?v=a511dc8d72);     mask-image: url(/icns-svg/menu-bar/search.svg?v=a511dc8d72); }
.fl-print   { -webkit-mask-image: url(/icns-svg/actions/printer.svg?v=c22bc95dcb);     mask-image: url(/icns-svg/actions/printer.svg?v=c22bc95dcb); }
.fl-compare { -webkit-mask-image: url(/icns-svg/menu-bar/compare.svg?v=ee214f72c7);    mask-image: url(/icns-svg/menu-bar/compare.svg?v=ee214f72c7); }
.fl-import  { -webkit-mask-image: url(/icns-svg/actions/import.svg?v=6ecba9920d);      mask-image: url(/icns-svg/actions/import.svg?v=6ecba9920d); }
.fl-columns { -webkit-mask-image: url(/icns-svg/menu-bar/columns.svg?v=2a91acae33);    mask-image: url(/icns-svg/menu-bar/columns.svg?v=2a91acae33); }
.fl-panels  { -webkit-mask-image: url(/icns-svg/menu-bar/test-panels.svg?v=03c4da87bc); mask-image: url(/icns-svg/menu-bar/test-panels.svg?v=03c4da87bc); }
.fl-cloud   { -webkit-mask-image: url(/icns-svg/workspace-files/cloud.svg?v=03a4e68391); mask-image: url(/icns-svg/workspace-files/cloud.svg?v=03a4e68391); }
.fl-doe     { -webkit-mask-image: url(/icns-svg/menu-bar/doe-matrix.svg?v=582941cd3e);   mask-image: url(/icns-svg/menu-bar/doe-matrix.svg?v=582941cd3e); }
.fl-file    { -webkit-mask-image: url(/icns-svg/menu-bar/file.svg?v=8f35aeef55);  mask-image: url(/icns-svg/menu-bar/file.svg?v=8f35aeef55); }
.fl-lock    { -webkit-mask-image: url(/icns-svg/actions/lock.svg?v=506eb4f3db);  mask-image: url(/icns-svg/actions/lock.svg?v=506eb4f3db); }
.fl-cross   { -webkit-mask-image: url(/icns-svg/actions/cross.svg?v=531369453c);  mask-image: url(/icns-svg/actions/cross.svg?v=531369453c); }
.fl-check   { -webkit-mask-image: url(/icns-svg/actions/check.svg?v=6e2d98ba30);  mask-image: url(/icns-svg/actions/check.svg?v=6e2d98ba30); }
.fl-warn    { -webkit-mask-image: url(/icns-svg/actions/warning.svg?v=564b99df90);  mask-image: url(/icns-svg/actions/warning.svg?v=564b99df90); }
/* Theme-aware FormLab flask mark (logo-black.svg, single-colour) — currentColor
   mask so it adapts to any surface (white on the dark sidebar, dark on light).
   Reserved for non-favicon placements; the top-left sidebar badge stays the
   blue favicon-blue.svg. */
.fl-logo    { -webkit-mask-image: url(/icns-svg/formlab-logos/logo-black.svg?v=51c613453d); mask-image: url(/icns-svg/formlab-logos/logo-black.svg?v=51c613453d); }
/* Searchable cell-dropdown trigger (Unit, Role) — matches the ingredient
   picker's button; the real <select> is hidden behind it. */
.ff-cell-trigger {
  width: 100%; flex: 1; min-width: 0;
  padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px;
  font-size: 13px; font-family: inherit; color: var(--text); background: #fff;
  text-align: left; cursor: pointer; display: flex; align-items: center; gap: 6px;
  transition: border-color 0.15s;
}
.ff-cell-trigger:hover { border-color: #94a3b8; }
.ff-cell-trigger:focus-visible { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
.ff-cell-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Cell-dropdown popover rows + group labels */
.fm-cell-row { padding: 6px 9px; font-size: 13px; border-radius: 5px; cursor: pointer; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fm-cell-row:hover { background: #f1f5f9; }
.fm-cell-row.active { background: #eff6ff; color: var(--primary); font-weight: 600; }
/* Two-line picker row: main label + muted metadata sub-line (status · date · project). */
.fm-cell-row-2l { white-space: normal; line-height: 1.25; }
.fm-cell-row-2l .fm-cell-main { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fm-cell-row-2l .fm-cell-meta { display: block; font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.fm-cell-row-2l.active .fm-cell-meta { color: var(--primary); opacity: 0.75; }
.fm-cell-group { font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); padding: 6px 9px 2px; }
.section-divider { margin: 16px 0; border: none; border-top: 1px solid var(--border); }
.section-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 10px; padding-bottom: 6px; border-bottom: 2px solid var(--primary); display: inline-block; }

/* Audit badge — small pill used in the audit log table */
.audit-badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; }

/* Notebook entry — Quill HTML is rendered raw, so re-apply the formatting
   styles outside the editor's own scoped CSS. */
.notebook-entry p          { margin: 0 0 6px; }
.notebook-entry p:last-child { margin-bottom: 0; }
.notebook-entry h2         { font-size: 16px; font-weight: 700; margin: 8px 0 6px; }
.notebook-entry h3         { font-size: 14px; font-weight: 600; margin: 8px 0 6px; }
.notebook-entry ul, .notebook-entry ol { margin: 4px 0 6px 22px; padding: 0; }
.notebook-entry li         { margin-bottom: 2px; }
.notebook-entry blockquote { border-left: 3px solid var(--border); padding: 2px 10px; margin: 6px 0; color: var(--text-muted); }
.notebook-entry pre, .notebook-entry .ql-code-block { background: #f1f5f9; padding: 8px 10px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; white-space: pre-wrap; margin: 6px 0; }
.notebook-entry a          { color: var(--primary); text-decoration: underline; }

/* Grouped form / detail sections (Ingredient modal + detail view) */
.form-section { display: flex; flex-direction: column; gap: 8px; padding-top: 14px; margin-top: 10px; border-top: 1px solid var(--border); }
.form-section:first-of-type { padding-top: 0; margin-top: 0; border-top: none; }
.ing-section-h { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin: 0 0 10px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }

/* GHS pictograms + H/P code pills */
.ghs-pictogram { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.ghs-code-pill { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; font-weight: 600; border: 1px solid; background: #fff; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* Vertical list layout for H/P codes when descriptions are shown inline.
   Replaces the previous title-tooltip-only chip grid (which left users
   chasing a question-mark cursor with no real affordance).
   The description sizes to its content (flex:0 1 auto, NOT flex:1) so
   the ✕ button sits right after the text rather than getting pushed
   to the far edge of a wide parent card. Also kept always-visible at
   opacity 0.5 so users don't have to hover-discover it. */
.ghs-code-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
.ghs-code-row .ghs-code-pill { flex-shrink: 0; }
.ghs-code-row .ghs-code-desc { font-size: 12.5px; color: var(--text); line-height: 1.4; flex: 0 1 auto; min-width: 0; }
.ghs-code-row .ghs-code-desc.unknown { color: var(--text-muted); font-style: italic; }
.ghs-code-row .ghs-code-del {
  background: none; border: 1px solid transparent; color: var(--text-muted);
  font-size: 12px; padding: 0 6px; border-radius: 4px; cursor: pointer;
  line-height: 1.4; opacity: 0.5; flex-shrink: 0;
  transition: opacity 0.12s, color 0.12s, border-color 0.12s;
}
.ghs-code-row:hover .ghs-code-del { opacity: 1; }
.ghs-code-row .ghs-code-del:hover { color: var(--danger); border-color: var(--danger); }

/* Editable pictogram (Safety tab): corner ✕ hidden until you hover the
   pictogram — keeps the page clean, and non-interactive while hidden so it
   can't be clicked by accident. */
.ghs-pic-wrap { position: relative; display: inline-flex; }
.ghs-pic-wrap .ghs-pic-del {
  position: absolute; top: -7px; right: -7px;
  width: 19px; height: 19px; border-radius: 50%;
  border: 1px solid var(--border); background: #fff; color: #dc2626;
  font-size: 12px; line-height: 1; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .18);
  opacity: 0; pointer-events: none; transition: opacity 0.12s;
}
.ghs-pic-wrap:hover .ghs-pic-del { opacity: 1; pointer-events: auto; }

/* Safety code picker rows (the multi-select modal). Use CSS grid with
   explicit column widths so the checkbox / chip / description columns
   stay aligned regardless of description length. The previous flex
   version collapsed columns inconsistently — chips and text would
   end up clipped against the modal's right edge with checkboxes
   floating mid-row. Grid gives bulletproof alignment. */
.safety-pick-row {
  display: grid;
  grid-template-columns: 22px 64px 1fr;
  gap: 10px;
  align-items: start;
  padding: 7px 12px;
  border-bottom: 1px solid #f1f5f9;
  cursor: pointer;
}
.safety-pick-row.disabled { cursor: not-allowed; opacity: 0.55; }
.safety-pick-row:hover:not(.disabled) { background: #f8fafc; }
.safety-pick-row input[type="checkbox"] { margin-top: 4px; }
.safety-pick-row .ghs-code-pill { text-align: center; }
.safety-pick-row .safety-pick-desc { font-size: 13px; color: var(--text); line-height: 1.4; }

/* Badges */
.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; font-weight: 500; }
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #dcfce7; color: #15803d; }
.badge-amber { background: #fef3c7; color: #b45309; }
.badge-red { background: #fee2e2; color: #b91c1c; }
.badge-gray { background: #f1f5f9; color: #475569; }
.badge-purple { background: #ede9fe; color: #6d28d9; }

/* Project chip — a muted, clickable pill used where a project link would
   otherwise compete visually with a primary (blue) entity link. */
.proj-chip {
  display: inline-flex; align-items: center; gap: 3px;
  max-width: 100%; padding: 1px 8px;
  font-size: 11px; line-height: 1.6; color: #7e22ce;
  background: #f5f3ff; border: 1px solid #e9d5ff; border-radius: 9999px;
  cursor: pointer; text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.proj-chip:hover { background: #ede9fe; border-color: #d8b4fe; }

/* Composition table */
.comp-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1.5fr 36px; gap: 6px; align-items: center; margin-bottom: 6px; }
.comp-header { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding-bottom: 4px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
.step-row { display: grid; grid-template-columns: 40px 1fr 80px 80px 100px 36px; gap: 6px; align-items: start; margin-bottom: 8px; }
.step-num { background: var(--primary); color: #fff; width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; flex-shrink: 0; margin-top: 2px; }

/* Detail view */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.detail-field { }
.detail-field .label { font-size: 11px; font-weight: 600; letter-spacing: normal; color: var(--text-muted); margin-bottom: 3px; }
.detail-field .value { font-size: 14px; }
.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: 18px; }
/* Ingredient detail: fuse the breadcrumb + tab bar with the top action bar into
   ONE pinned header. On this view the global top bar drops its divider/shadow so
   the two read as a single header; the head pins to the top while the panel
   scrolls beneath. The head goes full-bleed (cancels #content's 24px side pad) so
   its bottom divider spans the whole frame, and carries no bottom gap. */
/* Test Analytics: tighter side padding so Coverage Matrix + Rankings tables
   have more horizontal room for parameter columns without wasting space on
   large gutters that are only useful for forms/prose views. */
body[data-view="test-analytics"] #content { padding-left: 12px; padding-right: 12px; }

body[data-view="ingredient-detail"] #topbar { border-bottom: none; box-shadow: none; }
body[data-view="ingredient-detail"] #content { padding-top: 0; }
/* In inline Identification edit, drop #content's bottom padding so the sticky
   Save/Cancel bar (which bleeds to the side edges via margin:0 -24px) sits flush
   to the frame's bottom edge — no peek-through gap below it. Scoped to edit mode
   so read view keeps its normal bottom breathing room. */
body[data-view="ingredient-detail"] #content:has(.ing-inline-edit) { padding-bottom: 0; }
#ing-detail-head {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
  margin: 0 -24px;                          /* full-bleed band to the frame edges */
  /* 14px of OPAQUE bottom padding = breathing room between the tab bar and the
     content below. It's part of the sticky (opaque) head, so content slides
     cleanly underneath with no peek-through. */
  padding: 10px 24px 14px;
}
/* Divider as a line at the tab-row baseline (top of the 14px gap), via a
   pseudo-element, so the gap sits BELOW the divider rather than pushing it down. */
#ing-detail-head::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: 14px;
  border-bottom: 2px solid var(--border);
}
#ing-detail-head #ing-tabs {
  /* Cancel the head's 24px side padding so the scroll strip spans the FULL
     window width, then re-add 24px inner padding so the first tab still lines up
     with the breadcrumb — the tabs now scroll across the whole width. */
  margin: 0 -24px;
  padding: 0 24px;
  border-bottom: none;
  /* Scroll the strip left/right when the tabs don't fit, instead of wrapping. */
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
/* 3px was a hairline you couldn't grab. 10px of track with a 3px transparent
 * border on the thumb keeps it LOOKING ~4px while giving the pointer a real
 * target — the same trick the Analyze panes use. */
#ing-detail-head #ing-tabs::-webkit-scrollbar { height: 10px; }
#ing-detail-head #ing-tabs::-webkit-scrollbar-track { background: transparent; }
#ing-detail-head #ing-tabs::-webkit-scrollbar-thumb {
  background: rgba(100, 116, 139, .35);
  background-clip: content-box;
  border: 3px solid transparent;
  border-radius: 999px;
}
#ing-detail-head #ing-tabs::-webkit-scrollbar-thumb:hover {
  background: rgba(71, 85, 105, .6); background-clip: content-box;
}
#ing-detail-head #ing-tabs .tab { flex-shrink: 0; white-space: nowrap; }
#ing-detail-head .breadcrumb { margin-top: 0; margin-bottom: 10px; }
/* Inline edit (Identification tab) — TILED layout that mirrors the read view's
   box stack: each form section becomes its own card, laid out in a 2-column
   grid. Compact sections (Properties, Sourcing) tile side by side; everything
   else spans full width so wide inputs (Notes, SMILES, class panels) get room. */
#tab-ing-identification.ing-inline-edit {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
/* Full-width chrome: Save/Cancel bar, Preview block, expand/collapse toggles. */
#tab-ing-identification.ing-inline-edit > .ing-edit-bar,
#tab-ing-identification.ing-inline-edit > .ing-edit-preview,
#tab-ing-identification.ing-inline-edit > .ing-form-toggles,
#tab-ing-identification.ing-inline-edit > #ing-props-wrap,
#tab-ing-identification.ing-inline-edit > #ing-custom-fields-wrap { grid-column: 1 / -1; }
/* Sections render as cards (matching the read-view boxes); full width default. */
#tab-ing-identification.ing-inline-edit > .form-section {
  grid-column: 1 / -1;
  margin: 0;
  background: var(--surface, #fff);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
}
/* Properties + Sourcing are compact → tile two-up on a row. */
#tab-ing-identification.ing-inline-edit > [data-ing-section="physical-properties"],
#tab-ing-identification.ing-inline-edit > [data-ing-section="sourcing"] { grid-column: span 1; }
/* Inner field grids stay 2-col to match the read-view boxes; span-2 = full. */
#tab-ing-identification.ing-inline-edit .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
#tab-ing-identification.ing-inline-edit .form-group.span-2 { grid-column: span 2; }
@media (max-width: 760px) {
  #tab-ing-identification.ing-inline-edit { grid-template-columns: 1fr; }
  #tab-ing-identification.ing-inline-edit > [data-ing-section="physical-properties"],
  #tab-ing-identification.ing-inline-edit > [data-ing-section="sourcing"] { grid-column: 1 / -1; }
}
/* Edit mode reads as "editing": each box takes a slightly darkened (raised)
   background so the white inputs pop — signaling the tab is editable without
   changing the box layout. */
#tab-ing-identification.ing-inline-edit > .form-section,
#tab-ing-identification.ing-inline-edit > #ing-props-wrap { background: var(--surface-raised, #f1f5f9); }
/* Both names read slightly larger — in edit, the Name / Chemical Name inputs. */
#tab-ing-identification.ing-inline-edit #f-name { font-size: 16px; font-weight: 700; }
#tab-ing-identification.ing-inline-edit #f-chemname { font-size: 14.5px; font-weight: 600; }
/* ── 1:1 mirror editor ───────────────────────────────────────────────────────
   The tab renders read-shaped cards (NOT the form-section grid above), so undo
   the grid and lay them out as a plain block.

   The boxes used to darken here so that white boxed inputs would pop against
   them. Those inputs are transparent underlines now (the Accounts treatment),
   so the raise lost its job — and it was costing something visible: these cards
   have NO border, their edge is a soft shadow falloff, so adding a hard fill
   makes the edge land about a pixel further out and every box appears to bloom
   on entering edit. Same geometry, different edge. Leaving the background alone
   is both truer to "shift as little as possible" and one less thing to look at. */
#tab-ing-identification.ing-ident-editor { display: block; }
/* Fields not relevant to the chosen class are dimmed but stay fully editable. */
/* A dimmed Identification field means "unusual for this ingredient's class",
   NOT "disabled" — the field is still populated, still editable, and its Copy
   button still works. So the dim is applied to the CONTENT, never to the field
   itself: opacity establishes a compositing group, and a child cannot be
   raised back above its parent's opacity. Dimming the container made working
   Copy buttons look inert with no way to bring them back.
   `:not(.btn *)` keeps the icon inside a button at full strength too — it is a
   separate element, so it would otherwise dim on its own account. */
[id^="tab-ing-identification"] .ing-dim { opacity: 1; transition: opacity 0.12s ease; }
[id^="tab-ing-identification"] .ing-dim :is(.label, code, span, em, a, img, strong):not(.btn):not(.btn *) {
  opacity: 0.42;
  transition: opacity 0.12s ease;
}
[id^="tab-ing-identification"] .ing-dim:focus-within :is(.label, code, span, em, a, img, strong):not(.btn):not(.btn *) {
  opacity: 1;
}
/* The class picker reads as the primary control of the box. */
#tab-ing-identification.ing-ident-editor [data-eslot="classpicker"] select { font-size: 14px; font-weight: 600; padding: 7px 10px; }
/* Relocated form-groups: drop the modal's bottom margin so slots sit on the grid
   rhythm, and quiet the helper labels so each box reads like its view counterpart. */
#tab-ing-identification.ing-ident-editor .form-group { margin-bottom: 0; }
#tab-ing-identification.ing-ident-editor .form-group > label { font-size: 11px; letter-spacing: normal; color: var(--text-muted); font-weight: 600; }
.tab { padding: 8px 16px; font-size: 13.5px; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; color: var(--text-muted); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab:hover:not(.active) { color: var(--text); }
/* Formula detail: the tab strip + its (tab-switched) panel content render as
 * one shaded, full-bleed band so the whole tabbed area reads unmistakably as a
 * single section, set off from the summary bar above and the Batches/Samples
 * cards below. Both elements bleed out to the content-pane edges (#content has
 * 24px padding) and re-add matching padding so labels/content stay aligned; the
 * white cards inside pop against the grey band. */
#form-tabs {
  margin: 0 -24px 0;
  padding: 12px 24px 0;
  background: #f1f5f9;
  border-top: 1px solid var(--border);
}
#form-tab-panels {
  margin: 0 -24px 16px;
  padding: 16px 24px;
  background: #f1f5f9;
  border-bottom: 1px solid var(--border);
}
/* Test-analytics tab-bar icons: black SVG artwork on the inactive (light,
   .btn-secondary) tab; flipped white when the tab is active (.btn-primary,
   dark). The ⚠ Failure-Analysis glyph isn't an .ta-tab-ico — it inherits the
   button's text colour and adapts on its own. */
.btn .ta-tab-ico { width: 13px; height: 13px; vertical-align: -2px; object-fit: contain; display: inline-block; }
.btn-primary .ta-tab-ico { filter: brightness(0) invert(1); }
.ta-tab-glyph { font-variant-emoji: text; }
/* Demo-dataset tabs: uniform icon slot + vertical centering so the mixed
   emoji / symbol icons and their labels line up cleanly. */
#demo-tabs .tab { display: inline-flex; align-items: center; gap: 5px; }
#demo-tabs .demo-tab-ico { width: 16px; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; line-height: 1; flex-shrink: 0; }

/* Alerts */
.alert { padding: 12px 14px; border-radius: 6px; font-size: 13.5px; margin-bottom: 16px; }
.alert-info { background: #eff6ff; color: #1d4ed8; border-left: 3px solid var(--primary); }

/* Breadcrumb */
.breadcrumb { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-muted); margin-bottom: 16px; }
.breadcrumb a { color: var(--primary); cursor: pointer; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb .sep { color: var(--border); }

/* ── Formula detail: one grey field, and nothing floating loose on it ───────
 * Scoped with :has(#form-tabs) so it applies to the formula detail view only —
 * no wrapper class to thread through the renderer, and it can't leak to the
 * list views that share .breadcrumb and .tabs.
 *
 * (1) The workspace background is switched OFF here. Everywhere else the theme
 * shows through the gutters between panes; this view is a dense reading surface
 * where a mesh gradient behind half-transparent chrome fights the content. It
 * sits on the same grey the tab panels already use, so the whole view reads as
 * one continuous field instead of panels floating on a picture. */
/* TRANSPARENT, not a fill. Earlier passes painted this grey, then white, which
   was the wrong instrument entirely: floating means the cards sit ON the
   workspace background, and any enclosing fill — of any colour — is the docked
   look wearing a different coat. The view had one because #form-tab-panels and
   #content both carry their own; both are cleared here so the background runs
   behind the cards the way it does everywhere else in floating mode. */
#content:has(#form-tabs) {
  background: transparent;
}
/* ...and the same on the FRAME, not just the scroll area. #content stops below
   the title bar, so painting only it left the workspace background showing in
   the band behind the title and the action buttons — the theme still lurking
   over the top of a view that had opted out of it. #main is the nearest
   ancestor that covers both, and :has() reaches it from the same marker. */
#main:has(#form-tabs) { background: transparent; }
/* #form-tab-panels carries its own #f1f5f9 fill, which would sit ON TOP of the
   ground above and undo it across the entire panel area — the one region that
   actually matters. */
#content:has(#form-tabs) > #form-tab-panels { background: transparent; }
/* Cards stay GLASS here — they were briefly forced opaque while this view had
   an enclosing fill, which is no longer the case. The stock translucent
   treatment (transparent border, white top highlight, blurred ::before) is
   correct again now that there is a background behind them to tint. */
/* (2) The tab strip: tinted surface, 10px corner (= --radius, the app's tile
 * corner). It was a square, borderless strip whose only edge was a 2px rule
 * underneath — a border doing the work a surface should do, which is the thing
 * we have been moving away from. The rule goes; the fill defines it. */
#content:has(#form-tabs) > #form-tabs.tabs {
  background: color-mix(in srgb, var(--primary) 5%, var(--surface, #fff));
  border-bottom: 0;
  border-radius: var(--radius, 10px);
  padding: 4px 6px;
  gap: 2px;
  /* Cancel the -24px side bleed. The strip was full-bleed so its bottom RULE
     could span the whole frame; with the rule gone and a rounded fill in its
     place, bleeding just pushes the corners past the frame edge and clips them
     (measured 1029px inside a 981px container). Inset it and the corner reads. */
  margin: 0 0 14px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .05);
}
/* (3) The two things that sat bare on the ground — the breadcrumb, and the
 * pinned theoretical-parameter chips (the "ƒx P:B ratio" strip). Everything
 * else in this view is on a card; these were loose text on the background,
 * which is what made the view look half-finished. Same tint and corner as the
 * tab strip, so the three read as one header group rather than three
 * unrelated treatments. */
#content:has(#form-tabs) > .breadcrumb,
#content:has(#form-tabs) .tp-pinned-chips {
  background: color-mix(in srgb, var(--primary) 5%, var(--surface, #fff));
  border-radius: var(--radius, 10px);
  padding: 7px 11px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .05);
}
#content:has(#form-tabs) > .breadcrumb { margin-bottom: 10px; align-self: flex-start; }
/* The chip strip is a flex row of its own; keep its items breathing inside the
 * new padding rather than butting the edge. */
#content:has(#form-tabs) .tp-pinned-chips { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }

/* The cost strip's fill moved out of its inline style and onto this class, so
   the cascade can reach it — an inline background can only be overridden with
   !important, which is a worse thing to leave behind than a class name. */
.fm-cost-strip { background: #f8fafc; }

/* ── Translucent: the whole header group becomes glass ──────────────────────
 * Opaque near-white strips sitting on the workspace background were the last
 * thing in this view still saying "docked". Same neutral white + blur as every
 * other floating surface, so they tint the background rather than covering it. */
html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) > .breadcrumb,
html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) > #form-tabs.tabs,
html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) .tp-pinned-chips,
html[data-sb-float][data-sb-translucent] #main:has(#form-tabs) .fm-cost-strip {
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), 0 1px 3px rgba(15, 23, 42, .06);
}
/* The table fill inside a card is opaque white, which turned every glass card
   holding a table back into a solid slab — the card was glass and its contents
   were not. Let it through; the card's own blur is what carries legibility. */
html[data-sb-float][data-sb-translucent] #main:has(#form-tabs) .card .table-wrap {
  background: transparent;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) > .breadcrumb,
  html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) > #form-tabs.tabs,
  html[data-sb-float][data-sb-translucent] #content:has(#form-tabs) .tp-pinned-chips,
  html[data-sb-float][data-sb-translucent] #main:has(#form-tabs) .fm-cost-strip { background: #f4f6f9; }
}

/* Legacy responsive block removed — the mobile drawer + media query
 * below now handle sidebar / form-grid layout on narrow viewports. */

/* Tier 3 mobile-utility classes:
 *   .desktop-only — hidden below 768px (mobile)
 *   .mobile-warn  — a small banner that's hidden on desktop and
 *                   shown above content that doesn't translate well
 *                   to mobile (DOE Matrix, Composition editor, etc.) */
.mobile-warn { display: none; }
.mobile-warn-banner {
  align-items: flex-start; gap: 10px;
  background: #fffbeb; border: 1px solid #f59e0b; border-radius: 8px;
  padding: 10px 12px; margin: 0 0 14px 0;
  font-size: 13px; color: #92400e; line-height: 1.45;
}
.mobile-warn-banner .icon { font-size: 16px; flex-shrink: 0; line-height: 1; }
@media (max-width: 768px) {
  .desktop-only { display: none !important; }
  .mobile-warn { display: flex; }
}
thead th { position: relative; }
/* Column-width handle sits ABOVE the row-height handle (.row-rh, z-index 2) so a
   grab on the shared bottom-right corner resizes the column, not the row. */
.col-rh { position: absolute; right: 0; top: 0; height: 100%; width: 5px; cursor: col-resize; z-index: 4; user-select: none; }
.col-rh:hover, .col-rh:active { background: var(--primary); opacity: 0.4; border-radius: 2px; }
.col-picker-item:hover { background: #f1f5f9; }
.col-picker-item .col-drag-grip { opacity: 0.4; }
/* Column drag-to-reorder — data tables and compare table */
thead th[draggable="true"] { cursor: grab; }
thead th[draggable="true"]:active { cursor: grabbing; }
thead th.col-drag-over { box-shadow: -3px 0 0 var(--primary); }
.col-picker-item:hover .col-drag-grip { opacity: 1; }

/* Formula Analysis tabs */
.fa-tab {
  background: none; border: 1px solid var(--border); border-radius: 6px;
  padding: 5px 14px; cursor: pointer; font-size: 13px; color: var(--text-muted);
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  white-space: nowrap;
}
.fa-tab:hover { background: #f1f5f9; color: var(--text); }
.fa-tab-on { background: var(--primary); color: #fff; border-color: var(--primary); }
.fa-tab-on:hover { background: var(--primary-dark); }

/* ============================================================
   COLUMN HEADER FILTERS
   ============================================================ */
tr.filter-row th {
  background: #f1f5f9; padding: 5px 8px; border-bottom: 2px solid var(--border);
}
.col-filter {
  width: 100%; padding: 3px 6px; font-size: 12px;
  border: 1px solid #cbd5e1; border-radius: 4px;
  font-family: inherit; color: var(--text); background: #fff;
}
.col-filter:focus { outline: none; border-color: var(--primary); }
.col-filter-clear {
  background: none; border: none; cursor: pointer;
  color: #94a3b8; font-size: 14px; padding: 2px 4px;
  border-radius: 4px; line-height: 1;
}
.col-filter-clear:hover { color: var(--danger); background: #fee2e2; }

/* ============================================================
   BULK SELECTION
   ============================================================ */
.bulk-check-col { width: 36px; padding: 0 8px !important; text-align: center; }
.bulk-check-col input[type="checkbox"] {
  cursor: pointer; width: 15px; height: 15px;
  accent-color: var(--primary); vertical-align: middle;
}
/* Bulk-selection action bar — a floating, padded, rounded island pinned to the
   bottom-centre of the screen. position:fixed so it overlays the table instead of
   pushing any UI. _bulkUpdateBar flips it to flex when ≥1 row is selected. */
.bulk-bar {
  display: none; align-items: center; gap: 12px; flex-wrap: nowrap;
  position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%);
  z-index: 200; max-width: calc(100vw - 24px);
  padding: 8px 14px;
  /* Neutral grey island — the red Delete button clashed against a blue bar. */
  background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 14px;
  box-shadow: 0 8px 28px rgba(15, 23, 42, 0.22);
  font-size: 13px; color: #334155;
}
/* On desktop, nudge the island to the centre of the CONTENT area (right of the
   fixed sidebar) rather than the whole viewport. */
@media (min-width: 900px) { .bulk-bar { left: calc(50% + 120px); } }
.bulk-bar .bulk-count { font-weight: 600; white-space: nowrap; color: #334155; }
.bulk-bar-actions { display: flex; align-items: center; gap: 8px; }
/* Export… and Delete Selected share one width, with centred (symmetric) content. */
.bulk-bar-actions .bulk-export-btn,
.bulk-bar-actions > .btn-danger { min-width: 156px; justify-content: center; }
/* Clear = a bare "✕", no button chrome or background. */
.bulk-clear-x {
  background: none; border: none; cursor: pointer; font-size: 16px; line-height: 1;
  color: #64748b; padding: 4px 8px; border-radius: 6px;
}
.bulk-clear-x:hover { color: #0f172a; }
/* Delete button swaps to "Delete Now" while Shift is held (a no-confirm delete). */
.del-label-now { display: none; }
body.fl-shift-held .bulk-delete-btn .del-label { display: none; }
body.fl-shift-held .bulk-delete-btn .del-label-now { display: inline; }

/* Selection-count pill on the Test Reports "Compare" / "Stability view" buttons,
   so it's clear those act on the checked rows. Hidden when nothing is selected. */
.sel-pill {
  display: none; margin-left: 6px; min-width: 18px; height: 17px; padding: 0 5px;
  border-radius: 9px; background: rgba(255, 255, 255, 0.28); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 17px; text-align: center;
}
.sel-pill:not(:empty) { display: inline-block; }
.btn-workspace.has-sel { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.65) inset; }

/* Phones: stack into two rows — [count …… ✕] on top, the action buttons below. */
@media (max-width: 560px) {
  .bulk-bar { flex-wrap: wrap; left: 50%; transform: translateX(-50%); width: calc(100vw - 24px); row-gap: 8px; }
  .bulk-bar .bulk-count { order: 1; flex: 1 1 auto; }
  .bulk-clear-x { order: 2; }
  /* Two equal columns (1fr 1fr) so Export… and Delete are exactly the same width
     regardless of label length. */
  .bulk-bar-actions { order: 3; flex: 1 1 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .bulk-bar-actions > * { min-width: 0; }
  .bulk-bar-actions .bulk-export-wrap { display: flex; }
  .bulk-bar-actions .bulk-export-btn { min-width: 0; width: 100%; justify-content: center; }
  .bulk-bar-actions > .btn-danger { min-width: 0; width: 100%; }
  /* Odd count of action buttons → the last one (Delete when present) spans the
     full row instead of leaving a lonely half-width gap. */
  .bulk-bar-actions > *:last-child:nth-child(odd) { grid-column: 1 / -1; }
  /* Drop "Selected" so Delete matches the Export… button width in the split row. */
  .bulk-del-word { display: none; }
  /* Keep the Export… popover inside the frame — anchor it to the button's LEFT
     edge (right:0 pushed it off the left of the island on a phone). */
  .bulk-export-menu { left: 0; right: auto; max-width: calc(100vw - 40px); }
  .bulk-export-menu::before { left: 21px; right: auto; }
  .bulk-export-menu::after  { left: 22px; right: auto; }
}

/* Row press feedback: a light outline on mousedown, upgraded to the full blue
   selection outline + fill on release. Single row at a time, so no run logic. */
tbody tr.row-pressed td { background: #f8fafc; box-shadow: inset 0 1px 0 #bfdbfe, inset 0 -1px 0 #bfdbfe; }
tbody tr.row-pressed td:first-child { border-left: 1px solid #bfdbfe; }
tbody tr.row-pressed td:last-child  { border-right: 1px solid #bfdbfe; }

/* "Export…" popover — opens ABOVE its button (the bar sits at the bottom) with a
   downward triangle pointing at it. */
.bulk-export-wrap { position: relative; display: inline-flex; }
.bulk-export-menu {
  display: none; position: absolute; bottom: calc(100% + 11px); right: 0;
  min-width: 198px; padding: 6px; z-index: 210;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.2);
}
.bulk-export-menu.open { display: block; }
.bulk-export-menu button {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  background: none; border: none; border-radius: 7px; padding: 8px 10px;
  font-size: 13px; color: var(--text); cursor: pointer; white-space: nowrap;
}
.bulk-export-menu button:hover { background: #f1f5f9; }
/* Normalise the leading icon size (the bare print-icon img would otherwise render
   at its natural size outside a .btn context). */
.bulk-export-menu button img,
.bulk-export-menu button .svg-icon,
.bulk-export-menu button svg { width: 15px; height: 15px; object-fit: contain; flex-shrink: 0; }
.bulk-export-menu::before {   /* triangle border edge */
  content: ''; position: absolute; top: calc(100% + 1px); right: 21px;
  border: 8px solid transparent; border-top-color: var(--border);
}
.bulk-export-menu::after {    /* triangle fill */
  content: ''; position: absolute; top: 100%; right: 22px;
  border: 7px solid transparent; border-top-color: var(--surface);
}

/* Downward variant of the same menu — used by "Render…" in the ingredient
   form, where the button sits at the top of a tall panel rather than in the
   bottom-pinned bulk bar. Only the drop direction and the triangle flip. */
.bulk-export-menu.fl-menu-down {
  bottom: auto; top: calc(100% + 11px); left: 0; right: auto; min-width: 216px;
}
.bulk-export-menu.fl-menu-down::before {
  top: auto; bottom: calc(100% + 1px); left: 21px; right: auto;
  border-top-color: transparent; border-bottom-color: var(--border);
}
.bulk-export-menu.fl-menu-down::after {
  top: auto; bottom: 100%; left: 22px; right: auto;
  border-top-color: transparent; border-bottom-color: var(--surface);
}
.bulk-export-menu.fl-menu-down.fl-menu-right { left: auto; right: 0; }
.bulk-export-menu.fl-menu-down.fl-menu-right::before { left: auto; right: 21px; }
.bulk-export-menu.fl-menu-down.fl-menu-right::after  { left: auto; right: 22px; }
.bulk-export-menu.fl-menu-down button { align-items: flex-start; }

/* Group heading + rule, for a menu whose entries are not all peers. */
.bulk-export-menu .fl-menu-head {
  padding: 5px 10px 3px; font-size: 10.5px; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .4px;
}
.bulk-export-menu .fl-menu-rule {
  height: 1px; background: var(--border); margin: 5px 8px;
}

/* Render… docked inside the preview box it fills. Kept in flow (not absolute)
   so a wide structure can never end up underneath it. */
.struct-preview-tools {
  display: flex; justify-content: flex-end;
  margin: 0 -6px 2px 0; position: relative; z-index: 1; flex: 0 0 auto;
}

/* The structure viewport. Height is FIXED so the form never reflows when the
   picture changes source — only what is inside the stage changes. */
.struct-preview {
  margin-top: 8px; padding: 12px; position: relative;
  background: #f8fafc; border: 1px solid var(--border); border-radius: 8px;
  height: 272px; display: flex; flex-direction: column; text-align: center;
}
.struct-preview-stage {
  flex: 1 1 auto; min-height: 0; display: flex;
  align-items: center; justify-content: center;
}
/* Both shapes the stage can hold — a raster <img> and the <div> an inline SVG
   render leaves behind — scale down to the stage instead of setting its size. */
.struct-preview-stage > #struct-img { max-width: 100%; max-height: 100%; }
.struct-preview-stage > .struct-svg { max-width: 100%; max-height: 100%; display: flex; }
.struct-preview-stage > .struct-svg svg { max-width: 100%; max-height: 100%; height: auto; }
.struct-preview > #struct-link { flex: 0 0 auto; margin-top: 6px; }
.bulk-export-menu.fl-menu-down .fl-menu-sub code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px; color: var(--text); background: rgba(15, 23, 42, 0.05);
  padding: 0 3px; border-radius: 3px;
}
.bulk-export-menu.fl-menu-down .fl-menu-sub {
  display: block; font-size: 11px; color: var(--text-muted); margin-top: 2px; white-space: normal;
}

/* Scroll runway so the table's last rows can sit above the floating island
   instead of hiding behind it — only while a selection is active. */
body.has-bulk-selection .list-card [id$="-table-region"] { padding-bottom: 88px; }

/* ============================================================
   PRINT / PDF — shared helpers
   ============================================================ */

/* .print-only — hidden on screen, revealed during print. Used for
   page-1 report headers on test detail (and any future detail view
   that wants its own print header). */
.print-only { display: none !important; }

/* Test report print-header layout. The header is hidden on screen
   and only flows into the printed PDF. Keep it simple and dense —
   one block at the top of page 1, with key identifying fields in a
   compact 3×2 grid. */
.test-print-header {
  border-bottom: 2px solid #0f172a;
  padding-bottom: 12px;
  margin-bottom: 16px;
}
.test-print-brand {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #64748b;
  margin-bottom: 4px;
}
.test-print-title {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 10px 0;
  color: #0f172a;
}
.test-print-meta {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px 24px;
  font-size: 11.5px;
  color: #1e293b;
}
.test-print-meta > div span {
  display: inline-block;
  min-width: 90px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #64748b;
  margin-right: 4px;
}

/* ============================================================
   PRINT / PDF — Dashboard + Test Detail
   ============================================================ */
@media print {
  /* Reveal print-only blocks (e.g. test report header) */
  .print-only { display: block !important; }
}
@media print {
  /* Force color printing */
  *, *::before, *::after {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }
  /* …but NOT the workspace background. `exact` above overrides the browser's
   * default of dropping background graphics, so a custom background image would
   * otherwise print behind every page and every exported PDF. Paper is the
   * ground when printing. */
  html, body {
    background-image: none !important;
    background-color: #fff !important;
  }

  /* Hide all chrome except the dashboard content */
  #sidebar,
  #topbar,
  .breadcrumb,
  #import-file { display: none !important; }

  /* Remove arrange/print controls from output */
  #dash-arrange-btn,
  .dash-drag-handle,
  .dash-close,
  .dash-resize-handle,
  .dash-add-tile,
  .dash-add-menu,
  .feedback-fab,
  .sort-panel,
  .btn { display: none !important; }

  /* Make layout take full page width */
  body, #app, #main { display: block !important; width: 100% !important; }
  #content { padding: 0 !important; overflow: visible !important; }
  #main { overflow: visible !important; }

  /* Print header — date + title */
  #dash-grid::before {
    content: "FormLab Dashboard  ·  " attr(data-print-date);
    display: block;
    font-size: 11px;
    color: #64748b;
    text-align: right;
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 1px solid #e2e8f0;
    grid-column: 1 / -1;
  }

  /* Keep grid structure, allow wrapping across pages */
  .dash-grid {
    display: grid !important;
    grid-template-columns: repeat(3, 1fr) !important;
    grid-auto-rows: auto !important;
    gap: 12px !important;
  }

  /* Avoid splitting a panel card across a page break */
  .dash-panel { break-inside: avoid; }

  /* Stat grid */
  .stat-grid {
    display: grid !important;
    grid-template-columns: repeat(7, 1fr) !important;
    gap: 8px !important;
    margin-bottom: 14px !important;
  }
  .stat-card { padding: 10px 12px !important; }
  .stat-card .value { font-size: 20px !important; }

  /* Tables */
  .table-wrap { overflow: visible !important; }
  table { page-break-inside: auto; }
  tr { page-break-inside: avoid; }

  /* Cards */
  .card { box-shadow: none !important; border: 1px solid #e2e8f0 !important; }
  .card-header { padding: 8px 12px !important; }
  /* Replicate statistics card is compact — keep it (and its heading with its
     first table) intact on one page rather than splitting across a break. */
  .replicate-card { break-inside: avoid; }
  .replicate-card .card-header { break-after: avoid; }

  /* ---- Test report PDF specifics ----
     Hide interactive bits that don't make sense in a static PDF:
     attachment chips + "+ Attach" buttons, the "View history →" link
     in the audit footer, the "Pivot to broader view" links in the
     performance card footer, and the breadcrumb (the print header at
     the top already names the report). */
  .breadcrumb { display: none !important; }
  /* Attachment cell in measurements table — keep the chips visible
     (they document evidence files) but drop the "+ Attach" button. */
  button[onclick^="addMeasAttachmentClick"],
  button[onclick^="addTestAttachmentClick"],
  .meas-att-add, .meas-att-del, .meas-att-zoom,
  input[type="file"] { display: none !important; }
  /* Test-attachment drop zone (whole drag-drop area) — useless in print */
  [id^="test-attach-drop-"] { display: none !important; }
  /* Notebook composer (the "Log a note" input + editor) — useless in print.
     The list of saved notes below still renders normally. */
  #nb-composer { display: none !important; }
  /* Audit footer right-aligned "View history →" link — pointless in
     a PDF (no navigation context). */
  a[onclick="navigate('audit')"] { display: none !important; }
  /* Performance card pivot links — same reason. */
  a[onclick^="_formulaOpenInStability"],
  a[onclick^="_formulaOpenInCompare"],
  a[onclick^="navigate('formulation-detail'"],
  a[onclick^="navigate('samples')"],
  a[onclick^="navigate('test-detail'"] { color: inherit !important; text-decoration: none !important; cursor: default !important; }

  /* Topbar back/forward arrows — already hidden via #topbar rule
     above, but listing here for documentation. */
}

/* ============================================================
 * Mobile (Tier 1) — viewport <= 768px
 * Sidebar becomes a slide-in drawer, modals go full-screen,
 * form grids stack, topbar actions scroll horizontally.
 * ============================================================ */

/* Backdrop + burger are off by default; the @media block below
 * makes the burger visible and lets the backdrop show when the
 * sidebar's .mobile-open class is on. */
#sidebar-backdrop { display: none; }
#mobile-menu-btn {
  display: none;
  background: none; border: none; cursor: pointer;
  color: var(--text); padding: 4px 8px;
  font-size: 22px; line-height: 1;
  border-radius: 6px;
}
#mobile-menu-btn:hover { background: #f1f5f9; }

@media (max-width: 768px) {
  /* Square and the same size as the nav triangles beside it — it lives inside
   * the lead frame now, and its own 4px/8px padding + 6px radius made it 35x30
   * against their 34x34, which reads as a squashed box in the row. */
  #mobile-menu-btn {
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    height: 100%; aspect-ratio: 1; width: auto;
    padding: 0; border-radius: 10px;
    font-size: 19px;
  }

  /* Sidebar becomes a fixed drawer that slides in from the left */
  #sidebar {
    position: fixed; top: 0; left: 0;
    width: 240px !important;
    height: 100vh;
    height: 100dvh;
    z-index: 120;
    transform: translateX(-100%);
    transition: transform 0.25s ease;
    box-shadow: 0 0 24px rgba(0, 0, 0, 0.3);
  }
  #sidebar.mobile-open { transform: translateX(0); }
  /* On mobile, ignore desktop "collapsed" mode — drawer is binary */
  #sidebar.collapsed { width: 240px !important; }
  #sidebar.collapsed .logo-text,
  #sidebar.collapsed .nav-section,
  #sidebar.collapsed .nav-item { font-size: inherit; opacity: 1; height: auto; }
  #sidebar.collapsed .nav-item { padding: 8px 14px; gap: 10px; }
  #sidebar.collapsed .nav-item .icon { font-size: 16px; margin-right: 6px; }
  #sidebar-resizer { display: none; }
  /* Hide the collapse toggle on mobile (drawer is binary) but keep
   * the account button visible at the bottom of the drawer. */
  #sidebar .sidebar-footer { justify-content: flex-start; }
  #sidebar .sidebar-footer .sidebar-toggle { display: none; }

  /* Backdrop — visible whenever the drawer is open */
  #sidebar-backdrop {
    display: block;
    position: fixed; inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 110;
    opacity: 0; pointer-events: none;
    transition: opacity 0.25s ease;
  }
  body.sidebar-open #sidebar-backdrop { opacity: 1; pointer-events: auto; }

  /* Main takes the full viewport — sidebar overlays it */
  #main { width: 100%; }
  #topbar { padding: 0 12px; gap: 8px; position: relative; }
  #topbar h1 { font-size: 15px; }

  /* Topbar actions: overflow buttons collapse into the ⋯ menu
   * (js/topbar-overflow.js) rather than horizontal-scrolling. min-width:0 lets
   * the bar shrink so the controller can detect the overflow. */
  #topbar .actions { margin-left: 8px; }
  #topbar .actions .btn { flex-shrink: 0; }

  /* Modal: full-screen */
  .modal-backdrop { padding: 0; }
  .modal, .modal.wide, .modal.xwide {
    width: 100%; max-width: none;
    height: 100vh; max-height: 100vh;
    border-radius: 0;
  }
  .modal-body { padding: 14px; }
  .modal-header { padding: 12px 14px; }
  /* Touch-friendly footer: stack the actions full-width with big tap targets,
     primary action at the bottom (thumb zone). Fixes every confirm dialog on a
     phone — including the cross-workspace scan "Switch & open / Cancel" prompt. */
  .modal-footer { padding: 12px 14px; flex-direction: column; align-items: stretch; gap: 10px; }
  .modal-footer .btn { width: 100%; justify-content: center; min-height: 46px; padding: 12px 16px; font-size: 15px; }

  /* Form grids collapse to single column */
  .form-grid, .form-grid.cols-3 { grid-template-columns: 1fr; }
  .form-group.span-2, .form-group.span-3 { grid-column: span 1; }

  /* Bigger tap targets for primary interactive elements */
  .btn { padding: 9px 14px; min-height: 36px; }
  .btn-sm { padding: 6px 10px; min-height: 32px; }
  .btn-icon { min-width: 32px; min-height: 32px; padding: 4px 8px; }
  input, select, textarea { padding: 10px 12px; font-size: 15px; }
  .nav-item { min-height: 40px; }

  /* Data view grid collapses */
  .data-grid { grid-template-columns: 1fr; }

  /* ---- Tier 2: read-optimized detail / dashboard / table layouts ---- */

  /* Detail panel grids (info / stats / etc.) → single column. Several
   * call sites set grid-template-columns inline (e.g., "repeat(3,1fr)")
   * so we use !important to win the cascade. */
  .detail-grid { grid-template-columns: 1fr !important; }
  .detail-field[style*="grid-column"] { grid-column: span 1 !important; }

  /* Tabs scroll horizontally instead of wrapping */
  .tabs {
    overflow-x: auto; overflow-y: hidden;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
  }
  /* Same as the detail tabs: grabbable track, slim-looking thumb. */
  .tabs::-webkit-scrollbar { height: 10px; }
  .tabs::-webkit-scrollbar-track { background: transparent; }
  .tabs::-webkit-scrollbar-thumb {
    background: rgba(100, 116, 139, .35);
    background-clip: content-box;
    border: 3px solid transparent;
    border-radius: 999px;
  }
  .tab { flex-shrink: 0; white-space: nowrap; padding: 8px 12px; font-size: 13px; }

  /* Dashboard draggable grid → stack panels vertically */
  .dash-grid {
    grid-template-columns: 1fr !important;
    grid-auto-rows: auto !important;
    gap: 12px;
  }
  .dash-panel {
    grid-column: span 1 !important;
    grid-row: auto !important;
  }
  .dash-panel > .card { height: auto; min-height: 180px; overflow: visible; }
  /* Hide arrange/resize affordances on mobile — desktop-only workflow */
  .dash-resize-handle, .dash-close { display: none !important; }

  /* Stat grid: 2 columns instead of auto-fill minmax-180 (which leaves
   * too much padding on narrow viewports) */
  .stat-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .stat-card { padding: 14px; }
  .stat-card .value { font-size: 22px; }

  /* Breadcrumb tighter */
  .breadcrumb { font-size: 12px; margin-bottom: 12px; }

  /* Right-edge swipe hint for horizontally-scrollable tables. The shadow must be
   * anchored to the SCROLLPORT, not the content — so for list tables we move the
   * horizontal scroll onto the inner .table-wrap and pin the shadow to the
   * non-scrolling region via ::after. (An ::after on the scroller itself rides
   * off-screen with the content.) The sticky header + frozen first column keep
   * working since they only need a scroll ancestor, which .table-wrap now is. */
  #content .list-card [id$="-table-region"] { position: relative; overflow-x: hidden; }
  .list-card [id$="-table-region"] .table-wrap { overflow-x: auto !important; -webkit-overflow-scrolling: touch; }
  .list-card [id$="-table-region"]::after {
    content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 12px;
    background: linear-gradient(to left, rgba(0,0,0,0.10), transparent);
    pointer-events: none; z-index: 3;
  }
}

/* ===================================================================
   Print / Save-as-PDF — Test Compare workspace
   -------------------------------------------------------------------
   Triggered by the "⎙ Print / PDF" export option (which calls
   window.print()). When printing, hide everything that isn't part
   of the report — sidebar, topbar, controls bar, Smart Picks
   strip, picker rail, resize handle, viz controls — and let the
   report content flow naturally as a printable document:
     - Print header (config summary)
     - Selected items strip
     - Insights panel
     - Comparison table
     - Visualization (canvas snapshot of current plot)
   =================================================================== */
@media print {
  /* Page setup — leave room for browser-added headers/footers */
  @page { margin: 12mm; }

  /* Reset body / app shell to a flat printable canvas */
  html, body, #app, #main, #content {
    height: auto !important;
    overflow: visible !important;
    background: #fff !important;
    color: #000 !important;
  }

  /* Hide everything that isn't the comparison report */
  #sidebar,
  #topbar,
  #feedback-fab,
  #sidebar-backdrop,
  #mobile-menu-btn,
  .test-compare-resizer,
  #test-compare-picker-card,
  [data-tc-controls],
  [data-tc-suggestions],
  [data-tc-viz-chrome],
  .modal-backdrop { display: none !important; }

  /* Main column stretches edge-to-edge for print */
  #main { width: 100% !important; max-width: 100% !important; }
  #content { padding: 0 !important; }

  /* Workspace: collapse to a vertical document flow */
  #test-compare-workspace {
    display: block !important;
    min-height: 0 !important;
    height: auto !important;
    gap: 0 !important;
  }
  #test-compare-workspace > div { display: block !important; }

  /* The table-card row was a flex row in screen; flatten it for print */
  #test-compare-workspace > div[style*="display:flex"] {
    display: block !important;
    max-height: none !important;
  }
  #test-compare-workspace > div > div {
    max-height: none !important;
    overflow: visible !important;
    border-top: none !important;
    margin-bottom: 12px;
  }

  /* The comparison table needs to print as an actual table (no scroll) */
  #test-compare-table {
    overflow: visible !important;
    max-height: none !important;
  }
  #test-compare-table table {
    border-collapse: collapse !important;
    page-break-inside: auto;
  }
  #test-compare-table thead { display: table-header-group; }
  #test-compare-table tr { page-break-inside: avoid; }

  /* Viz panel — keep canvases visible, drop the chrome */
  #test-compare-viz-wrap {
    page-break-inside: avoid;
    border-top: 1px solid #ccc !important;
    margin-top: 16px !important;
    padding-top: 12px !important;
  }

  /* Print header — visible only in print, shows config summary */
  #test-compare-print-header { display: block !important; }
  #test-compare-print-header h1 {
    font-size: 18pt !important;
    margin-bottom: 6pt !important;
  }
  #test-compare-print-header .summary {
    font-size: 10pt !important;
    color: #444 !important;
    line-height: 1.5;
  }

  /* Color overrides — keep semantic chip backgrounds but force readable text */
  .test-compare-resizer, [class*="resizer"] { display: none !important; }

  /* ---- Stability workspace mirrors the same print scope ---- */
  #stab-picker-card,
  [data-stab-controls] { display: none !important; }
  #stability-workspace {
    display: block !important;
    min-height: 0 !important;
    height: auto !important;
    gap: 0 !important;
  }
  #stability-workspace > div { display: block !important; }
  #stability-workspace > div[style*="display:flex"] {
    display: block !important;
    max-height: none !important;
  }
  #stability-workspace > div > div {
    max-height: none !important;
    overflow: visible !important;
    border-top: none !important;
    margin-bottom: 12px;
  }
  #stab-chart-mount { overflow: visible !important; max-height: none !important; min-height: 400px !important; }
  #stab-print-header { display: block !important; }
  #stab-print-header h1 { font-size: 18pt !important; margin-bottom: 6pt !important; }
}

/* Compare workspace topbar dropdown buttons (Saved + Export).
   Locks both to the same width so the Compare topbar reads as a
   consistent row, regardless of the live (N) count in "Saved (N)".
   Maximum specificity + all width constraints — defeats stale-
   cache rendering, flex-shrink, AND padding-eats-width. */
#topbar #topbar-actions .btn.tc-topbar-pill {
  display: inline-flex !important;
  width: 150px !important;
  min-width: 150px !important;
  max-width: 150px !important;
  flex-shrink: 0 !important;
  justify-content: center !important;
  box-sizing: border-box !important;
  padding-left: 10px !important;
  padding-right: 10px !important;
}

/* ===================================================================
   Test Compare — clickable table headers (viz scope picker)
   -------------------------------------------------------------------
   The cells with onclick="_tcVizToggleRowFilter / _tcVizToggleColFilter"
   need a stronger hover signal than just cursor:pointer so the
   "click to scope the chart below" affordance is obvious. Subtle
   blue tint on hover; bolder when already in scope.
   =================================================================== */
#test-compare-table th[onclick],
#test-compare-table td[onclick] {
  transition: background-color 0.12s, box-shadow 0.12s;
}
#test-compare-table th[onclick]:hover,
#test-compare-table td[onclick]:hover {
  background: #eff6ff !important;
  box-shadow: inset 0 -2px 0 #2563eb;
}
body.testing-dark[data-view="test-compare"] #test-compare-table th[onclick]:hover,
body.testing-dark[data-view="test-compare"] #test-compare-table td[onclick]:hover {
  background: #1e3a8a !important;
  box-shadow: inset 0 -2px 0 #60a5fa;
}

/* ===================================================================
   Test Compare — on-plot annotations (DOE-Matrix-style overlay)
   -------------------------------------------------------------------
   Draggable rich-text notes positioned directly on top of the chart.
   The layer fills #tc-viz-canvas-wrap, sits above the chart with
   pointer-events:none so chart hover keeps working; per-note
   pointer-events:auto re-enables interaction on the notes themselves.
   Scoped tightly to .tc-viz-annot* so the page-level dark-mode
   broad selectors can't bleed into the editor / notes.
   =================================================================== */
.tc-viz-annot-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  /* Don't expand the layout — purely overlay */
  overflow: visible;
}
.tc-viz-annot {
  position: absolute;
  pointer-events: auto;
  transform: translate(-50%, -50%);
  max-width: 260px;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 11.5px;
  line-height: 1.45;
  color: #0f172a;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
  cursor: move;
  user-select: none;
  transition: box-shadow 0.12s, opacity 0.12s;
  white-space: normal;
}
.tc-viz-annot.tc-viz-annot-transparent {
  background: transparent;
  border: 1px dashed rgba(15, 23, 42, 0.4);
  box-shadow: none;
}
.tc-viz-annot:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18); }
.tc-viz-annot.tc-viz-annot-dragging {
  opacity: 0.85;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
  cursor: grabbing;
}
.tc-viz-annot .tc-viz-annot-content > * { margin: 0; }
.tc-viz-annot .tc-viz-annot-content p { margin: 0 0 4px; }
.tc-viz-annot .tc-viz-annot-content p:last-child { margin-bottom: 0; }
.tc-viz-annot .tc-viz-annot-content ul,
.tc-viz-annot .tc-viz-annot-content ol { margin: 4px 0 0 16px; padding: 0; }
.tc-viz-annot .tc-viz-annot-del {
  position: absolute;
  top: -8px; right: -8px;
  width: 18px; height: 18px;
  border-radius: 9999px;
  background: #fff;
  border: 1px solid #cbd5e1;
  color: #dc2626;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}
.tc-viz-annot:hover .tc-viz-annot-del { display: inline-flex; }

/* Dark mode — readable note bg + light text, scoped so the page's
   broad #0f172a → light rule doesn't rewrite our annotation text
   incorrectly. */
body.testing-dark[data-view="test-compare"] .tc-viz-annot {
  background: rgba(31, 41, 55, 0.96);
  border-color: #475569;
  color: #f1f5f9;
}
body.testing-dark[data-view="test-compare"] .tc-viz-annot.tc-viz-annot-transparent {
  background: transparent;
  border-color: rgba(241, 245, 249, 0.5);
}
body.testing-dark[data-view="test-compare"] .tc-viz-annot .tc-viz-annot-del {
  background: #1f2937;
  border-color: #475569;
  color: #fca5a5;
}

/* Print: annotations stay visible on the printed report, drop the
   delete button (not useful on paper). */
@media print {
  .tc-viz-annot .tc-viz-annot-del { display: none !important; }
  .tc-viz-annot { box-shadow: none !important; border-color: #999 !important; color: #000 !important; }
}

/* The annotation EDITOR modal: scope Quill rules tightly so we don't
   bleed into the notebook editor or the notes-panel editor. */
.tc-viz-annot-editor-host .ql-toolbar.ql-snow {
  border-radius: 6px 6px 0 0;
  background: #f8fafc;
}
.tc-viz-annot-editor-host .ql-container.ql-snow {
  border-radius: 0 0 6px 6px;
  font-family: inherit;
}
.tc-viz-annot-editor-host .ql-editor {
  min-height: 120px;
  color: #0f172a;
}
body.testing-dark[data-view="test-compare"] .tc-viz-annot-editor-host .ql-toolbar.ql-snow {
  background: #111827 !important;
  border-color: #374151 !important;
}
body.testing-dark[data-view="test-compare"] .tc-viz-annot-editor-host .ql-container.ql-snow {
  background: #1f2937 !important;
  border-color: #374151 !important;
}
body.testing-dark[data-view="test-compare"] .tc-viz-annot-editor-host .ql-editor {
  color: #f1f5f9 !important;
  background: #1f2937 !important;
}

/* ===================================================================
   Test Compare — picker resize handle
   -------------------------------------------------------------------
   Vertical strip between the picker rail and the comparison table.
   Generous hit area (16 px wide) with an always-visible grip — the
   picker's right-edge content (PASS / FAIL chips) sits very close,
   so the grip needs to read clearly without forcing the user to
   hunt for it. The grip uses the active mode's accent color via the
   --tc-accent custom property the JS sets at workspace render time,
   tying it visually to the mode pills + card top accents.
   =================================================================== */
.test-compare-resizer {
  flex: 0 0 16px;
  margin: 0 2px;
  align-self: stretch;
  cursor: col-resize;
  background: transparent;
  position: relative;
  user-select: none;
}
.test-compare-resizer::before {
  /* Always-visible grip — a ⋮⋮ pair of dashes rendered with two
     adjacent gradients so we don't need an extra DOM node. */
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 4px; height: 60px;
  background: var(--tc-accent, #94a3b8);
  border-radius: 2px;
  opacity: 0.55;
  transition: opacity 0.15s, height 0.15s, width 0.15s;
}
.test-compare-resizer::after {
  /* Tiny "drag" arrow indicators above + below the bar so the
     vertical line reads as "drag horizontally," not "border." */
  content: '⇔';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  font-size: 10px;
  color: var(--tc-accent, #64748b);
  opacity: 0;
  pointer-events: none;
  background: var(--surface, #fff);
  padding: 1px 3px;
  border-radius: 4px;
  transition: opacity 0.15s;
}
.test-compare-resizer:hover::before {
  opacity: 1;
  width: 5px;
  height: 80px;
}
.test-compare-resizer:hover::after {
  opacity: 1;
}
body.testing-dark[data-view="test-compare"] .test-compare-resizer::after {
  background: #1f2937;
  color: var(--tc-accent, #94a3b8);
}

/* ===================================================================
   Dark mode for the Test Results workflow
   -------------------------------------------------------------------
   Triggered by `body.testing-dark`. Scoped to the test-related views
   via `[data-view="..."]` so it never bleeds onto Dashboard /
   Ingredients / etc. even though the body class persists globally.
   Covers: Test Results list, Test Detail, Test Compare workspace,
   and any modal opened while on those views (Stability view).

   Strategy: override CSS custom properties so anything using
   var(--surface) / var(--text) / var(--border) flips automatically.
   Then a targeted set of !important rules covers the inline-styled
   light backgrounds (#fff, #f8fafc) that are common in compare /
   stability templates.
   =================================================================== */
body.testing-dark[data-view="testing"],
body.testing-dark[data-view="test-detail"],
body.testing-dark[data-view="test-compare"],
body.testing-dark[data-view="stability"] {
  --bg: #0f172a;
  --surface: #1f2937;
  --bg-soft: #273244;
  --border: #374151;
  --text: #f1f5f9;
  --text-muted: #94a3b8;
  /* surface-layer overrides — matches the suggested dark values in :root */
  --surface-subtle:  #1e293b;
  --surface-raised:  #334155;
  --surface-blue:    #1e3a5f;
  --surface-amber:   #2d1f00;
  --surface-danger:  #3b0f0f;
  --surface-success: #052e16;
  --surface-sub:     #1a2234;
  --text-faint:      #64748b;
}
body.testing-dark[data-view="testing"] #content,
body.testing-dark[data-view="test-detail"] #content,
body.testing-dark[data-view="test-compare"] #content {
  background: #0f172a;
}
body.testing-dark[data-view="testing"] #topbar,
body.testing-dark[data-view="test-detail"] #topbar,
body.testing-dark[data-view="test-compare"] #topbar {
  background: #0f172a;
  border-bottom-color: #374151;
}
body.testing-dark[data-view="testing"] #page-title,
body.testing-dark[data-view="test-detail"] #page-title,
body.testing-dark[data-view="test-compare"] #page-title { color: #f1f5f9; }

/* Card frames + tables (page-level) */
body.testing-dark[data-view="testing"] .card,
body.testing-dark[data-view="test-detail"] .card,
body.testing-dark[data-view="test-compare"] .card {
  background: #1f2937 !important;
  border-color: #374151 !important;
  color: #f1f5f9;
}
body.testing-dark[data-view="testing"] table,
body.testing-dark[data-view="test-detail"] table,
body.testing-dark[data-view="test-compare"] table {
  color: #f1f5f9;
}
body.testing-dark[data-view="testing"] thead th,
body.testing-dark[data-view="test-detail"] thead th,
body.testing-dark[data-view="test-compare"] thead th {
  background: #111827 !important;
  color: #e5e7eb !important;
  border-bottom-color: #374151 !important;
}
body.testing-dark[data-view="testing"] tbody td,
body.testing-dark[data-view="test-detail"] tbody td,
body.testing-dark[data-view="test-compare"] tbody td {
  border-bottom-color: #374151 !important;
}

/* Inline-styled #fff backgrounds inside the Test Compare workspace */
body.testing-dark[data-view="test-compare"] #test-compare-workspace > div[style*="background:#f8fafc"],
body.testing-dark[data-view="test-compare"] #test-compare-workspace [style*="background:#fff"] {
  background: #1f2937 !important;
  color: #f1f5f9;
  border-color: #374151 !important;
}
body.testing-dark[data-view="test-compare"] #test-compare-workspace td[style*="background:#fff"],
body.testing-dark[data-view="test-compare"] #test-compare-workspace th[style*="background:#f8fafc"] {
  background: #111827 !important;
  color: #e5e7eb !important;
}
/* Yellow diff highlight gets a darker amber on dark bg so it stays
   visible without burning out the eyes. */
body.testing-dark[data-view="test-compare"] [style*="background:#fffbeb"] {
  background: #3f2d0a !important;
  color: #fde68a !important;
}
/* Pass / fail / untested chips and tiles look fine on dark — leave
   the colored swatches alone but lighten the text where it was dark
   navy by default. */
body.testing-dark[data-view="testing"] .card *,
body.testing-dark[data-view="test-detail"] .card *,
body.testing-dark[data-view="test-compare"] #test-compare-workspace * {
  color: inherit;
}
/* But: re-allow muted hint text to stay grey */
body.testing-dark[data-view="testing"] .card [style*="color:#94a3b8"],
body.testing-dark[data-view="test-compare"] #test-compare-workspace [style*="color:#94a3b8"],
body.testing-dark[data-view="testing"] .card [style*="color:#64748b"],
body.testing-dark[data-view="test-compare"] #test-compare-workspace [style*="color:#64748b"] {
  color: #94a3b8 !important;
}
/* Headings and labels that were dark navy → light slate */
body.testing-dark[data-view="testing"] .card [style*="color:#0f172a"],
body.testing-dark[data-view="test-detail"] .card [style*="color:#0f172a"],
body.testing-dark[data-view="test-compare"] #test-compare-workspace [style*="color:#0f172a"] {
  color: #f1f5f9 !important;
}
/* Mid-grey text (used by inactive mode pills, etc.) → light slate */
body.testing-dark[data-view="test-compare"] #test-compare-workspace [style*="color:#334155"] {
  color: #e5e7eb !important;
}

/* Standard buttons inside dark testing views.
   ----------------------------------------------------------------
   .btn-secondary is hardcoded `background:#fff; color: var(--text)`.
   The page-level --text override flips text to light slate, which
   would leave a white pill with light text on it (invisible). Flip
   the button background + border to dark surface so the same
   --text color reads correctly. We DON'T touch buttons that already
   have an inline background (selected period chips, mode pills,
   layout pills, dark-mode toggle when on) — inline style wins via
   specificity, so their per-state styling is preserved. */
body.testing-dark[data-view="testing"] .btn-secondary,
body.testing-dark[data-view="test-detail"] .btn-secondary,
body.testing-dark[data-view="testing"] .btn-secondary,
body.testing-dark[data-view="test-detail"] .btn-secondary,
body.testing-dark[data-view="test-compare"] .btn-secondary,
body.testing-dark[data-view="stability"] .btn-secondary {
  background: #1f2937;
  color: #e5e7eb;
  border-color: #374151;
}
body.testing-dark[data-view="testing"] .btn-secondary:hover,
body.testing-dark[data-view="test-detail"] .btn-secondary:hover,
body.testing-dark[data-view="test-compare"] .btn-secondary:hover,
body.testing-dark[data-view="stability"] .btn-secondary:hover {
  background: #374151;
  color: #f8fafc;
}
/* Danger / success / primary keep their semantic colors but get
   text-color overrides so the label reads on the colored fill in
   either mode (these already work in light mode; this is just a
   safety net). */
body.testing-dark[data-view="testing"] .btn-danger,
body.testing-dark[data-view="test-detail"] .btn-danger,
body.testing-dark[data-view="test-compare"] .btn-danger {
  background: #7f1d1d;
  color: #fecaca;
  border-color: #b91c1c;
}
body.testing-dark[data-view="testing"] .btn-primary,
body.testing-dark[data-view="test-detail"] .btn-primary,
body.testing-dark[data-view="test-compare"] .btn-primary {
  color: #fff;
}
/* Disabled state — keep the dimmed look but on the dark palette */
body.testing-dark[data-view="testing"] .btn:disabled,
body.testing-dark[data-view="test-detail"] .btn:disabled,
body.testing-dark[data-view="test-compare"] .btn:disabled {
  background: #1f2937;
  color: #64748b;
  border-color: #374151;
  opacity: 0.6;
}
/* Inputs / selects in dark testing views — same problem: they
   default to `background:#fff; color: var(--text)` and the
   --text override breaks them. */
body.testing-dark[data-view="testing"] input[type="text"],
body.testing-dark[data-view="testing"] input[type="search"],
body.testing-dark[data-view="testing"] input[type="number"],
body.testing-dark[data-view="testing"] input[type="email"],
body.testing-dark[data-view="testing"] input[type="date"],
body.testing-dark[data-view="testing"] input[type="datetime-local"],
body.testing-dark[data-view="testing"] textarea,
body.testing-dark[data-view="testing"] select,
body.testing-dark[data-view="test-detail"] input[type="text"],
body.testing-dark[data-view="test-detail"] input[type="search"],
body.testing-dark[data-view="test-detail"] input[type="number"],
body.testing-dark[data-view="test-detail"] input[type="email"],
body.testing-dark[data-view="test-detail"] input[type="date"],
body.testing-dark[data-view="test-detail"] input[type="datetime-local"],
body.testing-dark[data-view="test-detail"] textarea,
body.testing-dark[data-view="test-detail"] select,
body.testing-dark[data-view="test-compare"] input[type="text"],
body.testing-dark[data-view="test-compare"] input[type="search"],
body.testing-dark[data-view="test-compare"] input[type="number"],
body.testing-dark[data-view="test-compare"] input[type="email"],
body.testing-dark[data-view="test-compare"] input[type="date"],
body.testing-dark[data-view="test-compare"] input[type="datetime-local"],
body.testing-dark[data-view="test-compare"] textarea,
body.testing-dark[data-view="test-compare"] select,
body.testing-dark[data-view="stability"] input[type="text"],
body.testing-dark[data-view="stability"] input[type="search"],
body.testing-dark[data-view="stability"] input[type="number"],
body.testing-dark[data-view="stability"] input[type="date"],
body.testing-dark[data-view="stability"] input[type="datetime-local"],
body.testing-dark[data-view="stability"] textarea,
body.testing-dark[data-view="stability"] select {
  background: #1f2937 !important;
  color: #e5e7eb !important;
  border-color: #374151 !important;
}
body.testing-dark[data-view="testing"] input::placeholder,
body.testing-dark[data-view="test-detail"] input::placeholder,
body.testing-dark[data-view="test-compare"] input::placeholder,
body.testing-dark[data-view="stability"] input::placeholder,
body.testing-dark[data-view="testing"] textarea::placeholder,
body.testing-dark[data-view="test-detail"] textarea::placeholder,
body.testing-dark[data-view="test-compare"] textarea::placeholder,
body.testing-dark[data-view="stability"] textarea::placeholder {
  color: #94a3b8;
}
body.testing-dark[data-view="testing"] select option,
body.testing-dark[data-view="test-detail"] select option,
body.testing-dark[data-view="test-compare"] select option,
body.testing-dark[data-view="stability"] select option {
  background: #1f2937;
  color: #e5e7eb;
}

/* ---- Stability workspace dark-mode focused overrides ----
   The page-level --bg / --surface / --text overrides at the top of
   the dark block already apply (we added [data-view="stability"]
   to the master selector list). These rules catch the inline-styled
   light backgrounds + text that show up in the stability picker /
   chart card / insights / chip strip. */
body.testing-dark[data-view="stability"] #stability-workspace [style*="background:#fff"] {
  background: #1f2937 !important;
  border-color: #374151 !important;
}
body.testing-dark[data-view="stability"] #stability-workspace [style*="background:#f8fafc"] {
  background: #111827 !important;
  color: #e5e7eb !important;
}
body.testing-dark[data-view="stability"] #stability-workspace [style*="color:#0f172a"] {
  color: #f1f5f9 !important;
}
body.testing-dark[data-view="stability"] #stability-workspace [style*="color:#334155"],
body.testing-dark[data-view="stability"] #stability-workspace [style*="color:#475569"] {
  color: #cbd5e1 !important;
}
body.testing-dark[data-view="stability"] #stab-picker-list label[style*="background:#eff6ff"] {
  background: #1e3a8a !important;
}
/* Picker list row borders */
body.testing-dark[data-view="test-compare"] #test-compare-list label {
  border-bottom-color: #374151 !important;
}
body.testing-dark[data-view="test-compare"] #test-compare-list label[style*="background:#eff6ff"] {
  background: #1e3a8a !important;
}

/* Stability modal — the modal frame itself when opened from testing */
body.testing-dark[data-view="testing"] .modal,
body.testing-dark[data-view="test-detail"] .modal,
body.testing-dark[data-view="test-compare"] .modal {
  background: #1f2937;
  color: #f1f5f9;
}
body.testing-dark[data-view="testing"] .modal-header,
body.testing-dark[data-view="testing"] .modal-footer,
body.testing-dark[data-view="test-detail"] .modal-header,
body.testing-dark[data-view="test-detail"] .modal-footer,
body.testing-dark[data-view="test-compare"] .modal-header,
body.testing-dark[data-view="test-compare"] .modal-footer {
  border-color: #374151;
}
body.testing-dark[data-view="testing"] .modal [style*="background:#fff"],
body.testing-dark[data-view="testing"] .modal [style*="background:#f8fafc"] {
  background: #111827 !important;
  border-color: #374151 !important;
}
/* The Stability chart's <canvas> sits inside a white box — flip it. */
body.testing-dark[data-view="testing"] #stab-chart {
  background: transparent !important;
}

/* ===================================================================
   FIND BY RESULTS — per-criterion distribution facets
   Histogram + dual-handle range slider + stat strip + suggest chips.
   Light-theme only (find-results isn't covered by testing-dark).
   =================================================================== */
.fr-crit { padding: 11px 12px; border: 1px solid var(--border); border-radius: 9px; margin-bottom: 10px; background: #f8fafc; }
.fr-crit-controls { display: flex; gap: 6px; align-items: center; }

/* FormLab dropdown style — a softer, house-styled native <select>: lighter
 * surface fill (so it reads as a control on white AND on tinted cards), the
 * same border/radius as inputs, a custom chevron, hover/focus affordances.
 * Reusable anywhere a plain native select looks too stark. Layout (width,
 * flex) stays on the element's inline style; visuals come from here. */
.fl-select {
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  padding: 5px 26px 5px 9px;                 /* right pad reserves caret room */
  border: 1px solid var(--border); border-radius: 6px;
  font-size: 13px; font-family: inherit; color: var(--text);
  background-color: var(--surface-subtle);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center; background-size: 9px 6px;
  cursor: pointer; transition: background-color .12s, border-color .12s, box-shadow .12s;
}
.fl-select:hover { background-color: var(--surface-raised); border-color: var(--text-muted); }
.fl-select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--surface-blue); }
.fl-select:disabled { opacity: .6; cursor: not-allowed; }
.fl-select option:disabled { color: #94a3b8; }
.fr-facet { margin-top: 11px; }
.fr-hist { width: 100%; height: 40px; display: block; overflow: visible; }
.fr-axis { display: flex; justify-content: space-between; font-size: 10px; color: #94a3b8; line-height: 1; margin-top: 2px; font-variant-numeric: tabular-nums; }
.fr-axis span:nth-child(2) { color: #cbd5e1; }
.fr-bar { transition: fill 0.1s; }
/* Backdrop layer = full (scoped) population — light outline that stays put. */
.fr-bar-bg { fill: #e2e8f0; }
.fr-bar-bg.in { fill: #bfdbfe; }
/* Overlay layer = subset that also passes the OTHER criteria — saturated. */
.fr-bar-fg { fill: #94a3b8; }
.fr-bar-fg.in { fill: var(--primary); }
.fr-slider { position: relative; height: 22px; margin-top: 1px; }
.fr-slider-track { position: absolute; left: 0; right: 0; top: 50%; height: 4px; transform: translateY(-50%); background: #e2e8f0; border-radius: 2px; }
.fr-slider-fill { position: absolute; top: 50%; height: 4px; transform: translateY(-50%); background: var(--primary); opacity: 0.55; border-radius: 2px; }
.fr-slider input[type=range] { -webkit-appearance: none; appearance: none; position: absolute; left: 0; top: 0; width: 100%; height: 22px; margin: 0; background: transparent; pointer-events: none; }
.fr-slider input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; pointer-events: auto; width: 14px; height: 14px; border-radius: 50%; background: #fff; border: 2px solid var(--primary); cursor: grab; box-shadow: 0 1px 2px rgba(0,0,0,0.35); }
.fr-slider input[type=range]::-webkit-slider-thumb:active { cursor: grabbing; }
.fr-slider input[type=range]::-moz-range-thumb { pointer-events: auto; width: 14px; height: 14px; border-radius: 50%; background: #fff; border: 2px solid var(--primary); cursor: grab; box-shadow: 0 1px 2px rgba(0,0,0,0.35); }
.fr-slider input[type=range]::-webkit-slider-runnable-track { background: transparent; border: none; }
.fr-slider input[type=range]::-moz-range-track { background: transparent; border: none; }
.fr-stat { font-size: 11.5px; color: var(--text-muted); margin-top: 7px; font-variant-numeric: tabular-nums; line-height: 1.5; }
.fr-stat strong { color: var(--text); font-weight: 600; }
.fr-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.fr-chip { font-size: 11px; padding: 3px 9px; border: 1px solid var(--border); border-radius: 999px; background: #fff; cursor: pointer; color: var(--text); transition: all 0.12s; white-space: nowrap; }
.fr-chip:hover { border-color: var(--primary); color: var(--primary); }
.fr-chip.spec { border-color: #16a34a; color: #16a34a; }
.fr-chip.spec:hover { background: #16a34a; color: #fff; border-color: #16a34a; }


/* ============================================================
   TEST ANALYTICS WORKSPACE — multi-select + matrix polish
   ============================================================ */

/* Common label style for filter-bar fields (re-used across multi-
   select buttons + date inputs). */
.ta-multi-label {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.ta-date-input {
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12.5px;
  height: 32px;
  background: #fff;
}
.ta-date-input:focus { outline: 2px solid var(--primary); outline-offset: -1px; }

/* Multi-select trigger button. Sized to match other filter inputs;
   shows the current selection summary + a caret. */
.ta-multi-btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #fff;
  font-size: 12.5px;
  font-family: inherit;
  color: var(--text);
  cursor: pointer;
  min-width: 150px;
  max-width: 200px;
  height: 32px;
  text-align: left;
  transition: border-color 0.12s, background 0.12s;
}
.ta-multi-btn:hover { border-color: var(--primary); }
.ta-multi-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -1px; }
.ta-multi-summary {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-muted);
  font-weight: 500;
}
.ta-multi-summary.has-sel { color: var(--primary); font-weight: 600; }
.ta-multi-caret {
  font-size: 10px;
  color: var(--text-muted);
  flex-shrink: 0;
  transition: transform 0.15s;
}
.ta-multi-panel.open ~ * .ta-multi-caret,
.ta-multi[data-multi-id] .ta-multi-panel.open + * .ta-multi-caret { transform: rotate(180deg); }

/* Drop-down panel. Hidden by default; .open flips display. Anchored
   absolutely under the button so it overlays whatever sits below
   without pushing layout around. */
.ta-multi-panel {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 4px;
  min-width: 220px;
  max-width: 280px;
  max-height: 320px;
  overflow: hidden;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
  z-index: 50;
  flex-direction: column;
}
.ta-multi-panel.open { display: flex; }
.ta-multi-actions {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  background: #f8fafc;
}
.ta-multi-action {
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: #fff;
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
  transition: border-color 0.12s, color 0.12s, background 0.12s;
}
.ta-multi-action:hover { border-color: var(--primary); color: var(--primary); }
.ta-multi-count {
  margin-left: auto;
  font-size: 10.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.ta-multi-list {
  overflow-y: auto;
  padding: 4px 0;
  max-height: 260px;
}
.ta-multi-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  font-size: 12.5px;
  color: var(--text);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ta-multi-item:hover { background: #f1f5f9; }
.ta-multi-item input[type="checkbox"] {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  margin: 0;
  accent-color: var(--primary);
  cursor: pointer;
}
.ta-multi-item span {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Coverage Matrix table polish ----
   Old version had border:1px solid #fff between cells which read as
   white grid lines fighting the cell backgrounds. Replaced with a
   single subtle outer border + bottom dividers; cell backgrounds
   provide the visual separation. Sticky first column gets a right-
   shadow so it stays anchored when the user scrolls horizontally
   through many parameters. */
.ta-cov-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: 12px;
}
.ta-cov-table th {
  position: sticky;
  top: 0;
  background: #f8fafc;
  padding: 9px 12px;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-align: left;
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
  z-index: 2;
}
.ta-cov-table th.ta-cov-rowhead {
  left: 0;
  z-index: 3;
  box-shadow: 1px 0 0 var(--border);
}
.ta-cov-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
  transition: filter 0.08s;
}
.ta-cov-table td.ta-cov-rowhead {
  position: sticky;
  left: 0;
  background: #fff;
  font-weight: 500;
  color: var(--text);
  padding: 8px 14px;
  border-right: 1px solid var(--border);
  box-shadow: 1px 0 0 var(--border);
  z-index: 1;
}
.ta-cov-table tbody tr:hover td.ta-cov-rowhead { background: #f8fafc; }
.ta-cov-table tbody tr:hover td:not(.ta-cov-rowhead) { filter: brightness(0.96); }
.ta-cov-cell.ta-cov-empty { color: #cbd5e1; text-align: center; font-weight: 400; padding: 8px; }
.ta-cov-cell-clickable { cursor: pointer; }
.ta-cov-cell-clickable:hover { filter: brightness(0.92); }
.ta-cov-cell-sym {
  font-size: 10px;
  margin-right: 4px;
  opacity: 0.65;
}
/* Aggregation markers: this cell stands in for >1 value. ×n = replicate
   readings collapsed to a mean; final/D50 = a reduced series/distribution.
   Muted + superscript so they read as a footnote, not part of the number.
   The full breakdown lives in the cell's title tooltip. */
.ta-cov-rep, .ta-cov-red {
  font-size: 8.5px;
  font-weight: 600;
  opacity: 0.6;
  margin-left: 1px;
  vertical-align: super;
  line-height: 0;
}
.ta-cov-red { font-weight: 500; letter-spacing: .02em; }
/* Markers are clickable → open the parameter drill-down (all readings + trend).
   Boost opacity + underline on hover so they read as interactive, not decoration. */
.ta-cov-agg-btn { cursor: pointer; }
.ta-cov-agg-btn:hover { opacity: 1; text-decoration: underline; }

/* ============================================================
   Shared Test-Analytics data-table affordances
   - Sortable column headers (click to sort, arrow indicator)
   - Multi-line header that wraps instead of truncating
   - Drag-to-resize column widths
   - Hover-row highlight
   Used by Coverage Matrix + Failed Measurements drill.
   ============================================================ */

/* Sortable headers — click anywhere on the th (except the resize
   handle) to sort by that column. Show direction arrow when active. */
.ta-data-table th.sortable {
  cursor: pointer;
  user-select: none;
  padding-right: 22px;
  /* No position override — base .ta-cov-table th and .ta-drill-table th
     set position:sticky so the header stays pinned during vertical
     scroll AND the first-column row label stays under its own header
     during horizontal scroll. position:relative here clobbered that
     and caused row labels to slide under the wrong column header
     during horizontal scroll (fixed 2026-06-01). The resize handle's
     position:absolute still works because sticky also establishes a
     positioning context for absolutely-positioned children. */
}
.ta-data-table th.sortable:hover { background: #eef2ff; color: var(--text); }
.ta-data-table th.sortable::after {
  content: '';
  position: absolute;
  right: 8px;
  bottom: 7px;
  width: 0;
  height: 0;
  opacity: 0.35;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}
.ta-data-table th.sortable[data-sort-dir='asc']::after  { border-bottom: 5px solid currentColor; opacity: 0.9; }
.ta-data-table th.sortable[data-sort-dir='desc']::after { border-top:    5px solid currentColor; opacity: 0.9; }
.ta-data-table th.sortable:not([data-sort-dir])::after  {
  /* faded dual-arrow as the "not yet sorted" affordance */
  border-bottom: 4px solid currentColor;
  border-top:    4px solid currentColor;
  height: 0;
  opacity: 0.25;
  box-sizing: content-box;
}

/* Multi-line header — wrap long parameter names across 2-3 lines
   instead of truncating with ellipsis. Min-height keeps short
   headers aligned to multi-line ones in the same row. */
.ta-data-table th.multiline {
  white-space: normal;
  word-wrap: break-word;
  line-height: 1.25;
  min-height: 32px;
  vertical-align: bottom;
  max-width: 140px;
}

/* Column-resize handle — small invisible div on the right edge of
   each header; dragging widens/narrows the column. */
.ta-data-table th .col-resize-handle {
  position: absolute;
  right: -3px;
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: col-resize;
  z-index: 5;
  user-select: none;
}
.ta-data-table th .col-resize-handle:hover,
.ta-data-table th .col-resize-handle.dragging {
  background: var(--primary);
  opacity: 0.4;
}

/* Hover-row highlight on the drill / detail tables (not the heatmap,
   which has its own per-cell hover already). */
.ta-data-table tbody tr:hover { background: #f8fafc; }

/* Empty-state copy when a table has no rows. */
.ta-data-table-empty {
  padding: 30px 20px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.6;
}

/* ============================================================
   Orphan-reference render — used wherever a field's id-foreign-key
   points to a deleted/trashed/missing entity. Output by
   entityRefHtml() in js/utils.js. Consistent rendering across
   batches list, samples list, formula composition table, DOE
   Matrix meta cells, Test Analytics labels, Find Results rows.
   ============================================================ */
.orphan-ref {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  color: var(--text-muted);
  font-style: italic;
}
.orphan-ref.orphan-trashed { color: #b45309; } /* amber — recoverable */
.orphan-ref.orphan-missing { color: #b91c1c; } /* red — broken    */
.orphan-chip {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 600;
  font-style: normal;
  letter-spacing: 0.02em;
  padding: 1px 6px;
  border-radius: 9999px;
  white-space: nowrap;
  vertical-align: 1px;
}
.orphan-trashed .orphan-chip { background: #fef3c7; color: #b45309; border: 1px solid #fde68a; }
.orphan-missing .orphan-chip { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }

/* Drill-table flavor (Failed Measurements list). Used inline-style
   previously, now extracted so the 6 header / 6 body cells share
   one class instead of 6 repeated style attributes per row. */
.ta-drill-table th {
  position: sticky;
  top: 0;
  background: #f8fafc;
  text-align: left;
  padding: 7px 8px;
  font-size: 10.5px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  z-index: 2;
}
.ta-drill-table td {
  padding: 5px 8px;
  font-size: 11.5px;
  color: var(--text-muted);
}
/* Key cell no longer forces blue — the inner paramRefHtml/link colors
   itself only when it actually deep-links (blue = clickable identity). */
.ta-drill-table td.ta-drill-cell-key      { font-size: 12px; font-weight: 500; }
.ta-drill-table td.ta-drill-cell-value    { font-size: 12px; color: #b91c1c; font-weight: 600; font-variant-numeric: tabular-nums; }
.ta-drill-table td.ta-drill-cell-nowrap   { font-size: 11px; white-space: nowrap; }
.ta-drill-table td.ta-drill-cell-thin     { font-size: 11px; }

/* Weigh-out row highlighted while its ingredient is being edited in the
   as-prepared editor below — left accent rail + a light wash (over any
   deviation tint) so the mapping is obvious as you type. */
tr.wo-row-focus > td { background: rgba(37, 99, 235, 0.09); }
tr.wo-row-focus > td:first-child { box-shadow: inset 3px 0 0 var(--primary); }

/* Shared identity deep-link across all Analyze modes: blue = clickable. */
.ta-link { color: var(--primary); cursor: pointer; text-decoration: none; }
.ta-link:hover { text-decoration: underline; }

/* Theoretical-parameter formula builder: click-to-insert field/aggregation
   chips, so non-programmers assemble a formula without memorizing syntax. */
.tp-palette-group { margin-bottom: 5px; line-height: 1.9; }
.tp-palette-label {
  display: inline-block; min-width: 118px;
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); vertical-align: middle;
}
.tp-chip {
  font-family: ui-monospace, monospace; font-size: 11.5px;
  padding: 2px 7px; margin: 2px 3px 2px 0;
  background: #fff; border: 1px solid var(--border); border-radius: 5px;
  color: var(--primary); cursor: pointer; vertical-align: middle;
}
.tp-chip:hover { background: #eff6ff; border-color: var(--primary); }
.tp-chip:active { background: #dbeafe; }

/* Live-preview result card — sits directly under the formula and reads as the
   headline output (big value, accent rail) so users see the effect of what
   they build without scrolling. */
.tp-preview-card {
  padding: 12px 14px;
  min-height: 52px;
  background: #f5f9ff;
  border: 1px solid #dbe7fb;
  border-left: 3px solid var(--primary);
  border-radius: 8px;
  font-size: 12.5px;
  line-height: 1.55;
}
.tp-preview-card.tp-preview-error {
  background: #fef2f2;
  border-color: #fecaca;
  border-left-color: var(--danger);
}
/* Theoretical-parameter editor: a bit wider than the default 640px modal so
   the formula, field palette and result card have room (but not full wide). */
.modal.tp-modal-wide { max-width: 780px; }

/* ============================================================
 * Cross-tab edit banner
 * ------------------------------------------------------------
 * Sticky top-of-page warning that another tab modified FormLab
 * data. Stays until user reloads or dismisses. Amber palette to
 * match the orphan-trashed warning chips (recoverable, not
 * destructive). Caught 2026-05-31 by multi-tab stress test.
 * ============================================================ */
.formlab-crosstab-banner {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
  padding: 10px 14px;
  background: #fef3c7;
  color: #92400e;
  border: 1px solid #fde68a;
  border-radius: 8px;
  box-shadow: 0 4px 14px rgba(146, 64, 14, 0.18);
  font-size: 13px;
  max-width: calc(100vw - 32px);
}
.formlab-crosstab-banner .ctb-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #b45309;
  color: white;
  font-weight: 700;
  flex: 0 0 22px;
}
.formlab-crosstab-banner .ctb-msg { flex: 1 1 auto; }
.formlab-crosstab-banner .ctb-reload {
  background: #b45309;
  color: white;
  border: none;
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.formlab-crosstab-banner .ctb-reload:hover { background: #92400e; }
.formlab-crosstab-banner .ctb-dismiss {
  background: transparent;
  border: none;
  color: #92400e;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
.formlab-crosstab-banner .ctb-dismiss:hover { color: #78350f; }

/* Navigation loading flourish — when a sidebar item is clicked, the animated
   flask logo appears in a padded circular frame over a dimmed backdrop, then
   fades out once the view has rendered. Built/toggled from js/router.js.
   Covers the WHOLE window, sidebar included: it used to be position:absolute
   inside #main, so the rail stayed sharp while the rest dimmed and the app
   looked half-loaded. Render is deferred a frame so it paints before a heavy
   view; fast views only flash it (the opacity fade barely starts), so no min
   delay. Still pointer-events:none — the loader never blocks a click, so you
   can re-navigate out of a slow view while it is showing. */
.nav-loader {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The scrim has to read on BOTH the light content area and the near-black
     sidebar. rgba(15,23,42,.45) is the sidebar's own navy, so over the rail it
     darkened nothing and the loader looked like it stopped at the frame edge.
     A neutral slate at lower alpha dims the content, and the heavier blur is
     what actually reads on the rail — blur does not care about luminance,
     which a single tint cannot manage across two very different surfaces. */
  background: rgba(51, 65, 85, 0.38);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
.nav-loader.on { opacity: 1; }
.nav-loader-circle {
  width: 120px;
  height: 120px;
  box-sizing: border-box;
  /* The blue-circle mark IS the circle now (matches the boot splash), so no
     white frame/padding — just a drop shadow under it. */
  border-radius: 50%;
  box-shadow: 0 14px 44px rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  transform: scale(0.99);
}
/* Pulse via transform only, so it runs on the COMPOSITOR thread and keeps
   animating even while a heavy view render blocks the main thread (the flask's
   own SMIL wave freezes during that block; this pulse doesn't). Kept subtle —
   a gentle ~2% breath rather than an obvious throb. */
.nav-loader.on .nav-loader-circle { animation: navLoaderPulse 1.1s ease-in-out infinite; }
@keyframes navLoaderPulse {
  0%, 100% { transform: scale(0.99); }
  50%      { transform: scale(1.025); }
}
.nav-loader-circle img { width: 100%; height: 100%; display: block; }
/* Batch Pivot: drill-down rows (click a group → filter the list). */
#batch-pivot-table tbody tr.bp-drill-row:hover { background: #f1f5f9; }
@media (prefers-reduced-motion: reduce) {
  .nav-loader, .nav-loader-circle { transition: none; }
  .nav-loader.on .nav-loader-circle { animation: none; }
}

/* Collapsible explainer blocks in the DOE generator. The detail is worth
   reading once; after that it is just vertical space between you and the
   controls, so the collapsed state is remembered per block. */
.asm-doe-collapse .asm-doe-collapse-detail { display: none; }
.asm-doe-collapse.is-open .asm-doe-collapse-detail { display: inline; }
.asm-doe-collapse:not(.is-open) .asm-doegen-desc > div,
.asm-doe-collapse:not(.is-open) .asm-doegen-desc > p { display: none; }
.asm-doe-collapse [data-doe-collapse-btn]:hover { color: #1e3a8a; }

/* The bulk row in the formula filter holds ACTIONS, not toggles — it sits right
   below chips that are stateful and light up when on, so without a label the
   identical pill shapes read as "which one is selected?". Naming the row is
   cheaper and clearer than giving verbs a fake selected state. */
.asm-filter-bulk-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: #94a3b8;
  align-self: center; margin-right: 2px;
}

/* Prediction profiler: one response curve per input, inside its slider row. */
.asm-profile-host { display: block; margin-top: 1px; position: relative; }
/* Stretched to the track width so the curve's x-axis IS the slider's axis and
   the current-value marker sits directly under the thumb. */
.asm-profile { display: block; width: 100%; height: 32px;
  border: 1px solid var(--border); border-radius: 4px; background: #fff; }
body.asm-darkplots .asm-profile { background: #1f2937; border-color: #374151; }

/* Curves control: on/off beside the output it plots, on ONE line. */
.asm-designer-curvebar {
  display: inline-flex; align-items: center; gap: 7px;
  font-weight: 400; font-size: 11px; color: #64748b; white-space: nowrap;
}
.asm-designer-curvebar select {
  font-size: 11px; padding: 1px 4px; max-width: 180px;
  border: 1px solid var(--border); border-radius: 4px; background: #fff;
}
/* Curves toggle + its output selector sit just after the "1 Inputs" title,
 * left-aligned, and stay together on ONE line (the whole bar wraps as a unit if
 * the column is too narrow). */
.asm-designer-curvebar { flex-wrap: nowrap; min-width: 0; white-space: nowrap; }
.asm-designer-curvebar > * { flex-shrink: 0; }
.asm-designer-curvebar select { max-width: 150px; }
.asm-designer-curvetoggle { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; flex-shrink: 0; }
.asm-designer-curvetoggle input { margin: 0; cursor: pointer; }
/* Ends in normal flow when there is no chart to overlay. */
.asm-designer-slider-ends.asm-ends-static { position: static; display: flex; text-shadow: none; }
body.asm-darkplots .asm-designer-curvebar select { background: #111827; border-color: #374151; color: #f1f5f9; }

/* ── Glass mode: the Analyze designer panels ────────────────────────────────
 * Frosted like the rest of the shell, but deliberately at the OPAQUE end
 * (.88 against the shell's .55): these panels are dense reading — slider
 * values, predictions, model notes — and they sit on the Analyze overlay's own
 * light ground, so a thin glass would wash the numbers out for no gain.
 * On a ::before, not the panel: the universal dropdown (.fl-dd-menu) is
 * position: fixed, and backdrop-filter on an ancestor makes it the containing
 * block — which would re-anchor those menus to the panel. */
html[data-sb-float][data-sb-translucent] .asm-designer-inputs,
html[data-sb-float][data-sb-translucent] .asm-designer-outputs {
  position: relative; isolation: isolate;
  background: transparent;
  border-color: transparent;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6),
              0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
html[data-sb-float][data-sb-translucent] .asm-designer-inputs::before,
html[data-sb-float][data-sb-translucent] .asm-designer-outputs::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(255, 255, 255, .88);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] .asm-designer-inputs::before,
  html[data-sb-float][data-sb-translucent] .asm-designer-outputs::before { background: #fbfcfe; }
}

/* ── Analyze workspace: let the theme reach it (floating only) ──────────────
 * Analyze was the one full-screen view the workspace background never reached.
 * Three opaque layers stacked between the body and the eye: the overlay's dark
 * scrim, the panel's full-bleed white, and .asm-analyze-body's own --ground —
 * that last one is why the ground COLOUR appeared correct while the texture and
 * any uploaded photo vanished, since both are background-image layers on <body>.
 * Floating drops all three fills so the real background runs behind the panes,
 * and the tiled gutters that already exist become the place it shows through.
 * DOCKED IS UNTOUCHED: no background is chosen there, so a scrim over an opaque
 * panel is still exactly right. */
html[data-sb-float] .asm-analyze-panel,
html[data-sb-float] .asm-analyze-body { background: transparent; }
/* The overlay REPAINTS the workspace background rather than going transparent.
 * Transparent looks equivalent and isn't: this is a fixed overlay ON TOP of the
 * live app, so see-through means seeing the dashboard and the topbar behind the
 * glass header — the app bleeding through itself, not the theme. Same layer
 * stack as <body> (see the body rule near the end of this file), so the ground,
 * a preset texture and an uploaded photo all line up with what the rest of the
 * app is showing, and the view beneath is covered. */
html[data-sb-float] .asm-analyze-overlay {
  background-color: var(--bg);
  background-image: var(--bg-image, none), var(--bg-texture, none);
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  background-repeat: no-repeat;
}
/* The sidebar was flush to the window edge (radius 0 on its left corners, and
 * the body's padding-left: 0 to match) so it read as tiled into the corner.
 * With the background showing it should sit ON that background like every other
 * pane, so the inset and all four corners come back. */
html[data-sb-float] .asm-analyze-body { padding-left: 10px; }
html[data-sb-float] .asm-analyze-sidebar { border-radius: 12px; }

/* The header floats, exactly like #topbar: the bar itself carries no fill and
 * the frame IS the surface. One frame around both rows rather than two stacked
 * pills — the title and its tab strip are one control cluster, and splitting
 * them reads as two unrelated toolbars. NO overflow:hidden: the titlebar opens
 * menus, and clipping them here is the classic way this breaks. */
html[data-sb-float] .asm-analyze-header {
  background: var(--surface);
  margin: 10px 10px 0;
  border-radius: 18px;
  box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
/* The rows carried the header's outer padding; the frame owns it now. */
html[data-sb-float] .asm-analyze-titlebar { padding: 8px 10px 6px; }
html[data-sb-float] .asm-analyze-tabbar { padding: 0 8px 8px; }

/* Glass, keyed to the MATERIAL setting like every other translucent surface —
 * opaque = you see a surface, translucent = you see the background. On a
 * ::before because backdrop-filter would make the header the containing block
 * for the position:fixed menus its own buttons open. */
html[data-sb-float][data-sb-translucent] .asm-analyze-header {
  background: transparent; position: relative; isolation: isolate;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5),
              0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
html[data-sb-float][data-sb-translucent] .asm-analyze-header::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] .asm-analyze-header::before { background: #f4f6f9; }
}
/* The panes stay OPAQUE in glass mode. Everything in them is data — a grid of
 * r values, a fitted surface, a correlations table — and data does not get to
 * be half-legible for a finish. Same rule the list tables follow. */
html[data-sb-float][data-sb-translucent] .asm-analyze-sidebar,
html[data-sb-float][data-sb-translucent] .asm-analyze-main { background: #fff; }


/* ── Analyze dark-mode gaps ─────────────────────────────────────────────────
 * Elements that kept light-mode values because nothing dark-scoped reached
 * them. Grouped here so the set is visible in one place. */
/* Range sliders (X/Y min-max). Two things were wrong: the input inherited the
 * global `input { background: #fff }` — a white 6px box that read as a light bar
 * across the dark panel — and the ::-webkit-slider-runnable-track override never
 * applied, because that pseudo-element is only styleable once appearance is
 * `none`, leaving a native thumb on a custom track (the hybrid look).
 * Rather than hand-build the whole control, hand the browser the theme:
 * `color-scheme: dark` makes every native widget in the overlay — sliders,
 * dropdown lists, scrollbars — render dark of its own accord, and accent-color
 * supplies the fill. */
#asm-analyze-overlay .asm-range-slider { background: transparent; }
body.asm-darkplots #asm-analyze-overlay { color-scheme: dark; }
body.asm-darkplots #asm-analyze-overlay .asm-range-slider { accent-color: var(--primary); }
/* Design Space "Fit to data" — a near-white floating control over a dark plot. */
body.asm-darkplots #asm-analyze-overlay .asm-ds-zoombtn {
  background: rgba(31, 41, 55, .92); color: #e5e7eb; border-color: #374151;
}
/* Monochrome SVG icons are authored dark, so they vanish on a dark ground.
 * Anything carrying its own inline filter (already inverted for a coloured
 * button) is untouched — inline styles win over this rule. */
body.asm-darkplots #asm-analyze-overlay img.emoji-icon,
body.asm-darkplots #asm-analyze-overlay img.emoji-icon-sm,
body.asm-darkplots #asm-analyze-overlay img.emoji-icon-lg {
  filter: invert(1) brightness(1.6);
}
/* Link-ish blue text (Less/More, "linear only") is too dark against #111827. */
body.asm-darkplots #asm-analyze-overlay a,
body.asm-darkplots #asm-analyze-overlay .asm-link { color: #93c5fd; }
/* .btn-ai: white card + violet outline reads as a bright chip on dark. */
body.asm-darkplots #asm-analyze-overlay .btn-ai {
  background: #2e1065; color: #ddd6fe; border-color: #6d28d9;
}
body.asm-darkplots #asm-analyze-overlay .btn-ai:hover {
  background: #4c1d95; color: #fff; border-color: #7c3aed;
}
/* …except the filled violet ones in the tab row, which are already correct. */
body.asm-darkplots #asm-analyze-overlay .asm-tabbar-actions .btn-ai {
  background: #6d28d9; color: #fff; border-color: transparent;
}

/* ── FormLab dropdown (Analyze) ─────────────────────────────────────────────
 * Replaces the native select's LOOK while the select itself stays in the DOM as
 * the source of truth. Motion is deliberately the workspace switcher's: the menu
 * unfolds downward via a clip-path reveal so it appears to grow out of its
 * trigger without the width ever changing, and fades on close. */
.fl-dd { position: relative; }
/* The native control stays interactive-free but present — it carries the value,
 * the options and every existing change handler. */
.fl-dd > select {
  position: absolute; inset: 0; opacity: 0; pointer-events: none;
  width: 100%; height: 100%; margin: 0;
}
.fl-dd-trigger {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 6px 8px; border: 1px solid transparent; border-radius: 6px;
  background: #f1f5f9; color: var(--text);
  font-family: inherit; font-size: 12.5px; font-weight: 400;
  text-align: left; cursor: pointer; line-height: 1.4;
}
.fl-dd-trigger:disabled { opacity: .55; cursor: default; }
.fl-dd-trigger:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.fl-dd-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Chevron drawn in CSS so it inherits colour and needs no asset. */
.fl-dd-chev {
  width: 7px; height: 7px; flex-shrink: 0; margin-right: 1px;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px); opacity: .55;
}
.fl-dd-menu {
  /* Top layer, same as the kebab menus — a dropdown must clear anything that
   * elevates itself, including the modal backdrop (300) when one is open. */
  position: fixed; z-index: 4000;
  max-height: 320px; overflow-y: auto;
  padding: 4px;
  background: #fff; border-radius: 10px;
  box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
  display: flex; flex-direction: column; gap: 1px;
}
@keyframes flDdOpen {
  from { clip-path: inset(0 0 calc(100% - 34px) 0 round 10px); opacity: .9; }
  to   { clip-path: inset(0 0 0 0 round 10px); opacity: 1; }
}
.fl-dd-menu.fl-dd-opening { animation: flDdOpen .17s ease-out; }
.fl-dd-menu.fl-dd-closing { opacity: 0; transition: opacity .13s ease-in; pointer-events: none; }
.fl-dd-opt {
  text-align: left; padding: 6px 9px; border: none; border-radius: 7px;
  background: transparent; color: var(--text);
  font-family: inherit; font-size: 12.5px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* The menu is a flex column with a max-height; with many options (an axis
     picker has ~100) the default flex-shrink would crush each row shorter than
     its text, overlapping adjacent rows into scrambled text. Keep natural height
     and let the menu scroll instead. */
  flex-shrink: 0;
}
.fl-dd-opt.on { background: var(--primary); color: #fff; }
.fl-dd-opt:disabled { opacity: .45; cursor: default; }
.fl-dd-group {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: #94a3b8; padding: 8px 9px 4px;
  flex-shrink: 0;   /* same reason as .fl-dd-opt — don't get squished in a tall menu */
}
body.asm-darkplots .fl-dd-trigger { background: #1f2937; color: #f1f5f9; border-color: transparent; }
body.asm-darkplots .fl-dd-menu { background: #1f2937; box-shadow: 0 12px 32px rgba(0, 0, 0, .55); }
body.asm-darkplots .fl-dd-opt { color: #e5e7eb; }
body.asm-darkplots .fl-dd-opt.on { background: var(--primary); color: #fff; }
body.asm-darkplots .fl-dd-group { color: #64748b; }
@media (prefers-reduced-motion: reduce) {
  .fl-dd-menu.fl-dd-opening { animation: none; }
}

/* ── Appearance: sidebar style (Preferences → Appearance) ───────────────────
 * Two independent switches, set as attributes on <html> before first paint.
 * FLOATING: the rail becomes a card inset from the window edges, so the chosen
 * background shows all the way around it — the same tile logic as the Analyze
 * panes, applied to the shell.
 * TRANSLUCENT: the rail's own colour is mixed with transparency and the content
 * behind is blurred through it. It only reads as translucent where there is
 * something behind to see, which is why it pairs naturally with floating. */
/* Only the RAIL floats; the content column stays docked to the window edges and
 * lets the background run straight through it. The page is already built from
 * cards, so those provide the surfaces — a second card wrapped around them just
 * hid the background everywhere except a thin gutter, which made choosing one
 * barely register. */
/* No background here: #app would paint an opaque colour OVER the body, hiding a
 * custom background image entirely. The body already carries the ground. */
html[data-sb-float] #app { padding: 0; gap: 0; }
/* Desktop only. Below 769px the rail is an off-canvas DRAWER (position: fixed,
 * translateX(-100%)) and this geometry breaks it: the 10px margin leaves a 10px
 * sliver on screen when it's translated shut, because -100% is the element's
 * own width and doesn't count the margin. The drawer is its own presentation —
 * full-height, flush, square. */
@media (min-width: 769px) {
  html[data-sb-float] #sidebar {
    margin: 10px;
    height: calc(100dvh - 20px);
    border-radius: 14px;
    box-shadow: 0 1px 3px rgba(15, 23, 42, .10), 0 8px 24px rgba(15, 23, 42, .06);
  }
}
/* Translucent is a MATERIAL, not a tint, and it only exists when FLOATING —
 * docked there is no background beside the rail for glass to reveal, so the
 * selector requires both attributes (JS won't set data-sb-translucent while
 * docked either; the CSS just refuses to be fooled).
 * Opaque floating keeps --sidebar-bg and hides the workspace background;
 * translucent does the reverse: neutral glass, the background showing through,
 * the colour ignored. */
/* The blur lives on a ::before layer, NOT on #sidebar itself. An element with
 * backdrop-filter becomes the containing block for its position:fixed
 * descendants, and the account popup / workspace dropdown are fixed — so
 * blurring the sidebar directly re-anchored their bottom/left to the sidebar's
 * inset box and shifted them down-right by the float inset. */
html[data-sb-float][data-sb-translucent] #sidebar {
  background: transparent;
  /* A hairline of light along the top-left edges is what sells glass as a
   * physical layer rather than a flat translucent fill. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55),
              inset 1px 0 0 rgba(255, 255, 255, .35),
              0 1px 3px rgba(15, 23, 42, .10), 0 8px 24px rgba(15, 23, 42, .08);
}
html[data-sb-float][data-sb-translucent] #sidebar::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  pointer-events: none;
}
/* Keep the glass BEHIND the rail's contents without touching the children at
 * all. Forcing `position: relative` on every child (the previous approach) also
 * overrode #sidebar-resizer's `position: absolute`, which turned it into a flex
 * item ~744px tall and pushed the logo, nav and footer off the bottom.
 * isolation makes #sidebar a stacking context so the layer's z-index: -1 stays
 * inside it — and isolation, unlike backdrop-filter, does NOT create a
 * containing block for fixed descendants, so the popups stay viewport-anchored. */
/* isolation keeps the glass layer (::before, z-index: -1) inside the rail
 * without touching the children. But it also makes #sidebar a stacking context,
 * and #main is a later sibling with z-index: auto — so the rail's whole subtree
 * painted UNDER the content column, and the account popup / workspace dropdown
 * (position: fixed, z-index 60) clipped behind the table whenever they were
 * wider than the rail. A z-index lifts the whole subtree instead of fighting it
 * per-popup. 40 clears the top bar (30) and stays under modals (300). */
#sidebar { isolation: isolate; }
/* Desktop only — the mobile drawer sets its OWN position: fixed / z-index: 120,
 * and this rule sits later in the file at the same specificity, so unscoped it
 * overrode both: the rail went back into flow (shoving #main off-screen) and
 * dropped below the 110 backdrop. */
@media (min-width: 769px) {
  #sidebar { position: relative; z-index: 40; }
}
/* No backdrop-filter (older Safari/Firefox): a flat 55% white would be an
 * unreadable haze, so fall back to an opaque near-white panel. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] #sidebar::before { background: #f4f6f9; }
}
/* The colour swatches do nothing while the rail is glass — show that rather
 * than letting them look live. */
html[data-sb-translucent] .sb-swatch-group { opacity: .4; pointer-events: none; }

/* ── Preferences → Theme: live preview ──────────────────────────────────────
 * A scale model of the shell, driven by the same four values the real one
 * reads, so it can't show a combination the app won't produce. Sized in px
 * rather than scaled with transform: a transform would blur the 1px card edges
 * and make the rail's radius lie about what you'll get. */
.fl-theme-preview { margin: 0 0 16px; }
.fl-theme-preview-ground {
  height: 104px; border-radius: 10px; overflow: hidden;
  display: flex; gap: 6px; padding: 6px;
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, .10);
}
.fl-theme-preview-rail {
  width: 34px; flex: 0 0 auto; border-radius: 0;
  align-self: stretch; margin: -6px 0 -6px -6px;
}
/* Floating: inset on all sides + rounded, mirroring the rail's 10px margin and
 * 14px radius at this scale. */
.fl-theme-preview-rail.is-floating { margin: 0; border-radius: 5px; }
.fl-theme-preview-rail.is-glass {
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(6px);
  backdrop-filter: saturate(180%) blur(6px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .8), 0 1px 3px rgba(15, 23, 42, .16);
}
.fl-theme-preview-main {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px;
}
/* The mock's parts carry a shadow for the same reason the real cards do: on a
 * pale ground a white-on-white pane has no edge, and the model has to READ as
 * structure at 104px or it is just a coloured rectangle. */
.fl-theme-preview-bar {
  height: 13px; border-radius: 4px; background: #fff;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .16);
}
.fl-theme-preview-card {
  flex: 1; border-radius: 4px; background: #fff;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .16);
}
.fl-theme-preview-rail:not(.is-glass) { box-shadow: 0 1px 3px rgba(15, 23, 42, .22); }

/* ── Preferences → Theme: dependency branches ───────────────────────────────
 * Each dependent setting sits in its own frame with a triangle pointing back at
 * the option that revealed it, so the chain reads structurally instead of the
 * prose having to say "available once…". Settings that don't apply are hidden
 * outright rather than greyed — a control you can't act on is noise once the
 * frame already explains the branch.
 * The triangle borrows the frame's own fill via a variable, so the two can
 * never drift apart; a nested branch steps one tone deeper to stay legible on
 * its parent. */
.fl-theme-branch {
  --fl-branch-bg: color-mix(in srgb, var(--text) 5%, var(--surface));
  position: relative;
  margin-top: 14px;
  padding: 14px;
  border-radius: 10px;
  background: var(--fl-branch-bg);
}
.fl-theme-branch::before {
  content: '';
  position: absolute; top: -7px; left: var(--fl-arrow, 55px);
  transform: translateX(-50%);
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-bottom: 7px solid var(--fl-branch-bg);
}
.fl-theme-branch .fl-theme-branch {
  --fl-branch-bg: color-mix(in srgb, var(--text) 10%, var(--surface));
}

/* ── Preferences → Theme: ground + texture swatches ─────────────────────────
 * Circular, like the sidebar palette — the name is a hover tooltip rather than
 * an inline label, so a dozen grounds stay a compact row instead of a wall of
 * text. The name still exists for assistive tech via aria-label; the tooltip is
 * decorative and hidden from it. */
.fl-bg-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.fl-bg-sublabel {
  font-size: 11px; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em; margin: 14px 0 7px;
}
.fl-bg-chip {
  position: relative;
  width: 26px; height: 26px; flex: 0 0 auto;
  padding: 0; border-radius: 50%;
  border: 1px solid var(--border); background: transparent;
  cursor: pointer;
  transition: transform .1s ease, box-shadow .1s ease;
}
.fl-bg-chip:hover { transform: scale(1.08); }
.fl-bg-chip.active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--primary); }
.fl-bg-dot { display: block; width: 100%; height: 100%; border-radius: 50%; }
/* Tooltip: 0.5s dwell before it appears, instant on the way out. The delay
 * lives on the :hover rule (not the base) precisely so leaving is immediate —
 * a delay on both sides leaves labels hanging as the pointer crosses a row. */
.fl-bg-chip::after {
  content: attr(data-name);
  position: absolute; bottom: calc(100% + 7px); left: 50%;
  transform: translateX(-50%);
  padding: 3px 7px; border-radius: 6px;
  background: #0f172a; color: #fff;
  font-size: 11px; font-weight: 500; line-height: 1.3;
  white-space: nowrap; pointer-events: none;
  opacity: 0; transition: opacity .12s ease; transition-delay: 0s;
  z-index: 5;
}
.fl-bg-chip:hover::after,
.fl-bg-chip:focus-visible::after { opacity: 1; transition-delay: .5s; }
@media (prefers-reduced-motion: reduce) {
  .fl-bg-chip { transition: none; }
  .fl-bg-chip:hover { transform: none; }
}
.asm-toggle:disabled { opacity: .4; cursor: default; }

/* ── Glass mode: the rail's colour-derived chrome becomes glass too ──────────
 * The workspace selector, its dropdown and the account popup all derive their
 * fill from --sidebar-bg (color-mix), so on a neutral glass rail they'd stay
 * tinted with a colour that is no longer shown anywhere — a crimson chip on
 * clear glass. Under glass they become their own lighter glass layers instead,
 * each with its own backdrop blur so they read as stacked material. */
/* No backdrop-filter here: this chip sits ON the rail's glass, so there is
 * nothing further behind it to blur — and it would become another containing
 * block for the dropdown it owns. */
html[data-sb-float][data-sb-translucent] .pro-ws-selector {
  background: rgba(255, 255, 255, .45);
}
html[data-sb-float][data-sb-translucent] .pro-ws-selector:hover {
  background: rgba(255, 255, 255, .62);
}
/* Popups float over page content, so they get the full-strength material — the
 * same treatment as the rail, not the lighter inset one. */
html[data-sb-float][data-sb-translucent] .sidebar-account-popup,
html[data-sb-float][data-sb-translucent] .pro-ws-dropdown {
  background: rgba(255, 255, 255, .72);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  color: var(--text);
  border-color: rgba(15, 23, 42, .10);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6), 0 12px 32px rgba(15, 23, 42, .18);
}
/* The popup's little pointer inherits the popup fill, so it has to follow. */
html[data-sb-float][data-sb-translucent] .sidebar-account-popup::after {
  border-top-color: rgba(255, 255, 255, .72);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] .pro-ws-selector { background: rgba(255,255,255,.7); }
  html[data-sb-float][data-sb-translucent] .sidebar-account-popup,
  html[data-sb-float][data-sb-translucent] .pro-ws-dropdown { background: #f4f6f9; }
}

/* ── The workspace background runs through the content column ────────────────
 * #content has no fill of its own, so the ground already shows behind the
 * cards; the topbar was the one opaque strip left across it. Dropping its fill
 * (and the rule + shadow under it, per the no-borders direction) lets the
 * chosen background run edge to edge, with the cards floating on it. On the
 * default Paper white this is visually identical to before. */
/* Docked keeps a solid bar — there's no inset for a frame to sit in, so the bar
 * itself is the surface. Floating drops it entirely: the lead frame and the
 * actions frame ARE the surface there, and a fill behind them would put a slab
 * across the background they're supposed to be floating on. */
#topbar { background: var(--surface); border-bottom-color: transparent; box-shadow: none; }
html[data-sb-float] #topbar { background: transparent; }

/* ── Glass mode: selection + hover must not come from the sidebar colour ─────
 * .nav-item.active mixes --sidebar-bg with black, which on light glass rendered
 * a near-black chip (#0d1320) — a colour that isn't shown anywhere else in the
 * theme. Under glass, selection and hover become brighter panes of the same
 * material instead, so the rail reads as one surface. */
html[data-sb-float][data-sb-translucent] #sidebar { --sb-hover: rgba(255, 255, 255, .55); }
html[data-sb-float][data-sb-translucent] #sidebar .nav-item:hover { background: rgba(255, 255, 255, .5); color: #0f172a; }
html[data-sb-float][data-sb-translucent] #sidebar .nav-item.active {
  /* Nearly solid, and lifted off the glass with a shadow: on a translucent rail
   * a merely-brighter pane still reads as part of the same sheet, so selection
   * needs elevation as well as value to stand out. */
  background: rgba(255, 255, 255, .94); color: #0f172a;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .14), 0 2px 8px rgba(15, 23, 42, .10);
}
html[data-sb-float][data-sb-translucent] #sidebar .nav-item.active .nav-count { color: #0f172a; }
/* The workspace tile needs to separate from the rail it sits on, so it's a
 * distinctly brighter pane rather than a near-identical one (.45 vs .55 read as
 * the same surface). */
html[data-sb-float][data-sb-translucent] .pro-ws-selector {
  background: rgba(255, 255, 255, .9);
  box-shadow: 0 1px 2px rgba(15, 23, 42, .12), 0 2px 8px rgba(15, 23, 42, .08);
}
html[data-sb-float][data-sb-translucent] .pro-ws-selector:hover { background: rgba(255, 255, 255, .98); }

/* ── Tables keep their own white surface ─────────────────────────────────────
 * With the workspace background running through the content column, a table
 * with no fill sat directly on the tinted ground. The page is built from cards,
 * so the table gets one too. */
.table-wrap { background: var(--surface, #fff); }

/* ── Glass mode: the list toolbar row becomes a tile ────────────────────────
 * Row 1 of a list view is the top bar's two frames; row 2 is this toolbar —
 * search, Favorites, the Table/Cards/Group toggle and the icon buttons. It was
 * the odd one out: bare controls floating on the glass with no holder, between
 * two rows that both have one.
 * It already runs on the same metrics as the top-bar frames (34px controls,
 * 10px corners, 8px gap), so it takes the identical treatment — 8px padding all
 * round, 18px radius (10 + 8, concentric), glass on a ::before so the frame
 * doesn't become a containing block for the column / filter menus opened from
 * inside it. Scoped to .list-card so ordinary card headers are untouched. */
/* Docked gets the fill but not the frame: the rounded holder is a floating-mode
 * idea (it needs the inset to sit in), while the white surface is what "not
 * translucent" means anywhere. Without this the row had no fill at all when
 * docked and sat straight on the workspace background — the same "it should be
 * white" gap, just in the mode the float-scoped rules below never reach. */
#content > .list-card > .card-header { background: var(--surface); }

/* ── Glass mode: the table itself ───────────────────────────────────────────
 * Rounded, and the rows are SOLID. Translucent rows put the record names at
 * 1.31:1 over a dark background photo (measured) — glass is the right material
 * for chrome, but a data grid is the thing you actually read, so it keeps an
 * opaque surface and the banding does the work: white, then a deeper #e2e8f0
 * ribbon, one step stronger than the #eef1f6 used outside glass.
 * The radius goes on the REGION, not on .table-wrap — the cells paint their own
 * fill and would cover a rounded wrapper's corners, whereas the region is the
 * scroll container and overflow clips to the radius. */
html[data-sb-float][data-sb-translucent] #content > .list-card > [id$="-table-region"] {
  border-radius: var(--radius);
  margin: 0 10px 10px 0;          /* line up with the toolbar frame above */
}
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] .table-wrap {
  background: transparent;
}
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] tbody tr {
  background: #fff;
}
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] tbody tr.row-band {
  background: #e2e8f0;
}
/* Restated at a higher specificity than the two above, which would otherwise
 * out-rank the base hover rule and kill the hover tint entirely. */
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] tbody tr:hover,
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] tbody tr.row-band:hover {
  background: #dce8fc;
}
/* The frozen checkbox column matches its row exactly. It's sticky, so cells
 * scroll UNDER it and it has to be opaque regardless. */
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] th.bulk-check-col,
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] td.bulk-check-col {
  background: #fff;
}
html[data-sb-float][data-sb-translucent] .list-card [id$="-table-region"] tbody tr.row-band td.bulk-check-col {
  background: #e2e8f0;
}

html[data-sb-float] #content > .list-card > .card-header {
  padding: 8px;
  /* Right only: the content column already starts 10px past the rail (the rail's
   * own margin), so a left margin here would double-count it — the same reason
   * #topbar drops its left padding when floating. The list card is full-bleed,
   * so without the right margin the row ran to the window edge while the
   * top-bar frames sat 10px in. */
  margin: 0 10px 10px 0;
  border-radius: 18px;
  position: relative; isolation: isolate;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5),
              0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
/* OPAQUE: the frame is a solid white holder — there's no glass to carry the
 * controls, so the surface has to come from the frame itself. */
html[data-sb-float] #content > .list-card > .card-header { background: var(--surface); }
/* TRANSLUCENT: the row frame becomes the same glass as the two top-bar frames,
 * so all three read as one material. On a ::before so the frame doesn't become
 * a containing block for the column / filter menus opened from inside it. */
html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header {
  background: transparent;
}
html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header::before { background: #f4f6f9; }
}
/* Controls sit a step lighter than the glass they're on, plus a lift — same as
 * the top bar, so the two rows read as the same material. */
html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header .btn {
  background: rgba(255, 255, 255, .82);
  border-color: transparent;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .14), 0 2px 6px rgba(15, 23, 42, .08);
}
html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header .btn:not(.btn-primary):hover {
  background: rgba(255, 255, 255, .95);
}
html[data-sb-float][data-sb-translucent] #content > .list-card > .card-header .btn-primary:hover {
  background: var(--primary-hover, #1d4ed8); color: #fff;
}

/* ── Custom workspace background image ──────────────────────────────────────
 * Layered ON the chosen colour, which stays as the base so there is never a
 * bare-white flash while the image loads from IndexedDB (that read is async and
 * so can't run before first paint the way the colour does). */
body {
  /* Two layers: an uploaded photo on top, the built-in preset beneath it, both
   * over the ground colour. A preset is a mesh gradient of 4-6 sub-layers, so
   * the image list is variable-length — hence SINGLE values for size / repeat /
   * position, which CSS applies to every layer. A two-value list cycled instead,
   * handing alternating layers different sizes. Both a cover photo and a
   * full-bleed gradient want exactly this, so nothing is lost. */
  background-image: var(--bg-image, none), var(--bg-texture, none);
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  background-repeat: no-repeat;
}

/* Translucent: the topbar becomes glass along with the rail. This is keyed to
 * the MATERIAL setting rather than to "an image is present" — an opaque shell
 * should stay opaque whatever the background is, and the frosted surface still
 * guarantees legibility over a photo because the .72 alpha alone composites to
 * ~9:1 for dark text even over pure black. */
/* No fill on the bar itself any more — the two frames carry the glass, so the
 * background runs between and around them. */
/* The detail view's sticky sub-header (breadcrumb + tab row) was the one shell
 * band that stayed opaque in glass mode — a solid white slab across the top of
 * an otherwise transparent page. Its job is to hide the content scrolling under
 * it, which blur does just as well as a flat fill.
 * On a ::before rather than the element: it's already a stacking context
 * (sticky + z-index), but backdrop-filter would additionally make it the
 * containing block for any position:fixed menu opened from the tab row. */
html[data-sb-float][data-sb-translucent] #ing-detail-head { background: transparent; }
html[data-sb-float][data-sb-translucent] #ing-detail-head::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: rgba(255, 255, 255, .79);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] #ing-detail-head::before { background: #f7f9fc; }
}

/* ── Glass mode: dark text by default ───────────────────────────────────────
 * The rail's colours were all picked for a DARK background — light blue, light
 * purple, translucent white. On light glass they wash out. Rather than patch
 * each one, glass sets dark text as the default for the whole rail and lets the
 * few chips that carry their own solid fill opt back out below. */
html[data-sb-float][data-sb-translucent] #sidebar,
html[data-sb-float][data-sb-translucent] #sidebar * { color: #0f172a; }
/* Chips with their own coloured fill keep white text — dark text on a blue or
 * green badge would be the same mistake in reverse. */
html[data-sb-float][data-sb-translucent] #sidebar .nav-count,
html[data-sb-float][data-sb-translucent] #sidebar .pro-presence-count,
html[data-sb-float][data-sb-translucent] #sidebar .sidebar-account-popup-dot,
html[data-sb-float][data-sb-translucent] #sidebar .badge { color: #fff; }
/* The brand mark keeps its blue — "Lab" and the PRO badge are identity, not
 * chrome, so they opt out of the dark default. */
/* The wordmark takes the accent on glass. The Pro pill is EXCLUDED: it used to
   be an outline, so accent-on-transparent was right, but it is a filled badge
   now and accent-on-accent is an invisible label. It keeps its white, and the
   selector below is specific enough to say so over this one. */
/* Scoped to DIRECT children so it reaches "Lab" and nothing inside a pill.
   Excluding the pill by :not() instead needs five class-level terms, which
   then out-specifies any rule trying to paint the pill's own label white —
   an arms race the child combinator avoids entirely. */
html[data-sb-float][data-sb-translucent] #sidebar .logo-text > span:not(.logo-pro):not(.logo-free) { color: var(--primary); }
/* The filled Pro badge keeps white on glass. Needed because the blanket
   `#sidebar * { color: #0f172a }` above reaches inside the pill; this is
   specific enough to win, and it no longer fights the accent rule, which is
   scoped to direct children and so never matches the pill's label. */
html[data-sb-float][data-sb-translucent] #sidebar .logo-text .logo-pro,
html[data-sb-float][data-sb-translucent] #sidebar .logo-text .logo-pro * { color: #fff; }
/* Glass rail: the Pro pill takes the accent, so the Free pill takes the muted
   text colour from the same themed set rather than a white it cannot rely on.
   The selector needs `span.logo-free` to outrank the `.logo-text span` rule
   directly above — that one exists to paint "Lab" in the wordmark, but the
   pill is a span inside `.logo-text` too, so it was painting the Free pill
   accent-blue and making it read as a second Pro badge. */
html[data-sb-float][data-sb-translucent] #sidebar { --logo-free-bd: var(--border); }
html[data-sb-float][data-sb-translucent] #sidebar .logo-text span.logo-free,
html[data-sb-float][data-sb-translucent] #sidebar .logo-text span.logo-free * { color: var(--text-muted); }
/* Muted text stays muted, just dark-muted instead of light-muted. */
html[data-sb-float][data-sb-translucent] #sidebar .nav-section,
html[data-sb-float][data-sb-translucent] #sidebar .pro-ws-sub,
html[data-sb-float][data-sb-translucent] #sidebar .logo-version { color: rgba(15, 23, 42, .55); }

/* ── Glass mode: the workspace cards become glass too ───────────────────────
 * Translucent is meant to expose the background as much as possible. The
 * content column is already fully transparent, so the last thing hiding the
 * background across the whole page was the cards' solid white fill — the rail
 * read as glass while the 80% of the window beside it stayed a white slab.
 *
 * Same ::before + isolation trick as the sidebar, and for the same reason:
 * backdrop-filter on the card itself would make it the containing block for
 * every position:fixed menu opened from inside it (kebabs, column pickers),
 * re-anchoring them to the card instead of the viewport. */
/* :not(.list-card) — a list view's outer card is a LAYOUT wrapper, not a
 * surface: it explicitly drops background, border, shadow and radius and just
 * fills the remaining height as a flex column. Glassing every .card re-added a
 * surface it had renounced, so a full-window sheet of glass (content-area
 * sized, .79 white + blur) sat behind the toolbar row and the table. */
html[data-sb-float][data-sb-translucent] .card:not(.list-card),
html[data-sb-float][data-sb-translucent] .stat-card {
  position: relative;
  isolation: isolate;
  background: transparent;
  border-color: transparent;
  /* The light hairline along the top edge is what sells a pane as a physical
   * layer instead of a flat wash — same cue the rail uses. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55),
              0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .05);
}
html[data-sb-float][data-sb-translucent] .card:not(.list-card)::before,
html[data-sb-float][data-sb-translucent] .stat-card::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit;
  /* .79, not the rail's .55: cards carry the actual reading material, and the
   * alpha is what sets the floor on contrast. The worst case is a pure-black
   * background photo, where blur has nothing lighter to pull in — .79 white
   * composites to rgb(201,201,201) there, measured at 10.7:1 for --text and
   * 4.58:1 for the darkened muted below. Lowering it trades legibility for
   * atmosphere: .78 already drops muted to 4.48 and under AA. */
  background: rgba(255, 255, 255, .79);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-sb-float][data-sb-translucent] .card:not(.list-card)::before,
  html[data-sb-float][data-sb-translucent] .stat-card::before { background: #f7f9fc; }
}
/* Muted text is the first thing a translucent surface breaks: #64748b reads
 * 4.8:1 on solid white but only 2.4:1 once the card is glass over a dark
 * photo. Darkening it one step inside glass restores AA without making it stop
 * reading as secondary — it's still clearly lighter than --text beside it.
 * Set at the root so it covers every light-grey run in the workspace, not just
 * the ones inside a card — section labels, timestamps, hints and empty-state
 * copy all draw from this token. The rail has its own dark-muted rule above. */
html[data-sb-float][data-sb-translucent] { --text-muted: #475569; }

/* ============================================================
   Settings > About — version, legal links, open-source licences.
   Deliberately low-chrome: rows are separated by fill contrast and
   spacing rather than rules, so the list reads as a list without
   drawing the eye to its edges.
   ============================================================ */
.about-ver { font-size: 20px; font-weight: 650; letter-spacing: -0.01em; }
.about-org { margin-top: 2px; font-size: 12.5px; color: #64748b; }
.about-links { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 12px; font-size: 12.5px; }
.about-links a { color: var(--primary); text-decoration: none; }
.about-links a:hover { text-decoration: underline; }
.about-intro { margin: 0 0 16px; font-size: 12.5px; color: #475569; line-height: 1.55; max-width: 62ch; }

.lic-group-title {
  margin: 18px 0 8px; font-size: 11px; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: #94a3b8;
}
.lic-group-title:first-of-type { margin-top: 0; }

.lic-row { padding: 11px 12px; border-radius: 9px; background: rgba(148,163,184,.14); }
.lic-row + .lic-row { margin-top: 7px; }

.lic-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.lic-name { font-size: 13.5px; font-weight: 620; }
.lic-ver { font-size: 11.5px; color: #64748b; font-variant-numeric: tabular-nums; }
.lic-badge {
  font-size: 10.5px; font-weight: 600; padding: 1px 7px; border-radius: 999px;
  background: rgba(37,99,235,.12); color: #1d4ed8;
}
.lic-host { font-size: 10.5px; color: #94a3b8; margin-left: auto; }
.lic-use { margin-top: 3px; font-size: 12px; color: #475569; }
.lic-copy { margin-top: 5px; font-size: 11.5px; color: #64748b; }
.lic-link { margin-top: 2px; font-size: 11.5px; }
.lic-link a { color: var(--primary); text-decoration: none; }
.lic-link a:hover { text-decoration: underline; }

.lic-details { margin-top: 8px; }
.lic-details > summary {
  cursor: pointer; font-size: 11.5px; color: #64748b;
  list-style: none; display: inline-flex; align-items: center; gap: 5px;
}
.lic-details > summary::-webkit-details-marker { display: none; }
.lic-details > summary::before { content: '▸'; font-size: 11px; line-height: 1; transition: transform .12s ease; }
.lic-details[open] > summary::before { transform: rotate(90deg); }
.lic-details > summary:hover { color: #334155; }
.lic-text {
  margin: 8px 0 0; padding: 10px 12px; border-radius: 7px;
  background: rgba(15,23,42,.045); color: #475569;
  font-size: 11px; line-height: 1.5; white-space: pre-wrap;
  max-height: 260px; overflow: auto;
}

/* NOTE: no [data-sb-translucent] override here on purpose. The Danger zone needs
   one because its surface IS the translucent sidebar; the Settings modal is an
   opaque panel in every theme, so a white wash would paint white-on-white and
   the rows would lose their fill entirely. Measured, not assumed. */

/* ============================================================
   Structure sketcher (js/sketcher.js) — OpenChemLib in a modal.
   The editor IS the content, so there is no separate footer bar: the
   actions float on the editor surface, bottom-right, inside the same
   rounded frame. One band of chrome instead of two.
   ============================================================ */
.modal.fl-sketch-modal {
  padding: 14px;
  border-radius: 16px;
  /* width AND max-width, so the shell cannot stretch past what it needs and
     strand dead space down one side. Everything inside is fluid off this. */
  width: min(94vw, 880px);
  max-width: min(94vw, 880px);
}
.fl-sketch-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 2px 4px 10px;
}
.fl-sketch-head h3 { margin: 0; font-size: 15px; font-weight: 640; }
.fl-sketch-x {
  border: 0; background: transparent; cursor: pointer;
  width: 26px; height: 26px; border-radius: 8px;
  color: #64748b; font-size: 13px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.fl-sketch-x:hover { background: rgba(148,163,184,.18); color: #334155; }

/* The stage is the positioning context for the floating actions. */
.fl-sketch-stage { position: relative; }

/* overflow:hidden is what actually rounds the editor — the canvases live in
   the element's shadow root, so the corners have to be clipped by the host. */
.fl-sketch-host {
  /* Fill the modal's content box rather than carrying a second, smaller cap —
     two independent width rules is exactly how the 32px of dead space on the
     right appeared. Height gives way on short viewports instead of overflowing. */
  width: 100%; height: clamp(300px, 62vh, 460px);
  border-radius: 10px; overflow: hidden;
  background: #fff;
  display: flex; align-items: center; justify-content: center;
  /* The editor paints white and the modal is white, so without this the 10px
     corners are invisible. Elevation rather than a border — same reason the
     rest of the app defines edges with fill and shadow. */
  box-shadow: 0 1px 3px rgba(15,23,42,.09), 0 0 0 1px rgba(15,23,42,.06);
}
/* Beat OpenChemLib's own 600x400 default. It injects that rule as a
   low-priority user-agent-ish stylesheet PREPENDED to <head>, so an equal
   specificity rule here wins on source order — no !important needed. */
.fl-sketch-host openchemlib-editor { display: block; width: 100%; height: 100%; }

/* Movement grid. Lives on the HOST, behind the editor's canvas — the forked
   bundle makes the canvas background transparent so this shows through
   (upstream paints it opaque white on every frame). It is an indicator, not
   decoration: it appears while the structure is being panned or zoomed and
   fades once movement stops, which is what tells you the canvas moved rather
   than the molecule changing shape. */
.fl-sketch-host::before {
  content: '';
  position: absolute; inset: 0;
  background-image: radial-gradient(circle, rgba(100,116,139,.45) var(--fl-dot, 1px), transparent var(--fl-dot, 1px));
  /* Size and offset are driven from js/sketcher.js so the dots track the
     structure's zoom and pan — the grid belongs to the drawing surface, not to
     the window. Fallbacks keep it sane before the first paint. */
  background-size: var(--fl-grid, 18px) var(--fl-grid, 18px);
  background-position: var(--fl-grid-x, 0px) var(--fl-grid-y, 0px);
  opacity: .5;
  transition: opacity .18s ease;
  pointer-events: none;   /* never intercept a draw */
  z-index: 0;
}
/* Always visible; lifts slightly while the canvas is actually moving. */
.fl-sketch-host.fl-sketch-moving::before { opacity: .95; }
/* The host is the positioning context for ::before, and the editor must stack
   above the dots. */
.fl-sketch-host { position: relative; }
.fl-sketch-host openchemlib-editor { position: relative; z-index: 1; }

/* Space held = pan mode. */
.fl-sketch-host.fl-sketch-pan { cursor: grab; }
.fl-sketch-host.fl-sketch-pan:active { cursor: grabbing; }

.fl-sketch-status { color: #64748b; font-size: 13px; padding: 16px; text-align: center; }
.fl-sketch-error { color: var(--danger, #dc2626); }

/* Docked actions — the row directly under the editor, where the hint text used
   to sit. Kept off the canvas so nothing covers drawable area. */
.fl-sketch-actions {
  display: flex; justify-content: flex-end; align-items: center; gap: 8px;
  margin-top: 10px; padding: 0 2px;
}

/* Zoom widget — bottom-right of the editor. Replaces the tool rail's Zoom
   tool, which was a click-drag that rotated as well as scaled. */
.fl-zoomwidget {
  position: absolute; right: 12px; bottom: 12px;
  display: flex; align-items: center; gap: 2px;
  padding: 3px;
  border-radius: 10px;
  background: rgba(255,255,255,.88);
  backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(15,23,42,.14), 0 0 0 1px rgba(15,23,42,.06);
  z-index: 2;
  user-select: none;
}
.fl-zoomwidget button {
  border: 0; background: transparent; cursor: pointer;
  color: #334155; border-radius: 7px;
  font: inherit; line-height: 1;
}
.fl-zoom-btn {
  width: 24px; height: 24px;
  font-size: 15px; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center;
}
.fl-zoom-pct {
  min-width: 50px; height: 24px; padding: 0 6px;
  font-size: 11.5px; font-weight: 600;
  font-variant-numeric: tabular-nums;   /* stop the widget twitching as digits change */
  color: #475569;
}
.fl-zoomwidget button:hover { background: rgba(148,163,184,.22); }
.fl-zoomwidget button:active { background: rgba(148,163,184,.34); }
.fl-zoomwidget button:focus-visible { outline: 2px solid var(--primary, #2563eb); outline-offset: 1px; }

/* ============================================================
   Sketcher toolbar — FormLab's replacement for OpenChemLib's tool rail.
   Rows 1–4 of the original grid stay on the left as actions; rows 5–17
   become the horizontal bar above the canvas.
   ============================================================ */
.fl-sketch-row { display: flex; align-items: stretch; gap: 8px; }

.fl-toolbar { display: flex; gap: 4px; }
/* nowrap on purpose — js/sketcher.js sizes the buttons to fit the row exactly,
   so wrapping would mean the fit calculation had failed rather than that the
   bar legitimately needs two lines. */
.fl-toolbar-top { margin-bottom: 8px; flex-wrap: nowrap; gap: 3px; }
.fl-toolbar-side { flex-direction: column; flex: 0 0 auto; flex-wrap: nowrap; }

/* 10px corners, grey tint. Buttons read as a set of soft chips rather than a
   grid of hairlines — edges come from fill, not borders. */
.fl-tool {
  width: var(--fl-tool-size, 30px); height: var(--fl-tool-size, 30px);
  flex: 0 0 auto; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 10px;
  background: rgba(148,163,184,.16);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.55), 0 1px 1.5px rgba(15,23,42,.08);
  cursor: pointer;
  transition: background .12s ease, box-shadow .12s ease, transform .06s ease;
}
.fl-tool img {
  display: block;
  width: var(--fl-tool-icon, 20px); height: var(--fl-tool-icon, 20px);
  object-fit: contain;
  /* The lifted artwork is OpenChemLib's own blue/red line art. Desaturating it
     slightly settles it into the grey chips without redrawing every icon. */
  filter: grayscale(.35);
}
.fl-tool:hover { background: rgba(148,163,184,.28); }
.fl-tool:active { transform: translateY(.5px); }
.fl-tool.is-active {
  background: rgba(37,99,235,.16);
  box-shadow: inset 0 0 0 1.5px rgba(37,99,235,.45);
}
.fl-tool.is-active img { filter: none; }
.fl-tool:focus-visible { outline: 2px solid var(--primary, #2563eb); outline-offset: 1px; }

/* With our own rail beside it, the canvas no longer reserves space for the
   stock one. */
.fl-sketch-host.fl-has-toolbar { flex: 1 1 auto; width: auto; min-width: 0; }

/* Element symbols and charges are drawn as real text rather than as artwork
   lifted off the rail — in the original they are bitmap crops of letters, which
   land soft inside a crisp DOM button. */
.fl-tool-txt span {
  font-size: var(--fl-tool-font, 13px);
  font-weight: 620;
  line-height: 1;
  color: #1e3a8a;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
.fl-tool-txt.is-active span { color: #1d4ed8; }

/* Group separator. Space, not a rule — the seam is legible from the gap alone,
   and a hairline here would be the sort of border the rest of the app avoids. */
.fl-tool-sep {
  flex: 0 0 auto;
  width: var(--fl-tool-sep, 11px);
  align-self: stretch;
  pointer-events: none;
}

/* Sketcher tool tooltips. Own element rather than the native title attribute:
   the native one waits about a second and cannot be styled. */
.fl-tip {
  position: absolute;
  z-index: 6;
  padding: 4px 8px;
  border-radius: 7px;
  background: rgba(15,23,42,.92);
  color: #f8fafc;
  font-size: 11px;
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;    /* never sit between the cursor and the button */
  box-shadow: 0 2px 8px rgba(15,23,42,.22);
}

/* Compact-mode popups — the ring dropdown and the overflow menu. */
.fl-toolpop {
  position: absolute; z-index: 7;
  display: flex; flex-wrap: wrap; gap: 4px;
  max-width: 260px;
  padding: 6px;
  border-radius: 12px;
  background: rgba(255,255,255,.97);
  backdrop-filter: blur(8px);
  box-shadow: 0 6px 20px rgba(15,23,42,.18), 0 0 0 1px rgba(15,23,42,.07);
}
.fl-toolpop .fl-tool { width: 30px; height: 30px; }
.fl-toolpop .fl-tool img { width: 20px; height: 20px; }
.fl-toolpop .fl-tool-txt span { font-size: 13px; }

/* A trigger carries a caret so it reads as opening something rather than
   selecting something. */
.fl-tool-pop { position: relative; }
.fl-tool-pop::after {
  content: '';
  position: absolute; right: 2px; bottom: 2px;
  border-left: 3px solid transparent;
  border-top: 3px solid rgba(30,58,138,.55);
  border-right: 0; border-bottom: 0;
}

/* Phone: a structure editor needs width, and there is no portrait layout that
   works at 375px. Ask for landscape rather than ship something unusable.
   768px is FormLab's own small-screen breakpoint — the app already draws the
   line there in eight other places, so the sketcher uses the same one rather
   than inventing a boundary of its own. Landscape is exempt: a phone turned
   sideways is what we asked for, and it gets the compact toolbar. */
.fl-sketch-rotate { display: none; }
@media (max-width: 768px) and (orientation: portrait) {
  .modal.fl-sketch-modal .fl-sketch-stage,
  .modal.fl-sketch-modal .fl-toolbar-side,
  .modal.fl-sketch-modal .fl-sketch-actions { display: none; }
  .fl-sketch-rotate {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 10px; text-align: center;
    min-height: 46vh; padding: 24px 18px;
  }
  .fl-rotate-icon { font-size: 30px; color: #64748b; }
  .fl-rotate-title { font-size: 15px; font-weight: 640; }
  .fl-rotate-sub { font-size: 12.5px; color: #64748b; max-width: 30ch; line-height: 1.5; }
}

/* ============================================================
   Ingredient DETAIL view — section headings styled as card headers.
   ------------------------------------------------------------
   Matches the Settings modal's card headers rather than approximating them:
   16px / 700 / var(--text), no uppercase, no letter-spacing, and a 1px rule
   that spans the card's FULL width. The negative inline margin is what
   full-bleeds the rule — .card-body has 18px padding, so without it the line
   stops short at both ends and reads as an underline under the words rather
   than as a header division.

   Scoped two ways on purpose:
     • [id^="tab-ing-"] — the DETAIL tabs only. ingredients-form.js reuses
       .ing-section-h for the edit form, where a heavier heading would fight
       the field labels.
     • min-width 769px — FormLab's own desktop breakpoint. At narrower widths
       the cards are tighter and the smaller uppercase label still reads better.
   ============================================================ */
@media (min-width: 769px) {
  [id^="tab-ing-"] .ing-section-h {
    /* Bigger type in a tighter band than the Settings header it is modelled on:
       14px text, 7px vertical padding. The title carries the weight; the empty
       space around it does not need to.
       The HORIZONTAL padding stays at 18px — it is what aligns the title with
       the fields beneath it, and it is the figure the negative margin cancels
       to full-bleed the rule, so trimming it would misalign the text and break
       the line in one go. */
    font-size: 14px;
    font-weight: 700;
    text-transform: none;
    letter-spacing: normal;
    color: var(--text);
    margin: -18px -18px 12px;
    padding: 7px 18px;
    border-bottom: 1px solid var(--border);
  }
}

/* The Name label is the one label on the card that earns the least. Its value
   is 17px/700 and repeats the page title directly above it, so nothing about
   that line is ambiguous — the label is scaffolding for scanning, not
   identification. Kept for alignment with the column beside it, but stepped
   down a level so it stops competing with the name itself. */
.ing-name-field .label {
  font-size: 10px;
  font-weight: 500;
  color: var(--text-faint);
}

/* ============================================================
   Ingredient inline edit — the Accounts field treatment.
   ------------------------------------------------------------
   Settings > Accounts edits in place: the field carries no box, keeps the
   typography of the value it replaces, and shows only a bottom rule that
   darkens on hover and turns FormLab blue on focus (see .pro-name-input).
   The point is that nothing jumps when you switch into edit — the text stays
   exactly where it was reading a moment ago.

   Boxed inputs cannot do that. A 1px border with 8px 10px padding shifts every
   value right and down, so entering edit mode reflows the whole card and you
   lose your place. Zero horizontal padding is what keeps the text on its
   original baseline column.
   ============================================================ */
#tab-ing-identification.ing-inline-edit input[type="text"],
#tab-ing-identification.ing-inline-edit input[type="number"],
#tab-ing-identification.ing-inline-edit input[type="date"],
#tab-ing-identification.ing-inline-edit input:not([type]),
#tab-ing-identification.ing-inline-edit textarea,
#tab-ing-identification.ing-inline-edit select {
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 2px 0 3px;
  font-family: inherit;
  color: var(--text);
  outline: none;
  transition: border-color .12s;
  /* The global input rule sets min-height:38px to make a BOXED control a
     comfortable target. With no box, that height becomes 14-17px of dead space
     between the text and its underline — the rule floats away from the value it
     belongs to. Height comes from the text here, as it does in Accounts. */
  min-height: 0;
  line-height: 1.35;
}
#tab-ing-identification.ing-inline-edit input:hover,
#tab-ing-identification.ing-inline-edit textarea:hover,
#tab-ing-identification.ing-inline-edit select:hover { border-bottom-color: var(--text-muted); }
/* FormLab blue underline, and no light-blue glow — the global input:focus
   box-shadow would put the box back that we just removed. */
#tab-ing-identification.ing-inline-edit input:focus,
#tab-ing-identification.ing-inline-edit textarea:focus,
#tab-ing-identification.ing-inline-edit select:focus {
  border-bottom-color: var(--primary);
  box-shadow: none;
}
#tab-ing-identification.ing-inline-edit textarea { padding: 4px 0; resize: vertical; }
/* A select still needs room for its own arrow. The class picker carries a more
   specific rule of its own ([data-eslot="classpicker"] select), so match its
   specificity rather than hope to outrank it by source order. */
#tab-ing-identification.ing-inline-edit select,
#tab-ing-identification.ing-inline-edit [data-eslot="classpicker"] select {
  padding: 2px 18px 3px 0;
  min-height: 0;
}

/* The name is the page's headline in read mode, so it stays the headline while
   being edited — the same trick Accounts plays with the 22px display name. */
#tab-ing-identification.ing-inline-edit #f-name {
  font-size: 17px;
  font-weight: 700;
}

/* Class & details — one description above, and the class dropdown living inside
   the detail panel's own title rather than in a row of its own. */
/* Heading with a caption beside it. baseline alignment so the small text sits
   on the title's baseline rather than floating mid-cap. */
.ing-section-h-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.ing-class-note {
  font-size: 11px; color: var(--text-muted);
  font-weight: 400; text-transform: none; letter-spacing: normal;
  /* line-height 1, not 1.5: at 11px a 1.5 line box is taller than the 14px
     title's own, so on a baseline-aligned row the caption — not the title —
     would set the height, and the heading would grow 1.5px the moment edit
     added it. The row must be sized by the title in both modes. */
  margin: 0; line-height: 1;
  /* No max-width. A measure cap earns its keep on a paragraph; this is one
     short helper line in a ~965px card, and capping it only forced a second
     row that the card had room to avoid. */
}
/* The panel title IS the picker now. The select inherits the title's type so it
   reads as a heading you can change, not a form control wearing a heading. */
.ing-class-title { display: flex; align-items: baseline; gap: 5px; }
#tab-ing-identification.ing-inline-edit .ing-class-title select {
  font-size: inherit;
  font-weight: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  width: auto;
  max-width: 22ch;
  padding: 0 16px 1px 0;
  border-bottom-color: transparent;   /* only underline on hover/focus */
}
#tab-ing-identification.ing-inline-edit .ing-class-title select:hover { border-bottom-color: currentColor; }
#tab-ing-identification.ing-inline-edit .ing-class-title select:focus { border-bottom-color: var(--primary); }

/* Dropdowns keep a box. A text field can lose its border and still read as
   editable because a caret appears in it; a select has no such cue, so with
   only an underline it looks like static text that happens to be clickable.
   10px to match the chips and buttons elsewhere in the editor.
   Declared after the underline rules above so it wins on source order at equal
   specificity — including the picker that now lives in the panel title. */
#tab-ing-identification.ing-inline-edit select,
#tab-ing-identification.ing-inline-edit [data-eslot="classpicker"] select,
#tab-ing-identification.ing-inline-edit .ing-class-title select {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 4px 24px 4px 9px;
  background-color: transparent;
  transition: border-color .12s;
}
#tab-ing-identification.ing-inline-edit select:hover,
#tab-ing-identification.ing-inline-edit .ing-class-title select:hover { border-color: var(--text-muted); }
#tab-ing-identification.ing-inline-edit select:focus,
#tab-ing-identification.ing-inline-edit .ing-class-title select:focus {
  border-color: var(--primary);
  box-shadow: none;
}
/* The title picker sits on a tinted panel, so give it a little breathing room
   from the word that follows it. */
#tab-ing-identification.ing-inline-edit .ing-class-title { gap: 7px; align-items: center; }

/* Reserve the Edit control's height in the ingredient detail header.
   The control is a .btn-sm (26.8px) and disappears in edit mode; without a
   floor the row shrinks to the breadcrumb's text height and everything below —
   tab strip, every card — rides up about 2px. Matching min-height keeps the
   page still across the mode switch. */
#ing-tab-edit-ctrl {
  min-height: 27px;
  display: flex;
  align-items: center;
}

/* Class detail panel title — same height in read and edit.
   In read it is a line of 12px uppercase text (~16px tall). In edit it is that
   line with the class dropdown standing in for the class name, and the dropdown
   is 26px — so the panel heading grew ~10px on entering edit and everything
   under it moved. Both forms now sit on the same 26px floor, so the heading
   holds its place whether it contains text or a control. */
#tab-ing-identification [data-class-row] > div:first-child,
#tab-ing-identification .ing-class-title {
  min-height: 26px;
  display: flex;
  align-items: center;
  gap: 7px;
}
