/* Full-bleed layout. The WebGL canvas fills the viewport; everything else
   floats on top of it. */

/* App version badge — small print, bottom-right of every screen.
   Pointer-events:none keeps it from intercepting clicks on whatever
   chrome sits below it. Hidden on phone-sized viewports (see the
   mobile media block at the foot of this file) where every pixel
   counts. */
.app-version {
  position: fixed;
  bottom: 0.4rem;
  right: 0.6rem;
  z-index: 5;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: #adb5bd;
  opacity: 0.55;
  pointer-events: none;
  white-space: nowrap;
}

/* "Create an account" call-to-action shown to guests (index.php?guest=1), pinned bottom-right
   just above the version badge. */
#guest-signup-btn {
  position: fixed;
  right: 0.7rem;
  bottom: 1.5rem;
  z-index: 30;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}
html[data-theme="light"] .app-version { color: #6c757d; opacity: 0.6; }

/* GDPR / cookie disclaimer banner. Pinned to the bottom centre with
   a fixed max-width so it doesn't shoulder past long auth-page lines
   on wide displays. Hidden by default; auth.php's inline JS reveals
   it on first visit and persists the dismissal in localStorage. */
.privacy-banner {
  position: fixed;
  left: 50%;
  bottom: 1rem;
  transform: translateX(-50%);
  z-index: 1400;
  max-width: 42rem;
  width: calc(100% - 2rem);
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.7rem 0.9rem 0.7rem 1rem;
  background: rgba(20, 22, 28, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.5rem;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
  color: #e9ecef;
  font-size: 0.78rem;
  line-height: 1.45;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
.privacy-banner[hidden] { display: none !important; }
.privacy-banner-body { flex: 1; min-width: 0; }
.privacy-banner-body strong {
  display: inline-block;
  margin-right: 0.3rem;
  letter-spacing: 0.04em;
  color: #fff;
}
.privacy-banner-link {
  color: #6ea8fe;
  margin-left: 0.4rem;
  white-space: nowrap;
  text-decoration: none;
}
.privacy-banner-link:hover { text-decoration: underline; }
.privacy-banner-btn {
  flex-shrink: 0;
  padding: 0.4rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  background: #6ea8fe;
  color: #052c65;
  border: 0;
  border-radius: 0.3rem;
  cursor: pointer;
}
.privacy-banner-btn:hover { filter: brightness(1.07); }
.privacy-banner-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}
.privacy-banner-btn-secondary {
  flex-shrink: 0;
  padding: 0.4rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 500;
  background: transparent;
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 0.3rem;
  cursor: pointer;
}
.privacy-banner-btn-secondary:hover {
  border-color: #6ea8fe;
  color: #6ea8fe;
}
@media (max-width: 600px) {
  .privacy-banner {
    flex-direction: column;
    align-items: stretch;
    bottom: 0.5rem;
  }
  .privacy-banner-actions { flex-direction: column; }
  .privacy-banner-btn,
  .privacy-banner-btn-secondary { width: 100%; }
}
html[data-theme="light"] .privacy-banner {
  background: rgba(255, 255, 255, 0.97);
  border-color: rgba(0, 0, 0, 0.12);
  color: #212529;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.18);
}
html[data-theme="light"] .privacy-banner-body strong { color: #0d6efd; }
html[data-theme="light"] .privacy-banner-link { color: #0d6efd; }
html[data-theme="light"] .privacy-banner-btn {
  background: #0d6efd;
  color: #ffffff;
}


html {
  /* Drop the 16px Bootstrap default to 13px so every `rem`-sized
     piece of UI (panels, HUD, dock icons, thumbnail cards, etc.)
     gets a touch more compact. Bootstrap's components scale
     gracefully with this. */
  font-size: 13px;
}

html, body {
  height: 100%;
  margin: 0;
  background: #000;
  color: #eee;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}

/* Font-family overrides driven by the appearance preset dropdown.
   `data-font` is set on <html> by main.js applying the chosen
   preset; the body's text inherits the family except where a
   panel / element pins its own (HUD, dock badges, monospace
   widgets stay on `ui-monospace` regardless). */
html[data-font="mono"] body {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
html[data-font="serif"] body {
  font-family: Georgia, "Times New Roman", "Source Serif 4", serif;
}
html[data-font="rounded"] body {
  font-family: ui-rounded, "SF Pro Rounded", "Hiragino Maru Gothic ProN",
               "Quicksand", "Comfortaa", system-ui, sans-serif;
}
html[data-font="narrow"] body {
  font-family: "Inter", "Helvetica Neue", "Arial Narrow",
               ui-sans-serif, system-ui, sans-serif;
  font-stretch: 90%;
  letter-spacing: 0.005em;
}

/* `.dev-only` is reserved for chrome we genuinely only want behind
   the `?dev` URL flag (e.g. the world-axes gizmo). The appearance
   preset selector below is no longer scoped to that flag. */
.dev-only { display: none; }
html[data-dev="1"] .dev-only { display: block; }

/* `.mobile-only` is the inverse — hidden on every viewport above
   the breakpoint, revealed inside the @media (max-width: 700px)
   block at the foot of this file. Currently used for the dock
   hamburger triggers, the account/privacy/logout drawer entries,
   and the divider that separates them from the tool icons.
   The `.dock-icon.mobile-only` companion below outranks the bare
   `.dock-icon { display: inline-flex }` rule further down in this
   file, which would otherwise keep the account anchors visible on
   desktop because of CSS source-order tie-breaking. */
.mobile-only { display: none; }
.dock-icon.mobile-only { display: none; }

/* Live-stream control group: grouped dock icons (play/stop, Active Stream, Subscribed
   Models, Stream File) that only matter once the user has connected to a stream.
   The container itself is hidden by default (toggled via [hidden] in JS on
   vixn:stream-connected / Disconnect) and gets a green border to make it obvious
   which icons belong to the live pipeline. The inner icons keep their normal
   .dock-icon styling — the only group-level chrome is the wrapper. */
.dock-live-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 0.35rem 0.25rem;
  margin: 0.25rem 0;
  border: 2px solid #2ea043;            /* GitHub-green; works in both themes */
  border-radius: 0.5rem;
  background: rgba(46, 160, 67, 0.08);  /* faint green wash so the border isn't lonely */
}
html[data-theme="light"] .dock-live-group {
  background: rgba(46, 160, 67, 0.12);
}
.dock-live-group[hidden] { display: none !important; }

/* Dock overflow: the standard panel icons. Normally shown inline as a vertical
   stack; while a stream is connected they collapse behind the ⋮ toggle. */
.dock-overflow-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
#dock-overflow-toggle { display: none; }   /* only appears in stream-focus */

/* Stream connected → focus the dock on the live icons; ⋮ reveals the rest. */
#dock.stream-focus #dock-overflow-toggle { display: inline-flex; }
#dock.stream-focus #dock-overflow-group { display: none; }
#dock-overflow-toggle[aria-expanded="true"] {
  background: rgba(110, 168, 254, 0.25);
  border-color: rgba(110, 168, 254, 0.6);
}
#dock.stream-focus.overflow-open #dock-overflow-group {
  display: flex;
  position: absolute;
  left: calc(100% + 0.5rem);
  top: 0;
  padding: 0.4rem;
  border-radius: 0.6rem;
  background: rgba(20, 22, 28, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  /* The popup only holds the two flyout toggles (Workbench / AI and models), so it
     never needs to scroll — and it MUST NOT clip: their sub-flyouts pop out to the
     right, and `overflow-y: auto` would promote overflow-x to auto and hide them. */
  overflow: visible;
  z-index: 1060;
}
html[data-theme="light"] #dock.stream-focus.overflow-open #dock-overflow-group {
  background: rgba(255, 255, 255, 0.97);
  border-color: rgba(0, 0, 0, 0.12);
}

/* Grouped dock sections (Workbench, AI and models): a single icon that expands
   its member panel icons in a flyout to the right. Mirrors the ⋮ overflow popup;
   wired by setupDockFlyout in js/ui.js. */
.dock-flyout { position: relative; }
.dock-flyout-group { display: none; }
.dock-flyout.open > .dock-flyout-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  position: absolute;
  left: calc(100% + 0.5rem);
  /* The dock is vertically centred, so a flyout anchored at the toggle's top
     would grow downward off the bottom of the screen and clip its last icon.
     Centre the flyout on its toggle and cap it to the viewport (scrolls if it
     still doesn't fit) so every icon stays reachable. */
  top: 50%;
  transform: translateY(-50%);
  padding: 0.4rem;
  border-radius: 0.6rem;
  background: rgba(20, 22, 28, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  max-height: 90vh;
  overflow-y: auto;
  z-index: 1060;
}
html[data-theme="light"] .dock-flyout.open > .dock-flyout-group {
  background: rgba(255, 255, 255, 0.97);
  border-color: rgba(0, 0, 0, 0.12);
}
.dock-flyout-toggle[aria-expanded="true"] {
  background: rgba(110, 168, 254, 0.25);
  border-color: rgba(110, 168, 254, 0.6);
}

/* Visual divider between tool dock-icons and the account dock-icons
   (Account / Privacy / Logout). Only meaningful when the drawer is
   open on mobile; the wrapper `.mobile-only` keeps it hidden on
   desktop. */
.dock-separator {
  border: 0;
  height: 1px;
  background: rgba(255, 255, 255, 0.18);
  width: 80%;
  margin: 0.4rem auto;
  align-self: center;
  flex-shrink: 0;
}
html[data-theme="light"] .dock-separator {
  background: rgba(0, 0, 0, 0.18);
}

/* Appearance preset selector. Always visible; bottom-left of the
   viewport so it doesn't compete with the dock / panels / file
   banner. */
#dev-preset {
  position: fixed;
  bottom: 0.5rem;
  left: 0.5rem;
  width: auto;
  max-width: 16rem;
  z-index: 50;
  background: rgba(20, 22, 28, 0.85);
  color: #e9ecef;
  border-color: rgba(255, 255, 255, 0.15);
  font-size: 0.75rem;
  padding: 0.25rem 1.6rem 0.25rem 0.55rem;
}

html[data-theme="light"] #dev-preset {
  background: rgba(255, 255, 255, 0.95);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.18);
}

@media (max-width: 700px) {
  /* On phone-sized viewports both the version badge (bottom-right)
     and the appearance preset chip (bottom-left) compete with the
     bottom-pinned dock + action-dock for limited screen real
     estate. Hide them — desktop users still get them. */
  #dev-preset { display: none !important; }
}

#gl-canvas {
  display: block;
  width: 100vw;
  height: 100vh;
  background: linear-gradient(180deg, #0b1020 0%, #1a2238 100%);
  touch-action: none;
}
#gl-canvas.over-surface { cursor: crosshair; }

/* World-axes overlay labels. Positioned by JS via translate(); the
   wrapper is `pointer-events: none` so it never blocks the canvas. */
#axes-overlay .axis-label {
  position: absolute;
  left: 0;
  top: 0;
  font: 600 0.78rem ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: 0.04em;
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
  display: none;
  pointer-events: none;
  user-select: none;
}
#axes-overlay .axis-label.visible { display: block; }
#axes-overlay .axis-x { color: #ff4d4d; }
#axes-overlay .axis-y { color: #34d399; }
#axes-overlay .axis-z { color: #6ea8fe; }

/* Vertical / horizontal dashed guide lines that follow the cursor
   over a rendered surface. Hidden by default; toggled via the
   `visible` class from `handleMouseMove` / `handleMouseLeave`. */
.crosshair-line {
  position: fixed;
  pointer-events: none;
  z-index: 5;
  border-style: dashed;
  border-color: rgba(110, 168, 254, 0.55);
  border-width: 0;
  display: none;
  will-change: transform;
}
.crosshair-line.crosshair-v {
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
  border-left-width: 1px;
}
.crosshair-line.crosshair-h {
  left: 0;
  right: 0;
  top: 0;
  height: 0;
  border-top-width: 1px;
}
.crosshair-line.visible { display: block; }
html[data-theme="light"] .crosshair-line {
  border-color: rgba(13, 110, 253, 0.45);
}

/* ---------- Boot splash ------------------------------------------ */

#splash {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, #0b1020 0%, #1a2238 100%);
  /* 1.8 s show, then fade out. `forwards` keeps the final keyframe
     so the overlay stays invisible and click-through afterwards. */
  animation: splash-fade 0.45s ease 1.8s forwards;
}
@keyframes splash-fade {
  from { opacity: 1; pointer-events: auto; }
  to   { opacity: 0; pointer-events: none; visibility: hidden; }
}

#splash .splash-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.6rem;
  color: #e9ecef;
  font-family: -apple-system, "SF Pro Display", "Segoe UI", system-ui,
               "Helvetica Neue", Arial, sans-serif;
  letter-spacing: 0.04em;
  /* Subtle entrance so the splash doesn't snap in. */
  animation: splash-rise 0.6s ease-out both;
}
@keyframes splash-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

#splash .splash-logo {
  display: inline-flex;
  align-items: stretch;
  gap: 1rem;
}
#splash .splash-stack {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
}
#splash .splash-line-1 {
  font-size: 2.2rem;
  font-weight: 600;
  line-height: 1;
}
#splash .splash-line-2 {
  font-size: 1.05rem;
  font-weight: 300;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(233, 236, 239, 0.7);
  margin-top: 0.2rem;
}
#splash .splash-divider {
  align-self: stretch;
  width: 1px;
  background: currentColor;
  opacity: 0.45;
}
#splash .splash-phi {
  display: inline-flex;
  align-items: flex-start;
  font-family: "Courier New", Courier, monospace;
  font-size: 3.4rem;
  font-weight: 200;
  line-height: 1;
  color: #34d399;
}

#splash .splash-company-logo {
  /* Anchored to the top of the divider — same as the φ — so the
     company mark sits in line with "IDent" rather than floating in
     the middle of the wordmark. Sized to fit the 4:1 cropper preset
     (16rem × 4rem) at full bleed; 2.5:1 logos pillarbox to
     ~10rem × 4rem under `object-fit: contain`. */
  height: 4rem;
  width: auto;
  max-width: 16rem;
  object-fit: contain;
  align-self: flex-start;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.45));
}
#splash .splash-divider-company {
  /* The second divider sits between the φ and the company mark — a
     little dimmer than the primary text/φ divider so the eye reads
     IDent dynamics + φ as one unit and the company logo as a
     companion mark beside it. */
  opacity: 0.32;
}

#splash .splash-bar {
  width: 280px;
  height: 4px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 2px;
  overflow: hidden;
}
#splash .splash-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #10b981, #6ee7b7);
  animation: splash-load 1.8s linear forwards;
}
@keyframes splash-load { to { width: 100%; } }

#splash .splash-tagline {
  margin-top: -0.6rem;
  font-size: 0.78rem;
  letter-spacing: 0.32em;
  text-transform: lowercase;
  color: rgba(110, 231, 183, 0.9);
  font-weight: 300;
}
html[data-theme="light"] #splash .splash-tagline {
  color: #15803d;
}

html[data-theme="light"] #splash {
  background: linear-gradient(180deg, #f4f6fb 0%, #e6ecf5 100%);
}
html[data-theme="light"] #splash .splash-card { color: #1f2937; }
html[data-theme="light"] #splash .splash-line-2 { color: #6c757d; }
html[data-theme="light"] #splash .splash-phi { color: #15803d; }

/* ---------- Playback transport (a movable .floating-panel) ---------- */

#play-panel {
  /* Override the .floating-panel default (left-centre): start near top-centre, just under the
     HUD. It's a normal floating panel otherwise — drag by the titlebar, minimise / close. */
  top: 4.5rem;
  left: 50%;
  transform: translateX(-50%);
}
#play-panel .play-rate-btn {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
#play-panel .play-rate-btn.active {
  background: rgba(110, 168, 254, 0.85);
  color: #fff;
  border-color: transparent;
}
#play-panel #play-time { letter-spacing: 0.02em; opacity: 0.85; }

/* In light mode the panel background is bright, so Bootstrap's outline-light
   (white text + border on transparent) disappears. Re-skin the play-panel
   buttons as outline-dark so they stay visible. The .active rate button keeps
   its blue fill in both themes — just give it readable text. */
html[data-theme="light"] #play-panel .btn-outline-light {
  color: #212529;
  border-color: rgba(0, 0, 0, 0.35);
  background-color: transparent;
}
html[data-theme="light"] #play-panel .btn-outline-light:hover,
html[data-theme="light"] #play-panel .btn-outline-light:focus-visible {
  color: #000;
  background-color: rgba(0, 0, 0, 0.06);
  border-color: rgba(0, 0, 0, 0.55);
}
html[data-theme="light"] #play-panel .play-rate-btn.active {
  color: #fff;
  border-color: transparent;
}
html[data-theme="light"] #play-time { color: #212529; }

#play-panel .play-seek {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  margin: 0;
  /* The unfilled track + the filled portion are blended into one
     gradient so the progress line is just the input itself — no
     extra DOM, no fight with the native thumb. JS updates
     `--p` (0..1) on every onTimeUpdate. */
  background:
    linear-gradient(to right,
      #f47b3a 0%,
      #f47b3a calc(var(--p, 0) * 100%),
      rgba(255, 255, 255, 0.18) calc(var(--p, 0) * 100%),
      rgba(255, 255, 255, 0.18) 100%);
  border-radius: 2px;
  outline: none;
  cursor: pointer;
}
#play-panel .play-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #f47b3a;
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  transition: transform 0.12s ease;
}
#play-panel .play-seek::-webkit-slider-thumb:hover,
#play-panel .play-seek:focus-visible::-webkit-slider-thumb {
  transform: scale(1.25);
}
#play-panel .play-seek::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #f47b3a;
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  cursor: pointer;
}
#play-panel .play-seek::-moz-range-track {
  height: 4px;
  background: transparent;  /* the gradient on the input itself draws it */
}

/* ---------- Active-file action dock (semicircular bottom bar) ----- */

.action-dock {
  /* A straight horizontal pill dock centred at the bottom of the render. Items lay out
     in-flow (flex), so a hidden item (e.g. the ensemble icon) collapses with no gap and
     no JS positioning is needed. */
  position: fixed;
  left: 50%;
  bottom: 1.1rem;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.55rem;
  background: rgba(20, 22, 28, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 1.9rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 6;
  pointer-events: auto;
}
html[data-theme="light"] .action-dock {
  background: rgba(255, 255, 255, 0.9);
  border-color: rgba(0, 0, 0, 0.12);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.16);
}

.action-dock[hidden] { display: none; }

.action-item {
  /* In-flow round icons in the flex pill (no JS-driven radial transform any more). */
  position: static;
  width: 2.8rem;
  height: 2.8rem;
  margin: 0;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 50%;
  background: rgba(20, 22, 28, 0.85);
  color: #e9ecef;
  font-size: 1.3rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  transform: none;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
              background 0.18s ease,
              box-shadow 0.18s ease,
              color 0.18s ease;
}

.action-item:hover,
.action-item:focus-visible {
  outline: none;
  background: rgba(110, 168, 254, 0.92);
  color: #fff;
  z-index: 1;
  transform: translateY(-4px) scale(1.12);
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.55);
}

/* Toggle buttons in the dock dim their icon (and set aria-pressed) when the
   corresponding layer is hidden. */
.action-item.is-off { color: rgba(233, 236, 239, 0.45); }
.action-item.is-off:hover,
.action-item.is-off:focus-visible { color: #fff; }
html[data-theme="light"] .action-item.is-off { color: rgba(33, 37, 41, 0.4); }

/* Ensemble members panel — legend + per-bot show/hide for a brahma pool run. */
/* `.action-item` sets display:inline-flex, which beats the UA [hidden] rule, so the
   ensemble dock icon needs an explicit hide until a pool run is active. */
.action-item[hidden] { display: none; }
#panel-ensemble-runs { width: 18rem; }
/* ML/GA run-setup panel — wider than the default panel and scrolls (the form is tall). */
#panel-run-submit { width: 30rem; max-width: 95vw; }
#panel-run-submit .panel-body { max-height: 78vh; overflow-y: auto; }
.ensemble-run-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0; }
.ensemble-run-total {
  border-bottom: 1px solid rgba(127, 127, 127, 0.25);
  padding-bottom: 0.3rem; margin-bottom: 0.2rem;
}
.ensemble-run-swatch {
  width: 0.85rem; height: 0.85rem; border-radius: 2px; flex: 0 0 auto;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.ensemble-run-name {
  flex: 1 1 auto; font-size: 0.8rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ensemble-run-eye {
  background: none; border: 0; color: inherit; cursor: pointer;
  padding: 0 0.2rem; font-size: 1rem; line-height: 1; opacity: 0.85;
}
.ensemble-run-eye.is-off { opacity: 0.4; }
.ensemble-run-eye:hover, .ensemble-run-eye:focus-visible { opacity: 1; }

/* Disabled action item (e.g. score analysis with no labels yet): greyed, no hover lift,
   not-allowed cursor — still shows its title tooltip on hover. */
.action-item:disabled,
.action-item[disabled] {
  color: rgba(233, 236, 239, 0.3);
  cursor: not-allowed;
  opacity: 0.6;
}
.action-item:disabled:hover,
.action-item[disabled]:hover,
.action-item:disabled:focus-visible {
  background: rgba(20, 22, 28, 0.85);
  color: rgba(233, 236, 239, 0.3);
  transform: var(--base-transform, none);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}
html[data-theme="light"] .action-item:disabled,
html[data-theme="light"] .action-item[disabled] { color: rgba(33, 37, 41, 0.3); }

/* Soft "pop" for the immediate neighbours of the hovered icon, so
   the dock feels like a connected arc rather than a row of buttons. */
.action-item:hover + .action-item,
.action-item:has(+ .action-item:hover) {
  background: rgba(110, 168, 254, 0.35);
}

html[data-theme="light"] .action-item {
  background: rgba(255, 255, 255, 0.92);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

html[data-theme="light"] .action-item:hover,
html[data-theme="light"] .action-item:focus-visible {
  background: #0d6efd;
  color: #fff;
  box-shadow: 0 10px 22px rgba(13, 110, 253, 0.35);
}

/* Top-right floating chrome. */

#view-reset,
#theme-toggle {
  width: 3rem;
  height: 3rem;
  font-size: 1.25rem;
  line-height: 1;
}

#top-right-controls {
  z-index: 1050;
}

/* User badge in the top-right chrome. */
#user-badge {
  font-size: 0.78rem;
  padding: 0.3rem 0.6rem;
  background: rgba(20, 22, 28, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.4rem;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  white-space: nowrap;
  color: #e9ecef;
}

#user-badge .user-name {
  font-weight: 600;
  color: #fff;
}

#user-badge .user-sep {
  margin: 0 0.35rem;
  color: #adb5bd;
}

#user-badge a {
  color: #3dd68c;
  text-decoration: none;
}
#user-badge a:hover { text-decoration: underline; }

html[data-theme="light"] #user-badge {
  background: rgba(255, 255, 255, 0.85);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] #user-badge .user-name {
  color: #212529;
}

html[data-theme="light"] #user-badge a {
  color: #198754;
}

/*
 * Top-left wordmark.
 * Layout: [IDent / dynamics stack] | [VIXN]
 * The divider extends slightly past the top of "IDent" and the
 * bottom of "dynamics" via vertical padding on the container. VIXN
 * is sized to roughly match the bar height.
 */
#app-logo {
  /* Sit just above the canvas background but behind every panel /
     dock / HUD so opening a window covers the logo cleanly. */
  z-index: 5;
  display: inline-flex;
  flex-direction: row;
  align-items: stretch;
  gap: 0.6rem;
  padding: 0.35rem 0;
  line-height: 1;
  user-select: none;
  pointer-events: none;
  color: #3dd68c;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 700;
}

#app-logo .logo-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.15rem;
  /* #app-logo itself is pointer-events:none so it never blocks the canvas; the wordmark, though,
     is a link home (see app_version_badge_html in auth.php), so re-enable hits just here. */
  pointer-events: auto;
  cursor: pointer;
}

#app-logo .logo-line-1 {
  font-size: 1.35rem;
  letter-spacing: 0.04em;
}

#app-logo .logo-line-2 {
  font-size: 0.95rem;
  letter-spacing: 0.18em;
  font-weight: 500;
  opacity: 0.9;
  text-transform: lowercase;
}

/* Verb list under "dynamics" — currently hidden, but kept in the
   layout (visibility:hidden, not display:none) so the logo's
   vertical divider retains its full height. */
#app-logo .logo-verb {
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: lowercase;
  visibility: hidden;
}

#app-logo .logo-verb-1 {
  margin-top: 0.55rem;
  opacity: 0.75;
}

#app-logo .logo-verb-2 {
  opacity: 0.5;
}

#app-logo .logo-verb-3 {
  opacity: 0.25;
}

#app-logo .logo-divider {
  align-self: stretch;
  width: 1px;
  background: currentColor;
  opacity: 0.45;
}

#app-logo .logo-phi {
  display: inline-flex;
  /* Anchor the φ at the top of the divider rather than centering. */
  align-items: flex-start;
  font-family: "Courier New", Courier, monospace;
  font-size: 2.4rem;
  font-weight: 200;
  line-height: 1;
  padding-top: -0.5rem

  /* Color inherits from #app-logo so it matches IDent / dynamics. */
}

#app-logo .company-logo {
  /* Vertically anchored to the top of the divider so the logo sits
     in line with the "IDent" wordmark. Sized to give corporate
     marks real prominence — the box accommodates the full 4:1 crop
     preset (12rem × 3rem) and pillarboxes a 2.5:1 logo to
     ~7.5rem × 3rem with `object-fit: contain`. */
  height: 3rem;
  width: auto;
  max-width: 12rem;
  object-fit: contain;
  align-self: flex-start;
  margin-left: 0.2rem;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
}
#app-logo .company-divider {
  /* The first logo-divider has its own bespoke styles; this second
     one inherits but uses a slightly slimmer alpha so the company
     logo doesn't read as a hard split between two equal brands. */
  opacity: 0.3;
}

@media (max-width: 700px) {
  #app-logo {
    display: none;
  }
}

html[data-theme="light"] #app-logo {
  color: #198754;
}

#view-reset:focus-visible,
#theme-toggle:focus-visible {
  outline: 2px solid #6ea8fe;
  outline-offset: 2px;
}

/* HUD + hint + label tags + loading overlay. */

#hint {
  z-index: 10;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

#hint.d-none {
  display: none !important;
}

#hud {
  z-index: 20;
  font-size: 0.7rem;
}

#hud .font-monospace {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

#hud .small {
  font-size: 0.7rem;
}

#label-tags {
  z-index: 15;
  overflow: hidden;
}

#label-tags .label-tag {
  position: absolute;
  transform: translate(-50%, -100%);
  margin-top: -4px;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 0.65rem;
  line-height: 1;
  font-weight: 600;
  background: rgba(0, 0, 0, 0.65);
  white-space: nowrap;
  letter-spacing: 0.02em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

#loading {
  z-index: 1100;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
}

#loading.visible {
  display: flex !important;
}

#status-container {
  z-index: 30;
  pointer-events: none;
  max-width: min(640px, 90vw);
}

#status-container .alert {
  pointer-events: auto;
  transition: opacity 600ms ease;
}

#status-container .alert.fading {
  opacity: 0;
}

/* Centred system-message notices (super-broadcast; see js/system-messages.js).
   Same Bootstrap .alert look as the transient status toast, but pinned to the
   middle of the screen, stacked, and dismissible. */
#system-messages {
  z-index: 250;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  width: min(560px, 92vw);
}
#system-messages .system-message {
  pointer-events: auto;
  position: relative;
  padding-right: 2.2rem;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
  transition: opacity 350ms ease;
}
#system-messages .system-message.fading { opacity: 0; }
#system-messages .system-message-title { font-weight: 700; margin-bottom: 0.2rem; }
#system-messages .system-message-body { white-space: normal; word-break: break-word; }
#system-messages .system-message-close {
  position: absolute;
  top: 0.1rem;
  right: 0.4rem;
  border: 0;
  background: transparent;
  color: inherit;
  font-size: 1.35rem;
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
  opacity: 0.6;
}
#system-messages .system-message-close:hover { opacity: 1; }

#gl-canvas.drag-over {
  outline: 3px dashed #6ea8fe;
  outline-offset: -12px;
}

#gl-canvas.selecting {
  cursor: crosshair;
}

/* Rubber-band rectangle drawn during a Ctrl-drag. */
#selection-rect {
  position: fixed;
  pointer-events: none;
  z-index: 1100;
  border: 1.5px dashed #6ea8fe;
  background: rgba(110, 168, 254, 0.18);
  border-radius: 0.15rem;
}

#selection-rect[hidden] { display: none; }

/* Selection-modal + sort-modal + streams-files-modal + labels-import-modal
   overlay. All four share the same dark-card chrome so the app's modals
   feel like one family. The streams-files and labels-import variants are
   wider and host their own internal layouts. */
#selection-modal,
#sort-modal,
#streams-files-modal,
#labels-import-modal {
  position: fixed;
  inset: 0;
  z-index: 1500;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

#selection-modal[hidden],
#sort-modal[hidden],
#streams-files-modal[hidden],
#labels-import-modal[hidden] { display: none !important; }

#selection-modal .selection-card,
#sort-modal .selection-card,
#streams-files-modal .selection-card,
#labels-import-modal .selection-card {
  width: min(28rem, 100%);
  background: rgba(20, 22, 28, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.45rem;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.6);
  color: #e9ecef;
  overflow: hidden;
}

/* The browse list needs room for four columns; widen the card and let
   it grow tall enough that mid-size folders don't scroll the page. */
#streams-files-modal .selection-card.streams-files-card {
  width: min(48rem, 100%);
  max-height: calc(100vh - 2rem);
  display: flex;
  flex-direction: column;
}

#selection-modal .selection-header,
#sort-modal .selection-header,
#streams-files-modal .selection-header,
#labels-import-modal .selection-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0.85rem;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

#selection-modal .selection-header h3,
#sort-modal .selection-header h3,
#streams-files-modal .selection-header h3,
#labels-import-modal .selection-header h3 {
  margin: 0;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #adb5bd;
  font-weight: 600;
}

#selection-modal #sel-close,
#sort-modal #sort-close,
#streams-files-modal #streams-files-close,
#labels-import-modal #labels-import-close {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
}

#selection-modal #sel-close:hover,
#sort-modal #sort-close:hover,
#streams-files-modal #streams-files-close:hover,
#labels-import-modal #labels-import-close:hover { color: #fff; }

/* Body of the Browse modal — vertical stack of status text, data
   table, and the actions row. The card itself is flex-column so the
   table-wrap can shrink when the viewport's tight. */
#streams-files-modal .streams-files-body {
  padding: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-height: 0;
}

#streams-files-modal .streams-files-status {
  margin: 0;
  font-size: 0.78rem;
  color: #adb5bd;
}

/* Cap the table at the available card height; the data-table-wrap's
   own max-height: 50vh still applies if it's smaller. */
#streams-files-modal .data-table-wrap {
  flex: 1 1 auto;
  min-height: 0;
}

#streams-files-modal .data-table .col-name {
  word-break: break-all;
  max-width: 22rem;
}
#streams-files-modal .data-table .col-size,
#streams-files-modal .data-table .col-modified {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: #adb5bd;
}
#streams-files-modal .data-table .col-modified {
  width: 11rem;
}
#streams-files-modal .data-table .col-size {
  width: 6rem;
}
#streams-files-modal .data-table .col-actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
#streams-files-modal .data-table .col-actions .btn {
  font-size: 0.72rem;
  padding: 0.15rem 0.5rem;
  margin-left: 0.3rem;
}

#selection-modal form,
#sort-modal form {
  padding: 0.9rem;
}

#selection-modal .selection-bounds {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.25rem 0.6rem;
  font-size: 0.78rem;
  color: #adb5bd;
  margin: 0 0 0.85rem;
  padding: 0.45rem 0.6rem;
  background: rgba(255, 255, 255, 0.03);
  border-radius: 0.3rem;
}

#selection-modal .selection-bounds dt {
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
  font-size: 0.7rem;
  color: #6c757d;
}

#selection-modal .selection-bounds dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  color: #fff;
}

#selection-modal .form-row { margin-bottom: 0.7rem; }

#selection-modal .form-row label {
  display: block;
  font-size: 0.75rem;
  color: #adb5bd;
  margin-bottom: 0.2rem;
}

#selection-modal .form-row input,
#selection-modal .form-row textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 0.4rem 0.55rem;
  background: #0f1116;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.3rem;
  font-size: 0.85rem;
  font-family: inherit;
  resize: vertical;
}

#selection-modal .form-row input:focus,
#selection-modal .form-row textarea:focus {
  outline: none;
  border-color: #6ea8fe;
  box-shadow: 0 0 0 0.15rem rgba(110, 168, 254, 0.22);
}

#selection-modal .selection-actions,
#sort-modal .selection-actions,
#labels-import-modal .selection-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.4rem;
}

#selection-modal .btn-primary,
#sort-modal .btn-primary,
#labels-import-modal .btn-primary {
  background: #6ea8fe;
  color: #052c65;
  border: 0;
  border-radius: 0.3rem;
  padding: 0.4rem 0.95rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
#selection-modal .btn-primary:hover,
#sort-modal .btn-primary:hover,
#labels-import-modal .btn-primary:hover { filter: brightness(1.07); }

#selection-modal .btn-secondary,
#sort-modal .btn-secondary,
#labels-import-modal .btn-secondary {
  background: transparent;
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 0.3rem;
  padding: 0.4rem 0.85rem;
  font-size: 0.85rem;
  cursor: pointer;
}
#selection-modal .btn-secondary:hover,
#sort-modal .btn-secondary:hover,
#labels-import-modal .btn-secondary:hover { border-color: #6ea8fe; color: #6ea8fe; }

/* Sort-modal-specific layout. */

#sort-modal .sort-intro {
  margin: 0 0 0.7rem;
  font-size: 0.78rem;
  color: #adb5bd;
}

#sort-modal .sort-options {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-bottom: 0.6rem;
}

#sort-modal .sort-option {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid #2a2f3a;
  border-radius: 0.3rem;
  background: #0f1116;
  cursor: pointer;
  font-size: 0.82rem;
}

#sort-modal .sort-option:hover { border-color: #6ea8fe; }

#sort-modal .sort-option input[type="radio"] {
  margin-top: 0.2rem;
  accent-color: #6ea8fe;
}

#sort-modal .sort-option-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 1;
  min-width: 0;
}

#sort-modal .sort-option-body code {
  color: #e9ecef;
  background: transparent;
  font-size: 0.82rem;
  padding: 0;
}

#sort-modal .sort-example {
  font-size: 0.72rem;
  color: #6c757d;
}

#sort-modal .sort-custom-label {
  font-size: 0.72rem;
  color: #adb5bd;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

#sort-modal #sort-custom {
  width: 100%;
  box-sizing: border-box;
  padding: 0.3rem 0.45rem;
  background: #1a1d24;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.25rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
}

#sort-modal #sort-custom:focus {
  outline: none;
  border-color: #6ea8fe;
  box-shadow: 0 0 0 0.15rem rgba(110, 168, 254, 0.22);
}

#sort-modal .sort-match-count {
  margin: 0 0 0.6rem;
  font-size: 0.75rem;
  color: #6c757d;
  font-variant-numeric: tabular-nums;
  min-height: 1em;
}

/* Labels-import modal: wider card so the preview row + column mapping
   selects don't crush together horizontally. The body is a vertical
   stack with a horizontally scrollable preview region in the middle —
   the table can be wider than the card on files with many columns. */
#labels-import-modal .selection-card.labels-import-card {
  width: min(48rem, 100%);
  max-height: calc(100vh - 2rem);
  display: flex;
  flex-direction: column;
}

#labels-import-modal .labels-import-body {
  padding: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  min-height: 0;
  overflow-y: auto;
}

#labels-import-modal .labels-import-intro {
  margin: 0;
  font-size: 0.78rem;
  color: #adb5bd;
}

#labels-import-modal .labels-import-source-label {
  margin: 0;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #6c757d;
}

#labels-import-modal textarea.labels-import-source {
  width: 100%;
  max-height: 10rem;
  min-height: 5rem;
  resize: vertical;
  background: #0f1116;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.3rem;
  padding: 0.45rem 0.55rem;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 0.74rem;
  line-height: 1.35;
  white-space: pre;
  overflow: auto;
}

#labels-import-modal textarea.labels-import-source:focus {
  outline: none;
  border-color: #6ea8fe;
}

#labels-import-modal .labels-import-format {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1.4rem;
  align-items: center;
  font-size: 0.78rem;
  color: #adb5bd;
}

#labels-import-modal .labels-import-format label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
}

#labels-import-modal .labels-import-format select {
  background: #1a1d24;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.25rem;
  padding: 0.2rem 0.45rem;
  font-size: 0.78rem;
}

#labels-import-modal .labels-import-format input[type="checkbox"] {
  accent-color: #6ea8fe;
}

#labels-import-modal .labels-import-preview-wrap {
  overflow-x: auto;
  border: 1px solid #2a2f3a;
  border-radius: 0.3rem;
  background: #0f1116;
}

#labels-import-modal .labels-import-preview {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  font-size: 0.78rem;
}

#labels-import-modal .labels-import-preview th,
#labels-import-modal .labels-import-preview td {
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid #2a2f3a;
  vertical-align: middle;
  white-space: nowrap;
}

#labels-import-modal .labels-import-preview tbody tr:last-child td {
  border-bottom: 0;
}

#labels-import-modal .labels-import-preview thead th {
  background: rgba(255, 255, 255, 0.04);
}

#labels-import-modal .labels-import-preview select {
  width: 100%;
  background: #1a1d24;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.25rem;
  padding: 0.18rem 0.35rem;
  font-size: 0.74rem;
}

#labels-import-modal .labels-import-preview .col-skip {
  color: #6c757d;
}

#labels-import-modal .labels-import-preview .col-mapped {
  color: #6ea8fe;
  font-variant-numeric: tabular-nums;
}

#labels-import-modal .labels-import-preview .label-cell-header {
  color: #adb5bd;
  font-style: italic;
}

#labels-import-modal .labels-import-count {
  margin: 0;
  font-size: 0.75rem;
  color: #6c757d;
  font-variant-numeric: tabular-nums;
  min-height: 1em;
}

#labels-import-modal .labels-import-defaults,
#labels-import-modal .labels-import-ignore {
  border: 1px solid #2a2f3a;
  border-radius: 0.3rem;
  padding: 0.45rem 0.7rem 0.6rem;
  margin: 0;
}

#labels-import-modal .labels-import-defaults legend,
#labels-import-modal .labels-import-ignore legend {
  padding: 0 0.4rem;
  font-size: 0.72rem;
  color: #adb5bd;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

#labels-import-modal .labels-import-ignore-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.4rem;
}

#labels-import-modal .labels-import-ignore-list.is-empty {
  font-size: 0.75rem;
  color: #6c757d;
  font-style: italic;
}

#labels-import-modal .labels-import-ignore-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  padding: 0.18rem 0.45rem;
  background: #1a1d24;
  border: 1px solid #2a2f3a;
  border-radius: 999px;
  font-size: 0.74rem;
  color: #e9ecef;
  cursor: pointer;
  user-select: none;
  line-height: 1.2;
}

#labels-import-modal .labels-import-ignore-chip:hover {
  border-color: #6ea8fe;
}

#labels-import-modal .labels-import-ignore-chip input[type="checkbox"] {
  margin: 0;
  accent-color: #dc3545;
}

#labels-import-modal .labels-import-ignore-chip .labels-import-ignore-chip-name {
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#labels-import-modal .labels-import-ignore-chip .labels-import-ignore-chip-count {
  font-variant-numeric: tabular-nums;
  color: #6c757d;
  font-size: 0.7rem;
}

#labels-import-modal .labels-import-ignore-chip.is-ignored {
  background: rgba(220, 53, 69, 0.15);
  border-color: rgba(220, 53, 69, 0.6);
  color: #adb5bd;
}

#labels-import-modal .labels-import-ignore-chip.is-ignored .labels-import-ignore-chip-name {
  text-decoration: line-through;
}

#labels-import-modal .labels-import-defaults-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.45rem 0.7rem;
}

#labels-import-modal .labels-import-defaults-grid label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.72rem;
  color: #adb5bd;
  margin: 0;
}

#labels-import-modal .labels-import-defaults-grid input {
  background: #1a1d24;
  color: #e9ecef;
  border: 1px solid #2a2f3a;
  border-radius: 0.25rem;
  padding: 0.28rem 0.45rem;
  font-size: 0.8rem;
}

#labels-import-modal .labels-import-defaults-grid input:focus {
  outline: none;
  border-color: #6ea8fe;
  box-shadow: 0 0 0 0.15rem rgba(110, 168, 254, 0.22);
}

/* Light-theme overrides for the bits that have their own background or
   border colours. The card chrome inherits from the shared selectors
   above; only the inner control surfaces need light variants. */
html[data-theme="light"] #labels-import-modal .labels-import-format select,
html[data-theme="light"] #labels-import-modal .labels-import-preview select,
html[data-theme="light"] #labels-import-modal .labels-import-defaults-grid input,
html[data-theme="light"] #labels-import-modal textarea.labels-import-source {
  background: #fff;
  color: #212529;
  border-color: rgba(0, 0, 0, 0.15);
}

html[data-theme="light"] #labels-import-modal .labels-import-preview-wrap,
html[data-theme="light"] #labels-import-modal .labels-import-defaults,
html[data-theme="light"] #labels-import-modal .labels-import-ignore {
  background: #fff;
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] #labels-import-modal .labels-import-ignore-chip {
  background: #f1f3f5;
  border-color: rgba(0, 0, 0, 0.12);
  color: #212529;
}

html[data-theme="light"] #labels-import-modal .labels-import-ignore-chip.is-ignored {
  background: rgba(220, 53, 69, 0.12);
  border-color: rgba(220, 53, 69, 0.45);
  color: #6c757d;
}

html[data-theme="light"] #labels-import-modal .labels-import-preview th,
html[data-theme="light"] #labels-import-modal .labels-import-preview td {
  border-bottom-color: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] #labels-import-modal .labels-import-preview thead th {
  background: rgba(0, 0, 0, 0.04);
}

/* Light theme variants for the modals. */

html[data-theme="light"] #selection-modal,
html[data-theme="light"] #sort-modal,
html[data-theme="light"] #streams-files-modal,
html[data-theme="light"] #labels-import-modal {
  background: rgba(0, 0, 0, 0.35);
}

html[data-theme="light"] #selection-modal .selection-card,
html[data-theme="light"] #sort-modal .selection-card,
html[data-theme="light"] #streams-files-modal .selection-card,
html[data-theme="light"] #labels-import-modal .selection-card {
  background: rgba(255, 255, 255, 0.97);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
}

html[data-theme="light"] #selection-modal .selection-header,
html[data-theme="light"] #sort-modal .selection-header,
html[data-theme="light"] #streams-files-modal .selection-header,
html[data-theme="light"] #labels-import-modal .selection-header {
  background: rgba(0, 0, 0, 0.04);
  border-bottom-color: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] #streams-files-modal .streams-files-status,
html[data-theme="light"] #streams-files-modal .data-table .col-size,
html[data-theme="light"] #streams-files-modal .data-table .col-modified {
  color: #6c757d;
}

html[data-theme="light"] #selection-modal .selection-bounds {
  background: rgba(0, 0, 0, 0.04);
}

html[data-theme="light"] #selection-modal .selection-bounds dd {
  color: #212529;
}

html[data-theme="light"] #selection-modal .form-row input,
html[data-theme="light"] #selection-modal .form-row textarea {
  background: #ffffff;
  color: #212529;
  border-color: #ced4da;
}

html[data-theme="light"] #sort-modal .sort-option {
  background: #ffffff;
  border-color: #ced4da;
}

html[data-theme="light"] #sort-modal .sort-option-body code {
  color: #212529;
}

html[data-theme="light"] #sort-modal #sort-custom {
  background: #ffffff;
  color: #212529;
  border-color: #ced4da;
}

/* ---------- Right-side thumbnail strip ---------- */

#thumbnail-strip {
  position: fixed;
  top: 4.5rem;
  right: 0.75rem;
  bottom: 4rem;
  width: 12rem;
  z-index: 40;
  display: flex;
  flex-direction: column;
  /* Centre the cards vertically inside the strip; `safe` falls back
     to flex-start when the cards overflow so the top one isn't
     clipped. */
  justify-content: safe center;
  gap: 0.5rem;
  /* Extra left padding reserves room for the active file's eye marker, which
     sits just outside each card's left border (and would otherwise be clipped). */
  padding: 0.25rem 0.25rem 0.25rem 1.4rem;
  overflow-y: auto;
  pointer-events: auto;
  /* Strip itself is fully transparent — only the cards have a
     backdrop. Scroll bars get a subtle thumb on dark theme. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

@media (max-width: 700px) {
  #thumbnail-strip {
    display: none;
  }
}

/* ---------- Stream decision-histogram navigator ---------- */

/* In stream mode — and in batch folder-run mode — the histogram replaces the
   classic centred strip. */
body[data-active-stream] #thumbnail-strip,
body[data-folder-project] #thumbnail-strip { display: none; }

#stream-histogram {
  position: fixed;
  top: 4.5rem;
  right: 0.75rem;
  bottom: 4rem;
  width: 12rem;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  pointer-events: auto;
  /* JS (LANE_H) and CSS share this lane/thumbnail height — keep in sync. */
  --lane-h: 26px;
}

#stream-histogram[hidden] { display: none; }

#histo-viewport {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  min-height: 0;
}

/* Bars live on the canvas, pinned to the viewport and redrawn on scroll. It
   sits behind the scrolling card overlay (pointer-events:none so wheel + clicks
   fall through to #histo-scroll). `color` is the single bar accent. */
#histo-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1;
  color: #4aa3ff;
}

/* The scroll owner: transparent so the canvas bars show through the gaps. */
#histo-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  z-index: 2;
  outline: none;
  cursor: pointer;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

/* Instant hover readout for the timeline (and its thumbnails) — used instead
   of the native `title` so it appears without the browser's delay. Follows the
   cursor (positioned in JS) and shows only the file's time. */
.histo-tip {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  transform: translate(12px, 14px);
  padding: 0.15rem 0.4rem;
  font-size: 0.7rem;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: #e9ecef;
  background: rgba(11, 16, 32, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 0.25rem;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.06s linear;
}
.histo-tip.is-visible { opacity: 1; visibility: visible; }

/* Sized in JS to nFiles * LANE_H — drives the scrollbar over the full timeline. */
#histo-content { position: relative; width: 100%; }
#histo-overlay { position: absolute; inset: 0; pointer-events: none; }

/* Compact thumbnail card overlaid on a loaded file's lane. Reuses .thumb-card
   chrome but shrinks to the lane height and drops the name/badges (the bar
   already carries the decision count). */
.histo-thumb.thumb-card {
  position: absolute;
  /* Shifted right to clear the fixed marker gutter (status / green / red / !
     dots are canvas-drawn at the lane's left edge — see MARK_*_X in
     stream-histogram.js — so they line up across loaded + unloaded lanes). */
  left: 2.9rem;
  width: 34%;
  height: var(--lane-h, 26px);
  margin: 0;
  padding: 0;
  border-radius: 0.25rem;
  opacity: 1;
  filter: none;
  overflow: hidden;
  pointer-events: auto;
}
/* Loading placeholder on a lane while its recording decodes (stream bulk load).
   Sits where the .histo-thumb card will land; an indeterminate bar slides across. */
.histo-loading {
  position: absolute;
  left: 0;
  width: 58%;
  height: var(--lane-h, 26px);
  display: flex;
  align-items: center;
  padding: 0 0.25rem;
  border-radius: 0.25rem;
  background: rgba(74, 163, 255, 0.1);
  overflow: hidden;
}
.histo-loading-bar {
  position: relative;
  flex: 1 1 auto;
  height: 4px;
  border-radius: 2px;
  background: rgba(74, 163, 255, 0.18);
  overflow: hidden;
}
.histo-loading-bar::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 40%;
  border-radius: 2px;
  background: #4aa3ff;
  animation: histo-loading-slide 1.1s ease-in-out infinite;
}
@keyframes histo-loading-slide {
  0%   { left: -40%; }
  100% { left: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .histo-loading-bar::after { animation: none; left: 0; width: 100%; opacity: 0.6; }
}

.histo-thumb .thumb-name,
.histo-thumb .thumb-badges { display: none; }
.histo-thumb .thumb-image-row { height: 100%; align-items: stretch; gap: 0.15rem; }
/* The lane's green/red/status markers are canvas-drawn in a fixed gutter (so
   they align across loaded + unloaded lanes), so the card itself doesn't repeat
   them. Selector is deliberately more specific than `.thumb-card .thumb-dots`
   (which sets display:flex later in the file) so this hide actually wins. */
.histo-thumb .thumb-image-row .thumb-dots { display: none; }
.histo-thumb .thumb-image {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  height: 100%;
  border-radius: 0.15rem;
}
.histo-thumb .thumb-placeholder {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  font-size: 0.55rem;
}
.histo-thumb .thumb-remove {
  width: 1rem;
  height: 1rem;
  line-height: 1rem;
}

/* The model selector sits above the lanes. Empty (≤1 model present) it collapses
   to nothing, so single-model streams/projects look exactly as before. */
#histo-controls { flex: 0 0 auto; }
#histo-controls:empty { display: none; }
#histo-controls:not(:empty) {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.45rem;
  background: rgba(20, 22, 28, 0.92);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.5rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}

.histo-model-label {
  margin: 0;
  flex: 0 0 auto;
  font-size: 0.72rem;
  color: #adb5bd;
}

.histo-model-select {
  flex: 1 1 auto;
  min-width: 0;
}

html[data-theme="light"] #histo-controls:not(:empty) {
  background: rgba(255, 255, 255, 0.95);
  border-color: rgba(0, 0, 0, 0.12);
}
html[data-theme="light"] .histo-model-label { color: #6c757d; }

@media (max-width: 700px) {
  #stream-histogram { display: none; }
}

/* ---------- Batch-run progress box (bottom-right, non-blocking) ---------- */
.run-progress {
  position: fixed;
  right: 0.9rem;
  bottom: 0.9rem;
  z-index: 1100;
  width: 24rem;
  min-width: 18rem;
  max-width: calc(100vw - 1.8rem);
  padding: 0.5rem 0.7rem;
  background: rgba(20, 22, 28, 0.92);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.5rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  color: #e9ecef;
  font-size: 0.8rem;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
  pointer-events: none;
}
.run-progress.is-visible { opacity: 1; transform: none; pointer-events: auto; }
.run-progress-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.run-progress-text { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.run-progress-cancel {
  flex: 0 0 auto;
  padding: 0.1rem 0.5rem;
  font-size: 0.72rem;
  line-height: 1.4;
  color: #f1b0b7;
  background: transparent;
  border: 1px solid rgba(220, 53, 69, 0.6);
  border-radius: 0.3rem;
  cursor: pointer;
}
.run-progress-cancel:hover:not(:disabled) { background: #c0392b; color: #fff; border-color: #c0392b; }
.run-progress-cancel:disabled { opacity: 0.6; cursor: default; }
.run-progress-track {
  height: 6px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  overflow: hidden;
}
.run-progress-fill {
  height: 100%;
  width: 0;
  background: rgb(74, 163, 255);
  transition: width 0.2s ease;
}
.run-progress.is-done .run-progress-fill { background: rgb(61, 220, 132); }
/* Average-per-file + ETA line below the bar. Wraps so nothing is clipped. */
.run-progress-detail {
  margin-top: 0.3rem;
  font-size: 0.72rem;
  line-height: 1.3;
  color: #adb5bd;
  white-space: normal;
  overflow-wrap: anywhere;
}
html[data-theme="light"] .run-progress-detail { color: #6c757d; }
html[data-theme="light"] .run-progress {
  background: rgba(255, 255, 255, 0.96);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.12);
}

/* Live-stream paging arrows in the thumbnail strip: ⬆ above the oldest card,
   ⬇ below the latest. flex-shrink:0 so they keep their height among the cards. */
.thumb-load-more {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  width: 100%;
  padding: 0.3rem 0.4rem;
  border: 1px dashed rgba(74, 163, 255, 0.6);
  border-radius: 0.35rem;
  background: rgba(74, 163, 255, 0.1);
  color: #cfe4ff;
  font-size: 0.75rem;
  cursor: pointer;
}
.thumb-load-more:hover { background: rgba(74, 163, 255, 0.2); }
.thumb-load-more:disabled { opacity: 0.5; cursor: default; }
.thumb-load-more span[aria-hidden] { font-size: 0.95rem; line-height: 1; }

/* "Live" toggle: a status dot that goes red + pulses when on. */
.thumb-live-dot {
  width: 0.6rem; height: 0.6rem; border-radius: 50%;
  background: #8a949e; flex: 0 0 auto;
}
.thumb-live.thumb-live-on {
  border-color: rgba(255, 90, 90, 0.7);
  background: rgba(255, 90, 90, 0.14);
  color: #ffd1d1;
}
.thumb-live.thumb-live-on .thumb-live-dot {
  background: #ff5a5a;
  box-shadow: 0 0 0 0 rgba(255, 90, 90, 0.6);
  animation: thumb-live-pulse 1.6s ease-out infinite;
}
@keyframes thumb-live-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255, 90, 90, 0.55); }
  70%  { box-shadow: 0 0 0 0.4rem rgba(255, 90, 90, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 90, 90, 0); }
}

.thumb-card {
  position: relative;
  background: rgba(20, 22, 28, 0.7);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.4rem;
  padding: 0.35rem 0.4rem 0.4rem;
  cursor: pointer;
  outline: none;
  flex-shrink: 0;
  /* Inactive cards are dimmed and softly blurred so the active one
     reads as the focal point. Hover and focus restore full clarity
     so the user can recognise filenames before clicking. */
  opacity: 0.55;
  filter: blur(1.5px);
  transition:
    background 120ms ease,
    border-color 120ms ease,
    opacity 180ms ease,
    filter 180ms ease;
}

.thumb-card:hover,
.thumb-card:focus-visible,
.thumb-card.active {
  opacity: 1;
  filter: none;
}

.thumb-card:hover {
  background: rgba(40, 44, 56, 0.85);
}

.thumb-card:focus-visible {
  outline: 2px solid #6ea8fe;
  outline-offset: 2px;
}

.thumb-card.active {
  background: rgba(61, 220, 132, 0.2);
  border-color: rgba(61, 220, 132, 0.85);
  box-shadow: 0 0 0 1px rgba(61, 220, 132, 0.5);
}

.thumb-card .thumb-name {
  font-size: 0.65rem;
  color: #adb5bd;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 0.25rem;
  padding-right: 1rem; /* leave room for the × */
}

.thumb-card.active .thumb-name {
  color: #fff;
}

.thumb-card .thumb-image {
  width: 100%;
  height: 3rem;
  display: block;
  border-radius: 0.2rem;
  background: #0a0c12;
  image-rendering: pixelated;
  -ms-interpolation-mode: nearest-neighbor;
}

.thumb-card .thumb-placeholder {
  width: 100%;
  height: 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.04);
  color: #6c757d;
  font-size: 0.65rem;
  border-radius: 0.2rem;
}

.thumb-card .thumb-badges {
  position: absolute;
  bottom: 0.5rem;
  right: 0.55rem;
  display: flex;
  gap: 0.2rem;
  pointer-events: none;
}

/* "Currently viewing" eye marker on the active file — transparent, vertically
   centred and sitting just OUTSIDE the card's (green) border, to its left. The
   strip / histogram reserve left space (below) so it isn't clipped. */
.thumb-card .thumb-eye {
  position: absolute;
  left: -1.1rem;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #3ddc84;
  line-height: 1;
  pointer-events: none;
  z-index: 3;
  text-shadow: 0 0 3px rgba(0, 0, 0, 0.85), 0 0 3px rgba(0, 0, 0, 0.85);
}
.thumb-card .thumb-eye i { font-size: 1.05rem; }
/* Compact for the histogram-lane card (≈26 px tall). */
/* The active lane already gets a full-width green highlight band, and the eye
   would collide with the canvas marker gutter — drop it in the histogram. */
.histo-thumb .thumb-eye { display: none; }

.thumb-card .thumb-badge {
  background: rgba(0, 0, 0, 0.7);
  color: #e9ecef;
  font-size: 0.55rem;
  font-weight: 600;
  padding: 0.05rem 0.28rem;
  border-radius: 0.2rem;
  letter-spacing: 0.02em;
}

.thumb-card .thumb-remove {
  position: absolute;
  top: 0.18rem;
  right: 0.3rem;
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0 0.2rem;
  cursor: pointer;
}

.thumb-card .thumb-remove:hover {
  color: #ff7777;
}

.thumb-card .thumb-select {
  position: absolute;
  top: 0.22rem;
  left: 0.3rem;
  display: inline-flex;
  align-items: center;
  margin: 0;
  padding: 0;
  cursor: pointer;
  z-index: 1;
}

.thumb-card .thumb-select input[type="checkbox"] {
  width: 0.85rem;
  height: 0.85rem;
  margin: 0;
  cursor: pointer;
  accent-color: #6ea8fe;
}

/* Image row: dots column on the left, then the thumbnail image fills
   the remaining width. */
.thumb-card .thumb-image-row {
  display: flex;
  align-items: center;
  /* Breathing room between the label/decision dots column and the thumbnail. */
  gap: 0.5rem;
}

.thumb-card .thumb-image-row .thumb-image,
.thumb-card .thumb-image-row .thumb-placeholder {
  flex: 1 1 auto;
  min-width: 0;
}

.thumb-card .thumb-dots {
  display: flex;
  flex-direction: row;          /* green + red side by side, not stacked */
  gap: 0.2rem;
  align-items: center;
  justify-content: center;
  /* Both slots (green, then red) are always present, so the pair is a fixed
     width — keeping the dots in the same spot on every card so they line up
     down the strip. */
  width: 1.1rem;
  flex-shrink: 0;
}

/* An absent label / decision still occupies its slot (so the pair stays the
   same width + the dots stay aligned across rows) but isn't painted. */
.thumb-card .thumb-dot.is-absent { visibility: hidden; }

/* Coloured dots flagging label / decision presence.
   Green = labels, red = decisions. */
.thumb-card .thumb-dot {
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
}
.thumb-card .thumb-dot-labels    { background: #3ddc84; }
.thumb-card .thumb-dot-decisions { background: #ff5d5d; }

/* Make sure the filename has room for both the checkbox (left) and the
   remove × (right). */
.thumb-card .thumb-name {
  padding-left: 1rem;
}

/* The merge panel reuses .floating-panel base styles. Position it
   between the dock and the thumbnail strip so it doesn't cover the
   files it summarises. */
#panel-merge {
  left: auto;
  right: 8rem;
  top: 8rem;
  transform: none;
  width: 18rem;
}

#panel-merge #merge-list li {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.15rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

#panel-merge #merge-list li:last-child {
  border-bottom: 0;
}

#panel-merge #merge-list .merge-row-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#panel-merge #merge-list .merge-row-meta {
  color: #adb5bd;
  font-size: 0.7rem;
  flex-shrink: 0;
}

/* The split panel mirrors the merge panel position and width so the
   two contextual file-actions panels feel consistent. */
#panel-split {
  left: auto;
  right: 8rem;
  top: 8rem;
  transform: none;
  width: 18rem;
}

/* ---------- Vertical icon dock ---------- */

#dock {
  z-index: 1050;
}

.dock-icon {
  position: relative;
  width: 3rem;
  height: 3rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.6rem;
  background: rgba(20, 22, 28, 0.7);
  color: #e9ecef;
  text-decoration: none;            /* `<a>`-styled dock items */
  /* Phosphor renders via font-size; bump up so the dock glyphs read
     at small viewport sizes. */
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition:
    background 150ms ease,
    transform 150ms ease,
    box-shadow 150ms ease;
}

.dock-icon.has-new::after {
  content: 'New';
  position: absolute;
  top: -6px;
  right: -8px;
  background: #f04438;
  color: #fff;
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 999px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}

/* Enterprise-locked dock icon (Agents, for non-enterprise users / guests).
   Dimmed with a small lock badge; the click handler in js/ui.js shows an
   upsell toast instead of opening it. Server-rendered .is-locked from
   is_enterprise() in index.php. */
.dock-icon.is-locked {
  opacity: 0.5;
  filter: grayscale(0.4);
}
.dock-icon.is-locked:hover { opacity: 0.75; }
.dock-lock-badge {
  position: absolute;
  bottom: -4px;
  right: -4px;
  font-size: 0.85rem;
  line-height: 1;
  color: #e9ecef;
  background: rgba(20, 22, 28, 0.95);
  border-radius: 999px;
  padding: 1px;
  pointer-events: none;
}

.dock-icon:hover {
  background: rgba(40, 44, 56, 0.9);
  transform: scale(1.18);
}

.dock-icon.active {
  background: rgba(110, 168, 254, 0.85);
  color: #fff;
  box-shadow: 0 0 0 2px rgba(110, 168, 254, 0.35);
  transform: scale(1.1);
}

.dock-icon.active:hover {
  transform: scale(1.22);
}

.dock-icon:focus-visible {
  outline: 2px solid #6ea8fe;
  outline-offset: 2px;
}

/* ---------- Dock chooser menu (Agents) ---------- */

/* Small popup anchored next to a dock icon (position set inline by JS).
   Mirrors the floating-panel glass treatment so it reads as part of the
   same surface family. */
.dock-menu {
  position: fixed;
  z-index: 200;
  min-width: 14rem;
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  background: rgba(20, 22, 28, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.6rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}

.dock-menu[hidden] { display: none; }

.dock-menu-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: 0;
  border-radius: 0.45rem;
  background: transparent;
  color: #e9ecef;
  text-align: left;
  cursor: pointer;
  transition: background 120ms ease;
}

.dock-menu-item:hover,
.dock-menu-item:focus-visible {
  background: rgba(110, 168, 254, 0.18);
  outline: none;
}

.dock-menu-item > i {
  font-size: 1.25rem;
  line-height: 1;
  color: #6ea8fe;
  flex: 0 0 auto;
}

.dock-menu-item-title {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
}

.dock-menu-item-sub {
  display: block;
  font-size: 0.72rem;
  opacity: 0.7;
}

html[data-theme="light"] .dock-menu {
  background: rgba(255, 255, 255, 0.96);
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] .dock-menu-item { color: #1f2329; }

/* ---------- Floating panels ---------- */

.floating-panel {
  position: fixed;
  width: 320px;
  min-width: 240px;
  max-width: 95vw;
  max-height: 95vh;
  background: rgba(20, 22, 28, 0.82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* Subtle top rounding, more pronounced bottom rounding. */
  border-radius: 0.25rem 0.25rem 0.65rem 0.65rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  color: #e9ecef;
  z-index: 100;
  /* Default open position: just clear of the dock on the left,
     vertically centred on the viewport. JS clears the transform on
     first drag so inline left/top math stays correct (see ui.js). */
  top: 50%;
  left: 6rem;
  transform: translateY(-50%);
  /* User-resizable from the bottom-right grip. The panel-body inside
     handles its own scrolling, so growing the panel reveals more of
     whatever's inside it. Display flex + flex-direction: column lets
     the body fill the resized height. */
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
/* When the panel grows, the body should fill the new height (titlebar
   stays its natural size at the top). */
.floating-panel > .panel-body {
  flex: 1 1 auto;
  min-height: 0;
}

.floating-panel[hidden] {
  display: none !important;
}

/* Half-screen panel (e.g. #panel-listening-agents). Opens occupying the
   right half of the viewport; still draggable + resizable like any panel,
   so these are just the open-state defaults. The width/height below are the
   open-state size only — the max-* caps are deliberately generous so the
   bottom-right `resize: both` grip can grow the panel well past half-screen. */
.floating-panel.panel-half {
  width: 50vw;
  max-width: calc(100vw - 2rem);
  height: calc(100vh - 2rem);
  max-height: calc(100vh - 1rem);
  top: 1rem;
  left: auto;
  right: 1rem;
  transform: none;
}

.floating-panel.dragging {
  opacity: 0.92;
  box-shadow: 0 14px 38px rgba(0, 0, 0, 0.55);
}

.panel-titlebar {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  padding: 0.4rem 0.65rem;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  /* Match the panel's slight top-corner rounding. */
  border-radius: 0.25rem 0.25rem 0 0;
  cursor: move;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

.panel-title {
  flex-grow: 1;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: #adb5bd;
}

/* Polling time-frame shown beside the AIS vessels / sensor-map titles. The
   title stops growing for these two panels and this span takes the slack
   instead (so the action buttons stay pinned right whether it's filled or
   empty), with the time frame sitting just after the title text. */
/* Connected stream name appended to a panel title (sensor map / diagnostics). */
.panel-title-stream { color: #6ea8fe; font-weight: 600; }

.panel-title-window {
  flex: 1 1 auto;
  min-width: 0;
  padding-left: 0.4rem;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: #6c757d;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#panel-vessels .panel-title,
#panel-sensor-map .panel-title { flex-grow: 0; }

.panel-close,
.panel-minimise {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
}

.panel-close:hover,
.panel-minimise:hover {
  color: #fff;
}

.panel-close:focus-visible,
.panel-minimise:focus-visible {
  outline: 2px solid #6ea8fe;
  outline-offset: 2px;
}

.floating-panel.minimised .panel-body {
  display: none;
}

.floating-panel.minimised {
  width: auto;
  min-width: 12rem;
}

/* Editor panels are wider than the rest because they host tables. */
#panel-labels-table {
  width: 36rem;
  max-width: 95vw;
}
#panel-decisions-table {
  width: 44rem;
  max-width: 95vw;
}
#panel-log {
  width: 32rem;
  max-width: 95vw;
  /* Default open position: bottom-left corner, just clear of the dock. (Still
     draggable / resizable — the user can move it anywhere.) */
  top: auto;
  bottom: 1rem;
  left: 4.75rem;
  transform: none;
  height: 15rem;
  /* Let the user drag the bottom-right corner to enlarge — long runs
     of brahma-config diagnostics fill the default height quickly. */
  resize: both;
  overflow: hidden;
  min-width: 22rem;
  min-height: 10rem;
  display: flex;
  flex-direction: column;
}

#panel-log .panel-body {
  /* The body owns the resize space: stretch to the panel's height
     (less the title bar) and let the inner table-wrap consume what's
     left after the toolbar row. */
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

#panel-log .log-table-wrap {
  flex: 1 1 auto;
  min-height: 6rem;
  max-height: none;
  overflow: auto;
}

#panel-log .data-table tbody td {
  /* Log rows are read-only; tighten padding so more lines fit. */
  padding: 0.05rem 0.3rem;
}

#log-table .log-time {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #adb5bd;
}

#log-table .log-category {
  text-transform: uppercase;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  color: #adb5bd;
}

#log-table .log-level {
  text-transform: uppercase;
  font-size: 0.62rem;
  letter-spacing: 0.06em;
  font-weight: 600;
}

#log-table .log-level-info    { color: #6c757d; }
#log-table .log-level-success { color: #3dd68c; }
#log-table .log-level-warn    { color: #f0c040; }
#log-table .log-level-error   { color: #ff7777; }

#log-table .log-message {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 24rem;
}

html[data-theme="light"] #log-table .log-time,
html[data-theme="light"] #log-table .log-category {
  color: #6c757d;
}

html[data-theme="light"] #log-table .log-level-info    { color: #495057; }
html[data-theme="light"] #log-table .log-level-success { color: #198754; }
html[data-theme="light"] #log-table .log-level-warn    { color: #997404; }
html[data-theme="light"] #log-table .log-level-error   { color: #b02a37; }

/* ---------- Editable data tables ---------- */

.data-table-wrap {
  max-height: 50vh;
  overflow: auto;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 0.3rem;
}

/* Per-tag colour swatches sitting above the labels table. */
.tag-colors-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0.5rem;
}

.tag-colors-strip.d-none {
  display: none;
}

.tag-color-row {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.45rem 0.15rem 0.2rem;
  border-radius: 0.25rem;
  background: rgba(255, 255, 255, 0.04);
  font-size: 0.72rem;
  white-space: nowrap;
}

.tag-color-row input[type="color"] {
  width: 1.55rem;
  height: 1.4rem;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.2rem;
  cursor: pointer;
  background: transparent;
}

.tag-color-row .tag-name {
  color: #e9ecef;
  letter-spacing: 0.02em;
}

.tag-color-row .tag-visibility {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.3rem;
  height: 1.3rem;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.2rem;
  background: transparent;
  color: #adb5bd;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}

.tag-color-row .tag-visibility:hover {
  color: #e9ecef;
  border-color: rgba(255, 255, 255, 0.25);
}

/* Per-signature "delete all" — same chrome as the eye toggle, danger on hover. */
.tag-color-row .tag-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.3rem;
  height: 1.3rem;
  padding: 0;
  margin-left: auto;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.2rem;
  background: transparent;
  color: #adb5bd;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}

.tag-color-row .tag-delete:hover {
  color: #fff;
  background: #c0392b;
  border-color: #c0392b;
}

html[data-theme="light"] .tag-color-row .tag-delete {
  border-color: rgba(0, 0, 0, 0.15);
  color: #495057;
}

.tag-color-row.is-hidden {
  opacity: 0.55;
}

.tag-color-row.is-hidden .tag-name {
  text-decoration: line-through;
  color: #adb5bd;
}

.tag-color-row.is-hidden .tag-visibility {
  color: #6c757d;
}

html[data-theme="light"] .tag-color-row {
  background: rgba(0, 0, 0, 0.05);
}

html[data-theme="light"] .tag-color-row input[type="color"] {
  border-color: rgba(0, 0, 0, 0.15);
}

html[data-theme="light"] .tag-color-row .tag-name {
  color: #212529;
}

html[data-theme="light"] .tag-color-row .tag-visibility {
  border-color: rgba(0, 0, 0, 0.15);
  color: #495057;
}

html[data-theme="light"] .tag-color-row .tag-visibility:hover {
  color: #212529;
  border-color: rgba(0, 0, 0, 0.3);
}

html[data-theme="light"] .tag-color-row.is-hidden .tag-name {
  color: #6c757d;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.7rem;
}

.data-table thead th {
  padding: 0.3rem 0.4rem;
  text-align: left;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  color: #adb5bd;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  position: sticky;
  top: 0;
  background: rgba(20, 22, 28, 0.95);
  z-index: 1;
}

.data-table tbody td {
  padding: 0.1rem 0.25rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  vertical-align: middle;
}

.data-table tbody tr:hover {
  background: rgba(255, 255, 255, 0.03);
}

/* "Borrowed" rows — records living on another file but projected into
   the active view via wall-clock overlap. Dimmed so the user notices
   they're not local. Hovering brings them back to full opacity. */
.data-table tbody tr.borrowed-row td {
  opacity: 0.65;
}
.data-table tbody tr.borrowed-row:hover td {
  opacity: 1;
}

.data-table .col-src {
  max-width: 8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 0.68rem;
}

.data-table input,
.data-table select {
  background: transparent;
  border: 0;
  color: inherit;
  width: 100%;
  font-size: 0.7rem;
  padding: 0.15rem 0.25rem;
  border-radius: 0.2rem;
  font-family: inherit;
  box-sizing: border-box;
}

.data-table input[type="number"] {
  font-variant-numeric: tabular-nums;
}

.data-table input:focus,
.data-table select:focus {
  background: rgba(110, 168, 254, 0.12);
  outline: 1px solid #6ea8fe;
}

.data-table .row-remove {
  background: transparent;
  border: 0;
  color: #adb5bd;
  cursor: pointer;
  padding: 0 0.35rem;
  font-size: 0.95rem;
  line-height: 1;
}

.data-table .row-remove:hover {
  color: #ff7777;
}

.data-table .row-locate {
  background: transparent;
  border: 0;
  color: #adb5bd;
  cursor: pointer;
  padding: 0 0.35rem;
  font-size: 0.95rem;
  line-height: 1;
}

.data-table .row-locate:hover {
  color: #6ea8fe;
}

html[data-theme="light"] .data-table .row-locate {
  color: #6c757d;
}

.data-table .col-narrow { width: 5rem; }
.data-table .col-mid    { width: 6.5rem; }
.data-table .col-url    { min-width: 7rem; }

html[data-theme="light"] .data-table-wrap {
  border-color: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] .data-table thead th {
  background: rgba(255, 255, 255, 0.95);
  color: #495057;
  border-bottom-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] .data-table tbody td {
  border-bottom-color: rgba(0, 0, 0, 0.04);
}

html[data-theme="light"] .data-table tbody tr:hover {
  background: rgba(0, 0, 0, 0.03);
}

html[data-theme="light"] .data-table input:focus,
html[data-theme="light"] .data-table select:focus {
  background: rgba(13, 110, 253, 0.08);
  outline-color: #0d6efd;
}

html[data-theme="light"] .data-table .row-remove {
  color: #6c757d;
}

html[data-theme="light"] .panel-minimise {
  color: #495057;
}

html[data-theme="light"] .panel-minimise:hover {
  color: #000;
}

.panel-body {
  padding: 0.75rem;
  overflow-y: auto;
  /* No fixed max-height — the parent .floating-panel is now a flex column with
     `max-height: 95vh`, and panel-body has `flex: 1 1 auto`, so it fills whatever
     vertical space the user has resized the panel to. */
}

/* Form-control colour scheme inside panels (replaces the old
   .offcanvas selectors). */

.floating-panel .form-select,
.floating-panel .form-control {
  background-color: #0f1116;
  color: #e9ecef;
  border-color: #2a2f3a;
}

.floating-panel .form-select:focus,
.floating-panel .form-control:focus {
  background-color: #0f1116;
  color: #e9ecef;
  border-color: #6ea8fe;
  box-shadow: 0 0 0 0.2rem rgba(110, 168, 254, 0.25);
}

.floating-panel .form-label {
  font-size: 0.8rem;
  margin-bottom: 0.25rem;
}

.panel-section-heading {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: #adb5bd;
  margin: 0.25rem 0 0.6rem;
  padding-bottom: 0.25rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.panel-body > .panel-section-heading:first-child {
  margin-top: 0;
}

.panel-body > .panel-section-heading:not(:first-child) {
  margin-top: 0.75rem;
}

html[data-theme="light"] .panel-section-heading {
  color: #495057;
  border-bottom-color: rgba(0, 0, 0, 0.08);
}

#file-meta dt {
  font-weight: 500;
}

#file-list {
  margin-bottom: 0.5rem;
}

#file-list:empty::before {
  content: "No files loaded.";
  display: block;
  color: #adb5bd;
  font-size: 0.78rem;
  padding: 0.25rem 0.5rem;
}

.file-list-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.5rem;
  border-radius: 0.3rem;
  cursor: pointer;
  outline: none;
}

.file-list-item:hover {
  background: rgba(255, 255, 255, 0.05);
}

.file-list-item:focus-visible {
  outline: 2px solid #6ea8fe;
  outline-offset: 1px;
}

.file-list-item.active {
  background: rgba(110, 168, 254, 0.18);
  color: #fff;
}

/* A ticked thumbnail checkbox marks the file for merge / multi-file model run.
   Use a green tint so it reads as a distinct state from the blue "active" row.
   When a row is both active *and* selected, the active blue background wins
   and the selection state surfaces through a green left edge instead. */
.file-list-item.selected {
  background: rgba(46, 204, 113, 0.18);
}
.file-list-item.active.selected {
  background: rgba(110, 168, 254, 0.18);
  box-shadow: inset 3px 0 0 rgba(46, 204, 113, 0.9);
}

.file-list-item .file-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.78rem;
}

.file-list-item .file-badge {
  font-size: 0.62rem;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.1);
  color: #adb5bd;
  padding: 0.05rem 0.32rem;
  border-radius: 0.25rem;
  white-space: nowrap;
}

.file-list-item .file-remove,
.file-list-item .file-split {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
}

.file-list-item .file-remove:hover {
  color: #ff7777;
}

.file-list-item .file-split {
  font-size: 0.9rem;
}

.file-list-item .file-split:hover {
  color: #6ea8fe;
}

html[data-theme="light"] .file-list-item:hover {
  background: rgba(0, 0, 0, 0.05);
}

html[data-theme="light"] .file-list-item.active {
  background: rgba(13, 110, 253, 0.15);
  color: #052c65;
}

html[data-theme="light"] .file-list-item.selected {
  background: rgba(25, 135, 84, 0.16);
  color: #0a3622;
}
html[data-theme="light"] .file-list-item.active.selected {
  background: rgba(13, 110, 253, 0.15);
  color: #052c65;
  box-shadow: inset 3px 0 0 rgba(25, 135, 84, 0.9);
}

html[data-theme="light"] .file-list-item .file-badge {
  background: rgba(0, 0, 0, 0.08);
  color: #495057;
}

/* Light-theme overrides for the thumbnail strip. */

html[data-theme="light"] #thumbnail-strip {
  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}

html[data-theme="light"] .thumb-card {
  background: rgba(255, 255, 255, 0.85);
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] .thumb-card:hover {
  background: rgba(245, 247, 252, 0.92);
}

html[data-theme="light"] .thumb-card.active {
  background: rgba(25, 135, 84, 0.16);
  border-color: rgba(25, 135, 84, 0.85);
  box-shadow: 0 0 0 1px rgba(25, 135, 84, 0.4);
}

html[data-theme="light"] .thumb-card .thumb-name {
  color: #495057;
}

/* Active thumbnail's filename: dark-mode rule paints it white; in
   light mode that becomes invisible against the pale-blue active
   background, so reuse the logo green for the highlight colour. */
html[data-theme="light"] .thumb-card.active .thumb-name {
  color: #198754;
}

html[data-theme="light"] .thumb-card.active .thumb-name {
  color: #052c65;
}

html[data-theme="light"] .thumb-card .thumb-placeholder {
  background: rgba(0, 0, 0, 0.04);
  color: #6c757d;
}

html[data-theme="light"] .thumb-card .thumb-badge {
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
}

html[data-theme="light"] .thumb-card .thumb-remove {
  color: #6c757d;
}

/* Status alert variants used by ui.js. */

#status.alert-error {
  background-color: #2c1418;
  color: #f5b7b7;
  border-color: #5a1f25;
}

#status.alert-success {
  background-color: #14271a;
  color: #b3e0c1;
  border-color: #1f5a30;
}

/* ---------- Light theme overrides ---------- */
/*
 * Triggered by `data-theme="light"` on <html>. Default (no
 * attribute, or "dark") keeps the existing dark palette.
 */

html[data-theme="light"] {
  background: #eef2f7;
}

html[data-theme="light"] body {
  background: #eef2f7;
  color: #212529;
}

html[data-theme="light"] #gl-canvas {
  background: linear-gradient(180deg, #e9eef7 0%, #b9c9e0 100%);
}

html[data-theme="light"] #view-reset,
html[data-theme="light"] #theme-toggle {
  background-color: #ffffff;
  color: #212529;
  border: 1px solid #ced4da;
}

html[data-theme="light"] #hud .bg-dark {
  background-color: rgba(255, 255, 255, 0.85) !important;
  color: #212529 !important;
}

html[data-theme="light"] #hud .text-warning,
html[data-theme="light"] #hud .text-info,
html[data-theme="light"] #hud .text-success {
  color: #212529 !important;
}

html[data-theme="light"] #label-tags .label-tag {
  background: rgba(255, 255, 255, 0.8);
  text-shadow: none;
}

html[data-theme="light"] #hint {
  text-shadow: none;
}

/* The hint headline ("Drop a wav file or folder") is the logo
   green in light mode so it stands against the bright canvas; the
   subtitle ("or click the file icon on the left") stays a quiet
   smoky grey in either theme. */
html[data-theme="light"] #hint .fs-5 {
  color: #198754 !important;
}

#hint .hint-sub {
  color: #adb5bd;
}

html[data-theme="light"] #hint .hint-sub {
  color: #6c757d;
}

/* In light mode, the thin blur on inactive thumbnail cards smudges
   the filename text against the bright background. Drop just the
   blur and keep the opacity dim so the active card still reads as
   the focal point. */
html[data-theme="light"] .thumb-card {
  filter: none;
}

html[data-theme="light"] .dock-icon {
  background: rgba(255, 255, 255, 0.82);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] .dock-icon:hover {
  background: rgba(220, 226, 234, 0.9);
}

html[data-theme="light"] .dock-icon.active {
  background: rgba(13, 110, 253, 0.9);
  color: #fff;
}

html[data-theme="light"] .floating-panel {
  background: rgba(255, 255, 255, 0.92);
  color: #212529;
  /* Slightly stronger 1px outline + a small shadow ring so the panel
     reads as a discrete card against the bright canvas. The drop
     shadow is widened a touch to compensate for the lighter
     background where the previous 0.18 alpha read as a smudge. */
  border-color: rgba(0, 0, 0, 0.22);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 14px 32px rgba(0, 0, 0, 0.22);
}

html[data-theme="light"] .panel-titlebar {
  background: rgba(0, 0, 0, 0.05);
  border-bottom-color: rgba(0, 0, 0, 0.18);
}

html[data-theme="light"] .panel-title {
  color: #495057;
}

html[data-theme="light"] .panel-close {
  color: #495057;
}

html[data-theme="light"] .panel-close:hover {
  color: #000;
}

html[data-theme="light"] .floating-panel .form-select,
html[data-theme="light"] .floating-panel .form-control {
  background-color: #ffffff;
  color: #212529;
  border-color: #ced4da;
}

html[data-theme="light"] .floating-panel .form-select:focus,
html[data-theme="light"] .floating-panel .form-control:focus {
  background-color: #ffffff;
  color: #212529;
  border-color: #6ea8fe;
}

html[data-theme="light"] #status.alert-error {
  background-color: #f8d7da;
  color: #842029;
  border-color: #f5c2c7;
}

html[data-theme="light"] #status.alert-success {
  background-color: #d1e7dd;
  color: #0f5132;
  border-color: #badbcc;
}

html[data-theme="light"] #status.alert-info {
  background-color: #cff4fc;
  color: #055160;
  border-color: #b6effb;
}

/* ---------- Tutorial spotlight + card ---------- */

#tutorial-root {
  position: fixed;
  inset: 0;
  /* Above the boot splash (#splash, z-index: 9000) so the auto-
     launched welcome card isn't buried during the splash fade. */
  z-index: 9500;
  pointer-events: none;
}

#tutorial-root .tutorial-dim {
  position: fixed;
  background: rgba(0, 0, 0, 0.55);
  pointer-events: auto;
}

#tutorial-root .tutorial-ring {
  position: fixed;
  border: 2px solid #6ea8fe;
  border-radius: 0.4rem;
  box-shadow: 0 0 0 4px rgba(110, 168, 254, 0.25);
  pointer-events: none;
}

#tutorial-root .tutorial-card {
  position: fixed;
  width: min(22rem, 90vw);
  background: rgba(20, 22, 28, 0.96);
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.45rem;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
  pointer-events: auto;
  font-size: 0.85rem;
}

#tutorial-root .tut-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.75rem;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.45rem 0.45rem 0 0;
}

#tutorial-root .tut-card-title {
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #adb5bd;
}

#tutorial-root .tut-card-close {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0 0.25rem;
  cursor: pointer;
}

#tutorial-root .tut-card-close:hover { color: #fff; }

#tutorial-root .tut-card-body {
  padding: 0.65rem 0.85rem 0.4rem;
  line-height: 1.45;
}

#tutorial-root .tut-card-body p { margin: 0 0 0.5rem; }

#tutorial-root .tut-card-body p:last-child { margin-bottom: 0; }

#tutorial-root .tut-card-body kbd {
  display: inline-block;
  padding: 0.05rem 0.3rem;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-bottom-width: 2px;
  border-radius: 0.2rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem;
}

#tutorial-root .tut-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.85rem 0.7rem;
  gap: 0.5rem;
}

/* Inline gate-not-satisfied error. Flashed in by spotlight.js when
   the user clicks Next on a `waitFor` chapter without performing
   the highlighted action. Auto-fades after a few seconds so the
   chapter copy isn't permanently squashed by the warning. */
#tutorial-root .tut-card-error {
  margin: 0 0.85rem 0.5rem;
  padding: 0.4rem 0.55rem;
  font-size: 0.78rem;
  line-height: 1.4;
  color: #fff;
  background: rgba(220, 53, 69, 0.85);
  border: 1px solid rgba(220, 53, 69, 0.6);
  border-radius: 0.3rem;
  opacity: 0;
}
#tutorial-root .tut-card-error.flashing {
  animation: tut-card-error-flash 3.5s ease-out forwards;
}
@keyframes tut-card-error-flash {
  0%   { opacity: 0; transform: translateY(-2px); }
  10%  { opacity: 1; transform: translateY(0); }
  85%  { opacity: 1; }
  100% { opacity: 0; }
}
html[data-theme="light"] #tutorial-root .tut-card-error {
  background: rgba(220, 53, 69, 0.95);
}

#tutorial-root .tut-card-progress {
  font-size: 0.7rem;
  color: #6c757d;
  letter-spacing: 0.04em;
}

#tutorial-root .tut-card-buttons {
  display: inline-flex;
  gap: 0.35rem;
}

#tutorial-root .tut-card-buttons button {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #e9ecef;
  border-radius: 0.3rem;
  padding: 0.25rem 0.7rem;
  font-size: 0.75rem;
  cursor: pointer;
}

#tutorial-root .tut-btn-next {
  background: #6ea8fe;
  color: #052c65;
  border-color: #6ea8fe;
  font-weight: 600;
}

#tutorial-root .tut-btn-next:hover { filter: brightness(1.07); }

#tutorial-root .tut-btn-skip:hover,
#tutorial-root .tut-btn-prev:hover {
  border-color: #6ea8fe;
  color: #6ea8fe;
}

/* Light-theme overrides. */

html[data-theme="light"] #tutorial-root .tutorial-dim {
  background: rgba(0, 0, 0, 0.35);
}

html[data-theme="light"] #tutorial-root .tutorial-ring {
  border-color: #0d6efd;
  box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.22);
}

html[data-theme="light"] #tutorial-root .tutorial-card {
  background: rgba(255, 255, 255, 0.97);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] #tutorial-root .tut-card-head {
  background: rgba(0, 0, 0, 0.04);
  border-bottom-color: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] #tutorial-root .tut-card-title { color: #6c757d; }
html[data-theme="light"] #tutorial-root .tut-card-close { color: #6c757d; }
html[data-theme="light"] #tutorial-root .tut-card-close:hover { color: #000; }

html[data-theme="light"] #tutorial-root .tut-card-body kbd {
  background: rgba(0, 0, 0, 0.06);
  border-color: rgba(0, 0, 0, 0.15);
  color: #212529;
}

html[data-theme="light"] #tutorial-root .tut-card-buttons button {
  border-color: rgba(0, 0, 0, 0.15);
  color: #212529;
}

html[data-theme="light"] #tutorial-root .tut-btn-next {
  background: #0d6efd;
  color: #fff;
  border-color: #0d6efd;
}

html[data-theme="light"] #tutorial-root .tut-btn-skip:hover,
html[data-theme="light"] #tutorial-root .tut-btn-prev:hover {
  border-color: #0d6efd;
  color: #0d6efd;
}

/* Tutorial picker menu (spotlight `items` mode). */

#tutorial-root .tut-menu-section + .tut-menu-section {
  margin-top: 0.85rem;
}

#tutorial-root .tut-menu-heading {
  margin: 0 0 0.4rem;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #adb5bd;
}

#tutorial-root .tut-menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

#tutorial-root .tut-menu-item {
  display: block;
  padding: 0.55rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.4rem;
  background: rgba(255, 255, 255, 0.03);
  cursor: pointer;
  transition: border-color 0.12s ease, background 0.12s ease;
}

#tutorial-root .tut-menu-item:hover,
#tutorial-root .tut-menu-item:focus {
  outline: none;
  border-color: #6ea8fe;
  background: rgba(110, 168, 254, 0.08);
}

#tutorial-root .tut-menu-item-title {
  display: block;
  font-weight: 600;
  color: #e9ecef;
  font-size: 0.82rem;
}

#tutorial-root .tut-menu-new-badge {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 1px 6px;
  background: #f04438;
  color: #fff;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: 999px;
  vertical-align: middle;
}

#tutorial-root .tut-menu-item-desc {
  display: block;
  margin-top: 0.15rem;
  color: #adb5bd;
  font-size: 0.74rem;
  line-height: 1.35;
}

html[data-theme="light"] #tutorial-root .tut-menu-heading {
  color: #6c757d;
}

html[data-theme="light"] #tutorial-root .tut-menu-item {
  border-color: rgba(0, 0, 0, 0.1);
  background: rgba(0, 0, 0, 0.02);
}

html[data-theme="light"] #tutorial-root .tut-menu-item:hover,
html[data-theme="light"] #tutorial-root .tut-menu-item:focus {
  border-color: #0d6efd;
  background: rgba(13, 110, 253, 0.06);
}

html[data-theme="light"] #tutorial-root .tut-menu-item-title { color: #212529; }
html[data-theme="light"] #tutorial-root .tut-menu-item-desc { color: #6c757d; }

/* Tutorial loading-progress overlay. */

#tutorial-root .tut-progress-card {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(22rem, 90vw);
  background: rgba(20, 22, 28, 0.96);
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.45rem;
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
  padding: 0.9rem 1.1rem;
  font-size: 0.85rem;
  pointer-events: auto;
}

#tutorial-root .tut-progress-label {
  margin-bottom: 0.6rem;
  color: #e9ecef;
  /* Long, space-less filenames (e.g. _2025…_p_001.flac) must wrap inside the
     fixed-width card instead of overflowing it. */
  overflow-wrap: anywhere;
  word-break: break-word;
}

#tutorial-root .tut-progress-track {
  height: 6px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 3px;
  overflow: hidden;
}

#tutorial-root .tut-progress-fill {
  height: 100%;
  background: #6ea8fe;
  width: 0%;
  transition: width 0.15s ease;
}

#tutorial-root .tut-progress-fill--indeterminate {
  /* Slide a 35%-wide stripe back and forth. The transition on width
     is disabled in this mode because the keyframes animate the
     transform instead. */
  width: 35% !important;
  transition: none;
  animation: tut-progress-slide 1.2s ease-in-out infinite;
}

@keyframes tut-progress-slide {
  0%   { transform: translateX(-15%); }
  50%  { transform: translateX(200%); }
  100% { transform: translateX(-15%); }
}

#tutorial-root .tut-progress-actions {
  margin-top: 0.7rem;
  display: flex;
  justify-content: flex-end;
}

#tutorial-root .tut-progress-cancel {
  appearance: none;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #e9ecef;
  font-size: 0.78rem;
  padding: 0.25rem 0.7rem;
  border-radius: 0.3rem;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}

#tutorial-root .tut-progress-cancel:hover,
#tutorial-root .tut-progress-cancel:focus-visible {
  outline: none;
  background: rgba(240, 68, 56, 0.18);
  border-color: #f04438;
  color: #fff;
}

html[data-theme="light"] #tutorial-root .tut-progress-cancel {
  border-color: rgba(0, 0, 0, 0.18);
  color: #212529;
}

html[data-theme="light"] #tutorial-root .tut-progress-cancel:hover,
html[data-theme="light"] #tutorial-root .tut-progress-cancel:focus-visible {
  background: rgba(240, 68, 56, 0.1);
  border-color: #f04438;
  color: #b42318;
}

html[data-theme="light"] #tutorial-root .tut-progress-card {
  background: rgba(255, 255, 255, 0.97);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] #tutorial-root .tut-progress-label {
  color: #212529;
}

html[data-theme="light"] #tutorial-root .tut-progress-track {
  background: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] #tutorial-root .tut-progress-fill {
  background: #0d6efd;
}

/* Saved files panel. */

#panel-saved .saved-usage {
  margin-bottom: 0.5rem;
}

#panel-saved .saved-usage-bar {
  margin-top: 0.25rem;
  height: 4px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 2px;
  overflow: hidden;
}

#panel-saved .saved-usage-fill {
  height: 100%;
  width: 0%;
  background: #6ea8fe;
  transition: width 0.2s ease;
}

/* Models panel — opens at ~80% of the viewport so the graph editor
   has real room to breathe, but the user can resize it via the
   bottom-right corner grip. The mobile media block at the bottom
   of this file already collapses every panel to a bottom sheet, so
   the resize handle is only useful above 700px.

   `resize` requires non-visible `overflow` and a definite size to
   draw a handle, so we set `overflow: hidden` here and let the
   panel-body underneath manage its own scroll. The min-* values
   stop the user from squashing the panel into something unusable
   by accident. */
#panel-models {
  width: 80vw;
  height: 80vh;
  min-width: 32rem;
  min-height: 22rem;
  max-width: 100vw;
  max-height: 100vh;
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* The model editor is dense; the underlying blur+translucent
     background made the rule graph hard to read against a busy
     spectrogram. Make this panel fully opaque (both themes). */
  background: #14161c;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html[data-theme="light"] #panel-models {
  background: #ffffff;
}
/* The panel-body fills the remaining vertical space and scrolls if
   the content overflows; the graph itself flexes to take whatever's
   left after the chrome rows above and below. The bottom-right
   inset clears the UA's `resize: both` grip on the panel — without
   it, anything in that corner (last "Active runs" row, in
   particular) gets eaten by the handle and clicks never reach the
   ×-button below. */
#panel-models .panel-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  padding-right: 1.2rem;
  padding-bottom: 1.4rem;
}

/* Minimised state — two specificity battles to win.
 *
 * 1. SIZE: `#panel-models { width: 80vw; height: 80vh }` is (1,0,0).
 *    `#panel-models.minimised` is (1,1,0) so our rule wins by class
 *    count. `!important`s defend against inline width/height that
 *    `resize: both` leaves behind on grip-drag (the JS minimise
 *    handler clears them, but a stale-cached session can keep
 *    them).
 *
 * 2. BODY VISIBILITY: the shared `.floating-panel.minimised
 *    .panel-body { display: none }` rule has specificity (0,2,1).
 *    My earlier `#panel-models .panel-body { display: flex }` is
 *    (1,1,0) — higher because of the ID — so it was beating the
 *    minimise rule and the body stayed visible. The
 *    `#panel-models.minimised .panel-body` rule below is (1,2,1)
 *    and beats my own (1,1,0) descendant rule.
 *
 * `display: block` on the panel sidesteps any flex-layout
 * reluctance to collapse a column to a single child. */
#panel-models.minimised {
  display: block !important;
  width: auto !important;
  min-width: 12rem !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  resize: none !important;
  overflow: hidden !important;
}
#panel-models.minimised .panel-body {
  display: none !important;
}

/* Two-column graph editor: palette on the left, free-form canvas on
   the right. Flexes to fill whatever vertical room the panel-body
   has after the saved-model picker, name, and the rules header. */
#panel-models .models-graph {
  display: flex;
  gap: 0.5rem;
  flex: 1 1 auto;
  min-height: 18rem;
  margin-bottom: 0.5rem;
}

/* Fullscreen state for the rules graph (driven by the maximise button →
   Fullscreen API). Fills the viewport with a solid backdrop so the graph
   is the only thing visible. Esc / the maximise button toggles back. */
#panel-models .models-graph:fullscreen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0.75rem;
  background: #14161c;
  box-sizing: border-box;
}
html[data-theme="light"] #panel-models .models-graph:fullscreen {
  background: #f5f7fa;
}

/* Pop-out focus mode: when the new window is loaded with
   ?popout=models&focus=rules, body.panel-focus-rules collapses the
   model panel down to just the rules graph (palette + canvas) so the
   window's whole real estate is the graph. */
body.panel-focus-rules #panel-models .panel-body > *:not(.models-graph),
body.panel-focus-rules #panel-models .panel-titlebar {
  display: none !important;
}
body.panel-focus-rules #panel-models {
  inset: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  max-width: none !important;
  max-height: none !important;
  border-radius: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
}
body.panel-focus-rules #panel-models .panel-body {
  padding: 0.75rem !important;
  height: 100% !important;
  display: flex;
  flex-direction: column;
}
body.panel-focus-rules #panel-models .models-graph {
  flex: 1 1 auto !important;
  min-height: 0 !important;
  margin: 0 !important;
}
body.panel-focus-rules #dock,
body.panel-focus-rules #app-logo,
body.panel-focus-rules #top-right-controls,
body.panel-focus-rules #hud,
body.panel-focus-rules #hint,
body.panel-focus-rules #action-dock,
body.panel-focus-rules #score-plot,
body.panel-focus-rules .privacy-banner { display: none !important; }
#panel-models .models-palette {
  /* Width is user-controllable: drag the bottom-right grip (CSS resize)
     or the chrome grows naturally up to the panel-body width. min-width
     keeps the chip text from wrapping into uselessness; max-width caps
     the palette so it can't crowd the canvas to nothing. */
  flex: 0 0 auto;
  width: 8.5rem;
  min-width: 7rem;
  max-width: 24rem;
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.35rem;
  padding: 0.5rem;
  background: rgba(255, 255, 255, 0.02);
  overflow: auto;
  resize: horizontal;
}
#panel-models .models-palette-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
#panel-models .palette-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.35rem;
  padding: 0.35rem 0.5rem;
  border-radius: 0.3rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(20, 22, 28, 0.65);
  color: #e9ecef;
  font-size: 0.76rem;
  text-align: left;
  cursor: grab;
  user-select: none;
  transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}
#panel-models .palette-chip:hover {
  border-color: #6ea8fe;
  background: rgba(110, 168, 254, 0.18);
  transform: translateX(2px);
}
#panel-models .palette-chip.dragging { opacity: 0.5; cursor: grabbing; }
#panel-models .palette-chip-add {
  flex: 0 0 auto;
  font-size: 0.85rem;
  color: rgba(110, 168, 254, 0.9);
  pointer-events: none;
}

#panel-models .models-canvas-wrap {
  flex: 1 1 auto;
  position: relative;
  overflow: auto;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.35rem;
  background:
    /* Subtle dot grid so users can see the canvas is a placement
       surface, not a free-form text area. */
    radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1.5px) 0 0 / 18px 18px,
    rgba(0, 0, 0, 0.18);
}
#panel-models .models-canvas {
  position: relative;
  /* Min size keeps the SVG happy and gives nodes room to spread out
     before scrolling kicks in. */
  min-width: 100%;
  min-height: 100%;
  width: 1100px;
  height: 600px;
}
#panel-models .models-canvas.drop-active {
  outline: 2px dashed rgba(110, 168, 254, 0.7);
  outline-offset: -3px;
}
#panel-models .models-edges {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
#panel-models .models-nodes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Rule node card — absolutely positioned via inline left/top set by
   the renderer. Header is the drag handle; body holds the param
   form generated from RULE_TYPES. `box-sizing: border-box` so the
   inner padding doesn't push the actual width past the chosen size
   (otherwise the param inputs leak past the right edge on browsers
   that default content-box).

   `resize: both` puts a small grip in the bottom-right corner; it
   needs `overflow: auto` (or anything non-visible) to draw, so we
   use `auto` here — content scrolls inside the card if the user
   resizes it smaller than the param form needs. The matching
   ResizeObserver in ui.js re-routes the SVG edges when the card's
   bounds change. */
#panel-models .rule-node {
  position: absolute;
  width: 16rem;
  min-width: 12rem;
  min-height: 8rem;
  box-sizing: border-box;
  background: rgba(20, 22, 28, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.4rem;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
  font-size: 0.78rem;
  z-index: 2;
  resize: both;
  overflow: auto;
}
#panel-models .rule-node.dragging {
  opacity: 0.9;
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.55);
  z-index: 3;
}
#panel-models .rule-node-header {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.4rem 0.55rem;
  background: rgba(255, 255, 255, 0.05);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.4rem 0.4rem 0 0;
  cursor: move;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
#panel-models .rule-node-title {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #e9ecef;
}
#panel-models .rule-node-weight {
  flex: 0 0 3.2rem;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: #e9ecef;
  border-radius: 0.25rem;
  padding: 0.1rem 0.3rem;
  font-size: 0.72rem;
  text-align: right;
}
#panel-models .rule-node-remove,
#panel-models .rule-node-minimise {
  flex: 0 0 auto;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.25rem;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.05rem 0.35rem;
  min-width: 1.4rem;
  text-align: center;
}
#panel-models .rule-node-remove:hover {
  color: #fff;
  background: #f04438;
  border-color: #f04438;
}
#panel-models .rule-node-minimise:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}

/* Collapsed rule node: hide everything below the header. The header
   stays draggable + holds the remove + minimise + preview controls. */
#panel-models .rule-node.rule-node-minimised > :not(.rule-node-header) {
  display: none !important;
}
#panel-models .rule-node.rule-node-minimised {
  /* Tighter footprint when collapsed — the node shrinks to header-only. */
  height: auto !important;
  min-height: 0 !important;
  resize: none;
}

#panel-models .rule-node-type {
  padding: 0.35rem 0.55rem 0;
}
#panel-models .rule-node-type select {
  width: 100%;
  font-size: 0.76rem;
  background: rgba(0, 0, 0, 0.3);
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.25rem;
  padding: 0.2rem 0.4rem;
}
#panel-models .rule-node-params {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.35rem 0.5rem;
  padding: 0.35rem 0.55rem 0.55rem;
}
/* Each param cell must not push the grid wider than the card. The
   `min-width: 0` on the label lets long captions truncate instead
   of forcing a wider column, and `overflow: hidden` + ellipsis on
   the caption gives a clear visual cue the field has been clipped. */
#panel-models .rule-node-params label {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.7rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* `min-width: 0` + `width: 100%` + `box-sizing: border-box` is the
   trio that stops a `<input type="number">` (whose default `size`
   attribute reserves ~20ch of intrinsic width) from forcing its
   grid cell wider than the card it sits inside. */
#panel-models .rule-node-params input {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  background: rgba(0, 0, 0, 0.3);
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.25rem;
  padding: 0.18rem 0.35rem;
  font-size: 0.76rem;
}
#panel-models .rule-node-port {
  /* Output port — visual cue the line runs from this side. */
  position: absolute;
  right: -6px;
  top: 1.1rem;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #6ea8fe;
  border: 2px solid rgba(20, 22, 28, 0.95);
  pointer-events: none;
}

/* Enterprise-locked rule chip — red border + dimmed text + not-allowed cursor.
   Set on the palette chip when the user isn't on an Enterprise plan and the
   rule type is Enterprise-only (e.g. brahma intel). Drag is disabled at the DOM
   level (draggable=false); the click handler shows an info toast. */
#panel-models .palette-chip-locked {
  border-color: #dc3545 !important;        /* Bootstrap btn-danger red, works in both themes */
  outline: 1px solid rgba(220, 53, 69, 0.35);
  outline-offset: 1px;
  opacity: 0.7;
  cursor: not-allowed;
}
#panel-models .palette-chip-locked:hover {
  background: rgba(220, 53, 69, 0.08);
}
#panel-models .palette-chip-locked .palette-chip-add {
  /* Lock glyph (set in JS) — keep the same colour as the border so the lock
     reads as part of the gating treatment rather than a separate element. */
  color: #dc3545;
}

/* ----- custom-rule node (an inline DSL expression — see js/dsp/dsl.js) ----- */
#panel-models .palette-chip-custom { font-style: italic; }
#panel-models .rule-node-custom { width: 22rem; min-width: 16rem; }
#panel-models .rule-node-custom-body {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.4rem 0.55rem 0.6rem;
}
#panel-models .rule-node-custom-name {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.6);
}
#panel-models .rule-node-custom-name input,
#panel-models .rule-node-custom-expr {
  width: 100%;
  box-sizing: border-box;
  min-width: 0;
  background: rgba(0, 0, 0, 0.3);
  color: #e9ecef;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.25rem;
}
#panel-models .rule-node-custom-name input { padding: 0.18rem 0.35rem; font-size: 0.76rem; }
#panel-models .rule-node-custom-expr {
  resize: vertical;
  min-height: 3.2rem;
  padding: 0.3rem 0.4rem;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  font-size: 0.72rem;
  line-height: 1.35;
}
#panel-models .rule-node-custom-status {
  font-size: 0.66rem;
  line-height: 1.25;
  word-break: break-word;
}
#panel-models .rule-node-custom-status.is-ok    { color: #51d88a; }
#panel-models .rule-node-custom-status.is-err   { color: #f08c8c; }
#panel-models .rule-node-custom-status.is-empty { color: rgba(255, 255, 255, 0.4); }
#panel-models .rule-node-custom-params-empty {
  grid-column: 1 / -1;
  font-size: 0.66rem;
  color: rgba(255, 255, 255, 0.35);
}
#panel-models .rule-node-custom-params { padding: 0; }   /* the body already pads */
#panel-models .rule-node-custom-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
#panel-models .rule-node-custom-actions .btn {
  font-size: 0.7rem;
  padding: 0.1rem 0.45rem;
}

/* Combine node — pinned visually but draggable like a rule, and
   resizable through the same bottom-right grip. */
#panel-models .combine-node {
  position: absolute;
  width: 9.5rem;
  min-width: 7.5rem;
  min-height: 5rem;
  box-sizing: border-box;
  background: rgba(20, 22, 28, 0.92);
  border: 1px solid rgba(110, 168, 254, 0.5);
  border-radius: 0.5rem;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
  z-index: 2;
  resize: both;
  overflow: auto;
}
#panel-models .combine-node-header {
  padding: 0.35rem 0.55rem;
  font-size: 0.7rem;
  color: rgba(110, 168, 254, 0.95);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  background: rgba(110, 168, 254, 0.12);
  border-radius: 0.5rem 0.5rem 0 0;
  cursor: move;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
#panel-models .combine-node-body { padding: 0.4rem 0.55rem 0.55rem; }
#panel-models .combine-node-body select {
  width: 100%;
  background: rgba(0, 0, 0, 0.3);
  color: #e9ecef;
  border: 1px solid rgba(110, 168, 254, 0.4);
  border-radius: 0.25rem;
  padding: 0.25rem 0.4rem;
  font-size: 0.78rem;
}
#panel-models .combine-node-port-in {
  position: absolute;
  left: -6px;
  top: 50%;
  transform: translateY(-50%);
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #6ea8fe;
  border: 2px solid rgba(20, 22, 28, 0.95);
  pointer-events: none;
}

/* Edge curves drawn between rule outputs and the combine input. The
   combine label sits midway along the line to read as a per-edge op
   even though all edges share the model's single combine setting. */
#panel-models .models-edges path {
  fill: none;
  stroke: rgba(110, 168, 254, 0.55);
  stroke-width: 1.6;
}
#panel-models .models-edges path.dimmed { stroke: rgba(255, 255, 255, 0.18); }

#panel-models .models-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.45);
  font-size: 0.8rem;
  text-align: center;
  padding: 1rem;
  pointer-events: none;
}

/* Light-theme variants. */
html[data-theme="light"] #panel-models .models-palette {
  background: rgba(0, 0, 0, 0.03);
  border-color: rgba(0, 0, 0, 0.08);
}
html[data-theme="light"] #panel-models .palette-chip {
  background: rgba(255, 255, 255, 0.85);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
}
html[data-theme="light"] #panel-models .palette-chip:hover {
  background: rgba(110, 168, 254, 0.16);
  border-color: #0d6efd;
}
html[data-theme="light"] #panel-models .models-canvas-wrap {
  border-color: rgba(0, 0, 0, 0.08);
  background:
    radial-gradient(circle, rgba(0, 0, 0, 0.07) 1px, transparent 1.5px) 0 0 / 18px 18px,
    rgba(0, 0, 0, 0.03);
}
html[data-theme="light"] #panel-models .rule-node,
html[data-theme="light"] #panel-models .combine-node {
  background: rgba(255, 255, 255, 0.95);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
}
html[data-theme="light"] #panel-models .rule-node-header { background: rgba(0, 0, 0, 0.04); }
html[data-theme="light"] #panel-models .rule-node-title { color: #212529; }
html[data-theme="light"] #panel-models .rule-node-weight,
html[data-theme="light"] #panel-models .rule-node-type select,
html[data-theme="light"] #panel-models .rule-node-params input,
html[data-theme="light"] #panel-models .combine-node-body select {
  background: #ffffff;
  color: #212529;
  border-color: #ced4da;
}
html[data-theme="light"] #panel-models .rule-node-params label { color: #6c757d; }
html[data-theme="light"] #panel-models .combine-node {
  border-color: rgba(13, 110, 253, 0.6);
}
html[data-theme="light"] #panel-models .combine-node-header {
  background: rgba(13, 110, 253, 0.12);
  color: #0d6efd;
}
html[data-theme="light"] #panel-models .combine-node-port-in,
html[data-theme="light"] #panel-models .rule-node-port {
  border-color: #fff;
}
html[data-theme="light"] #panel-models .models-edges path {
  stroke: rgba(13, 110, 253, 0.55);
}

/* On the mobile bottom-sheet, the graph is too cramped — let the
   canvas scroll and shrink the palette so it doesn't dominate. */
@media (max-width: 700px) {
  #panel-models { width: auto; }
  #panel-models .models-graph { height: 60vh; }
  #panel-models .models-palette {
    flex: 0 0 6.5rem;
    padding: 0.4rem;
  }
  #panel-models .palette-chip { font-size: 0.7rem; padding: 0.3rem 0.4rem; }
  #panel-models .rule-node { width: 12rem; }
}

/* ---------- Listening-agents pipeline designer (#panel-listening-agents) ----------
   Three-pane drag-and-drop canvas: an agent-type palette on the left, the
   user's subscribed streams on the right, and a free-form node canvas in the
   middle where agents are wired to streams. Mirrors the Models graph editor's
   look (dot-grid canvas, draggable node cards, SVG edges) with its own scope. */
.la-body { display: flex; flex-direction: column; }

.la-graph {
  display: flex;
  gap: 0.5rem;
  flex: 1 1 auto;
  min-height: 18rem;
}

.la-palette,
.la-streams {
  flex: 0 0 auto;
  width: 11rem;
  min-width: 8rem;
  max-width: 18rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.35rem;
  padding: 0.5rem;
  background: rgba(255, 255, 255, 0.02);
  overflow: auto;
  resize: horizontal;
}

.la-col-title {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
}

.la-chip-list { display: flex; flex-direction: column; gap: 0.3rem; }

.la-chip {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.5rem;
  border-radius: 0.3rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-left-width: 3px;
  background: rgba(20, 22, 28, 0.65);
  color: #e9ecef;
  font-size: 0.76rem;
  text-align: left;
  cursor: grab;
  user-select: none;
  transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}
.la-chip:hover {
  border-color: #6ea8fe;
  background: rgba(110, 168, 254, 0.18);
  transform: translateX(2px);
}
.la-chip.dragging { opacity: 0.5; cursor: grabbing; }
.la-chip > i { font-size: 1rem; flex: 0 0 auto; }
.la-chip > span {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.la-streams .la-chip:hover { transform: translateX(-2px); }
.la-streams .la-chip-empty {
  font-size: 0.7rem;
  color: rgba(255, 255, 255, 0.4);
  padding: 0.2rem 0.1rem;
}

/* One accent colour per section (left border + icon): agents blue,
   owner gold, streams teal. */
#la-agents-list .la-chip      { border-left-color: #6ea8fe; }
#la-agents-list .la-chip > i  { color: #6ea8fe; }
.la-owners .la-chip           { border-left-color: #e0b341; }
.la-owners .la-chip > i       { color: #e0b341; }
.la-owners .la-chip:hover     { border-color: #e0b341; background: rgba(224, 179, 65, 0.18); }
.la-streams .la-chip          { border-left-color: #40c4d8; }
.la-streams .la-chip > i      { color: #40c4d8; }

.la-canvas-wrap {
  flex: 1 1 auto;
  position: relative;
  overflow: auto;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.35rem;
  background:
    radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1.5px) 0 0 / 18px 18px,
    rgba(0, 0, 0, 0.18);
}
.la-canvas {
  position: relative;
  min-width: 100%;
  min-height: 100%;
  width: 1100px;
  height: 600px;
}
.la-canvas.drop-active {
  outline: 2px dashed rgba(110, 168, 254, 0.7);
  outline-offset: -3px;
}
/* Canvas border tracks the pipeline state (see isPipelineComplete in
   listening-agents.js): red until a complete owner → agent → stream chain
   exists, green once it does. */
.la-canvas-wrap {
  transition: border-color 200ms ease, box-shadow 200ms ease;
}
.la-canvas-wrap.la-incomplete {
  border-color: #e5484d;
  box-shadow: 0 0 0 1px rgba(229, 72, 77, 0.55) inset;
}
.la-canvas-wrap.la-complete {
  border-color: #2faa5b;
  box-shadow: 0 0 0 1px rgba(47, 170, 91, 0.55) inset;
}
.la-edges {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.la-edges path {
  fill: none;
  stroke: rgba(110, 168, 254, 0.6);
  stroke-width: 1.8;
}
.la-edges path.la-edge-pending { stroke-dasharray: 5 4; stroke: rgba(110, 168, 254, 0.85); }
.la-nodes { position: absolute; inset: 0; width: 100%; height: 100%; }

.la-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.45);
  font-size: 0.8rem;
  text-align: center;
  padding: 1rem;
  pointer-events: none;
}

/* Node card — absolutely positioned via inline left/top set by the renderer.
   The whole header is the drag handle; the port is the connect handle. */
.la-node {
  position: absolute;
  width: 11rem;
  min-width: 8rem;
  box-sizing: border-box;
  background: rgba(20, 22, 28, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 0.4rem;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
  font-size: 0.78rem;
  z-index: 2;
}
.la-node.dragging { opacity: 0.92; box-shadow: 0 14px 30px rgba(0, 0, 0, 0.55); z-index: 3; }
.la-node.is-agent  { border-top: 3px solid #6ea8fe; }
.la-node.is-owner  { border-top: 3px solid #e0b341; }
.la-node.is-stream { border-top: 3px solid #40c4d8; }
.la-node-header {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.55rem;
  cursor: move;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
.la-node-header > i { flex: 0 0 auto; font-size: 1rem; }
.la-node-title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 600;
}
.la-node-sub {
  padding: 0 0.55rem 0.45rem;
  font-size: 0.66rem;
  color: rgba(255, 255, 255, 0.5);
}
.la-node-remove {
  flex: 0 0 auto;
  background: none;
  border: 0;
  color: rgba(255, 255, 255, 0.5);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
  padding: 0 0.1rem;
}
.la-node-remove:hover { color: #f08c8c; }

/* Connect port — a small circle the user drags from to wire two nodes.
   Agent ports sit on the right edge, stream ports on the left, so edges
   read left-to-right (agent → stream). */
.la-node-port {
  position: absolute;
  top: 0.5rem;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: #6ea8fe;
  border: 2px solid rgba(20, 22, 28, 0.95);
  cursor: crosshair;
  z-index: 4;
}
.la-node.is-agent  .la-node-port { right: -7px; }
.la-node.is-owner  .la-node-port { right: -7px; background: #e0b341; }
.la-node.is-stream .la-node-port { left: -7px; background: #40c4d8; }
.la-node-port:hover { transform: scale(1.25); }
.la-canvas.linking { cursor: crosshair; }

/* Light-theme variants. */
html[data-theme="light"] .la-palette,
html[data-theme="light"] .la-streams {
  background: rgba(0, 0, 0, 0.03);
  border-color: rgba(0, 0, 0, 0.08);
}
html[data-theme="light"] .la-col-title { color: #6c757d; }
html[data-theme="light"] .la-chip {
  background: rgba(255, 255, 255, 0.85);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
}
html[data-theme="light"] .la-canvas-wrap {
  border-color: rgba(0, 0, 0, 0.08);
  background:
    radial-gradient(circle, rgba(0, 0, 0, 0.07) 1px, transparent 1.5px) 0 0 / 18px 18px,
    rgba(0, 0, 0, 0.03);
}
html[data-theme="light"] .la-node {
  background: rgba(255, 255, 255, 0.97);
  border-color: rgba(0, 0, 0, 0.1);
  color: #212529;
}
html[data-theme="light"] .la-node-sub { color: #6c757d; }
html[data-theme="light"] .la-node-port { border-color: #fff; }

/* Section accent colours in light mode. The light .la-node / .la-chip rules
   above set border-color (shorthand), which would otherwise wipe the coloured
   node top-border + chip left-border, so re-assert them here — deepened a touch
   for contrast on white. agents blue, owner gold, streams teal. */
html[data-theme="light"] #la-agents-list .la-chip     { border-left-color: #2f6fe0; }
html[data-theme="light"] #la-agents-list .la-chip > i { color: #2f6fe0; }
html[data-theme="light"] .la-owners .la-chip          { border-left-color: #b8860b; }
html[data-theme="light"] .la-owners .la-chip > i      { color: #b8860b; }
html[data-theme="light"] .la-streams .la-chip         { border-left-color: #1597a8; }
html[data-theme="light"] .la-streams .la-chip > i     { color: #1597a8; }
html[data-theme="light"] .la-node.is-agent  { border-top-color: #2f6fe0; }
html[data-theme="light"] .la-node.is-owner  { border-top-color: #b8860b; }
html[data-theme="light"] .la-node.is-stream { border-top-color: #1597a8; }
html[data-theme="light"] .la-node.is-agent  .la-node-port { background: #2f6fe0; }
html[data-theme="light"] .la-node.is-owner  .la-node-port { background: #b8860b; }
html[data-theme="light"] .la-node.is-stream .la-node-port { background: #1597a8; }

/* On a narrow viewport the three panes are too cramped — let the columns
   shrink and the canvas scroll. */
@media (max-width: 820px) {
  .la-palette, .la-streams { width: 7.5rem; min-width: 6rem; padding: 0.35rem; }
  .la-graph { min-height: 22rem; }
}

#panel-models .models-thr { width: 4.6rem; flex: 0 0 auto; text-align: center; }
#panel-models .models-runs-list { display: flex; flex-direction: column; gap: 0.25rem; }
#panel-models .models-run-row {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.25rem 0.45rem;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 0.3rem;
  background: rgba(255, 255, 255, 0.02);
}
#panel-models .models-run-swatch {
  flex: 0 0 auto;
  width: 0.85rem; height: 0.85rem;
  border-radius: 0.18rem;
  border: 1px solid rgba(0, 0, 0, 0.45);
}
#panel-models .models-run-name {
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#panel-models .models-run-meta { color: rgba(255, 255, 255, 0.55); }
#panel-models .models-run-row .btn {
  --bs-btn-padding-y: 0.05rem;
  --bs-btn-padding-x: 0.3rem;
  font-size: 0.7rem;
}
html[data-theme="light"] #panel-models .models-run-row {
  border-color: rgba(0, 0, 0, 0.08);
  background: rgba(0, 0, 0, 0.02);
}
html[data-theme="light"] #panel-models .models-run-meta { color: #6c757d; }


#panel-saved .saved-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.3rem;
  margin-bottom: 0.35rem;
  background: rgba(255, 255, 255, 0.03);
}

#panel-saved .saved-row-info {
  flex: 1 1 auto;
  min-width: 0;
}

#panel-saved .saved-row-name {
  font-weight: 600;
  font-size: 0.82rem;
  color: #e9ecef;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#panel-saved .saved-row-meta {
  font-size: 0.72rem;
  color: #adb5bd;
  margin-top: 0.1rem;
}

#panel-saved .saved-row-actions {
  display: flex;
  gap: 0.3rem;
  flex-shrink: 0;
}

#panel-saved .saved-row-actions .btn {
  font-size: 0.72rem;
  padding: 0.15rem 0.5rem;
}

html[data-theme="light"] #panel-saved .saved-usage-bar {
  background: rgba(0, 0, 0, 0.08);
}

html[data-theme="light"] #panel-saved .saved-usage-fill {
  background: #0d6efd;
}

html[data-theme="light"] #panel-saved .saved-row {
  background: rgba(0, 0, 0, 0.02);
  border-color: rgba(0, 0, 0, 0.1);
}

html[data-theme="light"] #panel-saved .saved-row-name {
  color: #212529;
}

html[data-theme="light"] #panel-saved .saved-row-meta {
  color: #6c757d;
}

/* =================================================================
   MOBILE OVERRIDES
   -----------------------------------------------------------------
   The desktop layout assumes a left-edge vertical dock, panels
   floating in free space, and a wide thumbnail strip down the
   right. None of that survives a phone-shaped viewport.

   At ≤700px we switch to a phone-first chrome:

   - Dock becomes a bottom-pinned horizontal scrollable tab bar.
   - Floating panels become full-width bottom sheets above the dock,
     scrollable inside, max 55vh tall so the spectrogram stays
     visible behind them.
   - Action-dock collapses from the radial arc into a flat
     horizontal row above the main dock.
   - Top-right controls compact (separators dropped, smaller font).
   - Privacy banner, HUD, file-banner pill repositioned so nothing
     hides behind the new bottom chrome.

   Heavy use of `!important` here is deliberate: the desktop rules
   set inline transforms (camera dock JS) and Bootstrap utility
   classes (`top-50 start-0 translate-middle-y`) that we have to
   beat without surgically rewriting the markup.
   ================================================================= */

/* Render navigation pad: desktop uses mouse-drag orbit + wheel zoom, so
   keep the pad out of the way entirely above the mobile breakpoint. The
   `hidden` attribute (file-not-loaded) is handled by main.js; this base
   rule stops it appearing on desktop once that attribute is removed. */
.render-nav { display: none; }

@media (max-width: 700px) {
  /* ----- Touch render navigation pad ----- */
  .render-nav:not([hidden]) {
    position: fixed;
    bottom: 0.5rem;
    left: 0.5rem;
    z-index: 1090;   /* above panels (100+), below the hamburgers (1100) */
    display: grid;
    grid-template-columns: repeat(3, 2.5rem);
    grid-template-rows: repeat(3, 2.5rem);
    grid-template-areas:
      "zout up    zin"
      "left reset right"
      ".    down  .";
    gap: 0.3rem;
    padding: 0.4rem;
    background: rgba(20, 22, 28, 0.85);
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: 0.6rem;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }
  .render-nav-btn {
    width: 2.5rem;
    height: 2.5rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 0.45rem;
    color: #e9ecef;
    font-size: 1.2rem;
    cursor: pointer;
    /* Keep press-and-hold repeat from being hijacked by browser pan. */
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
  }
  .render-nav-btn:active { background: rgba(110, 168, 254, 0.40); }
  .render-nav-btn[data-nav="zoom-out"]   { grid-area: zout; }
  .render-nav-btn[data-nav="orbit-up"]   { grid-area: up; }
  .render-nav-btn[data-nav="zoom-in"]    { grid-area: zin; }
  .render-nav-btn[data-nav="orbit-left"] { grid-area: left; }
  .render-nav-btn[data-nav="reset"]      { grid-area: reset; }
  .render-nav-btn[data-nav="orbit-right"]{ grid-area: right; }
  .render-nav-btn[data-nav="orbit-down"] { grid-area: down; }
  html[data-theme="light"] .render-nav:not([hidden]) {
    background: rgba(255, 255, 255, 0.95);
    border-color: rgba(0, 0, 0, 0.14);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
  }
  html[data-theme="light"] .render-nav-btn {
    background: rgba(0, 0, 0, 0.04);
    border-color: rgba(0, 0, 0, 0.12);
    color: #212529;
  }

  /* ----- Mobile-only hamburger triggers -----
     Two pinned tap-targets reveal the docks on demand:
     - top-left  → tools dock (always available)
     - bottom-right → active-file actions (only when a file is loaded;
                      the JS in main.js mirrors action-dock.hidden onto
                      the toggle's `hidden` attribute) */
  .mobile-only { display: inline-flex; }
  /* Match the desktop's higher-specificity hide: without this, the
     `.dock-icon.mobile-only { display: none }` rule (0,2,0) outranks
     the bare `.mobile-only` reveal (0,1,0) and the account anchors
     stay hidden even on phone widths. */
  .dock-icon.mobile-only { display: inline-flex; }
  #mobile-dock-toggle,
  #mobile-action-toggle {
    position: fixed;
    width: 2.8rem;
    height: 2.8rem;
    z-index: 1100;
    background: rgba(20, 22, 28, 0.85);
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: 0.55rem;
    color: #e9ecef;
    font-size: 1.4rem;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  }
  #mobile-dock-toggle   { top: 0.5rem;    left: 0.5rem; }
  #mobile-action-toggle { bottom: 0.5rem; right: 0.5rem; }
  #mobile-dock-toggle.open,
  #mobile-action-toggle.open {
    background: rgba(110, 168, 254, 0.85);
    color: #fff;
    border-color: rgba(110, 168, 254, 0.95);
  }


  /* ----- Tools dock — hidden by default; revealed by the top-left
     hamburger as a vertical column below itself, like a typical
     drawer menu. The card-style background reads as a single panel
     against the canvas, and the column scrolls if more icons are
     added than fit. ----- */
  #dock { display: none !important; }
  body.mobile-dock-open #dock {
    display: flex !important;
    top: 3.6rem !important;
    bottom: auto !important;
    left: 0.5rem !important;
    right: auto !important;
    margin: 0 !important;
    transform: none !important;
    flex-direction: column !important;
    gap: 0.4rem !important;
    padding: 0.45rem;
    background: rgba(20, 22, 28, 0.92);
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: 0.6rem;
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
    max-height: calc(100vh - 5rem);
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
  #dock::-webkit-scrollbar { display: none; }
  html[data-theme="light"] body.mobile-dock-open #dock {
    background: rgba(255, 255, 255, 0.96);
    border-color: rgba(0, 0, 0, 0.14);
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
  }
  .dock-icon {
    flex-shrink: 0;
    width: 2.6rem;
    height: 2.6rem;
    font-size: 1.3rem;
  }
  .dock-icon:hover { transform: scale(1.05); }   /* keep close to 1× — touch ≠ hover */
  .dock-icon.active { transform: scale(1.05); }

  /* In the mobile drawer the dock is a left-edge column, so a right-side flyout
     would fall off-screen — expand the group's icons inline (indented) instead. */
  .dock-flyout.open > .dock-flyout-group {
    position: static;
    left: auto;
    top: auto;
    transform: none;   /* undo the desktop centre-on-toggle shift */
    padding: 0.25rem 0 0.25rem 0.6rem;
    background: transparent;
    border: none;
    box-shadow: none;
    max-height: none;
    overflow: visible;
  }

  /* ----- Floating panels: bottom sheets above the dock ----- */
  .floating-panel {
    top: auto !important;
    /* `--sheet-bottom` is set inline by the title-bar drag (see
       makeDraggable in js/ui.js) so the user can slide the sheet up
       and down; it falls back to 4rem (clear of the bottom dock). */
    bottom: var(--sheet-bottom, 4rem) !important;
    left: 0.5rem !important;
    right: 0.5rem !important;
    width: auto !important;
    max-width: none !important;
    transform: none !important;
    /* Keep the surface visible behind the sheet. The body itself
       scrolls; the title bar pins. */
    max-height: 55vh;
    display: flex !important;
    flex-direction: column;
    overflow: hidden;
  }
  /* Editor panels are 36rem on desktop — collapse to bottom-sheet
     too. Wider than the regular panel by default but the same
     full-width-minus-margin treatment fits both. */
  #panel-labels-table,
  #panel-decisions-table {
    width: auto !important;
    max-width: none !important;
  }
  /* Title bar doubles as the drag handle: drag it up / down to slide
     the bottom sheet (makeDraggable drives `--sheet-bottom`). The body
     has its own scroll area, so claiming touch here doesn't trap the
     content scroll. */
  .floating-panel .panel-titlebar {
    cursor: grab;
    touch-action: none;
    flex-shrink: 0;
  }
  .floating-panel.dragging .panel-titlebar { cursor: grabbing; }
  .panel-close,
  .panel-minimise {
    /* Bigger hit areas — Apple HIG asks for ≥44px touch targets. */
    width: 2.4rem;
    height: 2.4rem;
    font-size: 1.4rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .panel-body {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Minimised panel collapses to just its title bar; bottom-aligned
     so it stacks neatly above the dock when several are open. */
  .floating-panel.minimised {
    width: auto !important;
    min-width: 0;
  }

  /* ----- Action-dock: arc → vertical stack above the bottom-right
     hamburger. Gated behind both `body.mobile-action-open` AND the
     dock's underlying `[hidden]` state, so the stack only shows
     when a file is active AND the user has tapped the hamburger.
     Card styling matches the tools dock above. ----- */
  .action-dock { display: none !important; }
  body.mobile-action-open .action-dock:not([hidden]) {
    display: flex !important;
    transform: none !important;
    bottom: 3.7rem !important;
    right: 0.5rem !important;
    left: auto !important;
    top: auto !important;
    width: auto !important;
    height: auto !important;
    flex-direction: column !important;
    gap: 0.4rem;
    padding: 0.45rem;
    background: rgba(20, 22, 28, 0.92);
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: 0.6rem;
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.45);
    max-height: calc(100vh - 6rem);
    overflow-y: auto;
    pointer-events: auto;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }
  html[data-theme="light"] body.mobile-action-open .action-dock:not([hidden]) {
    background: rgba(255, 255, 255, 0.96);
    border-color: rgba(0, 0, 0, 0.14);
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
  }
  .action-item {
    /* The desktop layout uses radial transforms set by JS via the
       --base-transform custom property. Override with `none` so flex
       takes over; clearing margin keeps the items from offsetting
       relative to a zero-size anchor that no longer exists. */
    position: static !important;
    width: 2.4rem !important;
    height: 2.4rem !important;
    margin: 0 !important;
    font-size: 1.1rem !important;
    transform: none !important;
    pointer-events: auto;
  }
  .action-item:hover,
  .action-item:focus-visible {
    transform: scale(1.05) !important;
  }
  /* Drop the "ripple to neighbour" hover effect; on touch it just
     paints a random sibling blue for no reason. */
  .action-item:hover + .action-item,
  .action-item:has(+ .action-item:hover) {
    background: rgba(20, 22, 28, 0.85);
  }
  html[data-theme="light"] .action-item:hover + .action-item,
  html[data-theme="light"] .action-item:has(+ .action-item:hover) {
    background: rgba(255, 255, 255, 0.92);
  }

  /* ----- Top-right controls: compact + user-badge moves into the
     hamburger menu so the corner stays uncluttered. ----- */
  #top-right-controls {
    gap: 0.35rem !important;
    margin: 0.5rem !important;
  }
  /* Username chip + Account / Privacy / Logout links live in the
     dock drawer on mobile (see the .mobile-only `<a>` items at the
     foot of #dock); nothing of the badge is shown in the corner. */
  #user-badge { display: none !important; }
  /* Theme + reset buttons: shrink slightly. */
  #theme-toggle,
  #view-reset {
    --bs-btn-padding-y: 0.2rem;
    --bs-btn-padding-x: 0.45rem;
    font-size: 0.85rem;
  }

  /* ----- HUD + file-banner: clear the new chrome ----- */
  #hud {
    margin-top: 3.5rem !important;
    font-size: 0.65rem !important;
    padding: 0.25rem 0.5rem !important;
  }
  #file-banner { margin-top: 1.4rem !important; }
  #file-banner-name { font-size: 0.65rem !important; max-width: 60vw; }
  #file-banner-time { font-size: 0.6rem !important; max-width: 50vw; }

  /* ----- Privacy banner: lift above dock + action dock zone ----- */
  .privacy-banner {
    bottom: 7.2rem;
  }

  /* Light-theme variant for the hamburger pills. */
  html[data-theme="light"] #mobile-dock-toggle,
  html[data-theme="light"] #mobile-action-toggle {
    background: rgba(255, 255, 255, 0.95);
    color: #212529;
    border-color: rgba(0, 0, 0, 0.14);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
  }
  html[data-theme="light"] #mobile-dock-toggle.open,
  html[data-theme="light"] #mobile-action-toggle.open {
    background: #0d6efd;
    color: #fff;
    border-color: #0d6efd;
  }

  /* ----- Modals: respect viewport edges ----- */
  #selection-modal .selection-card,
  #sort-modal .selection-card {
    width: calc(100vw - 1.2rem);
    max-width: 28rem;
  }

  /* ----- Play panel (transport): centre, full-width minus margins. */
  #play-panel {
    left: 0.5rem !important;
    right: 0.5rem !important;
    width: auto !important;
    transform: none !important;
  }

  /* ----- App version badge: hidden on mobile to free the bottom-
     right corner; desktop keeps it for build-tracking. ----- */
  .app-version { display: none !important; }
}

/* ===========================================================
 * "ML Runs" dock panel (#panel-runs, js/runs.js).
 * Theme-neutral: rgba()s that read fine on light or dark. The
 * sparkline + most text use `currentColor`.
 * =========================================================== */

/* Bigger than the default 320px panel (the detail view shows a wide
 * spectrogram image + metrics tables) and user-resizable. `resize: both`
 * needs non-visible overflow + a definite size to draw a grip, so we set
 * `overflow: hidden` here and let `.panel-body` scroll. The min-* values
 * stop the panel being squashed into nothing. Mirrors #panel-models. */
#panel-runs {
  width: 60rem;
  height: 70vh;
  min-width: 26rem;
  min-height: 16rem;
  max-width: 96vw;
  max-height: 94vh;
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Dense run metrics + best-bot images — make this panel fully
     opaque (both themes) so the underlying spectrogram doesn't
     bleed through. */
  background: #14161c;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html[data-theme="light"] #panel-runs {
  background: #ffffff;
}

/* "All Models" panel — wide table view, opaque background (matches
   #panel-models / #panel-runs styling). */
#panel-all-models {
  width: 52rem;
  height: 60vh;
  min-width: 30rem;
  min-height: 14rem;
  max-width: 96vw;
  max-height: 94vh;
  resize: both;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: #14161c;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html[data-theme="light"] #panel-all-models {
  background: #ffffff;
}
/* Bootstrap's .table defaults to dark text on a light background regardless of the rest of the
   page — that reads as invisible inside the panel's dark-mode body and clashes with the
   day/night toggle. Re-source its CSS variables from the panel's color cascade so the table
   takes the surrounding theme: text colour + striped/hover backgrounds adapt automatically.
   Borders also flip via a neutral grey that works on both backgrounds. */
#panel-all-models .table {
  --bs-table-color: inherit;
  --bs-table-bg: transparent;
  --bs-table-border-color: rgba(127, 127, 127, 0.25);
  --bs-table-striped-color: inherit;
  --bs-table-striped-bg: rgba(127, 127, 127, 0.08);
  --bs-table-hover-color: inherit;
  --bs-table-hover-bg: rgba(127, 127, 127, 0.14);
  --bs-table-active-color: inherit;
  --bs-table-active-bg: rgba(127, 127, 127, 0.18);
  color: inherit;
}
#panel-all-models .table > :not(caption) > * > * {
  background-color: transparent;
  color: inherit;
}
/* Clickable sort headers (Name / Macro F1 / Updated). A neutral ↕ marks a
   sortable column; the active one shows a ▲ (ascending) or ▼ (descending). */
#panel-all-models .table thead th.col-sortable {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
#panel-all-models .table thead th.col-sortable:hover {
  text-decoration: underline;
}
#panel-all-models .table thead th.col-sortable::after {
  content: "\2195"; /* ↕ */
  margin-left: 0.25rem;
  font-size: 0.85em;
  opacity: 0.35;
}
#panel-all-models .table thead th.col-sortable.sorted-asc::after { content: "\25B2"; opacity: 1; }  /* ▲ */
#panel-all-models .table thead th.col-sortable.sorted-desc::after { content: "\25BC"; opacity: 1; } /* ▼ */
/* Compact rows: smaller font + tighter padding so the list reads densely
   when the user has dozens of saved models. */
#panel-all-models .table {
  font-size: 0.78rem;
}
#panel-all-models .table > :not(caption) > * > * {
  padding: 0.25rem 0.4rem;
  line-height: 1.25;
}
#panel-all-models .table .badge {
  font-size: 0.65rem;
  padding: 0.15em 0.4em;
}
#panel-all-models .table .btn-sm,
#panel-all-models .table .btn-sm i {
  font-size: 0.78rem;
  padding: 0.15rem 0.35rem;
}
#panel-all-models .table .all-models-color {
  width: 1.1rem !important;
  height: 1.1rem !important;
}
#panel-all-models .panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 1.3rem;
  padding-right: 1.1rem;
}
#panel-all-models.minimised {
  display: block !important;
  width: auto !important;
  min-width: 12rem !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  resize: none !important;
  overflow: hidden !important;
}
#panel-all-models.minimised .panel-body {
  display: none !important;
}

/* Templates panel — reuse the My-Models theming so the table follows day/night the same way.
   Plus a cursor + subtle hover on the clickable rows that load the template into the main
   spectrogram view (see vixn:load-template-as-file). */
#panel-templates {
  width: 44rem;
  max-width: 95vw;
  background: #14161c;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
html[data-theme="light"] #panel-templates {
  background: #ffffff;
}
#panel-templates .table {
  --bs-table-color: inherit;
  --bs-table-bg: transparent;
  --bs-table-border-color: rgba(127, 127, 127, 0.25);
  --bs-table-striped-color: inherit;
  --bs-table-striped-bg: rgba(127, 127, 127, 0.08);
  --bs-table-hover-color: inherit;
  --bs-table-hover-bg: rgba(127, 127, 127, 0.14);
  --bs-table-active-color: inherit;
  --bs-table-active-bg: rgba(127, 127, 127, 0.18);
  color: inherit;
}
#panel-templates .table > :not(caption) > * > * {
  background-color: transparent;
  color: inherit;
}
#panel-templates .templates-row { cursor: pointer; }
#panel-templates .templates-row:hover {
  background-color: rgba(127, 127, 127, 0.14);
}

/* Rules summary table inside the model inspector — Bootstrap's default
   .table forces a white background + body-text colour via its CSS
   variables and an inset box-shadow on every cell. That paints the rows
   white-on-white when the panel is in dark mode (or light-on-light in
   inverted ops), so the rows render invisibly. Pin the variables to
   transparent + inherit, and kill the inset accent shadow that draws on
   top of the inherited text. !important is used because Bootstrap's
   `.table > :not(caption) > * > *` selector has the same specificity and
   wins on source order otherwise. */
#panel-models .models-rules-table-wrap .table,
#panel-models .models-rules-table-wrap .table * {
  --bs-table-color: inherit;
  --bs-table-bg: transparent;
  --bs-table-accent-bg: transparent;
  --bs-table-border-color: rgba(127, 127, 127, 0.25);
  --bs-table-striped-color: inherit;
  --bs-table-striped-bg: rgba(127, 127, 127, 0.08);
  --bs-table-hover-color: inherit;
  --bs-table-hover-bg: rgba(127, 127, 127, 0.14);
  --bs-table-active-color: inherit;
  --bs-table-active-bg: rgba(127, 127, 127, 0.18);
}
#panel-models .models-rules-table-wrap .table {
  color: inherit !important;
  background-color: transparent !important;
}
#panel-models .models-rules-table-wrap .table > :not(caption) > * > * {
  background-color: transparent !important;
  color: inherit !important;
  /* Bootstrap paints `box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg)`
     on every cell as its hover/stripe trick — that overlay covers the inherited
     text colour when --bs-table-accent-bg resolves to anything non-transparent. */
  box-shadow: none !important;
}
#panel-models .models-rules-table-wrap .table tbody tr,
#panel-models .models-rules-table-wrap .table tbody td {
  color: inherit !important;
  opacity: 1 !important;
}
/* Sticky thead needs an opaque-ish backdrop so scrolling rows don't bleed
   through. inherit the panel bg via a translucent overlay that works in
   either theme. */
#panel-models .models-rules-table-wrap thead.position-sticky {
  background-color: rgba(20, 22, 28, 0.92);
}
html[data-theme="light"] #panel-models .models-rules-table-wrap thead.position-sticky {
  background-color: rgba(255, 255, 255, 0.96);
}
#panel-templates .templates-row:focus-visible {
  outline: 2px solid var(--bs-info, #0dcaf0);
  outline-offset: -2px;
}

/* Body fills the remaining vertical space and scrolls; the extra
 * bottom-right padding keeps content clear of the `resize: both` grip. */
#panel-runs .panel-body {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
  padding-bottom: 1.3rem;
  padding-right: 1.1rem;
}
/* Minimised: collapse to just the titlebar and kill the resize grip;
 * `!important` defends against inline width/height the grip left behind. */
#panel-runs.minimised {
  display: block !important;
  width: auto !important;
  min-width: 12rem !important;
  height: auto !important;
  min-height: 0 !important;
  max-height: none !important;
  resize: none !important;
  overflow: hidden !important;
}

.runs-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.3rem;
  border-radius: 0.25rem;
  cursor: pointer;
}
.runs-row:hover { background: rgba(127, 127, 127, 0.14); }
.runs-row:focus-visible { background: rgba(127, 127, 127, 0.14); outline: 2px solid rgba(13, 110, 253, 0.6); outline-offset: -2px; }
.runs-row-info { flex: 1 1 auto; min-width: 0; }
.runs-row-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.runs-row-uid { font-family: var(--bs-font-monospace, monospace); font-size: 0.72rem; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.runs-row-meta { opacity: 0.7; font-size: 0.78rem; }
.runs-row-when { white-space: nowrap; }

.runs-status-badge {
  flex: 0 0 auto;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding: 0.12rem 0.4rem;
  border-radius: 0.35rem;
  background: rgba(127, 127, 127, 0.22);
}
.runs-status-done    { background: rgba(40, 167, 69, 0.22);  color: #2ea043; }
.runs-status-running { background: rgba(13, 110, 253, 0.22); color: #2997ff; }
.runs-status-queued  { background: rgba(127, 127, 127, 0.22); }
.runs-status-failed  { background: rgba(220, 53, 69, 0.22);  color: #e5534b; }

.runs-spark { width: 100%; height: 64px; display: block; color: currentColor; }
.runs-img { display: block; max-width: 100%; height: auto; border: 1px solid rgba(127, 127, 127, 0.3); border-radius: 0.25rem; }
.runs-img-figure { margin: 0 0 0.7rem; }
.runs-img-figure:last-child { margin-bottom: 0; }
.runs-img-cap { font-size: 0.72rem; opacity: 0.65; margin-bottom: 0.2rem; }

/* Analysis view: per-recording confusion table (grouped by split) + example-figure
   sections. The FP / correct / missed columns are tinted so the error profile reads
   at a glance; the figure grid wraps the three composite PNGs per category. */
.runs-analysis-split-title { font-size: 0.78rem; font-weight: 600; opacity: 0.85; margin: 0.5rem 0 0.2rem; }
.runs-analysis-confusion { margin-bottom: 0.4rem; }
.runs-analysis-confusion th { text-align: right; white-space: nowrap; }
.runs-analysis-confusion th:first-child { text-align: left; }
.runs-analysis-confusion td { padding: 0.1rem 0.5rem 0.1rem 0; text-align: right; white-space: nowrap; }
.runs-analysis-confusion td:first-child { text-align: left; overflow: hidden; text-overflow: ellipsis; max-width: 12rem; }
.runs-analysis-confusion tbody tr:nth-child(odd) { background: rgba(127, 127, 127, 0.08); }
.runs-analysis-fp { color: var(--bs-danger, #dc3545); font-weight: 600; }
.runs-analysis-cp { color: var(--bs-success, #198754); font-weight: 600; }
.runs-analysis-miss { color: var(--bs-warning, #ffc107); font-weight: 600; }
.runs-analysis-section { margin: 0 0 0.9rem; }
.runs-analysis-section-title { font-size: 0.8rem; font-weight: 600; opacity: 0.9; margin: 0 0 0.4rem; }
.runs-analysis-grid { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.runs-analysis-grid .runs-img-figure { flex: 1 1 14rem; min-width: 12rem; margin: 0; }

.runs-meta-table, .runs-eval-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.runs-meta-table th { text-align: left; opacity: 0.65; font-weight: 600; padding: 0.1rem 0.5rem 0.1rem 0; white-space: nowrap; vertical-align: top; }
.runs-meta-table td { padding: 0.1rem 0; }
.runs-eval-head { margin-bottom: 0.35rem; }
.runs-eval-table th, .runs-eval-table td { padding: 0.12rem 0.4rem 0.12rem 0; text-align: left; }
.runs-eval-table th { opacity: 0.65; font-weight: 600; }
.runs-eval-table tbody tr:nth-child(odd) { background: rgba(127, 127, 127, 0.08); }
/* Per-split eval blocks stacked in #runs-detail-eval — each with a left-border tint so
   train / val / test are visually distinct without needing extra chrome. The "val" line on the
   sparkline uses the same teal so the legend ties together. */
.runs-eval-block { margin: 0 0 0.6rem; padding: 0.25rem 0 0.25rem 0.55rem; border-left: 3px solid transparent; }
.runs-eval-block:last-child { margin-bottom: 0.2rem; }
.runs-eval-h { font-size: 0.78rem; margin: 0 0 0.25rem; opacity: 0.9; font-weight: 600; }
/* Pull the discriminator colours from Bootstrap's theme tokens so they shift with day/night
   instead of staying fixed-hue against a panel that's flipped from dark to light. The values
   resolve through the .floating-panel cascade — html[data-theme="light"] doesn't override --bs-*
   anywhere, so Bootstrap's defaults apply on both themes (they happen to read well against both
   the dark-translucent and white-translucent panel backgrounds). */
.runs-eval-train { border-left-color: rgba(127, 127, 127, 0.55); }
.runs-eval-val   { border-left-color: var(--bs-info, #0dcaf0); }
.runs-eval-test  { border-left-color: var(--bs-warning, #ffc107); }
.runs-spark-val  { stroke: var(--bs-info, #0dcaf0); }
/* Collapsible definitions block at the bottom of the eval area. Compact two-column dl: term
   left, definition right. Same disclosure styling as .runs-config above so the panel feels
   consistent. */
.runs-eval-defs { margin-top: 0.5rem; }
.runs-eval-defs summary { cursor: pointer; padding: 0.15rem 0; }
.runs-defs { display: grid; grid-template-columns: max-content 1fr; column-gap: 0.7rem; row-gap: 0.25rem;
             margin: 0.35rem 0 0; padding: 0.4rem 0.55rem; background: rgba(127, 127, 127, 0.08);
             border-radius: 0.25rem; }
.runs-defs dt { font-weight: 600; opacity: 0.85; white-space: nowrap; }
.runs-defs dd { margin: 0; opacity: 0.85; }
.runs-defs code { font-size: 0.85em; padding: 0 0.2em; background: rgba(127, 127, 127, 0.15); border-radius: 0.18rem; }

/* Benchmark panel pickers (model + file checkbox lists). Bounded height with scroll so the
   form stays usable when the user has many of either; same padded background as the
   definitions block for a consistent in-panel look. */
.benchmarks-picker { max-height: 12rem; overflow-y: auto; padding: 0.4rem 0.55rem;
                     background: rgba(127, 127, 127, 0.08); border-radius: 0.25rem; }
.benchmarks-picker .form-check { margin-bottom: 0.1rem; }
.benchmarks-picker .form-check:last-child { margin-bottom: 0; }
/* The benchmark panel hosts a 10-column per-cell table + a model+file picker, so the default
   floating-panel width of 320 px is far too narrow. Match the labels-table panel pattern: pin
   to a comfortable width, allow the panel to grow to the viewport, and clamp the body so it
   scrolls *inside* rather than overflowing onto the canvas. The wide cell table additionally
   scrolls horizontally on its own so a large model count doesn't push the panel wider. */
#panel-benchmarks { width: 52rem; max-width: 95vw; }
#panel-benchmarks .panel-body { max-height: 80vh; overflow-y: auto; }
#panel-benchmarks #benchmarks-detail-cells,
#panel-benchmarks #benchmarks-detail-ranking,
#panel-benchmarks #benchmarks-detail-per { overflow-x: auto; max-width: 100%; }
#panel-benchmarks .runs-eval-table { table-layout: auto; }
/* Long model / file / dataset names shouldn't squeeze every other column to nothing — let them
   wrap or truncate (truncate wins because the cells are dense and a tooltip on the row already
   shows the full name). */
#panel-benchmarks .runs-eval-table td:first-child,
#panel-benchmarks .runs-eval-table td:nth-child(2) {
  max-width: 18rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.runs-config { margin-top: 0.4rem; }
.runs-config summary { cursor: pointer; }
.runs-config pre { margin: 0.25rem 0 0; padding: 0.4rem; font-size: 0.72rem; background: rgba(127, 127, 127, 0.12); border-radius: 0.25rem; max-height: 14rem; overflow: auto; }
.runs-config-table { width: 100%; border-collapse: collapse; font-size: 0.76rem; margin-top: 0.3rem; table-layout: fixed; }
.runs-config-table th { text-align: left; opacity: 0.6; font-weight: 600; white-space: nowrap; padding: 0.08rem 0.35rem 0.08rem 0; }
.runs-config-table td { padding: 0.08rem 0.7rem 0.08rem 0; overflow: hidden; text-overflow: ellipsis; }
.runs-config-table tbody tr:nth-child(odd) { background: rgba(127, 127, 127, 0.07); }
/* Live training-output tail in the run detail view. */
.runs-log {
  margin: 0 0 0.5rem;
  padding: 0.5rem;
  font-family: var(--bs-font-monospace, monospace);
  font-size: 0.72rem;
  line-height: 1.35;
  white-space: pre-wrap;
  word-break: break-word;
  background: rgba(127, 127, 127, 0.12);
  border-radius: 0.25rem;
  max-height: 16rem;
  overflow: auto;
}
/* Live training-run log panel — one floating panel per active brahma
   run, cloned at runtime from #panel-run-log. Every clone carries the
   .run-log-panel class, so styling targets the class rather than the
   (template-only) id. */
.run-log-panel { width: min(560px, 92vw); }
.run-log-panel .run-log-pre { max-height: min(60vh, 26rem); margin: 0; }
.run-log-panel .run-log-head code { font-size: 0.72rem; }
.run-log-panel .run-log-head .runs-status-badge { vertical-align: baseline; }
/* Scrollable recording-picker in the "New run" form. */
.runs-submit-files { max-height: 11rem; overflow: auto; }
.runs-submit-files:empty::after { content: "no saved recordings"; opacity: 0.6; font-size: 0.78rem; }
.runs-submit-files .form-check { margin-bottom: 0.1rem; }
.runs-submit-files .form-check-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.runs-submit-adv summary { cursor: pointer; }

/* ===========================================================
 * Pop-out: the ⤢ button in the Models / ML Runs panel titlebars
 * opens the app in a new window with ?popout=<panel>, which
 * auto-opens + maximises that panel (see js/ui.js + js/main.js).
 * =========================================================== */
.panel-popout {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.panel-popout:hover { color: #fff; }
.panel-popout:focus-visible { outline: 2px solid #6ea8fe; outline-offset: 2px; }
html[data-theme="light"] .panel-popout { color: #6c757d; }
html[data-theme="light"] .panel-popout:hover { color: #000; }

/* The popped-out panel fills its window (the dock stays on the left).
 * `!important` is needed because #panel-models / #panel-runs set their
 * own width / height / resize and win on ID specificity otherwise. */
/* A popped-out window is a single-panel monitor — hide the dock (it isn't used)
   and let the maximised panel reclaim that space. */
body.popout-window #dock { display: none !important; }
/* Popout windows (incl. the dashboard tiles) are single-panel monitors — drop the
   account chrome (username · Account · Admin · Privacy · Logout) too. */
body.popout-window #top-right-controls { display: none !important; }
/* In a popout window every panel's own ⤢ pop-out button is redundant — the
   maximised panel already hides it (.popout-target rule below), and inside the
   dashboard the shell's expand overlay is the only expand control. Hide it on the
   non-maximised panels too (e.g. the Data panel in the render tile). The Sensor
   map's in-panel "maximise" (.panel-fullscreen, same ⤢ icon) is likewise
   redundant in a popout — hide it so only ONE expand icon shows per tile. */
body.popout-window .panel-popout,
body.popout-window .panel-fullscreen { display: none !important; }
/* The floating Application-time panel auto-reveals on stream connect, so it would
   pop up over every dashboard tile — but the Diagnostics tile already shows the
   application time. Suppress the standalone panel in popout windows. */
body.popout-window #panel-app-time { display: none !important; }
body.popout-window .floating-panel.popout-target { left: 0.5rem !important; }

.floating-panel.popout-target {
  position: fixed !important;
  top: 0.5rem !important;
  right: 0.5rem !important;
  bottom: 0.5rem !important;
  left: 5.5rem !important;
  width: auto !important;
  height: auto !important;
  max-width: none !important;
  max-height: none !important;
  min-width: 0 !important;
  min-height: 0 !important;
  resize: none !important;
  transform: none !important;
}
.floating-panel.popout-target .panel-body { max-height: none; flex: 1 1 auto; min-height: 0; }
/* In a popped-out window the panel is already maximised — hide its
 * own pop-out + minimise buttons (close still works → it hides). */
.popout-target .panel-popout,
.popout-target .panel-minimise { display: none; }

/* ===========================================================
 * Model score-vs-time plot (#model-score-plot, js/score-plot.js).
 * A translucent strip; opens below the spectrogram (clear of the
 * dock + thumbnail strip), then draggable by its grip and resizable
 * from the bottom-right corner (`resize: both`). Once the user moves
 * it, js/main.js switches it to inline left/top coords.
 * =========================================================== */
#model-score-plot-wrap {
  position: fixed;
  left: 6.5rem;
  right: 13.5rem;
  bottom: 0.6rem;
  height: 7rem;
  min-width: 14rem;
  min-height: 4rem;
  max-width: 96vw;
  max-height: 60vh;
  z-index: 45;
  display: flex;
  flex-direction: column;
  padding: 3px;
  resize: both;
  overflow: hidden;
  background: rgba(20, 22, 28, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.4rem;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  color: #e9ecef;
}
.score-plot-grip {
  flex: 0 0 auto;
  height: 13px;
  cursor: move;
  user-select: none;
  display: flex;
  align-items: center;
  padding: 0 5px;
  font-size: 8px;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.5;
}
.score-plot-grip:hover { opacity: 0.85; }
#model-score-plot { flex: 1 1 auto; min-height: 0; display: block; width: 100%; height: auto; }
html[data-theme="light"] #model-score-plot-wrap {
  background: rgba(255, 255, 255, 0.92);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.18);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 8px 22px rgba(0, 0, 0, 0.18);
}
@media (max-width: 700px) {
  #model-score-plot-wrap { left: 0.5rem; right: 0.5rem; bottom: 0.5rem; height: 5.5rem; }
}

/* Waveform plot — same chrome as the score plot, just stacked above it so the
   two strips share the bottom-of-canvas real estate without overlapping. The
   user can drag either to anywhere they prefer; CSS only positions the initial
   resting place. */
#waveform-plot-wrap {
  position: fixed;
  left: 6.5rem;
  right: 13.5rem;
  bottom: 8rem;           /* score-plot is at 0.6rem + 7rem high → start above it */
  height: 7rem;
  min-width: 14rem;
  min-height: 4rem;
  max-width: 96vw;
  max-height: 60vh;
  z-index: 45;
  display: flex;
  flex-direction: column;
  padding: 3px;
  resize: both;
  overflow: hidden;
  background: rgba(20, 22, 28, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0.4rem;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  color: #e9ecef;
}
#waveform-plot { flex: 1 1 auto; min-height: 0; display: block; width: 100%; height: auto; }
html[data-theme="light"] #waveform-plot-wrap {
  background: rgba(255, 255, 255, 0.92);
  color: #212529;
  border-color: rgba(0, 0, 0, 0.18);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 8px 22px rgba(0, 0, 0, 0.18);
}
@media (max-width: 700px) {
  #waveform-plot-wrap { left: 0.5rem; right: 0.5rem; bottom: 6.5rem; height: 5.5rem; }
}

/* ===========================================================
 * "Rule preview" panel (#panel-rule-preview) — opened by the ▷ button on a rule node; plots that
 * single rule's per-frame output via drawScorePlot (js/score-plot.js).
 * =========================================================== */
#panel-rule-preview {
  width: 42rem;
  max-width: 92vw;
  top: 4rem;
  left: auto;
  right: 0.8rem;
  transform: none;
}
#rule-preview-canvas { display: block; width: 100%; height: 12rem; }
#rule-preview-caption { word-break: break-word; }
/* the ▷ preview button on a rule node header */
#panel-models .rule-node-preview {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.2rem;
}
#panel-models .rule-node-preview:hover { color: #6ea8fe; }

/* ── Sensor map panel ─────────────────────────────────────────────────── */
/* Default a touch larger than the base panel so the map has room. */
#panel-sensor-map { width: 480px; height: 420px; }
#panel-sensor-map .panel-body { padding: 0; display: flex; flex-direction: column; }
#sensor-map-meta { flex: 0 0 auto; }
.sensor-map-canvas { flex: 1 1 auto; width: 100%; min-height: 320px; }
.sensor-map-utc { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Leaflet tiles must sit above the panel's translucent backdrop. */
.sensor-map-canvas .leaflet-container { background: #14161c; }
/* Leaflet 1.9 injects a Ukraine flag into the attribution control — drop it. */
.leaflet-attribution-flag { display: none !important; }
/* Permanent sensor-name tooltip — a solid high-contrast pill so the name
 * stays legible over any map tile (the default translucent label washed out). */
.leaflet-tooltip.sensor-name-label {
  background: rgba(12, 16, 24, 0.92);
  color: #eaf2ff;
  border: 1px solid #4aa3ff;
  border-radius: 4px;
  padding: 2px 7px;
  font-weight: 700;
  font-size: 13px;
  white-space: nowrap;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.leaflet-tooltip.sensor-name-label::before { border-right-color: #4aa3ff; }
/* Range-ring distance labels: light, shadowed, click-through. */
.sensor-range-label {
  background: none; border: 0; box-shadow: none; padding: 0;
  font-size: 11px; font-weight: 600; color: #cfe4ff; white-space: nowrap;
  text-shadow: 0 0 3px #000, 0 0 3px #000; pointer-events: none;
}
/* AIS vessel markers: an arrow pointing along course-over-ground (or a dot). */
.sensor-vessel { background: none; border: 0; }
.sensor-vessel-arrow,
.sensor-vessel-dot {
  display: block; width: 14px; height: 14px; margin: 1px;
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7));
}
.sensor-vessel-arrow {
  /* Upward-pointing triangle; rotated inline to the vessel's heading. */
  width: 0; height: 0; margin: 1px 2px;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 13px solid #ffb347;
  transform-origin: 50% 65%;
}
.sensor-vessel-dot {
  width: 9px; height: 9px; margin: 3px;
  border-radius: 50%; background: #ffb347;
}
/* Selected vessel's "current" arrow — a touch larger than a track arrow. */
.sensor-vessel-selected .sensor-vessel-arrow {
  border-left-width: 7px; border-right-width: 7px; border-bottom-width: 16px;
}
/* "minutes since last AIS report" line in vessel popups. */
.sensor-vessel-age { font-weight: 700; color: #ffd166; }

/* "Going to time & position" progress strip at the top of the map, shown while a
   clicked track point loads its files + seeks. Sits just above the canvas. */
.sensor-map-loading {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 4px 8px;
  background: rgba(12, 16, 24, 0.92);
  border-bottom: 1px solid rgba(74, 163, 255, 0.4);
  font-size: 12px;
  color: #cfe4ff;
}
.sensor-map-loading[hidden] { display: none; }
.sensor-map-loading-label { white-space: nowrap; font-weight: 600; }
.sensor-map-loading-bar {
  position: relative;
  flex: 1 1 auto;
  height: 4px;
  border-radius: 2px;
  background: rgba(74, 163, 255, 0.18);
  overflow: hidden;
}
.sensor-map-loading-bar::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 35%;
  border-radius: 2px;
  background: #4aa3ff;
  animation: sensor-map-loading-slide 1.1s ease-in-out infinite;
}
@keyframes sensor-map-loading-slide {
  0%   { left: -35%; }
  100% { left: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .sensor-map-loading-bar::after { animation: none; left: 0; width: 100%; opacity: 0.6; }
}

/* Hover detail tooltip for AIS track points — dark, to match the vessel popups. */
.leaflet-tooltip.sensor-vessel-tip {
  background: rgba(12, 16, 24, 0.94);
  color: #eaf2ff;
  border: 1px solid rgba(120, 150, 190, 0.5);
  border-radius: 4px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  font-size: 12px;
  line-height: 1.35;
}
.leaflet-tooltip.sensor-vessel-tip.leaflet-tooltip-top::before { border-top-color: rgba(120, 150, 190, 0.5); }
.leaflet-tooltip.sensor-vessel-tip.leaflet-tooltip-bottom::before { border-bottom-color: rgba(120, 150, 190, 0.5); }
.leaflet-tooltip.sensor-vessel-tip.leaflet-tooltip-left::before { border-left-color: rgba(120, 150, 190, 0.5); }
.leaflet-tooltip.sensor-vessel-tip.leaflet-tooltip-right::before { border-right-color: rgba(120, 150, 190, 0.5); }

/* Custom "Vessels" map control: per-vessel track toggles + All/Clear. Styled to
 * sit alongside Leaflet's own layer control (.leaflet-bar) but dark-themed. */
.sensor-vessel-control {
  background: rgba(12, 16, 24, 0.92);
  color: #eaf2ff;
  border: 1px solid rgba(120, 150, 190, 0.4);
  border-radius: 5px;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5);
  font-size: 12px;
  width: 190px;
  overflow: hidden;
}
.sensor-vessel-control .svc-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 6px; padding: 5px 8px;
  border-bottom: 1px solid rgba(120, 150, 190, 0.28);
}
.sensor-vessel-control .svc-title { font-weight: 700; }
.sensor-vessel-control .svc-actions { display: flex; gap: 4px; }
.sensor-vessel-control .svc-btn {
  background: rgba(74, 163, 255, 0.16);
  color: #cfe4ff;
  border: 1px solid rgba(74, 163, 255, 0.5);
  border-radius: 3px;
  padding: 1px 7px; font-size: 11px; font-weight: 600; cursor: pointer;
}
.sensor-vessel-control .svc-btn:hover { background: rgba(74, 163, 255, 0.3); }
.sensor-vessel-control .svc-list { max-height: 220px; overflow-y: auto; }
.sensor-vessel-control .svc-empty { padding: 7px 8px; color: #9fb2cc; }
.sensor-vessel-control .svc-row {
  display: flex; align-items: center; gap: 6px;
  padding: 3px 8px; margin: 0; cursor: default;
}
.sensor-vessel-control .svc-row:hover { background: rgba(74, 163, 255, 0.1); }
.sensor-vessel-control .svc-row.selected { background: rgba(74, 163, 255, 0.24); }
.sensor-vessel-control .svc-toggle { cursor: pointer; flex: 0 0 auto; }
.sensor-vessel-control .svc-swatch {
  flex: 0 0 auto; width: 11px; height: 11px; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
}
.sensor-vessel-control .svc-name {
  flex: 1 1 auto; min-width: 0; cursor: default;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Vessels currently within 5 km of the sensor: bold amber name + faint row wash
   and a small "≤5 km" chip (purely class-driven, so it tracks the app time). */
.sensor-vessel-control .svc-row.near { background: rgba(255, 193, 7, 0.14); }
.sensor-vessel-control .svc-row.near .svc-name { color: #ffc107; font-weight: 700; }
.sensor-vessel-control .svc-row.near .svc-name::after {
  content: "≤5 km";
  margin-left: 0.4rem; padding: 0.05rem 0.32rem;
  font-size: 9px; font-weight: 700; letter-spacing: 0.02em;
  color: #1b1d22; background: #ffc107; border-radius: 0.5rem;
  vertical-align: middle;
}
.sensor-vessel-control .svc-count {
  flex: 0 0 auto; color: #9fb2cc; font-variant-numeric: tabular-nums;
}
.sensor-vessel-control .svc-interp {
  display: flex; align-items: center; gap: 6px;
  padding: 5px 8px; margin: 0;
  border-top: 1px solid rgba(120, 150, 190, 0.28);
  font-size: 11px; cursor: pointer;
}
.sensor-vessel-control .svc-interp input { cursor: pointer; flex: 0 0 auto; }
.sensor-vessel-control .svc-sim-status {
  padding: 0 8px 6px; font-size: 10px; color: #ffd166; min-height: 0;
}
.sensor-vessel-control .svc-sim-status:empty { display: none; }
/* Sim start (A) / finish (B) labels on the map. */
.sensor-sim-tag {
  background: none; border: 0; box-shadow: none;
  font-weight: 800; font-size: 12px; line-height: 16px; text-align: center;
  text-shadow: 0 0 3px #000, 0 0 3px #000; pointer-events: none;
}

/* Stream diagnostics panel: one card per feed check, with an OK/Warning/Error
 * badge and a row of metric chips. */
#panel-diagnostics { width: 420px; }
/* Application-time readout at the top of the diagnostics panel — the UTC instant
 * this window is currently showing (live in popped-out dashboard windows). */
.diag-app-time {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 7px 10px; margin-bottom: 10px;
  border: 1px solid rgba(120, 150, 190, 0.25); border-radius: 5px;
  background: rgba(74, 163, 255, 0.07);
}
.diag-app-time-k {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; color: #9fb2cc;
  display: inline-flex; align-items: center; gap: 5px;
}
.diag-app-time-v { font-size: 14px; font-weight: 600; color: #eaf2ff; font-variant-numeric: tabular-nums; }
.diagnostics-list { display: flex; flex-direction: column; gap: 8px; }
.diag-row {
  border: 1px solid rgba(120, 150, 190, 0.25);
  border-left-width: 3px;
  border-radius: 5px;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.02);
}
.diag-ok    { border-left-color: #4caf72; }
.diag-warn  { border-left-color: #e0a32e; }
.diag-error { border-left-color: #e0524a; }
.diag-head { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
.diag-label { font-weight: 700; }
.diag-badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 700; line-height: 1;
  padding: 3px 7px; border-radius: 3px; white-space: nowrap;
}
.diag-badge i { font-size: 13px; }
.diag-badge-ok    { background: rgba(76, 175, 114, 0.18); color: #8be0a8; }
.diag-badge-warn  { background: rgba(224, 163, 46, 0.18); color: #f0cd84; }
.diag-badge-error { background: rgba(224, 82, 74, 0.2);  color: #f3a39d; }
.diag-summary { font-size: 13px; color: #d4def0; }
.diag-metrics { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.diag-metric {
  display: inline-flex; flex-direction: column; gap: 1px;
  padding: 3px 7px; border-radius: 3px;
  background: rgba(120, 150, 190, 0.12);
}
.diag-metric-k { font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em; color: #9fb2cc; }
.diag-metric-v { font-size: 12px; font-weight: 600; color: #eaf2ff; font-variant-numeric: tabular-nums; }

/* ── My streams overview (#panel-my-streams) ────────────────────────────────
   A slim full-height rail hugging the LEFT edge, just to the right of the dock
   (dock: 1rem margin + 3rem icons → clear it by ~4.75rem). Like .panel-half
   these are just the open-state defaults — still draggable / resizable. */
.floating-panel.panel-side {
  width: min(26rem, calc(100vw - 5.75rem));
  max-width: calc(100vw - 5.75rem);
  height: calc(100vh - 2rem);
  max-height: calc(100vh - 1rem);
  top: 1rem;
  left: 4.75rem;
  right: auto;
  transform: none;
}

.stream-cards { display: flex; flex-direction: column; gap: 0.5rem; }
.stream-card {
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 0.5rem;
  background: rgba(255, 255, 255, 0.03);
  padding: 0.55rem 0.65rem;
}
.stream-card.is-active {
  border-color: rgba(46, 160, 67, 0.7);
  background: rgba(46, 160, 67, 0.08);
}
.stream-card-head { display: flex; align-items: center; gap: 0.5rem; }
.stream-card-name {
  font-weight: 600; font-size: 0.9rem; color: #e9ecef;
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stream-card-time {
  font-size: 0.72rem; color: #adb5bd; margin-top: 0.2rem;
  font-variant-numeric: tabular-nums;
}
.stream-card-err { color: #f3a39d; }
.stream-dots { display: flex; align-items: center; gap: 0.4rem; flex-shrink: 0; }
.stream-dot {
  width: 0.7rem; height: 0.7rem; border-radius: 50%;
  background: #6c757d; cursor: help;
}
.stream-dot.ok    { background: #4caf72; box-shadow: 0 0 6px 1px rgba(76, 175, 114, 0.85); }
.stream-dot.warn  { background: #e0a32e; box-shadow: 0 0 6px 1px rgba(224, 163, 46, 0.85); }
.stream-dot.error {
  background: #e0524a; box-shadow: 0 0 6px 1px rgba(224, 82, 74, 0.9);
  animation: stream-dot-pulse 1.4s ease-in-out infinite;
}
@keyframes stream-dot-pulse {
  0%, 100% { box-shadow: 0 0 5px 1px rgba(224, 82, 74, 0.55); }
  50%      { box-shadow: 0 0 11px 3px rgba(224, 82, 74, 0.95); }
}
.stream-card-actions { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.5rem; flex-wrap: wrap; }
.stream-card-detail { margin-top: 0.5rem; }

html[data-theme="light"] .stream-card {
  border-color: rgba(0, 0, 0, 0.12);
  background: rgba(0, 0, 0, 0.02);
}
html[data-theme="light"] .stream-card-name { color: #212529; }
html[data-theme="light"] .stream-card.is-active { background: rgba(46, 160, 67, 0.12); }

@media (prefers-reduced-motion: reduce) {
  .stream-dot.error { animation: none; }
}

/* ── AIS activity heat maps (GitHub-style) ──────────────────────────────── */
.ais-calendar { margin-top: 0.6rem; border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 0.5rem; }
.ais-calendar-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.5rem; margin-bottom: 0.35rem;
}
.ais-calendar-title { font-size: 0.75rem; font-weight: 600; color: #cfe4ff; }
.ais-calendar-legend { display: inline-flex; align-items: center; gap: 3px; }
.ais-cal-legend-label { font-size: 0.6rem; color: #9fb2cc; }

/* Shared heat cell — small rounded square; intensity via heat-l0..l4. Used as
   grid items in the calendars and inline in the legend. */
.heat-cell {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.06);
  vertical-align: middle;
}
.heat-empty { background: transparent; }
.heat-l0 { background: rgba(255, 255, 255, 0.07); }
.heat-l1 { background: rgba(61, 220, 132, 0.28); }
.heat-l2 { background: rgba(61, 220, 132, 0.5); }
.heat-l3 { background: rgba(61, 220, 132, 0.74); }
.heat-l4 { background: #3ddc84; }
.heat-click { cursor: pointer; }
.heat-click:hover, .heat-cell:focus-visible {
  outline: 1px solid rgba(61, 220, 132, 0.9);
  outline-offset: 1px;
}

/* Day grid: 7 rows (Sun–Sat), columns auto-flow per week (like GitHub). */
.ais-calendar-grid {
  display: grid;
  grid-template-rows: repeat(7, 11px);
  grid-auto-flow: column;
  grid-auto-columns: 11px;
  gap: 2px;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: thin;
}

/* Hours grid: 24 cells, 12 per row, each labelled with its hour. */
.ais-hours-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 3px;
}
.heat-hour {
  width: auto;
  height: 1.6rem;
  border-radius: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.ais-hour-label {
  font-size: 0.6rem;
  font-weight: 600;
  color: #eaf2ff;
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.8), 0 0 2px rgba(0, 0, 0, 0.8);
}
#panel-ais-hours, #panel-rec-hours { width: 320px; }

/* ── Active Stream: collapsible sections + notifications table ───────────── */
.active-stream-section { margin: 0; }
.active-stream-summary {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
  list-style: none;
  padding: 0.15rem 0;
  font-size: 0.8rem;
  font-weight: 600;
  color: #cfe4ff;
}
.active-stream-summary::-webkit-details-marker { display: none; }
.active-stream-summary::before {
  content: "\25B8"; /* ▸ */
  color: #9fb2cc;
  transition: transform 120ms ease;
}
details[open] > .active-stream-summary::before { transform: rotate(90deg); }
.active-stream-summary > span:first-of-type { flex: 1 1 auto; }
.active-stream-notif-table th { font-weight: 600; color: #9fb2cc; white-space: nowrap; }
.notif-channel {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 3px;
  background: rgba(74, 163, 255, 0.18);
  color: #cfe4ff;
  font-size: 0.7rem;
  font-weight: 600;
}
.active-stream-notif-when {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: #6ea8fe;
  cursor: pointer;
  white-space: nowrap;
}
.active-stream-notif-when:hover { text-decoration: underline; }

/* The maximise button mirrors .panel-popout's styling but carries its own
 * class so ui.js's pop-out-into-a-new-window wiring (which binds .panel-popout)
 * doesn't hijack it — this button drives the Fullscreen API instead. */
.panel-fullscreen {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.panel-fullscreen:hover { color: #fff; }
.panel-fullscreen:focus-visible { outline: 2px solid #6ea8fe; outline-offset: 2px; }
html[data-theme="light"] .panel-fullscreen { color: #6c757d; }
html[data-theme="light"] .panel-fullscreen:hover { color: #000; }

/* Generic titlebar icon-button (e.g. the AIS vessels refresh button). */
.panel-action {
  background: transparent;
  border: 0;
  color: #adb5bd;
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.panel-action:hover { color: #fff; }
.panel-action:focus-visible { outline: 2px solid #6ea8fe; outline-offset: 2px; }
html[data-theme="light"] .panel-action { color: #6c757d; }
html[data-theme="light"] .panel-action:hover { color: #000; }

/* AIS vessels table — keep the panel from getting absurdly wide. */
#panel-vessels { width: 640px; }
#panel-vessel-search { width: 560px; }
.vessels-table th { white-space: nowrap; font-weight: 600; }
.vessels-table td { vertical-align: middle; }
.vessels-table .vessel-name { font-weight: 600; }
.vessels-table tbody tr { cursor: pointer; }
.vessels-table tbody tr:hover { background: rgba(255, 255, 255, 0.05); }
.vessels-table tbody tr.selected { background: rgba(110, 168, 254, 0.22); }
.vessels-table tbody tr.selected .vessel-name { color: #cfe4ff; }
/* Proximity-tracking bell toggle. */
.vessel-track-btn {
  background: none; border: 0; padding: 0 0.15rem; cursor: pointer;
  color: #8a949e; line-height: 1; font-size: 1rem;
}
.vessel-track-btn:hover { color: #cfe4ff; }
.vessel-track-btn.tracking { color: #ffcf4d; }
.vessel-track-btn:disabled { opacity: 0.5; cursor: default; }
.vessel-plot-btn {
  background: none; border: 0; padding: 0 0.15rem; cursor: pointer;
  color: #8a949e; line-height: 1; font-size: 1rem;
}
.vessel-plot-btn:hover { color: #cfe4ff; }
.vessels-table td.vessel-track { width: 2.8rem; text-align: center; white-space: nowrap; }
/* Distance-from-sensor cell: green within 5 km, purple when farther but moving. */
.vessels-table td.vessel-dist { white-space: nowrap; }
.vessels-table td.vessel-dist.near { background: rgba(40, 167, 69, 0.45); color: #eaffef; }
.vessels-table td.vessel-dist.moving { background: rgba(138, 79, 255, 0.45); color: #f3ecff; }

/* Vessel distance-vs-time plot (#panel-vessel-distance) + application-time panel. */
#panel-vessel-distance {
  width: 32rem;
  max-width: 92vw;
  top: 4rem;
  left: auto;
  right: 0.8rem;
  transform: none;
  resize: both;
  overflow: auto;
}
#vessel-distance-canvas {
  display: block; width: 100%; height: 13rem; cursor: crosshair;
}
#panel-app-time { width: 16rem; }
#app-time-value { letter-spacing: 0.02em; }

