/* ==========================================================================
   Iron Mind AI — Orchestrator Dashboard
   See docs/brandbook.md for the design system this file implements.
   ========================================================================== */

:root {
  /* Color system — see docs/brandbook.md "Color System". Variable NAMES kept
     as-is (--bg, --accent, etc.) to avoid touching thousands of usages
     across this file; only the values are remapped to the Iron Mind AI
     brand palette. */
  --bg: #0E0E0E;
  --bg-elevated: #161616;
  --bg-elevated-2: #1C1C1C;
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);
  --text: #E8EAED;
  --text-muted: #A6A6A6;
  --text-faint: #888888;
  --accent: #E87C2E;
  --accent-hover: #F08C3E;
  --accent-dim: rgba(232, 124, 46, 0.12);

  /* Status colors — brandbook only defines amber/success/error semantics for
     the marketing site; the 5 run states below are a judgment call
     (documented in the handoff) to keep them visually distinct while
     protecting the Amber Rule AND the brand's monochrome+one-warm-accent
     temperature. Revised per user feedback: vivid tailwind-style hues
     (#22c55e green / #ef4444 red / #5B9BD5 blue / #D9A441 gold) read as
     "foreign" against the near-monochrome #0E0E0E/#161616/#1C1C1C field —
     every status color below is now heavily desaturated and warm-leaning
     ("grayed color with a hint of hue," not a colored element), each tuned
     for ~4.5:1 text contrast against --bg/--bg-elevated. --accent (brand
     amber, #E87C2E) is intentionally NOT reused for any status — it stays
     reserved for primary CTAs, focus rings, and active nav so it never
     exceeds ~10% coverage. */
  --status-running: #8A94A3;   /* desaturated warm slate-gray — neutral "in progress", no hue competing with amber */
  --status-completed: #7FA88B; /* desaturated sage green — success signal without a saturated tailwind green */
  --status-failed: #B36A5E;    /* muted brick/terracotta — error signal, warm-leaning instead of pure red */
  --status-stopped: #B08A4E;   /* muted, duller gold — same amber family as --accent but clearly subordinate to it */
  --status-orphaned: #767676;  /* neutral gray, muted/unknown state — unchanged, already on-temperature */

  --font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: 'Barlow Condensed', var(--font-ui);
  --font-mono: 'JetBrains Mono', ui-monospace, "SF Mono", "Cascadia Code", "Fira Code", Consolas, monospace;

  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 8px;
  --radius-pill: 999px;

  --transition-fast: 150ms ease;
  --transition-base: 200ms ease;
  --transition-slow: 300ms ease-out;
}

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

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.5;
  overflow-x: hidden;
}

/* Slim themed scrollbars in place of the default OS scrollbar, used
   wherever an inner panel scrolls (prompt/log previews, modals, tables). */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
*::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
*::-webkit-scrollbar-track {
  background: transparent;
}
*::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--radius-pill);
}
*::-webkit-scrollbar-thumb:hover {
  background: var(--text-faint);
}

.hidden { display: none !important; }

button, input, textarea, select {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

/* Native <select> dropdown option theming — ensures options rendered by the
   browser's native picker use the dark surface color rather than the OS
   light-gray default. Respected by Firefox and Chromium on Windows/Linux;
   macOS renders native popovers outside CSS control but setting these values
   is still correct and safe. color-scheme: dark tells the browser to render
   the native widget chrome (scrollbar, focus ring, popup frame) in dark mode. */
select {
  color-scheme: dark;
}
select option {
  background-color: #1C1C1C; /* --bg-elevated-2 */
  color: #E8EAED;            /* --text */
}
select option:disabled {
  color: #888888;            /* --text-faint */
}

a { color: var(--accent); }

/* ---------- Buttons ---------- see docs/brandbook.md "Buttons" ---------- */
.btn {
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-elevated-2);
  color: var(--text);
  padding: 8px 14px;
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  transition: background var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast), filter var(--transition-fast);
}
.btn:hover { border-color: var(--border-strong); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* Primary — the single amber CTA per view (Amber Rule). */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #0E0E0E;
  font-weight: 500;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.08); }

/* Ghost — used pervasively across the dashboard for secondary/neutral
   actions (Logout, table row actions, cancel, etc.). The brandbook's
   marketing-site "Ghost" button (amber border/text) is a single-CTA pattern
   and would badly break the Amber Rule if applied to all ~27 .btn-ghost
   usages here — instead this follows the brandbook's Admin Panel
   `.btn-admin--muted` pattern (subtle neutral border, no amber) since that
   is the correct analog for a dense dashboard with many secondary actions
   per view. Judgment call — see handoff notes. */
.btn-ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text-muted);
}
.btn-ghost:hover { background: var(--bg-elevated-2); border-color: var(--border-strong); color: var(--text); }

.btn-danger {
  background: transparent;
  border-color: var(--status-failed);
  color: var(--status-failed);
}
.btn-danger:hover { background: rgba(179, 106, 94, 0.1); }

.btn-sm { padding: 5px 10px; font-size: 12.5px; }
.btn-block { width: 100%; }

/* ---------- Auth screen ----------
   Two-column layout: a minimal brand hero (logo, large headline, one short
   supporting line) paired with the login form, separated by a subtle
   vertical divider. Collapses to a single column below 860px — hero
   stacks above the form (divider becomes horizontal, then fades out once
   spacing alone reads as separation) so the form stays the primary focus
   on narrow viewports. See docs/brandbook.md "Auth screen — hero + login". */
.auth-screen {
  min-height: 100vh;
  display: flex;
  align-items: stretch;
  justify-content: center;
  padding: 0;
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(60ch 60ch at 18% 15%, rgba(232, 124, 46, 0.08), transparent 60%),
    radial-gradient(50ch 50ch at 85% 85%, rgba(232, 124, 46, 0.05), transparent 60%),
    var(--bg);
}
/* Faint dot grid — circuit-board texture, low opacity so it stays ambient. */
.auth-screen::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--border-strong) 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.35;
  mask-image: radial-gradient(circle at 50% 40%, black, transparent 75%);
  -webkit-mask-image: radial-gradient(circle at 50% 40%, black, transparent 75%);
  pointer-events: none;
}
/* Hairline scanlines — barely-there CRT/terminal texture on top of the grid. */
.auth-screen::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.025),
    rgba(255, 255, 255, 0.025) 1px,
    transparent 1px,
    transparent 3px
  );
  pointer-events: none;
}
.auth-layout {
  width: 100%;
  max-width: 1080px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1.1fr 0.9fr;
  align-items: center;
  gap: 48px;
  padding: 24px;
  position: relative;
  z-index: 1;
}
/* Subtle vertical divider between hero and form columns — a soft low-
   contrast rule (not a hard border), faded at both ends via a gradient
   mask so it reads as ambient separation rather than a drawn line. */
.auth-layout::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1px;
  height: 220px;
  transform: translate(-50%, -50%);
  background: linear-gradient(
    to bottom,
    transparent,
    var(--border) 30%,
    var(--border) 70%,
    transparent
  );
}

/* Hero column — brandbook: "Auth screen — hero + login" */
.auth-hero {
  padding: 32px 48px 32px 8px;
  display: flex;
  flex-direction: column;
}
.auth-hero-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 60px;
  line-height: 1.0;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 0 0 16px;
}
.auth-hero-subtitle {
  font-family: var(--font-ui);
  font-weight: 300;
  font-size: 15px;
  line-height: 1.5;
  color: var(--text-muted);
  margin: 0 0 20px;
  max-width: 38ch;
}
.auth-hero-copy {
  color: var(--text-muted);
  font-size: 14.5px;
  line-height: 1.6;
  margin: 0;
  max-width: 40ch;
}
/* "by Iron Mind AI" attribution — flex row, pinned to bottom of the
   flex column via margin-top: auto so it always rests at the foot of
   the hero regardless of copy length. */
.auth-hero-byline {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: 40px;
}
.auth-hero-byline-label {
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}
/* SVG fill is #ffffff — visible against --bg: #0E0E0E. Slight opacity
   keeps it subordinate to the headline hierarchy. */
.auth-hero-byline-logo {
  height: 20px;
  width: auto;
  opacity: 0.55;
}

/* Form column */
.auth-form-col {
  display: flex;
  justify-content: center;
}
.auth-card {
  width: 100%;
  max-width: 360px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 32px 28px;
}
.auth-card-logo {
  height: 26px;
  width: auto;
  margin-bottom: 20px;
  /* Hidden on wide viewports — the hero column already shows the logo
     prominently; shown again here once the layout collapses to one column
     (see the 860px media query below) so the form still reads as branded
     on its own. */
  display: none;
}
.auth-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 24px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0 0 6px;
}
.auth-sub { color: var(--text-muted); font-size: 13px; margin: 0 0 20px; }
#auth-form { display: flex; flex-direction: column; gap: 12px; }
#auth-password-input {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--text);
}
#auth-password-input:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }
.auth-error {
  margin-top: 14px;
  color: var(--status-failed);
  font-size: 12.5px;
  background: rgba(179, 106, 94, 0.08);
  border: 1px solid rgba(179, 106, 94, 0.25);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}

/* Sign in / Create account tabs inside the auth card — same .view-tabs/
   .view-tab shell used everywhere else in the dashboard (see brandbook
   "View tabs"), just scoped narrower via .auth-tabs for the card's own
   spacing. */
.auth-tabs { margin-bottom: 20px; }
#signin-form, #signup-form, #forgot-form, #reset-password-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
#signin-form input, #signup-form input, #forgot-form input, #reset-password-form input {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--text);
}
#signin-form input:focus-visible, #signup-form input:focus-visible,
#forgot-form input:focus-visible, #reset-password-form input:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}
.auth-forgot-link {
  display: block;
  margin: 14px auto 0;
  text-align: center;
}
.auth-view.hidden { display: none; }

/* Verification splash: reuses the .auth-screen full-viewport shell (same
   dot-grid/scanline background) but centers a single card — no hero column,
   since this isn't a marketing moment, it's a blocking gate. */
.auth-layout-single {
  grid-template-columns: 1fr;
  max-width: 420px;
}
.auth-layout-single::before { display: none; }
.verify-splash-card { text-align: center; }
.verify-splash-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 8px;
}
#verify-splash-note:not(.hidden) {
  color: var(--status-completed);
  background: rgba(127, 168, 139, 0.08);
  border-color: rgba(127, 168, 139, 0.25);
}

/* Single-column collapse: hero stacks above the form, de-emphasized
   (smaller headline, tighter spacing) so the form stays the primary
   focus. The vertical divider becomes a horizontal one between the
   stacked columns, same soft-fade treatment. */
@media (max-width: 860px) {
  .auth-layout {
    grid-template-columns: 1fr;
    gap: 8px;
    padding: 32px 20px;
  }
  /* The absolutely-positioned vertical divider only makes sense against a
     fixed-height two-column grid — on a single stacked column its height
     is content-dependent, so it's dropped in favor of a plain horizontal
     rule flowing naturally in-between the hero and form (same soft-fade
     gradient treatment, via a real element in normal flow rather than a
     positioned pseudo-element). */
  .auth-layout::before {
    display: none;
  }
  .auth-hero {
    padding: 0 0 24px;
    text-align: center;
  }
  .auth-hero-title { font-size: 38px; }
  .auth-hero-subtitle { margin-left: auto; margin-right: auto; }
  .auth-hero-copy { margin: 0 auto; }
  /* On narrow viewports the hero is not a tall flex column — reset
     margin-top: auto so the byline follows copy naturally with fixed spacing. */
  .auth-hero-byline {
    margin-top: 28px;
    padding-top: 0;
    justify-content: center;
  }
  .auth-form-col {
    margin-top: 24px;
    padding-top: 24px;
    background: linear-gradient(
      to right,
      transparent,
      var(--border) 20%,
      var(--border) 80%,
      transparent
    ) top / 100% 1px no-repeat;
  }
  .auth-card-logo { display: block; margin-left: auto; margin-right: auto; }
}

/* ---------- Top bar ---------- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar-left { display: flex; align-items: center; gap: 10px; }
.brand-logo {
  height: 22px;
  width: auto;
  display: block;
}
.brand-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin: 0;
}
.topbar-right { display: flex; align-items: center; gap: 12px; }
.health-indicator { color: var(--status-orphaned); font-size: 12px; transition: color 0.2s ease; }
.health-indicator.ok { color: var(--status-completed); }
.health-indicator.down { color: var(--status-failed); }

/* Telemetry card: Claude plan usage / ElevenLabs credits / server telemetry,
   stacked above the runs table (.runs-column) as a grid of individual stat
   tiles — see docs/brandbook.md "Telemetry card". Same .panel chrome as
   every other card; each widget (.usage-widget/.elevenlabs-widget/
   .telemetry-widget/.token-usage-widget) renders one or more .stat-tile
   children that wrap in a grid. Collapses to zero height (no
   border/padding) when all four widgets are still hidden, so it never
   shows as an empty card before first data / on fetch failure. */
.telemetry-card {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
}
.telemetry-card:has(> .token-usage-widget.hidden):has(> .openrouter-usage-widget.hidden):has(> .elevenlabs-usage-widget.hidden):has(> .claude-plan-widget.hidden) {
  display: none;
}
/* Each widget container is itself a tile-group — usually a single tile
   (ElevenLabs, Token Usage), several tiles (CPU/RAM/Disk/Server), or one
   tile packing several stacked rows (the usage widget's 5h/7d windows and
   the token usage widget's Day/Week/Month rows, via .stat-tile-rows) so
   related metrics stay visually grouped together under one tile rather
   than spreading into separate cards. */
.token-usage-widget,
.openrouter-usage-widget,
.elevenlabs-usage-widget,
.claude-plan-widget {
  display: contents;
}

/* Claude plan widget's server picker — sits inside the tile, below the
   5h/7d rows, as a compact select rather than a separate control outside
   the tile, since the tile IS the whole widget (display:contents). */
.claude-plan-picker-row { margin-top: 2px; }
.claude-plan-server-select {
  width: 100%;
  font-size: 11.5px;
  padding: 4px 6px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
}
.claude-plan-server-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------------------------- Stat tile ----------------------------------
   Shared "stat tile" component used by all four telemetry-card widgets:
   glyph + label on top, big value below, an optional mini progress bar for
   percentage-based metrics, and optional small muted secondary text (e.g.
   reset countdown, uptime, network throughput). See docs/brandbook.md
   "Stat tile" for the full pattern writeup. */
.stat-tile {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.stat-tile-head {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.stat-tile-glyph {
  font-size: 13px;
  line-height: 1;
}
.stat-tile-badge {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--text-muted);
  background: var(--bg-elevated);
}
.stat-tile-value {
  font-size: 20px;
  font-weight: 600;
  font-family: var(--font-mono);
  line-height: 1.1;
  cursor: default;
}
.stat-tile-value.stat-green { color: var(--status-completed); }
.stat-tile-value.stat-orange { color: var(--status-stopped); }
.stat-tile-value.stat-red { color: var(--status-failed); }
.stat-tile-sub {
  font-size: 11px;
  color: var(--text-faint);
  font-style: italic;
  cursor: default;
}
.stat-tile-sub.stat-red { color: var(--status-failed); font-style: normal; font-weight: 600; }

/* Mini progress bar: used by every percentage-based tile (5h/7d/CPU/RAM/
   disk/ElevenLabs). Fill color matches the tile's status level — color is
   never the sole indicator since the value text is always shown above it. */
.stat-tile-bar {
  height: 5px;
  border-radius: 999px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  overflow: hidden;
}
.stat-tile-bar-fill {
  height: 100%;
  border-radius: 999px;
  transition: width 0.3s ease;
}
.stat-tile-bar-fill.stat-green { background: var(--status-completed); }
.stat-tile-bar-fill.stat-orange { background: var(--status-stopped); }
.stat-tile-bar-fill.stat-red { background: var(--status-failed); }

/* Usage-unavailable / na state: a single muted tile, no bar. */
.stat-tile-na .stat-tile-value { color: var(--text-faint); font-weight: 400; font-style: italic; font-family: var(--font-ui); font-size: 14px; }

/* A tile that packs two related metrics (5h/7d windows) stacked instead of
   one big value — used by the usage widget so its plan badge/spend context
   stays grouped under one tile rather than spreading into separate cards. */
.stat-tile-rows { display: flex; flex-direction: column; gap: 8px; }
.stat-tile-row { display: flex; flex-direction: column; gap: 3px; }
.stat-tile-row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
}
.stat-tile-row-label { color: var(--text-muted); font-weight: 600; }
.stat-tile-row-value {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 13px;
}
.stat-tile-row-value.stat-green { color: var(--status-completed); }
.stat-tile-row-value.stat-orange { color: var(--status-stopped); }
.stat-tile-row-value.stat-red { color: var(--status-failed); }

/* The telemetry card only appears above .runs-panel, which itself collapses
   to a single stacked column at 900px and is fully replaced by the
   purpose-built mobile UI at 768px — follow the runs-panel's own
   collapse/hide rules rather than a separate breakpoint, so the card never
   sits visibly broken between those two points. Nothing extra is needed at
   900px (it simply reflows to full width in the single-column .layout);
   below 768px it's dropped alongside the rest of #runs-view.layout — see
   the '#runs-view.layout { display: none; }' rule further down. */

/* CC version label, top bar far right — separate from the usage widget. */
.cc-version {
  color: var(--text-faint);
  font-size: 11px;
  font-family: var(--font-mono);
  cursor: default;
}

/* Run completion alert toggle, top bar — compact icon button that cycles
   Off (🔇) -> Sound (🔔) -> Speech (🗣) on click. Same ghost-icon language
   as the health dot: no border/fill by default, subtle hover/focus state,
   accent tint once a non-off mode is active so its state reads at a glance
   even before hovering for the tooltip. Choice persists in localStorage
   (`completionAlert`) — see docs/webui-templates-index.md. */
.completion-alert-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.completion-alert-toggle:hover {
  border-color: var(--border);
  background: var(--bg-elevated-2);
}
.completion-alert-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.completion-alert-toggle[data-alert-mode="sound"],
.completion-alert-toggle[data-alert-mode="speech"] {
  color: var(--accent-hover);
  border-color: var(--accent-dim);
  background: var(--accent-dim);
}

/* Generic top-bar ghost icon button (.icon-ghost-btn) — e.g. the Settings
   cog. Same transparent-at-rest/bordered-on-hover language as
   .completion-alert-toggle/.health-indicator, but with no persistent
   "active" tint variant since this one only ever opens a modal, it never
   toggles a state. Renders a .row-icon-svg (same stroke-SVG family as the
   runs table's row-action icons) rather than a unicode glyph — a deliberate,
   narrowly-scoped exception matching that family's own precedent, since a
   settings gear has no clean unicode-glyph equivalent in this dashboard's
   existing glyph set. See docs/brandbook.md "Row-action icon family". */
.icon-ghost-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.icon-ghost-btn:hover,
.icon-ghost-btn:focus-visible {
  color: var(--accent-hover);
  border-color: var(--border);
  background: var(--bg-elevated-2);
}
.icon-ghost-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Active runs counter, top bar — derived from the runs table's existing
   facets.status.running (no extra polling), same muted accent-tinted
   language as other small top-bar readouts. Hidden when 0. */
.active-runs-counter {
  display: inline-flex;
  align-items: center;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--accent-hover);
  cursor: default;
}
.active-runs-counter::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--status-running);
  margin-right: 5px;
  animation: pulse 1.2s ease-in-out infinite;
}

/* ---------- Layout ---------- */
.layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  padding: 16px;
  align-items: start;
}
@media (max-width: 900px) {
  .layout { grid-template-columns: 1fr; }
}

/* Runs column: stacks the telemetry card above the runs panel, both at the
   runs panel's width, inside the same grid cell (column 2) the runs panel
   used to occupy alone — the New Run column (column 1) is unaffected. See
   docs/brandbook.md "Telemetry card". */
.runs-column {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.panel {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 16px;
}
.panel-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  margin: 0 0 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* ---------- Launch panel ---------- */
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; font-weight: 600; }

.field input[type="text"],
.field input[type="password"],
.field input[type="datetime-local"],
.field input[type="number"],
.field select,
.field textarea {
  width: 100%;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  color: var(--text);
}
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}
.field select + input[type="text"] { margin-top: 8px; }
.field textarea { resize: vertical; font-family: var(--font-mono); font-size: 12.5px; }

/* File input (e.g. server editor's "Upload key file"): the native control is
   an OS-chrome "Choose File" button + filename readout that can't be fully
   restyled without a custom trigger element, but ::file-selector-button can
   be themed to match .btn/.field's dark surface language so it no longer
   looks like an unstyled foreign control dropped into the dashboard. Scoped
   to .field so it stays consistent with every other .field input without a
   separate wrapper class. */
.field input[type="file"] {
  width: 100%;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  color: var(--text-muted);
  font-size: 12.5px;
}
.field input[type="file"]::file-selector-button {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 6px 12px;
  margin-right: 10px;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.field input[type="file"]:hover::file-selector-button,
.field input[type="file"]::file-selector-button:hover {
  border-color: var(--border-strong);
}
.field input[type="file"]:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}

.option-list {
  margin-top: 6px;
  max-height: 160px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated-2);
}
.option-list:empty { display: none; }
.option-item {
  padding: 7px 10px;
  cursor: pointer;
  font-size: 13px;
  border-bottom: 1px solid var(--border);
}
.option-item:last-child { border-bottom: none; }
.option-item:hover, .option-item.active { background: var(--accent-dim); color: var(--accent-hover); }
.option-empty { padding: 8px 10px; color: var(--text-faint); font-size: 12.5px; }

.selected-chip {
  margin-top: 8px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--accent-dim);
  color: var(--accent-hover);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  padding: 4px 10px;
  font-size: 12.5px;
}

.mode-toggle {
  display: flex;
  gap: 4px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 3px;
  margin-bottom: 14px;
}
.mode-btn {
  flex: 1;
  background: transparent;
  border: none;
  color: var(--text-muted);
  padding: 7px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
}
.mode-btn.active { background: var(--accent); color: #0E0E0E; }

.preset-description {
  font-size: 12.5px;
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin: 0 0 12px;
}

.preset-vars { display: flex; flex-direction: column; gap: 10px; margin-bottom: 4px; }
.preset-var-field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
.preset-var-field input {
  width: 100%;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 7px 9px;
}

.prompt-preview {
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  max-height: 220px;
  max-width: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  margin: 0;
  box-sizing: border-box;
}

/* ---------- Runs table ---------- */
.runs-panel-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.runs-count { color: var(--text-faint); font-size: 12px; }

/* ---------- Runs filter bar (search-as-you-type, status chips, project,
   date range) — sits above the table, wraps on narrow viewports. ---------- */
.runs-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 12px 0;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.runs-search-input {
  flex: 1 1 220px;
  min-width: 160px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  color: var(--text);
}
.runs-search-input::placeholder { color: var(--text-faint); }
.runs-search-input:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }

.status-chip-group { display: flex; flex-wrap: wrap; gap: 6px; }
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  border-radius: var(--radius-pill);
  padding: 5px 10px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.status-chip:hover { border-color: var(--border-strong); }
.status-chip .status-chip-count { color: var(--text-faint); font-weight: 400; }
.status-chip.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent-hover); }
.status-chip.active .status-chip-count { color: var(--accent-hover); }
/* Reuse status colors for each chip's dot, same convention as .badge::before. */
.status-chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.55; }
.status-chip.active::before { opacity: 1; }
.status-chip[data-status="running"] { color: var(--status-running); }
.status-chip[data-status="completed"] { color: var(--status-completed); }
.status-chip[data-status="failed"] { color: var(--status-failed); }
.status-chip[data-status="stopped"] { color: var(--status-stopped); }
.status-chip[data-status="orphaned"] { color: var(--status-orphaned); }
.status-chip[data-status="running"]:not(.active) { color: var(--text-muted); }
.status-chip[data-status="completed"]:not(.active) { color: var(--text-muted); }
.status-chip[data-status="failed"]:not(.active) { color: var(--text-muted); }
.status-chip[data-status="stopped"]:not(.active) { color: var(--text-muted); }
.status-chip[data-status="orphaned"]:not(.active) { color: var(--text-muted); }
.status-chip[data-status="running"]:not(.active)::before { background: var(--status-running); }
.status-chip[data-status="completed"]:not(.active)::before { background: var(--status-completed); }
.status-chip[data-status="failed"]:not(.active)::before { background: var(--status-failed); }
.status-chip[data-status="stopped"]:not(.active)::before { background: var(--status-stopped); }
.status-chip[data-status="orphaned"]:not(.active)::before { background: var(--status-orphaned); }
.status-chip.active[data-status="running"] { color: var(--status-running); border-color: rgba(138,148,163,0.25); background: rgba(138,148,163,0.1); }
.status-chip.active[data-status="completed"] { color: var(--status-completed); border-color: rgba(127,168,139,0.25); background: rgba(127,168,139,0.1); }
.status-chip.active[data-status="failed"] { color: var(--status-failed); border-color: rgba(179,106,94,0.25); background: rgba(179,106,94,0.1); }
.status-chip.active[data-status="stopped"] { color: var(--status-stopped); border-color: rgba(176,138,78,0.25); background: rgba(176,138,78,0.1); }
.status-chip.active[data-status="orphaned"] { color: var(--status-orphaned); border-color: rgba(118,118,118,0.25); background: rgba(118,118,118,0.1); }
.status-chip.active .status-chip-count { color: inherit; opacity: 0.75; }
.status-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.runs-filter-select {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 7px 9px;
  color: var(--text);
  max-width: 200px;
  /* A touch of extra breathing room before the date-chip group immediately
     after it, so the project select and the chip groups read as distinct
     clusters in the filter bar rather than running together — deliberately
     small (4px on top of the bar's own 10px `gap`), not a bar redesign. */
  margin-right: 4px;
}
.runs-filter-select:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }

/* Date preset chips (Today / Week / All time): same .status-chip pill shape
   and active-state (accent-dim bg + accent border) as the status filter
   group, but single-select (not multi) and deliberately without the
   per-status color / ::before dot rules — a date preset has no semantic
   status color to reuse, so it stays neutral/accent-only. */
.date-chip::before { display: none; }
.date-chip { color: var(--text-muted); }
.date-chip.active { color: var(--accent-hover); }

/* Archived-runs visibility (#archived-filter-btn / #archived-filter-menu):
   replaces an earlier three-chip Hidden/Shown/Only group (same .date-chip
   pattern as the date presets above) that read as unlabeled and visually
   fused with the date-chip group beside it — two sets of orange actives
   side by side with no label to tell them apart. Now a single compact icon
   button reusing the row-icon family's exact ghost-icon language
   (.completion-audio-btn/.archive-row-btn — transparent at rest,
   accent-tinted hover/focus, ~28px hit target, .row-icon-svg 20px sizing)
   so it reads as "one small icon-only control," not a chip group, even
   though it now lives in the filter bar rather than a table row. See
   docs/brandbook.md "Archived-runs visibility menu" for the full writeup —
   this introduces the app's first custom dropdown menu. */
.archived-filter-menu-wrap {
  position: relative;
}
.archived-filter-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.archived-filter-btn:hover,
.archived-filter-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.archived-filter-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Lit accent-colored whenever the value is non-default (include/only) — same
   "persistent tint for an active toggle" convention as .pin-btn-active/
   .completion-alert-toggle — so the control still communicates its state at
   a glance even though the three-way choice itself moved into a menu. */
.archived-filter-btn-active {
  color: var(--accent);
  background: var(--accent-dim);
}
.archived-filter-btn-active:hover { color: var(--accent-hover); }

/* Dropdown menu panel — absolutely positioned against
   .archived-filter-menu-wrap (`position: relative`), right-aligned under
   the trigger since the trigger sits near the filter bar's right edge and
   a left-aligned panel would overflow. Same `.modal`-adjacent surface
   language (--bg-elevated bg, --radius-sm, border, shadow) as every other
   floating panel in this dashboard, just without a backdrop — dismissal is
   outside-click/Escape/selection instead (see initArchivedFilterMenu() in
   app.js), appropriate for a lightweight 3-item menu vs. a full modal. */
.archived-filter-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 20;
  min-width: 160px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.archived-filter-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: 4px;
  padding: 7px 10px;
  font-size: 12.5px;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.1s ease, color 0.1s ease;
}
.archived-filter-menu-item:hover,
.archived-filter-menu-item:focus-visible {
  background: var(--bg-elevated-2);
  color: var(--text);
}
.archived-filter-menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Checked item: a small leading accent dot (::before) rather than
   re-tinting the whole row — keeps the "currently selected" indicator
   readable even on :hover/:focus-visible's own background change, and
   matches this dashboard's general "small glyph/dot as state indicator"
   convention (see .status-chip::before) rather than inventing a checkmark
   glyph. */
.archived-filter-menu-item[aria-checked="true"] {
  color: var(--accent-hover);
  font-weight: 600;
}
.archived-filter-menu-item[aria-checked="true"]::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}
.archived-filter-menu-item[aria-checked="false"]::before {
  content: "";
  width: 6px;
  height: 6px;
  flex-shrink: 0;
}

.runs-clear-filters { margin-left: auto; }

/* Active filter pills row — individually clearable, shown below the bar. */
.active-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: -4px 0 12px;
}
.active-filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--accent-dim);
  color: var(--accent-hover);
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  padding: 3px 6px 3px 10px;
  font-size: 11.5px;
}
.active-filter-pill button {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  padding: 0 3px;
  font-size: 12px;
  line-height: 1;
}
.active-filter-pill button:hover { color: var(--text); }
.active-filter-pill button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Dimming instead of spinner/blank state while a new page of results loads. */
#runs-table-dim-wrap { transition: opacity 0.15s ease; }
#runs-table-dim-wrap.loading { opacity: 0.5; }

/* ---------- Pagination footer ---------- */
.runs-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 12px;
  gap: 12px;
}
.runs-page-info { color: var(--text-faint); font-size: 12px; }
.runs-pagination-btns { display: flex; gap: 8px; }

@media (max-width: 700px) {
  .runs-filter-bar { flex-direction: column; align-items: stretch; }
  .runs-clear-filters { margin-left: 0; align-self: flex-start; }
  .date-chip-group { flex-wrap: wrap; }
}

/* overflow-x: auto is required for horizontal scroll on narrow viewports,
   but it also clips any child that relies on overflowing its bounds (e.g.
   .prompt-cell's hover tooltip below) on the Y axis. overflow-x: auto with
   no explicit overflow-y implies overflow-y: auto too (clipping), so we
   pin overflow-y: visible explicitly — X-axis scroll still works exactly
   as before, the tooltip can now escape vertically. */
.table-wrap { overflow-x: auto; overflow-y: visible; }
.runs-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.runs-table th {
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 11px;
  color: var(--text-faint);
  font-weight: 600;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.runs-table td {
  padding: 9px 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  white-space: nowrap;
}
.runs-table td.prompt-cell {
  max-width: 320px;
  white-space: nowrap;
  cursor: default;
}
.runs-table td.prompt-cell .run-title-view {
  max-width: 100%;
}
.runs-table td.prompt-cell .run-title-view .prompt-tooltip-trigger {
  max-width: calc(100% - 22px);
}
/* Custom prompt tooltip (replaces the native `title` attribute so it can be
   fully brand-styled — see data-tooltip on .prompt-tooltip-trigger and
   initPromptTooltip() in app.js). Positioned via JS as a position: fixed
   element appended to <body>, not a CSS ::after anchored to the trigger:
   .table-wrap needs overflow-x: auto for horizontal scroll, and per the
   CSS overflow spec that forces the paired overflow-y to behave as
   non-visible too, so a CSS-anchored tooltip near the bottom of the table
   got clipped no matter what overflow-y was explicitly set to. The JS
   version escapes every ancestor's overflow/stacking context and flips
   above/below + clamps left/right against the actual viewport on hover. */
.prompt-tooltip-trigger {
  display: inline-block;
  max-width: 100%;
  vertical-align: bottom;
}
.prompt-tooltip-text {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}
.prompt-tooltip-trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}
.prompt-hover-tooltip {
  position: fixed;
  z-index: 1000;
  width: max-content;
  max-width: 420px;
  padding: 10px 12px;
  background: var(--bg-elevated-2);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  pointer-events: none;
}
.prompt-hover-tooltip.hidden {
  display: none;
}
/* Run title (session_name) — inline-editable, Claude/ChatGPT style. Shown
   as the runs table's Prompt-cell heading (falls back to the truncated
   prompt when session_name is null) and, when present, in the detail
   drawer header. A hover-revealed pencil (✎, same unicode-glyph convention
   as the rest of the dashboard) swaps the static text for an inline input
   via startEditRunTitle()/commitRunTitle() in app.js. */
.run-title-view {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-width: 0;
}
.run-title-edit-btn {
  flex-shrink: 0;
  border: none;
  background: transparent;
  color: var(--text-faint);
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  opacity: 0;
  transition: opacity 0.1s ease, color 0.1s ease, background 0.1s ease;
}
.runs-table tbody tr:hover .run-title-edit-btn,
.run-title-edit-btn:focus-visible,
.detail-title-view .run-title-edit-btn {
  opacity: 1;
}
.run-title-edit-btn:hover,
.run-title-edit-btn:focus-visible {
  color: var(--accent-hover);
  background: var(--bg-elevated-2);
  outline: none;
}
/* The detail header's own pencil is always visible (no row-hover context to
   key off of there), same rationale as the mobile pin button overriding the
   desktop hover-reveal rule. */
.detail-title-view .run-title-edit-btn { opacity: 0.6; }
.detail-title-view .run-title-edit-btn:hover,
.detail-title-view .run-title-edit-btn:focus-visible { opacity: 1; }
.run-title-edit-btn-always {
  font-size: 12px;
  color: var(--text-faint);
  font-style: italic;
  opacity: 0.85 !important;
}
.run-title-input {
  width: 100%;
  max-width: 300px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 12.5px;
  padding: 3px 7px;
  outline: none;
}
.run-title-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-dim);
}

/* Live preview line: subtle second line under the running run's truncated
   prompt, single-line with ellipsis so table rows never grow tall. */
.live-preview {
  margin-top: 2px;
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-faint);
}
.live-preview-text {
  font-family: var(--font-mono);
}
.live-preview-tool,
.live-preview-waiting {
  font-style: italic;
  color: var(--text-faint);
}
.live-preview-tool {
  animation: pulse 1.8s ease-in-out infinite;
}
.runs-table tbody tr { cursor: pointer; transition: background 0.1s ease; }
.runs-table tbody tr:hover { background: var(--bg-elevated-2); }
.runs-table tbody tr.selected { background: var(--accent-dim); }
.empty-row { color: var(--text-faint); text-align: center; padding: 24px !important; white-space: normal !important; }

/* Pinned row indicator (.row-pinned, see .pin-btn below): a persistent, low-
   key marker for a pinned run — a thin 2px accent-colored left edge on the
   row's first cell, same restrained "accent as the only marker" language as
   the rest of this dashboard rather than a filled/tinted row background
   (which would fight .selected/.row-freshly-completed's own backgrounds and
   read as louder than a "sort priority" hint deserves). Deliberately not
   color-only: the row's .pin-btn is always visibly filled/accent-tinted too
   (see .pin-btn-active), so pinned state is legible without relying on this
   edge alone. `:hover`/`.selected` backgrounds still apply normally on top —
   this is a border, not a background, so it never competes with them. */
.runs-table tbody tr.row-pinned td:first-child {
  box-shadow: inset 3px 0 0 var(--accent);
}

/* Freshly-completed row highlight: a ~20s subtle background PULSE applied by
   app.js the moment a run is OBSERVED transitioning from running -> a
   terminal status during this session (never on rows already terminal on
   initial page load — see detectCompletionTransitions()/
   markRowFreshlyCompleted() in app.js). A gentle "breathing" tint — low
   amplitude, ~2.3s per cycle, ease-in-out — rather than a fade, since a fade
   reads as nearly invisible after ~10s and this needs to stay scannable for
   the full 20s window when watching many runs at once; a slow pulse keeps it
   noticeable without being "clowny" (kept to a small intensity delta, no
   color-cycling, no scale/transform). Two tasteful variants reusing the
   existing status color tokens: a green tint for `completed`, a red tint for
   `failed`/`stopped`/`orphaned` (grouped as one "not the happy path" tint
   rather than three near-identical ones). `:hover`/`.selected` still win over
   the tint (declared after it, and hover's own background is opaque) so the
   highlight never fights the table's existing interaction states. The
   `animation-delay` is set inline per-row from app.js (a negative offset
   equal to the row's elapsed age) so the pulse phase survives the table's 5s
   poll re-renders without visually "restarting" — see
   rowFreshlyCompletedStyle(). Both keyframe sets are a single 20s timeline
   (not `iteration-count` on a short single-cycle keyframe) so the very last
   cycle can decay to transparent at 100% — the pulse ends cleanly rather than
   cutting off abruptly mid-breath. `prefers-reduced-motion` gets a static
   flat tint instead (see media query below) — never a pulsing animation. */
@keyframes row-pulse-completed {
  0%   { background: rgba(127, 168, 139, 0.08); }
  4.5% { background: rgba(127, 168, 139, 0.16); }
  9%   { background: rgba(127, 168, 139, 0.08); }
  13.5%{ background: rgba(127, 168, 139, 0.16); }
  18%  { background: rgba(127, 168, 139, 0.08); }
  22.5%{ background: rgba(127, 168, 139, 0.16); }
  27%  { background: rgba(127, 168, 139, 0.08); }
  31.5%{ background: rgba(127, 168, 139, 0.16); }
  36%  { background: rgba(127, 168, 139, 0.08); }
  40.5%{ background: rgba(127, 168, 139, 0.16); }
  45%  { background: rgba(127, 168, 139, 0.08); }
  49.5%{ background: rgba(127, 168, 139, 0.16); }
  54%  { background: rgba(127, 168, 139, 0.08); }
  58.5%{ background: rgba(127, 168, 139, 0.16); }
  63%  { background: rgba(127, 168, 139, 0.08); }
  67.5%{ background: rgba(127, 168, 139, 0.16); }
  72%  { background: rgba(127, 168, 139, 0.08); }
  76.5%{ background: rgba(127, 168, 139, 0.16); }
  81%  { background: rgba(127, 168, 139, 0.08); }
  100% { background: transparent; }
}
@keyframes row-pulse-failed {
  0%   { background: rgba(179, 106, 94, 0.07); }
  4.5% { background: rgba(179, 106, 94, 0.14); }
  9%   { background: rgba(179, 106, 94, 0.07); }
  13.5%{ background: rgba(179, 106, 94, 0.14); }
  18%  { background: rgba(179, 106, 94, 0.07); }
  22.5%{ background: rgba(179, 106, 94, 0.14); }
  27%  { background: rgba(179, 106, 94, 0.07); }
  31.5%{ background: rgba(179, 106, 94, 0.14); }
  36%  { background: rgba(179, 106, 94, 0.07); }
  40.5%{ background: rgba(179, 106, 94, 0.14); }
  45%  { background: rgba(179, 106, 94, 0.07); }
  49.5%{ background: rgba(179, 106, 94, 0.14); }
  54%  { background: rgba(179, 106, 94, 0.07); }
  58.5%{ background: rgba(179, 106, 94, 0.14); }
  63%  { background: rgba(179, 106, 94, 0.07); }
  67.5%{ background: rgba(179, 106, 94, 0.14); }
  72%  { background: rgba(179, 106, 94, 0.07); }
  76.5%{ background: rgba(179, 106, 94, 0.14); }
  81%  { background: rgba(179, 106, 94, 0.07); }
  100% { background: transparent; }
}
.runs-table tbody tr.row-freshly-completed {
  animation: row-pulse-completed 20s ease-in-out forwards;
}
.runs-table tbody tr.row-freshly-failed {
  animation: row-pulse-failed 20s ease-in-out forwards;
}
/* Reduced motion: a calm static tint instead of a pulsing animation — same
   colors, roughly the pulse's midpoint intensity, no keyframe/animation at
   all. Still clears after 20s via app.js's own Map-based expiry (which drives
   whether the class gets re-applied on the next re-render), just without any
   CSS-driven transition — a plain background color that's simply absent once
   the row's entry ages out. */
@media (prefers-reduced-motion: reduce) {
  .runs-table tbody tr.row-freshly-completed {
    animation: none;
    background: rgba(127, 168, 139, 0.12);
  }
  .runs-table tbody tr.row-freshly-failed {
    animation: none;
    background: rgba(179, 106, 94, 0.1);
  }
}
/* !important needed here: a running CSS animation otherwise wins over any
   non-animated `background` declaration for the same property regardless of
   selector specificity/cascade order, so without this a freshly-completed
   row's hover/selected state would be invisible until the 20s pulse finishes. */
.runs-table tbody tr.row-freshly-completed:hover,
.runs-table tbody tr.row-freshly-failed:hover {
  background: var(--bg-elevated-2) !important;
}
.runs-table tbody tr.row-freshly-completed.selected,
.runs-table tbody tr.row-freshly-failed.selected {
  background: var(--accent-dim) !important;
}

/* Row state tints (.row-running / .row-queued / .row-running-queued): a much
   quieter sibling of the freshly-completed pulse above — static (no
   animation) for three ONGOING row states rather than one-time transitions:
   - .row-running: applied to every `running` row with an empty queue, a
     barely-there wash of a dedicated blue (`rgba(79, 140, 255, 0.04)`,
     #4f8cff at 4% alpha) — NOT `--accent` (which is orange, #E87C2E, used
     for selection/hover states elsewhere); this tint intentionally uses its
     own blue so it doesn't compete with or get confused with the
     selected-row highlight. Kept at this original near-imperceptible
     level — running rows are already obvious from their status badge/spinner
     elsewhere in the row, so this tint is genuinely just ambient reinforcement.
   - .row-queued: applied to any non-running row with a non-empty
     `run.queued_followups` (see rowStateTintClass() in app.js), a soft
     violet distinct from both running-blue and the completion pulse's
     green/red — `rgba(169, 127, 214, 0.1)` (~#a97fd6 at 10% alpha) —
     signals "something is pending on this row" without implying
     success/failure/activity, which is why it deliberately avoids every
     other semantic status color already in use. UPDATED per user feedback:
     the original 4.5% alpha (matched to .row-running's 4%) turned out to be
     indistinguishable from a plain row against this theme's dark
     `--bg`/`--bg-elevated-2` (#0E0E0E/#1C1C1C) — unlike `.row-running`,
     queued rows have no other visual signal (no badge/spinner) marking them
     as pending, so this tint needs to actually carry the information alone.
     10% alpha was chosen as the point where the violet wash reads clearly at
     a glance against both background colors while still being a flat color
     wash rather than a loud highlight — roughly 2x the running tint's
     intensity, appropriate since running rows have that extra badge/spinner
     signal doing most of the work and queued rows don't.
   - .row-running-queued: applied when a row is BOTH `running` AND has a
     non-empty queue (rowStateTintClass() now checks queue state first and
     only falls back to plain `.row-running` when the queue is empty) —
     this used to collapse into plain `.row-running` under a "running wins"
     precedence rule, but that made running+queued rows indistinguishable
     from plain running rows, which user feedback flagged as a real gap:
     "running AND about to run something else" is a distinct, useful signal.
     Rather than blending the two translucent washes into a muddier third
     color (blending is still avoided, per the original rationale below),
     this keeps the running-blue background wash as-is and layers the
     queued-violet on as a 3px inset left-edge bar
     (`box-shadow: inset 3px 0 0 rgba(169, 127, 214, 0.6)`, ~6x the base
     violet's alpha since a 3px edge needs more punch than a full-row wash
     to read at a glance) — so both signals stay visually separable: blue
     wash = running, violet edge = queue pending, together = both.
   Per explicit user direction, .row-running's alpha stays intentionally near
   the floor of what's perceptible; .row-queued's does not, and is
   deliberately stronger — see rationale above. The three classes are
   mutually exclusive in the DOM (rowStateTintClass() only ever returns one
   class per row) so no background-blending logic is needed; only the edge
   bar is ever layered on top of a wash, and only for the one combined state.
   Skipped entirely by app.js whenever a row is already
   `.row-freshly-completed`/`.row-freshly-failed` (that pulse takes
   precedence over these tints rather than stacking two backgrounds — see
   the `tintCls` logic in renderRunsTable()). `:hover`/`.selected` still win
   over all three tints for the same reason they win over the pulse above —
   declared after them here, and hover/selected's own backgrounds are
   opaque enough to read cleanly on top — but only once explicitly
   re-asserted for the `:hover`/`.selected` combinations below, since a
   single extra class (`.row-running`/`.row-queued`/`.row-running-queued`)
   has the SAME specificity as `.runs-table tbody tr:hover`/`tr.selected`
   (both are two class-selectors deep), so without these combined-selector
   overrides the tint would incorrectly win over hover/selected purely by
   being declared later in the file — same reasoning as the
   freshly-completed pulse's own `:hover`/`.selected` overrides above, just
   without needing `!important` here since none of this is a running CSS
   animation. Note: `.row-running-queued`'s edge bar is NOT reset in the
   `:hover`/`.selected` overrides below — an inset box-shadow survives a
   background-color override naturally (they don't conflict), and keeping
   the edge visible on hover/selected is actually desirable here: it means
   "this run still has a pending queue" stays visible even while the row is
   focused/selected, which is exactly when a user is most likely to care. */
.runs-table tbody tr.row-running {
  background: rgba(79, 140, 255, 0.04);
}
.runs-table tbody tr.row-queued {
  background: rgba(169, 127, 214, 0.1);
}
.runs-table tbody tr.row-running-queued {
  background: rgba(79, 140, 255, 0.04);
  box-shadow: inset 3px 0 0 rgba(169, 127, 214, 0.6);
}
.runs-table tbody tr.row-running:hover,
.runs-table tbody tr.row-queued:hover,
.runs-table tbody tr.row-running-queued:hover {
  background: var(--bg-elevated-2);
}
.runs-table tbody tr.row-running.selected,
.runs-table tbody tr.row-queued.selected,
.runs-table tbody tr.row-running-queued.selected {
  background: var(--accent-dim);
}

/* ---------- Session id cell / chip (table + detail drawer) ---------- */
.session-cell { white-space: nowrap; }
.session-cell-empty { color: var(--text-faint); }
.session-id-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 4px 2px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}
.session-id-text { letter-spacing: 0.02em; }
.copy-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 2px 5px;
  border-radius: 4px;
  font-size: 11px;
  line-height: 1;
  transition: color 0.15s ease, background 0.15s ease;
}
.copy-btn:hover { color: var(--accent-hover); background: var(--accent-dim); }
.copy-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.copy-feedback-active { color: var(--status-completed) !important; }

/* ---------- Context usage (.context-usage, table Context column + detail
   drawer "Context usage" field) ----------
   Renders the nullable run.context {used_tokens, limit_tokens, pct} field:
   a small horizontal bar + percent, color-coded normal/warning/danger via
   the same status tokens used elsewhere. Exact token counts always live in
   the title tooltip (compact) or inline text (detail drawer) — color/percent
   alone is never the only signal. Null renders a plain muted "—". */
.context-cell { white-space: nowrap; }
.context-cell-empty { color: var(--text-faint); }

.context-usage {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  vertical-align: middle;
}
.context-bar {
  display: inline-block;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.context-bar-fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--status-completed);
  transition: width 0.3s ease;
}
.context-normal .context-bar-fill { background: var(--status-completed); }
.context-warning .context-bar-fill { background: var(--status-stopped); }
.context-danger .context-bar-fill { background: var(--status-failed); }
.context-normal .context-pct { color: var(--text-muted); }
.context-warning .context-pct { color: var(--status-stopped); }
.context-danger .context-pct { color: var(--status-failed); }

/* Compact table cell: small bar + "NN%", plus a muted "84k / 200k" line
   underneath (abbreviated per brandbook density conventions). */
.context-usage-compact .context-bar { width: 44px; height: 6px; }
.context-usage-compact .context-pct {
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.context-abbr {
  display: block;
  margin-top: 2px;
  font-size: 10.5px;
  color: var(--text-faint);
  font-family: var(--font-mono);
}

/* Larger labeled version for the detail drawer. */
.detail-context { padding: 2px 0 4px; }
.context-usage-lg .context-bar { width: 140px; height: 8px; }
.context-usage-lg .context-pct {
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  min-width: 34px;
}
.context-usage-lg .context-tokens {
  font-size: 11.5px;
  color: var(--text-faint);
  font-family: var(--font-mono);
  margin-left: 2px;
}

.resumed-badge {
  display: inline-block;
  margin-left: 6px;
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-faint);
}

/* Follow-up count pill (.followup-badge): small accent-tinted pill shown
   on rows/session cells where followup_count > 0, same pill language as
   .badge/.status-chip but tiny and non-status (no ::before dot — this is
   a count, not a state). */
.followup-badge {
  display: inline-flex;
  align-items: center;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--accent-hover);
  background: var(--accent-dim);
  border: 1px solid rgba(138, 148, 163, 0.35);
}

/* Queued-follow-up pill (.queued-badge): the shared pill styling for a run's
   queued follow-up(s) — same "small pill + inline action" language as
   .active-filter-pill/.attach-chip: neutral (not accent-tinted like
   .followup-badge, since this describes a *pending* action, not a completed
   one) with a borderless ✕ cancel affordance. Full prompt text always in
   `title` (never color/label-only). Used by the desktop detail drawer's
   queued-follow-ups list (#detail-queued-wrap, see
   renderDetailQueuedFollowups() in app.js, below) and the mobile detail
   screen's equivalent composer-area list — NOT the runs table itself, which
   no longer shows queue details inline at all (removed per user feedback;
   the table instead signals "this row has queued follow-ups" via the
   subtle `.row-queued` background tint — see "Row state tints" further up
   in this file and in `docs/brandbook.md`). */
.queued-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 6px;
  max-width: 220px;
  padding: 2px 4px 2px 8px;
  border-radius: var(--radius-pill);
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  vertical-align: middle;
}
.queued-badge-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.queued-badge-cancel {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 0 2px;
  font-size: 11px;
  line-height: 1;
}
.queued-badge-cancel:hover { color: var(--status-failed); }
.queued-badge-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Detail drawer's queued-follow-ups section (#detail-queued-wrap /
   .detail-queued-list, see renderDetailQueuedFollowups() in app.js): a FIFO
   list of a run's queued follow-ups. This — plus the mobile detail screen's
   own equivalent — is now the ONLY place a run's queue is shown in any
   detail; the runs table's earlier .queued-badge chip was removed (see the
   note on .queued-badge above). Each row reuses the same neutral/
   italic "pending action" pill language as .queued-badge (not a new visual
   shape) but laid out as a full-width row rather than a compact truncated
   chip, since the drawer has room to show every item at once. Hidden
   entirely (the whole .field wrapper) when the queue is empty. */
.detail-queued-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.detail-queued-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  font-size: 12.5px;
}
.detail-queued-item-index {
  flex-shrink: 0;
  color: var(--text-faint);
  font-size: 11px;
}
.detail-queued-item-prompt {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-style: italic;
  color: var(--text-muted);
}

/* "Queue a follow-up" mode of #resume-modal (see showResumeModalForRun() in
   app.js) — same modal chrome as the default "Send a follow-up" (resume)
   mode, just with a subtle accent-tinted header so the two are visually
   distinct at a glance (queuing doesn't send anything now; resuming does). */
.resume-modal-queue-mode .modal-header {
  background: var(--accent-dim);
  border-bottom-color: var(--accent);
}
.resume-modal-queue-mode #resume-modal-title::before {
  content: "⏳ ";
}

/* Mobile detail screen's queued-follow-up chip strip, directly above the
   bottom composer bar — reuses .queued-badge verbatim, just wrapped in a
   full-width padded strip matching the composer's own horizontal padding. */
.mobile-queued-wrap {
  flex-shrink: 0;
  padding: 6px 12px 0;
}
.mobile-queued-wrap .queued-badge {
  max-width: 100%;
  width: 100%;
  box-sizing: border-box;
}

/* ---------- Row actions (Resume/Queue, play, preview, stop, archive) ------
   All icon-only now (see "Row-action icon family" below) — left to right,
   in frequency/importance order: Resume (non-running rows) or Queue
   (running rows, mutually exclusive with Resume by status) leads with
   `margin-left: 0`, then completion-audio play/stop, then response
   preview, then a gap, then the trailing "removal-ish"/disruptive group —
   Stop (running rows only, see .stop-run-btn below) followed by
   Archive/Restore (see .archive-row-btn below) — no divider needed, the
   `margin-left` gap alone separates the group. See renderRowActions() in
   app.js for the exact render order. */
.actions-cell {
  white-space: nowrap;
  width: 1%;
}
.row-actions-empty { color: var(--text-faint); }

/* Row-action icon family: a single coherent feather/Lucide-style inline-SVG
   stroke set used for every icon-only control in the runs table — pin
   (.pin-btn, leading the Project cell), resume/queue (.resume-icon-btn/
   .queue-row-btn, leading the Actions column, mutually exclusive by
   run.status), completion-audio play/stop (.completion-audio-btn), response
   preview (.response-preview-btn), and the trailing "removal-ish"/
   disruptive group — stop-run (.stop-run-btn, running rows only) and
   archive/restore (.archive-row-btn) — both with extra breathing room
   before them — plus their mobile chat-header counterparts
   (#mobile-detail-pin-btn/#mobile-detail-audio-btn in index.html).
   Replaces an earlier mix of emoji (📌🙈👁) and unicode glyphs (▶■🗨💬) per
   explicit user feedback that the mix read as "homemade" — see
   ICON_PIN_SVG/ICON_RESUME_SVG/ICON_QUEUE_SVG/ICON_VOLUME_SVG/
   ICON_STOP_SVG/ICON_MESSAGE_SQUARE_SVG/ICON_STOP_RUN_SVG/ICON_EYE_SVG/
   ICON_EYE_OFF_SVG in app.js and `docs/brandbook.md` "Row-action icon
   family" for the app-wide convention. Every button in this family shares:
   transparent-at-rest ghost-icon background, `color: var(--text-faint)` at
   rest / `var(--accent-hover)` on hover-focus (driving the SVG's
   `stroke="currentColor"` — icon color is never hardcoded per-icon) — with
   ONE deliberate exception: `.stop-run-btn`'s hover/focus is
   `--status-failed`-tinted (danger) rather than accent-tinted, to signal
   its disruptive nature. A ~28px+ hit target (`min-width`/
   `min-height: 28px`, 4px padding, `display: inline-flex` centering,
   doubled from an original ~20px cramped footprint per a prior 2x sizing
   pass), and the shared `.row-icon-svg` class (20x20px) for uniform icon
   dimensions regardless of which button renders it. Row height
   intentionally does NOT grow to match — icons get bigger/more tappable,
   the table stays dense. Actions-column icons (resume/queue -> play ->
   preview -> [gap] -> stop-run -> archive/restore) are always visible (no
   hover-reveal, that column is icon-dense by design); the Project cell's
   leading `.pin-btn` is the one hover-revealed exception, per its
   Gmail-star-style placement in an otherwise quiet content cell. */

/* Completion voice-line replay (.completion-audio-btn, runs table Actions
   column, see renderCompletionAudioBtn()/toggleCompletionAudio() in app.js):
   a small borderless icon reusing .copy-btn's exact "ghost icon" language
   (transparent at rest, accent-tinted on hover/focus) — rendered only when
   run.has_completion_audio is true, absent entirely otherwise so the table
   never carries dead/disabled icons on every row. No visible player —
   clicking plays audio directly and the icon itself toggles between a
   speaker/volume icon (ICON_VOLUME_SVG) and a stop-square (ICON_STOP_SVG)
   while it plays; clicking again (or letting it finish) stops it. Uses a
   speaker rather than a play triangle — a ▶ in this context falsely implied
   "resume the run" rather than "play the cached voice line." */
.completion-audio-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-left: 4px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.completion-audio-btn:hover { color: var(--accent-hover); background: var(--accent-dim); }
.completion-audio-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Pin-to-top toggle (.pin-btn, runs table Project cell, see
   renderPinBtn()/togglePin() in app.js): same ghost-icon language as
   .completion-audio-btn/.response-preview-btn — transparent at rest,
   accent-tinted on hover/focus. Always rendered (pinning is a valid action
   on every row, not conditional on data being present). Rendered LEADING
   the Project cell — a slim leading slot at the very start of the row,
   before the project name (Gmail-star pattern; `.project-cell-name`,
   below, is nudged right via its own `margin-left` to make room) — rather
   than the Actions column, since "pin" is conceptually about row/run
   identity, not a status-dependent action. Deliberately HOVER-REVEALED by
   default (`opacity: 0` at rest, shown via the row's `:hover` —
   `.runs-table tbody tr:hover .pin-btn` — or the button's own
   `:focus-visible`, for keyboard reachability without a mouse) so an
   unpinned row's leading slot stays visually quiet. The one exception:
   once a run IS pinned, `.pin-btn-active` forces it unconditionally visible
   (`opacity: 1 !important`) — otherwise there would be no way to see which
   runs are pinned without hovering every row, defeating the point of
   pinning as an at-a-glance signal (paired with `.row-pinned`'s row-level
   edge indicator below). Always paired with a `title`/`aria-label` spelling
   out "Pin to top"/"Unpin" and `aria-pressed`, since the icon/tint alone is
   never the sole indicator. Icon is the stroke pin SVG (ICON_PIN_SVG in
   app.js, `.row-icon-svg` sizing) — pinned state is indicated purely via
   `.pin-btn-active`'s `color: var(--accent)`, which the SVG's
   `stroke="currentColor"` inherits automatically; there is no separate
   "filled" icon variant or color-in-the-glyph-itself trick. */
.pin-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-right: 2px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: color 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}
.runs-table tbody tr:hover .pin-btn,
.pin-btn:focus-visible {
  opacity: 1;
}
.pin-btn:hover,
.pin-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.pin-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pin-btn-active {
  color: var(--accent);
  background: var(--accent-dim);
  opacity: 1 !important;
}
.pin-btn-active:hover { color: var(--accent-hover); }
.project-cell { white-space: nowrap; }
/* Project name text, nudged right of the leading .pin-btn slot (see above)
   — an inline-flex wrapper isn't needed since .pin-btn is already
   `display: inline-flex` and sits before this span in source order; this
   class exists purely so the name's own spacing/vertical-align is
   independently tunable from the pin icon's. */
.project-cell-name {
  vertical-align: middle;
}

/* Resume icon (.resume-icon-btn, runs table Actions column, see
   renderResumeIconBtn()/openResumeModal() in app.js): the leading
   (highest-priority) icon in the Actions column's left-to-right
   frequency/importance ordering (resume/queue -> play -> preview -> gap ->
   archive/restore — see renderRowActions() in app.js). History: this
   briefly lived in the Project cell (hover-revealed, ahead of .pin-btn) per
   an earlier user-feedback pass about mis-clicks; moved back into the
   Actions column, now as its FIRST icon rather than its last, once the
   whole column settled into one consistent icon family/ordering and the
   Project cell's leading slot was reserved for the pin toggle alone (see
   .pin-btn below). Same always-visible ghost-icon treatment as the other
   Actions-column icons (.completion-audio-btn/.response-preview-btn/
   .archive-row-btn) — no hover-reveal here, unlike .pin-btn, since the
   Actions column is already icon-dense-by-design and every icon there is
   meant to be visible without hovering. Always paired with a `title`/
   `aria-label` spelling out "Resume — send a follow-up" in words. Icon is
   ICON_RESUME_SVG (a curved reply/continue arrow), deliberately distinct
   from .response-preview-btn's message-square icon so the two
   conversational-adjacent controls never read as the same action. */
.resume-icon-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-left: 0;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.resume-icon-btn:hover,
.resume-icon-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.resume-icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Queue icon (.queue-row-btn, runs table Actions column, running rows only,
   see renderQueueIconBtn()/openResumeModal(runId, {queueMode: true}) in
   app.js): occupies the same leading Actions-column slot as .resume-icon-btn
   (the two are mutually exclusive per run.status — a row is never both
   resumable and queueable at once), converted from a "Queue" text button to
   match the rest of the row-icon family (ICON_QUEUE_SVG, a list-plus glyph).
   Same always-visible ghost-icon treatment, sizing, and hover/focus CSS as
   .resume-icon-btn — intentionally identical since they occupy the same
   visual "slot." */
.queue-row-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-left: 0;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.queue-row-btn:hover,
.queue-row-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.queue-row-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Mobile chat header's pin button (#mobile-detail-pin-btn, .mobile-pin-btn):
   touch has no hover state, so the desktop hover-reveal rule above would
   make pinning unreachable on mobile — this override keeps it always
   visible (same footprint/tap-target sizing as the header's other icon
   buttons, e.g. #mobile-detail-audio-btn), while still gaining
   .pin-btn-active's persistent accent tint when the open run is pinned. */
.mobile-pin-btn {
  opacity: 1;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-sm);
}
.completion-audio-btn:disabled { color: var(--text-faint); cursor: default; opacity: 0.7; }

/* Response preview icon + modal (runs table Actions column, see
   renderResponsePreviewBtn()/showResponsePreviewModal() in app.js): click
   opens #response-preview-modal, the same .modal-overlay/.modal pattern used
   by #resume-modal/#schedule-modal elsewhere in this file — no bespoke
   floating-panel styling needed. Trigger reuses .completion-audio-btn's
   exact "ghost icon" language (transparent at rest, accent-tinted
   hover/focus) for visual consistency with the other icon buttons in this
   cell. Icon is ICON_MESSAGE_SQUARE_SVG (a plain stroke message-square),
   deliberately distinct from .resume-icon-btn's reply-arrow icon. */
.response-preview-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-left: 4px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.response-preview-btn:hover,
.response-preview-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.response-preview-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Stop-run icon (.stop-run-btn, runs table Actions column, running rows
   only — see renderStopRunBtn()/stopRunFromTable() in app.js): leads the
   trailing "removal-ish"/disruptive group alongside Archive/Restore
   immediately below (same `margin-left: 14px` gap separating both from the
   preceding Queue/play/preview cluster — see renderRowActions() ordering in
   app.js) rather than sitting with the lower-stakes icons. Same ghost-icon
   footprint as the rest of the row-icon family (padding/radius/sizing/
   transition), but its hover/focus state is intentionally DANGER-tinted
   (`--status-failed` text on a matching translucent tint) instead of the
   family's usual accent hover — the one deliberate exception in this
   family — to signal that this action is disruptive (kills the run's
   process) even while visually belonging to the same icon set. Icon is
   ICON_STOP_RUN_SVG, an outline stroke square distinct from
   ICON_STOP_SVG (the filled solid square used by `.completion-audio-btn`'s
   playing state) — different class, different data attribute
   (`data-stop-run-id`), different click handler; the two "stop" actions
   are unrelated and must never share wiring. Always paired with a
   `title`/`aria-label` reading "Stop run". Confirmation lives in
   `stopRunFromTable()` (a native `confirm()`) rather than here — this
   button is a plain trigger, same "dumb button, smart handler" split as
   every other row-icon control. */
.stop-run-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  margin-left: 14px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.stop-run-btn:hover,
.stop-run-btn:focus-visible {
  color: var(--status-failed);
  background: rgba(179, 106, 94, 0.16);
}
.stop-run-btn:focus-visible { outline: 2px solid var(--status-failed); outline-offset: 1px; }

/* Archive/Restore icon toggle (.archive-row-btn, runs table Actions column,
   TRAILING position — see renderArchiveBtn()/toggleArchive() in app.js):
   replaces the earlier "ARCHIVE"/"RESTORE" text buttons with an icon-only
   control, per user feedback that spelled-out text buttons in this column
   were the biggest contributor to visual clutter/mis-clicks. Shares the
   exact same ghost-icon footprint/hover language as the rest of the
   Actions-column icon cluster (.resume-icon-btn/.queue-row-btn/
   .completion-audio-btn/.response-preview-btn above) so the whole column
   reads as one consistent family (same padding, radius, transition) —
   except its `margin-left`, which is deliberately larger than the other
   icons' spacing to visually separate it as the "removal-ish" action at
   the end of the row (see renderRowActions() ordering in app.js). Archive
   shows a classic eye-off/eye-slash SVG icon, Restore a plain open-eye SVG
   icon (ICON_EYE_OFF_SVG/ICON_EYE_SVG in app.js, sized via .row-icon-svg
   below) — exactly one renders at a time based on run.archived, same as
   the text buttons it replaces. Always paired with a `title`/`aria-label`
   spelling out "Archive this run"/"Restore this run" in words, since the
   icon alone is never the sole indicator. */
.archive-row-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 4px;
  /* Extra breathing room before the "removal-ish" Archive/Restore action —
     visually separates it from the preceding resume/queue/play/preview
     cluster without needing a divider, per the row-action ordering
     convention (see renderRowActions() in app.js and "Row-action icon
     family" in docs/brandbook.md). */
  margin-left: 14px;
  border-radius: 4px;
  line-height: 1;
  vertical-align: middle;
  min-width: 28px;
  min-height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s ease, background 0.15s ease;
}
.archive-row-btn:hover,
.archive-row-btn:focus-visible { color: var(--accent-hover); background: var(--accent-dim); }
.archive-row-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Shared SVG sizing for the entire row-action icon family — pin, resume,
   queue, completion-audio play/stop, response preview, stop-run,
   archive/restore, and their mobile chat-header counterparts (see
   ICON_PIN_SVG/ICON_RESUME_SVG/ICON_QUEUE_SVG/ICON_VOLUME_SVG/
   ICON_STOP_SVG/ICON_MESSAGE_SQUARE_SVG/ICON_STOP_RUN_SVG/ICON_EYE_SVG/
   ICON_EYE_OFF_SVG in app.js). 20px square — every icon in the family uses
   this one class so they're all guaranteed the same footprint regardless of
   which button renders them. `stroke="currentColor"` is set inline on each
   SVG, so every icon inherits its parent button's `color` transitions on
   hover/focus/active for free — no icon ever hardcodes its own stroke
   color. See `docs/brandbook.md` "Row-action icon family" for the
   app-wide convention this establishes. */
.row-icon-svg {
  width: 20px;
  height: 20px;
  display: block;
}

#response-preview-modal .modal { max-width: 520px; }
#response-preview-modal-body .markdown-box {
  max-height: 60vh;
  overflow-y: auto;
}

/* ---------- Inline resume prompt (resume modal, opened directly from the
   runs table's "Resume" row action) ---------- */
.resume-modal-context {
  margin-bottom: 8px;
}
/* Shared layout wrapper for the "expandable conversation history" list
   (.resume-history-item children) — used by both the resume modal's "Run"
   field (#resume-modal-prompt) and the detail drawer's Conversation field
   (#detail-conversation), see renderConversationHistoryItems() in app.js.
   .resume-modal-prompt is kept as a bare alias (modal-specific id, no extra
   rules of its own) rather than renaming the existing markup. */
.resume-history-list,
.resume-modal-prompt {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 220px;
  overflow-y: auto;
}

/* Expandable prompt/response history inside the resume modal's "Run" field
   (renderResumeModalHistory() in app.js) — a compact list of native
   <details>/<summary> items, one per prompt segment (plus a trailing
   "Latest response" item when the run has a result_summary), styled to
   match the dark UI instead of using browser-default disclosure chrome.
   Replaces an earlier single <pre class="prompt-preview"> that dumped
   truncate(run.prompt, 400) verbatim. */
.resume-history-item {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.resume-history-item + .resume-history-item {
  margin-top: 0;
}
.resume-history-summary {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 10px;
  cursor: pointer;
  list-style: none;
  min-width: 0;
}
.resume-history-summary::-webkit-details-marker {
  display: none;
}
.resume-history-summary::before {
  content: "▸";
  color: var(--text-faint);
  font-size: 10px;
  flex: none;
  transition: transform 0.15s ease;
}
.resume-history-item[open] > .resume-history-summary::before {
  transform: rotate(90deg);
}
.resume-history-label {
  flex: none;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-faint);
}
.resume-history-preview {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}
.resume-history-item[open] > .resume-history-summary .resume-history-preview {
  display: none;
}
/* Response items (renderResumeModalHistory(), assistant replies interleaved
   after each prompt segment via GET /api/runs/{id}/responses) — same
   .resume-history-item shape, just a subtle accent-tinted label instead of
   the default muted uppercase one, so a response reads as distinct from the
   prompt segment immediately above it without a heavier visual treatment. */
.resume-history-label-response {
  color: var(--accent-hover);
}
.resume-history-body {
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  padding: 0 10px 10px 26px;
  max-height: 200px;
  overflow-y: auto;
  /* Constrain to the item's own width (rather than the ancestor's implicit
     content width) so a markdown response's block elements below — table,
     pre/code — clip/scroll internally instead of ever forcing the whole
     .resume-history-item (and, in the resume modal, the modal card itself)
     wider than intended. Belt-and-suspenders alongside the pre/table
     overflow-x rules below. */
  max-width: 100%;
  box-sizing: border-box;
}
/* Response items rendered via renderAssistantMarkdown() (see the "isMarkdown"
   branch in renderConversationHistoryItems(), app.js) — used by both the
   detail drawer's Conversation field and the resume modal's history list.
   Layers the shared ".log-markdown, .markdown-box" ruleset (headings, lists,
   code, tables, blockquotes — see "Markdown (rendered LLM/assistant text)"
   above) on top of .resume-history-body's own layout (indent, scroll,
   max-height) rather than .markdown-box's card chrome (bg/border/padding),
   since .resume-history-body already sits inside the .resume-history-item
   card and doesn't need a second nested surface. UI font/size (not
   .log-markdown's monospace-log-viewer sizing) since this list isn't a log
   line stream in either surface. */
.resume-history-body.markdown-box {
  font-family: var(--font-ui);
  font-size: 12.5px;
  background: none;
  border: none;
  border-radius: 0;
}
/* Code blocks inside a markdown response body: this list lives in a
   comparatively narrow column in BOTH surfaces (the detail drawer's fixed
   width and, more tightly, the resume modal's .modal card) — long unbroken
   lines in a fenced code block must scroll horizontally within the block
   itself rather than overflowing .resume-history-body/.resume-history-item
   or forcing the modal wider. The shared ".log-markdown pre, .markdown-box
   pre" rule already sets overflow-x: auto; this adds the width constraint
   that makes that overflow actually kick in inside this narrower container
   instead of the pre intrinsically sizing to its longest line. */
.resume-history-body.markdown-box pre {
  max-width: 100%;
}
.resume-history-body.markdown-box table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
}
.resume-inline {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}
/* Follow-up prompt is a small multi-line textarea (Enter = newline,
   Cmd/Ctrl+Enter = send — see app.js), same "field textarea" base styling
   (bg/border/radius/focus ring) as the new-run custom-prompt textarea, but
   UI font/size and a lower max-height since it's a compact inline field
   rather than the launch panel's primary prompt input. */
.resume-inline textarea {
  flex: 1;
  min-height: 38px;
  max-height: 160px;
  font-family: var(--font-ui);
  font-size: 13px;
}
.resume-inline .btn {
  flex-shrink: 0;
}
/* Compact model override row under the follow-up textarea — reuses the
   `.field select`/`.field input[type="text"]` box styling (bg/border/radius/
   focus ring) but laid out inline with a small label, since this is a single
   lightweight control, not a full `.field`. Defaults to "Same model
   (default)" every time the modal is opened (see openResumeModal()/
   closeResumeModal() in app.js) — resolves via the shared
   getModelSelectValue()/setModelSelectValue() helpers, same as the launch
   panel and schedule modal Model fields. */
.resume-model-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  min-width: 0;
  max-width: 100%;
}
.resume-model-label {
  font-size: 12px;
  color: var(--text-muted);
  font-weight: 600;
  flex-shrink: 0;
}
.resume-model-select {
  flex: 0 0 auto;
  width: auto;
  min-width: 150px;
  max-width: 220px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  color: var(--text);
  font-size: 12.5px;
}
.resume-model-select:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}
.resume-model-custom-input {
  flex: 1;
  min-width: 0;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  color: var(--text);
  font-size: 12.5px;
}
.resume-model-custom-input:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}

/* ---------- Status badges ---------- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: var(--radius-pill);
  padding: 3px 9px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border: 1px solid transparent;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.badge-running { color: var(--status-running); background: rgba(138, 148, 163, 0.12); border-color: rgba(138, 148, 163, 0.25); }
.badge-running::before { animation: pulse 1.2s ease-in-out infinite; }
.badge-completed { color: var(--status-completed); background: rgba(127, 168, 139, 0.12); border-color: rgba(127, 168, 139, 0.25); }
.badge-failed { color: var(--status-failed); background: rgba(179, 106, 94, 0.12); border-color: rgba(179, 106, 94, 0.25); }
.badge-stopped { color: var(--status-stopped); background: rgba(176, 138, 78, 0.12); border-color: rgba(176, 138, 78, 0.25); }
.badge-orphaned { color: var(--status-orphaned); background: rgba(118, 118, 118, 0.12); border-color: rgba(118, 118, 118, 0.25); }

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* ---------- Detail drawer ----------
   Slides in/out via `transform: translateX` rather than display:none, so the
   open/close is an actual animated transition instead of an instant snap.
   Stays in the DOM and positioned (`position: fixed`) at all times; the
   `.hidden` utility class is intentionally NOT used here (it would still be
   display:none, which can't be transitioned) — visibility while closed is
   handled entirely by the translateX(100%) offscreen position plus
   `visibility`/`pointer-events`, both flipped only after the closing
   transition finishes (see openDetail()/closeDetail() in app.js) so the
   off-screen drawer is never focusable or clickable and never sits in the
   tab order while closed. 260ms ease-out on open reads as a deliberate but
   brisk reveal (matches this dashboard's "information-dense, not flashy"
   tone); the close uses the same duration/easing for symmetry. Transitioning
   opacity alongside transform gives the slide a touch of polish without
   turning it into a fade — motion is still the primary cue. */
.detail-drawer {
  position: fixed;
  top: 0; right: 0;
  height: 100vh;
  width: 560px;
  max-width: 92vw;
  background: var(--bg-elevated);
  border-left: 1px solid var(--border);
  box-shadow: -12px 0 32px rgba(0,0,0,0.4);
  z-index: 20;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: transform 0.26s ease-out, opacity 0.26s ease-out, visibility 0s linear 0.26s;
}
.detail-drawer.open {
  transform: translateX(0);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.26s ease-out, opacity 0.26s ease-out, visibility 0s linear 0s;
}
/* Respect reduced-motion preferences: keep the show/hide functional (still
   toggles visibility/pointer-events correctly) but drop the animated slide
   in favor of an instant cut, same as any other motion in the dashboard. */
@media (prefers-reduced-motion: reduce) {
  .detail-drawer,
  .detail-drawer.open {
    transition-duration: 0.001ms;
  }
}

/* ---------- Launch panel (New Run) — sliding overlay drawer ----------
   Left-anchored mirror of .detail-drawer above: same always-mounted,
   transform+opacity `.open`-class pattern (never display:none, so it stays
   animatable) — closed state sits off-screen via translateX(-100%) instead
   of the drawer's translateX(100%), everything else (duration/easing,
   visibility/pointer-events toggling, reduced-motion handling) is
   identical. Triggered by #open-launch-panel-btn in the runs panel header;
   closed via #close-launch-panel-btn, the #launch-panel-backdrop click, or
   Escape (see openLaunchPanel()/closeLaunchPanel() in app.js). Replaces the
   earlier always-visible 360px launch-panel column in #runs-view.layout —
   see docs/webui-templates-index.md "Launch panel". */
.launch-panel {
  position: fixed;
  top: 0; left: 0;
  height: 100vh;
  width: 420px;
  max-width: 92vw;
  background: var(--bg-elevated);
  border-right: 1px solid var(--border);
  box-shadow: 12px 0 32px rgba(0,0,0,0.4);
  z-index: 20;
  display: flex;
  flex-direction: column;
  transform: translateX(-100%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: transform 0.26s ease-out, opacity 0.26s ease-out, visibility 0s linear 0.26s;
}
.launch-panel.open {
  transform: translateX(0);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.26s ease-out, opacity 0.26s ease-out, visibility 0s linear 0s;
}
@media (prefers-reduced-motion: reduce) {
  .launch-panel,
  .launch-panel.open {
    transition-duration: 0.001ms;
  }
}
.launch-panel-body { padding: 16px; overflow-y: auto; flex: 1; }
.launch-panel-fab { margin-left: auto; }

/* Launch panel pull-tab (#launch-panel-tab) — desktop-only fixed trigger,
   replacing the old always-inline "+ New run" button (.launch-panel-fab,
   still present as the mobile fallback below 768px — see the phone
   breakpoint override at the bottom of this block and
   docs/webui-templates-index.md "Launch panel"). Styled as a small circular
   handle sitting just outside the panel's closed off-screen edge so it reads
   as "grab here to pull the drawer out": half-hidden against the left
   viewport edge (the visible half is a `--accent` filled circle, same
   primary-button color as any other `.btn-primary`), vertically positioned
   near the top-left rather than dead-center so it doesn't collide with
   vertically-centered content lower on the page.
   Slides in lockstep with `.launch-panel` itself — translateX, not
   opacity/fade — since the panel's own `.open` transition is a transform
   slide (see "Launch panel slide animation" above); mirroring that same
   transform means the tab visually reads as "part of the drawer's edge"
   rather than a separate control that pops in/out independently. Uses the
   exact same 0.26s ease-out duration/easing and reduced-motion collapse as
   `.launch-panel` for symmetry. */
.launch-panel-tab {
  position: fixed;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  z-index: 19; /* just under .launch-panel (20) so the panel slides over it */
  width: 48px;
  height: 48px;
  border-radius: 0 999px 999px 0;
  border: 1px solid var(--border-strong);
  border-left: none;
  background: var(--accent);
  color: #0b0d10;
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 4px;
  cursor: pointer;
  box-shadow: 2px 0 10px rgba(0,0,0,0.35);
  transition: transform 0.26s ease-out, background-color 0.15s ease-out;
}
.launch-panel-tab:hover,
.launch-panel-tab:focus-visible {
  background: var(--accent-hover);
}
.launch-panel-tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Slide the tab along with the panel while it's open — the panel's own
   420px width (max 92vw) means translateX(420px) comfortably clears it on
   any desktop viewport (>=900px, well above the 92vw cap boundary), so the
   tab reappears at the drawer's new right edge as a visual "close handle"
   twin of the header's Close ✕ button rather than vanishing. */
.launch-panel.open ~ .launch-panel-tab {
  transform: translate(420px, -50%);
}
@media (prefers-reduced-motion: reduce) {
  .launch-panel-tab,
  .launch-panel.open ~ .launch-panel-tab {
    transition-duration: 0.001ms;
  }
}
/* Mobile fallback (see "Mobile breakpoint" in docs/brandbook.md — 768px is
   the real phone-only breakpoint; .runs-panel-header and the rest of
   #runs-view.layout are hidden entirely below it in favor of the
   purpose-built mobile UI, so there is no meaningful "narrow desktop" gap to
   worry about here). Above 768px: hide the inline header button, show the
   fixed edge tab. At/below 768px: the desktop .layout is display:none
   anyway (see the phone breakpoint block further down), so both rules below
   are redundant with that but kept explicit/self-documenting here. */
@media (min-width: 769px) {
  .launch-panel-fab { display: none; }
}
@media (max-width: 768px) {
  .launch-panel-tab { display: none; }
}

/* Simple dim backdrop behind the launch panel — clicking it closes the
   panel (see closeLaunchPanel() in app.js). Lower z-index than .launch-panel
   itself so the panel renders on top; `.hidden` (display:none) is fine here
   (unlike the panel, the backdrop has no transition of its own to preserve). */
.modal-overlay-simple {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 19;
}

.detail-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.detail-header-actions { display: flex; gap: 8px; }
.detail-body { padding: 16px; overflow-y: auto; flex: 1; }

/* CLI login banner: shown on a failed run whose detail response carries
   needs_cli_login — see docs/webui-templates-index.md "CLI login banner".
   Same danger-tinted language as .stop-run-btn's hover state (muted brick/
   terracotta, the "failed" status color) since this represents a blocked
   run, but rendered as a persistent inline banner rather than a hover
   state. */
.cli-login-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(179, 106, 94, 0.1);
  border: 1px solid rgba(179, 106, 94, 0.3);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--text);
}
.cli-login-banner-icon { color: var(--status-failed); font-size: 15px; flex-shrink: 0; }
.cli-login-banner-text { flex: 1; }
.cli-login-banner-text strong { color: var(--status-failed); }

/* Detail drawer header title area: "Run detail" label plus, when the run
   has a session_name, the run's own title inline after it (editable — see
   .run-title-view below). min-width: 0 so a long title still truncates via
   .prompt-tooltip-trigger's own max-width rather than pushing the Close
   button out of the header. */
.detail-header-title-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  overflow: hidden;
}
.detail-header-title-wrap .panel-title { margin: 0; flex-shrink: 0; }
.detail-title-view {
  min-width: 0;
  overflow: hidden;
  color: var(--text-muted);
  font-size: 13px;
}
.detail-title-view:not(:empty)::before {
  content: "—";
  margin-right: 10px;
  color: var(--text-faint);
}
.detail-title-view .run-title-text { color: var(--text); font-weight: 600; }
.detail-title-view .run-title-input { font-size: 13px; max-width: 240px; }

.detail-meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 16px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 16px;
  font-size: 12.5px;
}
.meta-label { display: block; color: var(--text-faint); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.03em; }
.meta-value { display: block; margin-top: 1px; }

.log-status { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--text-faint); margin-left: 6px; }

.log-viewer {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px;
  max-height: 420px;
  max-width: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  font-family: var(--font-mono);
  font-size: 11.5px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  box-sizing: border-box;
}

.log-line { padding: 4px 6px; border-radius: 4px; border-bottom: 1px solid rgba(255,255,255,0.03); min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
.log-line:hover { background: rgba(255,255,255,0.02); }
.log-tag {
  display: inline-block;
  font-weight: 700;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 1px 6px;
  border-radius: 3px;
  margin-right: 6px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.log-tag-assistant { color: var(--accent-hover); border-color: rgba(138,148,163,0.25); }
.log-tag-user { color: var(--status-completed); border-color: rgba(127,168,139,0.25); }
.log-tag-system { color: var(--status-orphaned); }
.log-tag-result { color: var(--status-stopped); border-color: rgba(176,138,78,0.25); }
.log-tag-error { color: var(--status-failed); border-color: rgba(179,106,94,0.25); }
.log-text { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; color: var(--text); }
.log-sub { word-break: break-word; overflow-wrap: anywhere; color: var(--text-faint); }
.log-tool { word-break: break-word; overflow-wrap: anywhere; color: var(--accent-hover); }
.log-raw { color: var(--text-faint); white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }

/* ---------- Markdown (rendered LLM/assistant text) ----------
   Applies to: .log-markdown (inside the monospace log viewer, per assistant
   message) and .markdown-box (the UI-font "Result summary" block). Shared
   rules first, font-family differs per context. Compact spacing — this is a
   monitoring panel, not a blog. */
.log-markdown,
.markdown-box {
  color: var(--text);
}
.log-markdown > *:first-child,
.markdown-box > *:first-child { margin-top: 0; }
.log-markdown > *:last-child,
.markdown-box > *:last-child { margin-bottom: 0; }

.log-markdown p,
.markdown-box p { margin: 0 0 6px; }

.log-markdown h1, .log-markdown h2, .log-markdown h3,
.log-markdown h4, .log-markdown h5, .log-markdown h6,
.markdown-box h1, .markdown-box h2, .markdown-box h3,
.markdown-box h4, .markdown-box h5, .markdown-box h6 {
  margin: 10px 0 6px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
}
.log-markdown h1, .markdown-box h1 { font-size: 1.25em; }
.log-markdown h2, .markdown-box h2 { font-size: 1.15em; }
.log-markdown h3, .markdown-box h3 { font-size: 1.05em; }
.log-markdown h4, .log-markdown h5, .log-markdown h6,
.markdown-box h4, .markdown-box h5, .markdown-box h6 { font-size: 1em; color: var(--text-muted); }

.log-markdown ul, .log-markdown ol,
.markdown-box ul, .markdown-box ol {
  margin: 0 0 6px;
  padding-left: 1.4em;
}
.log-markdown li, .markdown-box li { margin: 2px 0; }

.log-markdown blockquote,
.markdown-box blockquote {
  margin: 0 0 6px;
  padding: 2px 10px;
  border-left: 3px solid var(--border-strong);
  color: var(--text-muted);
}

.log-markdown hr, .markdown-box hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 10px 0;
}

.log-markdown a, .markdown-box a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: rgba(138, 148, 163, 0.4);
}
.log-markdown a:hover, .markdown-box a:hover { text-decoration-color: var(--accent); }

.log-markdown code, .markdown-box code {
  font-family: var(--font-mono);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 0.92em;
}
.log-markdown pre, .markdown-box pre {
  margin: 0 0 6px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  overflow-x: auto;
}
.log-markdown pre code, .markdown-box pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: 0.9em;
  white-space: pre;
}

.log-markdown table, .markdown-box table {
  border-collapse: collapse;
  margin: 0 0 8px;
  font-size: 0.92em;
}
.log-markdown th, .log-markdown td,
.markdown-box th, .markdown-box td {
  border: 1px solid var(--border);
  padding: 4px 8px;
  text-align: left;
}
.log-markdown th, .markdown-box th {
  background: var(--bg-elevated);
  color: var(--text-muted);
  font-weight: 600;
}

.log-markdown del, .markdown-box del { color: var(--text-faint); }

/* Log viewer context: keep it monospace/dense to match sibling log lines. */
.log-markdown { font-family: var(--font-mono); font-size: 11.5px; }

/* Result summary box: UI font, card surface to match other detail fields. */
.markdown-box {
  font-family: var(--font-ui);
  font-size: 13px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  max-height: 320px;
  overflow-y: auto;
}

/* ---------- Field header (label + inline action) ---------- */
.field-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 6px;
}
.field-header label { margin-bottom: 0; }

/* ---------- Link-style button (inline text action, e.g. "Manage presets") ---------- */
.link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.link-btn:hover { color: var(--accent-hover); text-decoration: underline; }
.link-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

.field-hint {
  margin: 6px 0 0;
  font-size: 11.5px;
  color: var(--text-faint);
}
.field-hint code {
  font-family: var(--font-mono);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 5px;
  color: var(--text-muted);
}

/* ---------- Modal (preset manager, delete confirm) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 24px;
}
.modal {
  width: 100%;
  max-width: 820px;
  max-height: 88vh;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-sm { max-width: 420px; }
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.modal-body { padding: 16px; overflow-y: auto; }

/* Settings modal (public build, Phase 2 — BYOK): default model + OpenRouter/
   ElevenLabs API keys, all backed by the single plain GET/PUT /api/settings
   route (no /api/settings/full — that host-level .env/System-tab editor was
   removed for the public build, see docs/public-migration-plan.md
   "Removals"). One flat shared form, no tab strip needed (few enough fields
   that the internal build's API Keys/Models/System tab split doesn't apply
   here). See docs/webui-templates-index.md "Settings modal (public build,
   BYOK)". */
#settings-modal .modal { max-width: 480px; }
.settings-tab-section { display: flex; flex-direction: column; gap: 4px; }
.settings-tab-section .field { margin-top: 12px; }
.settings-tab-section .field:first-child { margin-top: 0; }
/* Set/not-set status line above a masked key input — reuses the muted
   field-hint tone; the masked hint itself is monospace to read as a literal
   partial value rather than prose. */
.settings-key-status {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 4px;
  font-family: var(--font-mono);
}
.settings-section-heading {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 20px 0 4px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.settings-tab-section:first-child + .settings-section-heading,
form#settings-form > .settings-tab-section:first-of-type {
  border-top: none;
}
.field-required {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--accent-hover);
}
.field-optional {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--text-faint);
}
.settings-key-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 2px;
}
.settings-key-remove-link { color: var(--status-failed); }
.settings-key-remove-link:hover { color: var(--status-failed); text-decoration: underline; }

/* Red top banner (BYOK OpenRouter key missing / out of credit) — persistent,
   full-width, above the view-tabs so it spans both the desktop layout and
   the mobile UI without a second copy of the markup. Dismissible per session
   (state.keysBannerDismissed) but reappears on next load while the
   underlying condition still holds — see docs/public-migration-plan.md
   "Phase 2 — BYOK keys, widgets, red banner". */
.keys-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  background: rgba(179, 106, 94, 0.16);
  border-bottom: 1px solid rgba(179, 106, 94, 0.4);
  color: var(--text);
  font-size: 13px;
}
.keys-banner-text {
  flex: 1;
  cursor: pointer;
  color: var(--status-failed);
  font-weight: 600;
}
.keys-banner-dismiss {
  background: transparent;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  font-size: 13px;
  padding: 2px 4px;
  line-height: 1;
}
.keys-banner-dismiss:hover { color: var(--text); }
.keys-banner-dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (max-width: 768px) {
  .keys-banner { padding: 10px 14px; flex-wrap: wrap; }
  .keys-banner-text { flex-basis: 100%; }
}
.settings-key-status .settings-key-masked {
  font-family: var(--font-mono);
  color: var(--text-faint);
}
.settings-key-status.settings-key-set::before { content: "● "; color: var(--status-completed); }
.settings-key-status.settings-key-unset::before { content: "● "; color: var(--status-orphaned); }
/* Read-only PORT / CLAUDE_BIN display rows — same look as a disabled input
   without actually being one, since these are never editable at all. */
.settings-readonly-value {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}
.settings-restarting-note {
  margin-top: 16px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  color: var(--accent-hover);
  font-size: 13px;
  font-weight: 500;
}

/* Preset manager two-column layout */
.preset-manager-layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 16px;
  align-items: start;
}
@media (max-width: 640px) {
  .preset-manager-layout { grid-template-columns: 1fr; }
}

.preset-manager-list-col { display: flex; flex-direction: column; gap: 10px; }
.preset-manager-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 60vh;
  overflow-y: auto;
}
.preset-manager-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  text-align: left;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  cursor: pointer;
  color: var(--text);
}
.preset-manager-item:hover { border-color: var(--border-strong); }
.preset-manager-item.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent-hover); }
.preset-manager-item-name { font-size: 13px; font-weight: 600; }
.preset-manager-item-desc {
  font-size: 11px;
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
.preset-manager-list-empty { color: var(--text-faint); font-size: 12.5px; padding: 8px 2px; }

/* Archived-projects group label + muted item state (Projects manager list).
   Reuses .preset-manager-item verbatim with a muted modifier — no new list
   component — same convention as .preset-manager-item.active. */
.preset-manager-list-group-label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-faint);
  padding: 6px 2px 2px;
}
/* Generic muted-item modifier — Projects manager's archived rows and the
   runs table's archived rows (.runs-table tbody tr.archived-item, shown only
   when the archived filter is "Shown"/"Only", see renderRunsTable() in
   app.js) both reuse this verbatim rather than a new dimmed-row class. */
.archived-item { opacity: 0.55; }
.archived-item:hover { opacity: 0.8; }
.archived-item.active { opacity: 0.85; }

.preset-editor-empty {
  color: var(--text-faint);
  font-size: 13px;
  padding: 24px 12px;
  text-align: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
}
.preset-editor-form .field:last-of-type { margin-bottom: 0; }
.preset-editor-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
}
.preset-editor-actions-spacer { flex: 1; }

.confirm-text { font-size: 13px; color: var(--text-muted); margin: 0 0 16px; }

/* ---------- View tabs (Runs / Schedules) ---------- */
.view-tabs {
  display: flex;
  gap: 4px;
  padding: 0 16px;
  margin-top: 16px;
  border-bottom: 1px solid var(--border);
}
.view-tab {
  background: transparent;
  border: 1px solid transparent;
  border-bottom: none;
  color: var(--text-muted);
  padding: 8px 16px;
  margin-bottom: -1px;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  transition: color var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
}
.view-tab:hover { color: var(--text); }
.view-tab.active {
  background: var(--bg-elevated);
  color: var(--accent);
  border-color: var(--border);
  border-bottom: 1px solid var(--bg-elevated);
}

.layout-single { grid-template-columns: 1fr; }

/* ---------- Schedules panel ---------- */
.schedules-header-actions { display: flex; align-items: center; gap: 12px; }
.schedules-table td.prompt-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.schedule-summary { display: flex; flex-direction: column; gap: 1px; }
.schedule-summary-kind { font-weight: 600; }
.schedule-summary-detail { color: var(--text-faint); font-size: 11px; }

.schedule-row.disabled-row { opacity: 0.5; }
.schedule-row.disabled-row:hover { opacity: 0.75; }

.next-run-relative { white-space: nowrap; }
.last-run-link { color: var(--accent); cursor: pointer; text-decoration: underline; text-decoration-color: rgba(138,148,163,0.4); background: none; border: none; padding: 0; font: inherit; }
.last-run-link:hover { text-decoration-color: var(--accent); }

.session-badge {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  white-space: nowrap;
}
.session-badge-continue { color: var(--accent-hover); border-color: rgba(138,148,163,0.25); background: var(--accent-dim); }

/* Model badge (.model-badge): tiny neutral label shown on run rows/detail
   and schedule rows only when a non-default model was requested (empty/
   absent = "Default", rendered as nothing per brandbook convention — color
   is never the only signal, so this always carries the resolved model
   string as text, e.g. "sonnet" or a full custom model id). Same pill
   language as .session-badge (neutral) rather than .badge (status) since
   this isn't a state — reuse rather than inventing a new shape. */
.model-badge {
  display: inline-block;
  margin-left: 6px;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0.02em;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  white-space: nowrap;
  font-family: var(--font-mono);
}

/* Server badge (.remote-badge, combined with .session-badge): tiny neutral
   pill appended after a project name in any project picker/chip naming the
   server it lives on — every project belongs to a server, see
   docs/public-migration-plan.md "No local projects, ever" — same neutral
   pill shape as .session-badge (reused, not a new shape), just adds the
   inline spacing needed when it trails plain text rather than sitting
   inside a table cell. */
.remote-badge { margin-left: 6px; }

.done-label { color: var(--text-faint); font-size: 11.5px; font-style: italic; }

/* Toggle switch (enable/disable) */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 34px;
  height: 19px;
  vertical-align: middle;
}
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-switch .toggle-track {
  position: absolute;
  cursor: pointer;
  inset: 0;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.toggle-switch .toggle-track::before {
  content: "";
  position: absolute;
  height: 13px; width: 13px;
  left: 2px; top: 2px;
  background: var(--text-faint);
  border-radius: 50%;
  transition: transform 0.15s ease, background 0.15s ease;
}
.toggle-switch input:checked + .toggle-track { background: var(--accent-dim); border-color: var(--accent); }
.toggle-switch input:checked + .toggle-track::before { transform: translateX(15px); background: var(--accent); }
.toggle-switch input:focus-visible + .toggle-track { outline: 2px solid var(--accent); outline-offset: 1px; }
.toggle-switch input:disabled + .toggle-track { opacity: 0.5; cursor: not-allowed; }

.schedules-actions-cell { display: flex; gap: 6px; white-space: nowrap; }

/* ---------- Schedule builder (uses shared .modal / .modal-overlay above) ---------- */
#schedule-modal .modal { max-width: 520px; }
.freq-panel input[type="number"],
.freq-panel input[type="time"],
.freq-panel input[type="text"] {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 7px 9px;
  color: var(--text);
  width: auto;
  margin-right: 6px;
}
.freq-panel select,
#schedule-frequency {
  width: 100%;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  color: var(--text);
}
.field-suffix { color: var(--text-muted); font-size: 12.5px; }
.field-label-secondary { display: block; font-size: 12px; color: var(--text-muted); margin: 10px 0 6px; font-weight: 600; }

.weekday-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; }
.chip-toggle {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  border-radius: var(--radius-pill);
  padding: 5px 11px;
  font-size: 12px;
  cursor: pointer;
}
.chip-toggle:hover { border-color: var(--border-strong); }
.chip-toggle.active { background: var(--accent-dim); color: var(--accent-hover); border-color: var(--accent); }

.cron-preview {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin: 8px 0 14px;
  font-size: 12px;
}
.cron-preview-desc { color: var(--text); font-weight: 600; margin-bottom: 3px; }
.cron-preview-next { color: var(--text-faint); font-family: var(--font-mono); font-size: 11px; }
.cron-preview-error {
  color: var(--status-failed);
  font-size: 12px;
  background: rgba(179, 106, 94, 0.08);
  border: 1px solid rgba(179, 106, 94, 0.25);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin: 8px 0 14px;
}

.session-mode-options { display: flex; flex-direction: column; gap: 8px; }
.session-mode-option {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 9px 10px;
  cursor: pointer;
}
.session-mode-option:has(input:checked) { border-color: var(--accent); background: var(--accent-dim); }
.session-mode-option input { margin-top: 3px; }
.session-mode-option span { display: flex; flex-direction: column; gap: 1px; }
.session-mode-option small { color: var(--text-muted); font-size: 11.5px; }

/* Server credentials form: auth-method toggle reuses .session-mode-options
   verbatim but the two cards sit side by side (there's no helper-text-heavy
   content that needs full width here) — see docs/webui-templates-index.md
   "Servers editor column". */
.server-auth-method-group { flex-direction: row; }
.server-auth-method-group .session-mode-option { flex: 1; }
.mono-textarea {
  font-family: var(--font-mono);
  font-size: 12px;
  resize: vertical;
}

/* ---------- Projects manager (unified local + SSH-remote registry) ---------- */
/* Remote-project badge shown in any project picker (launch panel, mobile
   picker, projects manager list) — reuses the neutral .session-badge pill
   shape/language rather than a new one, since it's a descriptive tag, not a
   status. Shows the project's server_name for remote projects. */
.remote-badge { margin-left: 6px; text-transform: none; font-size: 10px; }

/* Test-connection result line (server editor's "Test connection" action):
   success/failure, same red/green tokens as the rest of the dashboard's
   status language — color is paired with a check/cross glyph and text,
   never color alone. */
.remote-test-result {
  font-size: 12.5px;
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin: 4px 0 10px;
}
.remote-test-ok {
  color: var(--status-completed);
  background: rgba(127, 168, 139, 0.08);
  border: 1px solid rgba(127, 168, 139, 0.25);
}
.remote-test-fail {
  color: var(--status-failed);
  background: rgba(179, 106, 94, 0.08);
  border: 1px solid rgba(179, 106, 94, 0.25);
}

/* Folder browser modal (project editor's "Browse…" flow, always on the
   project's chosen server — no local option): current path readout +
   clickable listing, reuses .option-list/.option-item for the directory
   rows. */
.remote-browse-path {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  margin-bottom: 10px;
  word-break: break-all;
}
.remote-browse-list {
  margin-top: 0;
  max-height: 280px;
}
.remote-browse-up { color: var(--text-muted); font-style: italic; }

/* Read-only path field in the project editor (populated only via the
   Browse… folder picker, never typed directly) — same muted treatment as a
   disabled input so it visually reads as "not directly editable" without
   fully greying out like :disabled would. */
.field input[readonly] { color: var(--text-muted); cursor: default; }

/* ==========================================================================
   Mobile experience (phone widths only — max-width: 768px) — take 2
   ==========================================================================
   A genuinely minimal, purpose-built mobile UI modeled on the ChatGPT/Claude
   mobile apps: a flat session list, a "start a new chat" composer, and a
   chat-transcript detail view with a bottom message bar. This intentionally
   throws out the first pass's approach of mirroring the desktop drawer's
   dense meta-grid/resume-panel layout onto a narrow screen ("desktop wearing
   a mobile costume") — every mobile-only surface below is designed for the
   screen it's on: single-column flow, no fixed-width/ported elements, real
   mobile padding scale, metadata tucked behind disclosures instead of shown
   inline by default. Desktop (`.layout`, `.detail-drawer`, and all
   non-mobile-prefixed functions) is completely untouched above the
   breakpoint. Below it, `.mobile-runs` takes over; `#tab-schedules-btn`'s
   Schedules view still uses the existing `.layout-single` table — mobile
   scope for this pass is the Runs/session experience only. See
   docs/webui-templates-index.md. ------------------------------------------- */

.mobile-runs { display: none; }

@media (max-width: 768px) {
  /* Hide desktop-only chrome */
  #runs-view.layout { display: none; }
  .detail-drawer { display: none !important; }

  /* Prevent any horizontal scroll/pinch-zoom surface anywhere on phone.
     Also lock BODY itself from ever scrolling vertically: mobile browsers'
     dynamic address-bar chrome makes a raw `100vh` box (below) occasionally
     taller than the *actual* visible viewport, which — if body were allowed
     to scroll — makes the page itself the scroller instead of the intended
     inner `.mobile-runs-list` container. That silently breaks the
     `#mobile-runs-list` `scroll` listener infinite-scroll relies on (see
     initMobileRunsUI() in app.js): scroll events would fire on `window`,
     never on the inner list, so `maybeLoadMoreMobileRuns()` never runs.
     Locking body's overflow forces `.mobile-runs-list` to be the only
     scroller, guaranteeing scroll events land where the listener expects. */
  html, body { overflow-x: hidden; overflow-y: hidden; }

  .mobile-runs {
    display: flex;
    flex-direction: column;
    /* 100dvh (dynamic viewport height) tracks the browser's *actual*
       visible area as address-bar chrome shows/hides, unlike 100vh which
       is pinned to the largest possible viewport. Falls back to 100vh on
       older browsers that don't support dvh (the height: rule above still
       applies as the fallback since unsupported values are ignored). */
    height: calc(100vh - 106px); /* topbar + view-tabs */
    height: calc(100dvh - 106px); /* topbar + view-tabs */
  }
  .mobile-runs.hidden { display: none; }

  .mobile-screen {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }
  .mobile-screen.hidden { display: none; }
}

/* ---------- Mobile: session list screen ---------- */
.mobile-list-toolbar { padding: 10px 16px 8px; flex-shrink: 0; }
/* font-size: 16px prevents iOS Safari's automatic zoom-on-focus for text
   inputs <16px — a real-device requirement, not just a desktop nicety. A
   quieter, borderless-until-focus field (vs. the desktop search input's
   boxed style) to match the lighter list-screen chrome. */
.mobile-search-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 9px 14px;
  color: var(--text);
  font-size: 16px;
}
.mobile-search-input::placeholder { color: var(--text-faint); }
.mobile-search-input:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }

/* Persistent "New session" floating action button — opens
   #mobile-new-run-screen. Positioned relative to .mobile-runs-list-screen
   (a .mobile-screen, position: static by default) so it stays anchored to
   the bottom-right of the visible list area. */
#mobile-runs-list-screen { position: relative; }
.mobile-new-run-fab {
  position: absolute;
  right: 16px;
  bottom: 16px;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: #0E0E0E;
  font-size: 26px;
  font-weight: 600;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(0,0,0,0.4);
  z-index: 5;
}
.mobile-new-run-fab:active { background: var(--accent-hover); }

.mobile-runs-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  padding-bottom: 72px; /* keeps the last row clear of the FAB */
}
.mobile-runs-list .empty-row { padding: 40px 16px; }

/* Infinite-scroll "loading more" sentinel row, appended at the end of the
   list while the next backend page is in flight — muted, centered, no
   spinner (matches the app's general no-spinner-flicker convention; the
   text itself is enough feedback for a lightweight inline append). */
.mobile-runs-loading-more {
  padding: 16px;
  text-align: center;
  color: var(--text-faint);
  font-size: 12.5px;
}

/* Flat list row (not a boxed card) — closer to a native chat-app
   conversation list: a status dot, a title + small muted project line, and
   a trailing relative time, separated by a hairline rather than each row
   being its own bordered card. Tap target spans the full row, min 44px
   touch height per basic mobile a11y guidance. */
.mobile-run-row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 56px;
  max-width: 100%;
  min-width: 0;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 10px 16px;
  text-align: left;
  cursor: pointer;
  box-sizing: border-box;
  -webkit-tap-highlight-color: rgba(232, 124, 46, 0.15);
}
.mobile-run-row:active { background: var(--bg-elevated); }

/* Small glanceable status dot — replaces the desktop's verbose text badge
   on the list row; always paired with a title tooltip (see
   mobileStatusDot() in app.js) so color is never the sole signal. */
.mobile-status-dot {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--status-orphaned);
}
.mobile-status-dot-running { background: var(--status-running); animation: pulse 1.2s ease-in-out infinite; }
.mobile-status-dot-completed { background: var(--status-completed); }
.mobile-status-dot-failed { background: var(--status-failed); }
.mobile-status-dot-stopped { background: var(--status-stopped); }
.mobile-status-dot-orphaned { background: var(--status-orphaned); }

.mobile-run-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.mobile-run-row-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Project is plain inline metadata text, not a badge/pill — deliberately
   not a navigation/folder affordance (see brandbook). */
.mobile-run-row-meta {
  font-size: 12px;
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mobile-run-row-time {
  flex-shrink: 0;
  font-size: 11.5px;
  color: var(--text-faint);
  white-space: nowrap;
}

/* ---------- Mobile: chat-style session detail screen ---------- */
.mobile-chat-header {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 8px 8px 4px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.mobile-back-btn {
  background: transparent;
  border: none;
  color: var(--accent);
  font-size: 24px;
  font-weight: 600;
  line-height: 1;
  padding: 6px 8px;
  cursor: pointer;
  flex-shrink: 0;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mobile-back-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.mobile-chat-title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.mobile-chat-project {
  font-size: 13.5px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mobile-chat-status {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--text-faint);
}
.mobile-chat-status-text { text-transform: capitalize; }

/* Info disclosure button — tucks metadata away by default; a monitoring
   glance is one tap away, not laid out inline on every visit. */
.mobile-info-btn {
  background: transparent;
  border: none;
  color: var(--text-faint);
  font-size: 18px;
  line-height: 1;
  padding: 6px 8px;
  cursor: pointer;
  flex-shrink: 0;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
}
.mobile-info-btn[aria-expanded="true"] { color: var(--accent-hover); background: var(--accent-dim); }
.mobile-info-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Completion voice-line replay button, mobile chat header — same
   .completion-audio-btn base as the desktop table, sized up to this
   header's 44px touch targets (.mobile-info-btn/.mobile-back-btn). */
.mobile-completion-audio-btn {
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-sm);
}
/* Collapsed metadata strip — a small expandable panel, not an always-on
   meta grid. Same field-label/value language as the desktop drawer's meta
   grid (reused tokens) but rendered as stacked rows in a compact strip. */
.mobile-info-panel {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 16px;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--border);
  max-height: 40vh;
  overflow-y: auto;
  box-sizing: border-box;
}
.mobile-info-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 12px;
  min-width: 0;
}
.mobile-info-row .meta-value {
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.mobile-info-row-full {
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}
.mobile-info-row-full .meta-value,
.mobile-detail-prompt { text-align: left; white-space: pre-wrap; overflow-wrap: anywhere; }
.mobile-detail-prompt { max-height: 160px; max-width: 100%; }

/* Primary content: the transcript fills the remaining space, like a chat
   thread — reuses .log-viewer/.log-line styling verbatim (same rendering as
   the desktop drawer's log, just a second DOM mirror target), but flexed to
   fill the screen instead of capped at a max-height. */
.mobile-log-viewer {
  flex: 1;
  min-height: 0;
  max-width: 100%;
  max-height: none;
  border: none;
  border-radius: 0;
  background: var(--bg);
  padding: 12px 16px;
}
.mobile-summary-wrap { flex-shrink: 0; padding: 0 16px 8px; }
.mobile-detail-summary { max-width: 100%; overflow-wrap: anywhere; }
.mobile-log-status-row {
  flex-shrink: 0;
  padding: 0 16px 6px;
  text-align: right;
}

/* Bottom composer bar — fixed to the bottom of the detail screen like a
   chat app's message box, shown only when a follow-up is actually possible
   (hidden entirely otherwise, not just disabled/greyed). .mobile-composer-wrap
   is the fixed-bottom container (border/background/safe-area padding);
   .mobile-composer inside it is just the input row, so a pending-attachment
   chip strip (.attach-chips-mobile) can render above the input row without
   sitting outside the bar's background/border. */
.mobile-composer-wrap {
  flex-shrink: 0;
  padding: 8px 12px;
  padding-bottom: max(8px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
}
.mobile-composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}
.mobile-composer textarea {
  flex: 1;
  min-height: 40px;
  max-height: 130px;
  resize: none;
  box-sizing: border-box;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 9px 14px;
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px; /* prevents iOS zoom-on-focus */
  line-height: 1.4;
}
.mobile-composer textarea:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }
.mobile-composer-send {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: #0E0E0E;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mobile-composer-send:hover:not(:disabled) { background: var(--accent-hover); }
.mobile-composer-send:disabled { opacity: 0.45; cursor: not-allowed; }
.mobile-composer-send:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Attach-a-file affordance, mobile — a plain icon button (no boxed
   .link-btn chrome, unlike desktop) sized as a proper touch target (40px,
   matching .mobile-composer-send) so it feels native inside a chat-style
   composer bar rather than a ported desktop control. Reused in both the
   new-session message field's header and the detail composer bar. */
.mobile-attach-btn {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mobile-attach-btn:hover { background: var(--bg-elevated-2); color: var(--text); }
.mobile-attach-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.mobile-composer-attach { width: 36px; height: 36px; }
.mobile-field-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.mobile-field-header label { margin: 0; }

/* Compact variant of the shared .attach-chips/.attach-chip pattern (see
   docs/brandbook.md "Attach-to-prompt chip") — same neutral pill language,
   just slightly larger touch target for the remove button and full-width
   wrapping to fit the narrower phone composer. */
.attach-chips-mobile { margin: 6px 0; }
.attach-chips-mobile .attach-chip { font-size: 12.5px; padding: 5px 6px 5px 12px; max-width: 70vw; }
.attach-chips-mobile .attach-chip-remove { font-size: 16px; padding: 2px 4px; }

/* ---------- Mobile: new-session screen ("start a new chat") ---------------
   Project + a single message field are the primary, unavoidable form —
   preset/model live behind the Options disclosure below, secondary to the
   plain-message flow, per the "minimal, not comprehensive" brief. */
.mobile-new-run-body {
  flex: 1;
  min-height: 0;
  min-width: 0;
  max-width: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  box-sizing: border-box;
}
.mobile-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.mobile-field label { font-size: 12px; color: var(--text-muted); font-weight: 600; }
.mobile-field input[type="text"],
.mobile-field select {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--text);
  font-size: 16px; /* prevents iOS zoom-on-focus */
}
.mobile-field input:focus-visible, .mobile-field select:focus-visible {
  outline: 2px solid var(--accent);
  border-color: var(--accent);
}
.mobile-prompt-field textarea {
  width: 100%;
  box-sizing: border-box;
  resize: vertical;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px;
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 16px; /* prevents iOS zoom-on-focus */
  line-height: 1.4;
}
.mobile-prompt-field textarea:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }

.mobile-preset-selected-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}
.mobile-preset-selected-row .selected-chip { margin-top: 0; }

/* Options disclosure — collapsed by default; preset/model selection are
   secondary affordances reached by tapping this, never shown as a full
   desktop-style form up front. */
.mobile-options-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.mobile-options-toggle[aria-expanded="true"] { color: var(--text); border-color: var(--border-strong); }
.mobile-options-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
#mobile-options-chevron { color: var(--text-faint); font-size: 15px; }
.mobile-options-panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: -4px;
  padding: 12px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
.mobile-options-panel .option-list { max-height: 140px; }

/* ---------- Toast ---------- */
.toast {
  position: fixed;
  bottom: 20px;
  right: 20px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--status-failed);
  color: var(--text);
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  max-width: 360px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  z-index: 100;
}
.toast.info { border-color: var(--accent); }
.toast.success { border-color: var(--status-completed); }

/* ---------- Files (attachments) view ----------
   New "Files" tab (view-tab pattern, .files-view = .layout-single like
   Schedules). Reuses .runs-table / .table-wrap / .runs-panel-header /
   .schedules-header-actions verbatim rather than introducing a parallel
   table component — see docs/webui-templates-index.md. */
.files-table td.prompt-cell { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Attach-to-prompt chips ----------
   Shared by the launch panel's custom-prompt field and the detail drawer's
   follow-up/resume field (see docs/webui-templates-index.md "Attach to
   prompt"). A row of small removable chips rendered directly above the
   textarea, one per pending (uploaded but not-yet-submitted) attachment —
   same "small pill + inline action" language as .active-filter-pill, but
   neutral (not accent-tinted) since these aren't filters. */
.attach-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0;
}
.attach-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 220px;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 3px 6px 3px 10px;
  font-size: 11.5px;
  color: var(--text-muted);
}
.attach-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 140px;
}
.attach-chip-size { color: var(--text-faint); white-space: nowrap; }
.attach-chip-remove {
  background: none;
  border: none;
  color: var(--text-faint);
  cursor: pointer;
  padding: 0 2px;
  font-size: 13px;
  line-height: 1;
}
.attach-chip-remove:hover { color: var(--status-failed); }
.attach-chip-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- Logs tab (#logs-view) ----------
   Command log of shell/Bash invocations executed by Claude during runs
   (GET /api/logs/commands). Follows the same shell as the Files view
   (.runs-panel-header + .table-wrap + .runs-table + .runs-filter-bar) rather
   than a new page layout — see docs/webui-templates-index.md "Logs tab". */
.logs-table td.log-command-cell {
  max-width: 420px;
  font-family: var(--font-mono);
  font-size: 12px;
  cursor: pointer;
}
/* Collapsed (default): single-line ellipsis truncation, same convention as
   .prompt-cell elsewhere. Expanded (.log-command-expanded, toggled on click):
   wraps and shows the full command — no modal, since this is meant to be a
   quick glance-and-collapse affordance, not a separate view. */
.logs-table td.log-command-cell .log-command-text {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.logs-table td.log-command-cell.log-command-expanded .log-command-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.logs-table td.log-command-cell .log-command-hint {
  display: block;
  margin-top: 2px;
  font-family: var(--font-ui);
  font-size: 10px;
  color: var(--text-faint);
}
.logs-table td.log-description-cell {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: 12.5px;
}
.logs-table td.log-flags-cell { white-space: nowrap; }
/* Flag badges: reuses the neutral .session-badge pill shape (not .badge,
   since these aren't run-status states) for "subagent"/"background", and the
   existing .badge-failed status-color pill for the error flag — same
   "color is never the sole indicator" rule (text label always accompanies
   the color). */
.log-flag-badge {
  display: inline-block;
  margin-right: 4px;
  font-size: 10px;
}
.log-flag-badge:last-child { margin-right: 0; }
.log-flag-error { color: var(--status-failed); background: rgba(179, 106, 94, 0.12); border-color: rgba(179, 106, 94, 0.25); }
/* "launch" flag: an accent-tinted variant of the same pill language, marking
   rows whose command is the actual `claude ...` CLI invocation the
   orchestrator ran for a prompt submission (source: "launch"), distinct from
   plain shell/Bash tool-use commands (source: "bash"). Accent (not a status
   color) since this isn't a run outcome — see docs/webui-templates-index.md
   "Logs tab — source filter". */
.log-flag-launch { color: var(--accent-hover); background: var(--accent-dim); border-color: var(--accent); }

/* Source filter chip group: reuses .status-chip/.date-chip's single-select
   pill shape verbatim (see "Date preset chip group" in docs/brandbook.md) —
   All / Claude launches / Shell commands. */
#logs-source-chip-group { margin-right: 4px; }

/* ---------- Browse tab (#browse-view) ----------
   Per-project file browser + editor. Same .panel/.runs-panel-header/
   .runs-filter-bar shell as Files/Logs/Schedules, but full-width
   (.layout-single, no left launch-panel column) since the tree+editor pair
   needs the whole viewport. See docs/webui-templates-index.md "Browse tab". */
.browse-layout { padding: 16px; }
.browse-panel { display: flex; flex-direction: column; min-height: 0; }
.browse-body {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: 12px;
  margin-top: 12px;
  flex: 1;
  min-height: 60vh;
}
@media (max-width: 900px) {
  .browse-body { grid-template-columns: 1fr; min-height: 0; }
  .browse-tree-col { max-height: 260px; }
}

.browse-tree-col {
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow-y: auto;
}
.browse-tree { padding: 6px 0; }
.browse-tree-list { list-style: none; margin: 0; padding: 0; }
.browse-tree-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  font-size: 12.5px;
  cursor: pointer;
  white-space: nowrap;
}
.browse-tree-dir:hover, .browse-tree-file:hover { background: var(--accent-dim); }
.browse-tree-file-active { background: var(--accent-dim); color: var(--accent-hover); }
.browse-tree-caret { width: 12px; color: var(--text-faint); flex-shrink: 0; }
.browse-tree-icon { flex-shrink: 0; }
.browse-tree-name { overflow: hidden; text-overflow: ellipsis; }
.browse-tree-file-size { margin-left: auto; padding-left: 8px; color: var(--text-faint); font-size: 11px; flex-shrink: 0; }
.browse-tree-loading { font-size: 11.5px; }

.browse-editor-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.browse-editor-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.browse-current-path {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 45%;
}
.browse-editor-toolbar-spacer { flex: 1; }
.browse-truncated-note, .browse-binary-note { margin: 0; white-space: nowrap; }
.browse-conflict-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: rgba(176, 138, 78, 0.12);
  border-bottom: 1px solid var(--status-stopped);
  color: var(--status-stopped);
  font-size: 12.5px;
}
.browse-editor-mount {
  flex: 1;
  min-height: 0;
  overflow: auto;
  font-family: var(--font-mono);
  font-size: 12.5px;
}
.browse-editor-mount .empty-row { padding: 16px; color: var(--text-faint); font-family: var(--font-ui); }
/* Shown when a file fails to load (non-binary error) or the lazy-loaded
   CodeMirror module graph itself fails to import — reuses the same tinted-
   box + semantic-red language as .cron-preview-error/.auth-error rather
   than the neutral .empty-row treatment, so a real failure reads visibly
   differently from the normal "nothing selected yet" placeholder. */
.browse-editor-mount .browse-editor-error {
  color: var(--status-failed);
  background: rgba(179, 106, 94, 0.1);
  border: 1px solid var(--status-failed);
  border-radius: var(--radius-sm);
  margin: 12px;
  text-align: left;
  white-space: pre-wrap;
  word-break: break-word;
}
/* CodeMirror mounts a .cm-editor element filling this container — sized to
   fill the pane, themed via the imported oneDark extension rather than
   overriding CodeMirror's internal classes here (keeps this file decoupled
   from CodeMirror's own DOM structure). */
.browse-editor-mount .cm-editor { height: 100%; }
.browse-editor-mount .cm-scroller { font-family: var(--font-mono); }

/* Media preview (image/video/audio/pdf) — rendered instead of CodeMirror,
   never loads the editor for these files. A subtle dark checkerboard behind
   images (transparent PNGs/SVGs read clearly against it) and a plain
   --bg-elevated neutral fill for video/audio/pdf, both centered within the
   pane. See docs/webui-templates-index.md "Browse tab — Media preview". */
.browse-media-pane {
  height: 100%;
  min-height: 320px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background-color: var(--bg-elevated);
  background-image:
    linear-gradient(45deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%),
    linear-gradient(-45deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.04) 75%),
    linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.04) 75%);
  background-size: 20px 20px;
  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.browse-media-pane-pdf { padding: 0; }
.browse-media-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.browse-media-video {
  max-width: 100%;
  max-height: 100%;
}
.browse-media-audio { width: 100%; max-width: 480px; }
.browse-media-pdf {
  width: 100%;
  height: 100%;
  min-height: 60vh;
  border: none;
}
.browse-media-error { margin: 12px; }

/* ---------- Console tab (#console-view) ----------
   Interactive terminal over a WebSocket to one of the user's registered
   servers (no local shell target), rendered with xterm.js (CDN, pinned —
   see docs/webui-templates-index.md "Console tab" for the CDN-exception
   rationale). Same .panel/.runs-panel-header/.runs-filter-bar shell as
   Files/Browse
   (.layout-single, full width) — only the terminal surface itself and its
   connect/disconnect controls are new. */
.console-panel { display: flex; flex-direction: column; min-height: 0; }

/* Connection status pill, reuses the same dot-prefixed pill language as
   .status-chip/.badge (color + text together, never color alone). */
.console-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--bg-elevated-2);
}
.console-status-disconnected { color: var(--text-faint); }
.console-status-connecting { color: var(--status-stopped); }
.console-status-connected { color: var(--status-completed); }
.console-status-error { color: var(--status-failed); border-color: rgba(179, 106, 94, 0.35); }

/* Target picker row reuses .runs-filter-bar verbatim (search-bar shell) —
   here it's just a <select> + Connect/Disconnect buttons instead of search
   inputs/chips, still the same flex-wrap row with the bottom rule. */
#console-target-select { max-width: 260px; }

/* Terminal surface — a fixed-height dark panel (xterm.js paints its own
   background via the theme passed at construction, matched to
   --bg/--bg-elevated-2 below so the surrounding chrome and the terminal
   itself read as one continuous dark surface, not a mismatched inset). Kept
   alive across tab switches (never destroyed on setView() away from
   Console) — only Disconnect or a server-initiated close tears down the
   WebSocket/xterm instance, see connectConsole()/disconnectConsole() in
   app.js. */
.console-terminal-wrap {
  position: relative;
  margin-top: 12px;
  flex: 1;
  min-height: 60vh;
  background: #0a0a0a;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.console-terminal {
  height: 100%;
  width: 100%;
  padding: 8px;
  box-sizing: border-box;
}
/* xterm.js mounts its own .xterm element filling the container — sized via
   the fit addon (see fitConsoleTerminal() in app.js) rather than overriding
   xterm's internal DOM here. */
.console-terminal .xterm { height: 100%; }

/* Idle/disconnected/ended overlay — centered message + optional Reconnect
   button, shown whenever there's no live connection so the empty dark pane
   never looks broken or blank. Hidden entirely (not just faded) while
   connected so it never intercepts terminal clicks/focus. */
.console-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  text-align: center;
  background: rgba(10, 10, 10, 0.88);
  color: var(--text-muted);
  font-size: 13px;
}
.console-overlay.hidden { display: none; }
#console-overlay-text { margin: 0; max-width: 42ch; }

/* Subtle shortcuts hint under the terminal — reuses .field-hint's muted
   text treatment verbatim, just re-anchored with a bit more top margin
   since it sits directly under the terminal surface, not a form field. */
.console-hint { margin-top: 8px; text-align: center; }
