/* Tailwind (generated by `npm run build:css`) handles layout/typography. This
   file holds WebUI styles that aren't expressible with the shipped Tailwind
   snapshot — both live SSE/JS-driven pieces (status chips, spinners, live
   age / click-to-copy IP cells) and static component styling (modals, filter
   pills, row hover). */

/* App-wide row hover: every table body row gets a faint cyan tint and inset
   ring on hover so it reads as interactive. Header rows (thead) are excluded. */
tbody tr {
  transition: background-color 0.12s ease, box-shadow 0.12s ease;
}
tbody tr:hover {
  background-color: rgba(34, 211, 238, 0.06);
  box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.18);
}

/* Native <select> arrow. The browser draws its dropdown indicator inside the
   element's right padding; with the Tailwind px-2 / px-3 the selects carry, the
   arrow lands hard against the right border and reads as clipped (dashboard
   Sessions toolbar, New-session modal, project settings/modal, settings page,
   question assign-role). Replace the OS arrow with our own chevron — one shared
   rule so a new UI select inherits it automatically, and it looks identical
   across browsers. The chevron is zinc-400 (#a1a1aa), visible on the dark
   bg-zinc-950 fields, and sits ~0.6rem inside the right border, mirroring the
   left text padding; padding-right reserves room so the text never runs under it.
   Scoped to class-bearing selects (every UI select has a Tailwind class) so the
   selector outranks the px-* utility and its padding-right override actually wins
   — a bare element rule would lose to the single-class px-* on specificity. */
select[class] {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 0.75rem;
  padding-right: 2rem;
}

.output {
  background: #09090b;
  border: 1px solid #3f3f46;
  border-radius: 8px;
  padding: 1rem;
  height: 26rem;
  overflow: auto;
}

/* "Waiting for output" overlay, centered over the empty output pane until the
   first content streams in (transcript.js toggles [hidden]; sse.js keeps its
   message in sync with the live session state). Positioned here rather than with
   Tailwind utilities because the shipped tailwind.css is a fixed snapshot that
   doesn't include absolute/inset-0. */
.output-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0 2rem;
  text-align: center;
  pointer-events: none;
}
/* display:flex above would otherwise beat the base [hidden]{display:none}. */
.output-empty[hidden] { display: none; }

/* Toolbar above the output pane: section heading on the left, raw/rendered
   toggle on the right. */
.output-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}

/* Rendered Markdown (default). Untrusted output is sanitized in transcript.js
   before it reaches innerHTML. */
.output.markdown {
  font: 14px/1.6 ui-sans-serif, system-ui, sans-serif;
  color: #e4e4e7;
}
.output.markdown h1, .output.markdown h2, .output.markdown h3 { margin: 0.8em 0 0.4em; font-weight: 600; line-height: 1.3; }
.output.markdown p { margin: 0.5em 0; }
.output.markdown ul, .output.markdown ol { margin: 0.5em 0; padding-left: 1.5em; }
.output.markdown a { color: #22d3ee; text-decoration: underline; }
.output.markdown code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }
.output.markdown pre { background: #0b1020; padding: 0.75rem; border-radius: 6px; overflow: auto; }
.output.markdown pre code { color: #86efac; }

/* Rendered-transcript blockquote — generic markdown blockquotes in agent
   output. The shipped Tailwind snapshot has no blockquote styling, so give it
   a left accent bar and a faint fill to read as a distinct inline note rather
   than plain text. */
.output.markdown blockquote {
  margin: 0.6em 0;
  padding: 0.4rem 0.75rem;
  border-left: 3px solid #22d3ee;
  background: rgba(34, 211, 238, 0.06);
  border-radius: 0 6px 6px 0;
  color: #cffafe;
}
.output.markdown blockquote p { margin: 0; }

/* Inline session lifecycle markers (session-markers.ts). A full-width top rule
   fences the marker off from adjacent agent output; a per-type colored left bar
   + faint fill code the transition; the time sits right-aligned. */
.output.markdown .evt {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0.6em 0;
  padding: 0.35rem 0.6rem;
  border-top: 1px solid #3f3f46;      /* the horizontal line */
  border-left: 3px solid #52525b;     /* accent bar, recolored per type below */
  background: rgba(255, 255, 255, 0.02);
  border-radius: 0 6px 6px 0;
  font-size: 0.85rem;
}
.output.markdown .evt-ico { flex: 0 0 auto; }
.output.markdown .evt-msg { color: #e4e4e7; }
.output.markdown .evt-time {
  margin-left: auto;                  /* right-align */
  color: #a1a1aa;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 0.8rem;
}
.output.markdown .evt--suspended    { border-left-color: #f59e0b; background: rgba(245, 158, 11, 0.06); }
.output.markdown .evt--resumed      { border-left-color: #8b5cf6; background: rgba(139, 92, 246, 0.06); }
.output.markdown .evt--worker-ready { border-left-color: #22d3ee; background: rgba(34, 211, 238, 0.05); }
.output.markdown .evt--complete     { border-left-color: #22c55e; background: rgba(34, 197, 94, 0.06); }
.output.markdown .evt--error,
.output.markdown .evt--killed       { border-left-color: #ef4444; background: rgba(239, 68, 68, 0.06); }
.output.markdown .evt--answered     { border-left-color: #14b8a6; background: rgba(20, 184, 166, 0.06); }

/* Raw mode: the classic green monospace terminal, literal text (set as
   textContent, so no Markdown). Declared last so it overrides .markdown for the
   shared props when the pane carries both classes. */
.output.raw {
  font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #86efac;
  white-space: pre-wrap;
}

/* Tone tokens (spec 2026-09-25 §1) — the only status colours in the UI. */
:root {
  --tone-act: #f59e0b;     /* needs you — the only FILLED chip */
  --tone-act-fg: #1c1102;
  --tone-work: #38bdf8;    /* in progress (+ spinner when data-spin) */
  --tone-pause: #818cf8;   /* paused / nothing to answer */
  --tone-fail: #f87171;    /* failed / killed */
  --tone-done: #71717a;    /* finished — nothing left to do */
  /* Sixth tone, TEXT ONLY (never a chip): a session row's positive outcome note —
     a gate that passed, a clean review, all findings resolved. A muted green tuned
     to sit next to --tone-work/--tone-fail. The UI is dark-only; contrast on the
     dark row background (#09090b) is ~11:1, well past 4.5:1. The five chip tones
     above are unchanged. */
  --tone-ok: #4ade80;
  /* Statistics charts (#847). public/stats.js reads these via getComputedStyle so the
     ECharts chrome (axis text, grid split-lines, tooltip) follows the tone and re-themes
     on a light/dark switch; the series colours are fixed categorical palette hexes in
     stats.js (one order for every chart), not themed. Dark-only today, but reading tokens
     keeps a future `.light` override working with no JS change. */
  --chart-text: #a1a1aa;        /* zinc-400 — axis labels, legend */
  --chart-muted: #71717a;       /* zinc-500 — secondary text, available-slots line */
  --chart-grid: #27272a;        /* zinc-800 — split lines */
  --chart-axis: #3f3f46;        /* zinc-700 — axis lines */
  --chart-tooltip-bg: #18181b;  /* zinc-900 */
  --chart-tooltip-fg: #e4e4e7;  /* zinc-200 */
}

/* Status chip. Markup comes from tones.ts chipHtml() (server) or sse.js
   cpChipHtml()/paintChip() (live). data-state keeps the raw state for readers;
   colour comes ONLY from data-tone, the spinner ONLY from data-spin. */
.chip {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  line-height: 1;
  font-weight: 600;
  font-variant: small-caps;
  letter-spacing: 0.02em;
  border: 1px solid currentColor;
  color: var(--tone-done); /* no tone (labels, unknown) reads as neutral */
  white-space: nowrap;
}
/* The Work header deploy chip (#1598) carries a long phrase
   ("deploying cp+worker · running setup (worker) · since HH:MM" + a ticking elapsed/countdown),
   which as a single nowrap pill overflows the status line at phone widths (#1602). Let this one
   chip wrap instead; `overflow-wrap: anywhere` also shrinks its flex min-content so the status
   line can never be forced wider than the viewport. Other chips stay nowrap. */
.chip[data-deploy] { white-space: normal; overflow-wrap: anywhere; }
.chip[data-tone="act"] { background: var(--tone-act); border-color: var(--tone-act); color: var(--tone-act-fg); }
.chip[data-tone="work"] { color: var(--tone-work); }
.chip[data-tone="pause"] { color: var(--tone-pause); }
.chip[data-tone="fail"] { color: var(--tone-fail); }
.chip[data-tone="done"] { color: var(--tone-done); }
/* A busy slot's state chip, tinted with its project colour (#2153): --proj is set on the chip
   (projVarStyle, palette-only) so border + text read in the project hue over a faint tint of it,
   letting an operator see at a glance which project occupies which slot. The blue --tone-work
   fallback keeps a missing/off-palette colour exactly as before. The spinner (currentColor) tints
   with the text. */
.chip.slot-chip--proj[data-tone="work"] {
  color: var(--proj, var(--tone-work));
  border-color: var(--proj, var(--tone-work));
  background: color-mix(in srgb, var(--proj, transparent) 14%, transparent);
}

/* Why-waiting cell (Work "Up next" + Queue, #1515, #1797): SHORT status pills stacked vertically
   over an optional muted, truncating detail. A long reason (an "overlaps #1500 on
   src/…/state-machine.ts" path, a "#1778 parked (needs you: …) — PR #1787" note, or a "blocked by
   #a, #b, #c" list) is split so each pill holds only the kind + issue/PR and the free text drops to
   the muted line below — the wrapper caps the column so the Title column keeps its share instead of
   being squeezed by an ~800px pill. Belt-and-braces: every pill (never the detail) is capped at the
   column width with an ellipsis so no future reason can widen the table. */
.why-waiting { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; max-width: min(18rem, 100%); vertical-align: top; }
.why-waiting > :not(.why-detail) { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.why-detail {
  display: block;
  margin-top: 2px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.7rem;
  line-height: 1.25;
  color: var(--tone-done); /* zinc-500 — muted, secondary to the pill */
}

/* Inline spinner — Kill button during teardown, empty-output placeholder. */
.spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 9999px;
  animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.chip[data-spin]::after {
  content: "";
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-left: 0.4em;
  vertical-align: -0.05em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 9999px;
  animation: spin 0.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .chip[data-spin]::after, .spinner { animation: none; }
}

/* Slot byte-progress bar (image pull / best-effort install). Injected by the
   dashboard view and the live SSE rebuild — hence lives here, not tailwind.css. */
.slot-progress {
  display: inline-block; width: 7rem; height: 4px; margin-left: 0.5rem;
  vertical-align: middle; background: #3f3f46; border-radius: 9999px; overflow: hidden;
}
.slot-progress-bar { display: block; height: 100%; background: #fbbf24; transition: width 0.3s linear; }
.slot-progress-text { margin-left: 0.4rem; font-size: 0.75rem; color: #a1a1aa; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Reserve a stable width and use tabular figures so the live-ticking age never
   reflows the neighbouring columns. */
.age {
  display: inline-block;
  min-width: 5rem;
  font-variant-numeric: tabular-nums;
}

/* Teardown countdown ("suspends in 5m 3s" / "shutdown in …"). Its text changes
   length every tick (e.g. "1m 0s" → "59s"), which would otherwise reflow the
   Runtime/Age column each second. Reserve a min-width and use tabular figures so
   its footprint holds steady once shown. 9.5rem is sized against the widest string
   fmtAge (sse.js) can produce in bold text-xs — "shutdown in 23h 59m" (label + the
   hour/day form); if the label or fmtAge format changes, re-check this width.
   inline-block would beat the base [hidden]{display:none}, so restore that
   explicitly for the not-scheduled state. */
/* Suspend/shutdown countdown: grey, red only in its last 10 minutes
   (COUNTDOWN_WARN_MS; the class is set server-side and by sse.js tickReap). */
.reap-countdown {
  display: inline-block;
  min-width: 9.5rem;
  font-variant-numeric: tabular-nums;
  color: #71717a;
}
.reap-countdown--warn { color: var(--tone-fail); }
.reap-countdown[hidden] { display: none; }

/* Click-to-copy IP. Reserve the worst-case IPv4 width so the placeholder "—"
   swapping to a real address over SSE never reflows the table, and overlay the
   copy ✓ outside the flow so it can't widen the cell either. */
.ip {
  display: inline-block;
  min-width: 15ch; /* 255.255.255.255 */
  position: relative;
}
.ip[data-copy] { cursor: pointer; }
.ip[data-copy]:hover { color: #e4e4e7; }
.ip.copied::after {
  content: "✓";
  color: #4ade80;
  position: absolute;
  left: 100%;
  margin-left: 0.4ch;
}

/* Click-to-copy session id in the detail heading. Shares the .copied class with
   the IP cell (toggled by sse.js copyToClipboard) but its own feedback: a short
   cyan background flash on the id so the click is visible, plus a small
   "Copied ✓" badge just after it, both fading out over ~1.2 s. */
.copy-id {
  cursor: pointer;
  position: relative;
  border-radius: 3px;
  padding: 0 2px;
}
.copy-id:hover { background-color: rgba(34, 211, 238, .1); }
.copy-id.copied { animation: copy-id-flash 1.2s ease-out; }
@keyframes copy-id-flash {
  from { background-color: rgba(34, 211, 238, .18); }
  to { background-color: transparent; }
}
.copy-id.copied::after {
  content: "Copied ✓";
  color: #4ade80;
  font-size: 0.7em;
  font-weight: 600;
  position: absolute;
  left: 100%;
  bottom: 0.2em;
  margin-left: 0.6ch;
  white-space: nowrap;
  animation: copy-id-fade 1.2s ease-out forwards;
}
@keyframes copy-id-fade {
  0%, 60% { opacity: 1; }
  100% { opacity: 0; }
}


/* Absolute datetime cell — formatted in the viewer's locale by sse.js. Keep
   date and time on a single line (below md the time drops under the date, see
   the max-width:767px block) and use tabular figures so live-updating rows
   don't reflow. */
.datetime {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Sessions tone-filter pills (spec §4). Neutral buttons, NOT status chips: a
   tone dot names the filter, the count shows how many groups carry that tone.
   Toggled in sse.js via is-active; a ✓/✗ mark shows selection at a glance. */
.session-pill {
  display: inline-flex; align-items: center; gap: 0.375rem;
  border: 1px solid #3f3f46; border-radius: 9999px;
  padding: 0.125rem 0.625rem; font-size: 0.75rem; color: #a1a1aa;
  background: transparent; opacity: 0.55;
  transition: opacity 0.1s ease, background-color 0.1s ease;
}
.session-pill:hover { opacity: 0.85; }
.session-pill.is-active { opacity: 1; color: #f4f4f5; background: #27272a; }
.session-pill:focus-visible { outline: 2px solid #22d3ee; outline-offset: 2px; }
.session-pill .pill-count { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; color: #f4f4f5; }
.tone-dot { width: 0.5rem; height: 0.5rem; border-radius: 9999px; display: inline-block; background: var(--tone-done); }
.tone-dot[data-tone="act"] { background: var(--tone-act); }
.tone-dot[data-tone="work"] { background: var(--tone-work); }
.tone-dot[data-tone="pause"] { background: var(--tone-pause); }
.tone-dot[data-tone="fail"] { background: var(--tone-fail); }
.session-pill .pill-mark { font-size: 0.9em; font-weight: 700; }
.session-pill.is-active .pill-mark::before { content: "✓"; color: #4ade80; }
.session-pill:not(.is-active) .pill-mark::before { content: "✗"; color: #f87171; }

/* ── Shared hover-action overlay (#1980) ──────────────────────────────────────
   ONE pattern for every place that reveals buttons on hover — Servers (Kill /
   Decommission), Work "Up next" admin actions, and the per-session delete. The
   buttons float at the row's right edge on a gradient that is transparent on the
   left and fades to the row's OWN background at full opacity, so the row content
   fades out under the buttons with no hard edge and nothing showing through. Each
   host carries `.hover-host` (position:relative) and sets `--row-bg` to its real
   background in every state the overlay can be seen in, so the gradient blends in;
   the overlay reveals on host hover, on anything inside the host taking focus, or on
   a button inside the overlay itself taking focus (so it is keyboard-reachable).
   Replaces the three separate hover-reveal patterns that preceded it — the opaque
   Up-next panel, the server-head action grid track, and the session delete column. */
.hover-host { position: relative; }
.hover-actions {
  position: absolute;
  top: 50%;
  right: 14px;
  transform: translateY(-50%);
  z-index: 20;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.375rem;
  padding-left: 40px;
  background: linear-gradient(to right, transparent, var(--row-bg, #18181b) 40px);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
}
.hover-actions form { display: inline-flex; margin: 0; }
.hover-host:hover .hover-actions,
.hover-host:focus-within .hover-actions,
.hover-actions:focus-within { opacity: 1; pointer-events: auto; }
/* No hover (touch) OR phone widths (the existing stacked in-flow layouts): drop the
   overlay back into the normal flow with no gradient. On touch it is also always
   visible, since the hover that would reveal it can never happen. */
@media (hover: none), (max-width: 767px) {
  .hover-actions {
    position: static;
    transform: none;
    padding-left: 0;
    background: none;
    flex-wrap: wrap;
    white-space: normal;
  }
}
@media (hover: none) {
  .hover-actions { opacity: 1; pointer-events: auto; }
}

/* Per-session delete trash: rides the shared overlay. The row (.tr / .session-row) is
   the hover-host (set in session-row.ts) and sets `--row-bg` to its background in each
   state so the gradient blends into a hovered / current row; the trash sits flush to the
   row's right edge (no reserved delete column any more). The button keeps its Tailwind
   colour classes (text-zinc-500 / hover:text-red-400). */
.tr .hover-actions { right: 0; }
.tr { --row-bg: #141418; }                 /* the open-group backdrop a row sits on */
.tr:hover:not(.th) { --row-bg: #151f25; }  /* + the cyan hover wash */
.tr.is-current { --row-bg: #15272d; }      /* + the stronger current-row wash */

/* Settings → Claude Credentials row actions (#2508): there is NO Actions column — the actions are a
   small toolbar laid OVER the row's right edge, revealed only on row hover / `:focus-within` (so
   tabbing into the row shows it) or while the inline delete-confirm is open (`.cred-confirming`). A
   dark card with a left shadow so it covers the cells it overlaps; it fades in/out on opacity. The
   Status cell is its position:relative anchor. On touch / phone widths it drops into flow, always
   visible (the hover that would reveal it can never happen).

   The status toggle pill is rendered INSIDE the toolbar (#2583): `flex-direction: row-reverse` pins it
   (the first child) to the right edge and it is ALWAYS visible, while only the action buttons
   (`.cred-actions`) and the opaque card + left shadow fade in on hover/focus. Before, the pill was a
   sibling UNDERNEATH this opaque toolbar, so on hover the toolbar covered it and the mouse could not
   click the toggle (keyboard focus still reached it, which hid the regression). */
.cred-actions-cell { position: relative; }
.cred-toolbar {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  gap: 0.375rem;
  padding: 0 0.75rem 0 2.5rem;
  white-space: nowrap;
  transition: background 0.12s ease, box-shadow 0.12s ease;
}
.cred-toolbar form { display: inline-flex; margin: 0; }
.cred-toolbar .cred-actions { opacity: 0; pointer-events: none; transition: opacity 0.12s ease; }
.cred-row:hover .cred-toolbar,
.cred-row:focus-within .cred-toolbar,
.cred-row.cred-confirming .cred-toolbar {
  background: #1c1c22;
  box-shadow: -14px 0 14px -6px rgba(0, 0, 0, 0.65);
}
.cred-row:hover .cred-toolbar .cred-actions,
.cred-row:focus-within .cred-toolbar .cred-actions,
.cred-row.cred-confirming .cred-toolbar .cred-actions { opacity: 1; pointer-events: auto; }
@media (hover: none), (max-width: 767px) {
  .cred-toolbar {
    position: static;
    height: auto;
    flex-direction: row;
    padding: 0;
    background: none;
    box-shadow: none;
    flex-wrap: wrap;
    white-space: normal;
  }
  .cred-toolbar .cred-actions { opacity: 1; pointer-events: auto; }
}

/* In-place editable fields on the credentials table (#2509): the credential name and the stop-at
   value render as a plain-looking trigger button that reveals a dashed underline (and a text cursor)
   on hover, so the operator sees it is editable. Clicking swaps in the sibling edit form
   (credentials-edit.js flips `hidden`; the server renders BOTH variants — one renderer, #586). */
.cred-name-display,
.cred-stop-display,
.cred-weekly-stop-display,
.cred-plan-display,
.cred-api-credit-display {
  display: inline-block;
  max-width: 100%;
  padding: 0;
  text-align: left;
  background: none;
  border: 0;
  color: inherit;
  cursor: text;
}
.cred-name-display:hover,
.cred-stop-display:hover,
.cred-weekly-stop-display:hover,
.cred-plan-display:hover,
.cred-api-credit-display:hover {
  text-decoration: underline dashed;
  text-underline-offset: 3px;
}
.cred-inplace { margin: 0; }

/* The per-row "Credentials" line (#2509): opened under a row by the toolbar's Credentials action
   (credentials-edit.js flips the row's `hidden`), it holds the replace-token and usage-login forms in a
   4-column grid — label | input | Save | Remove. Each <form> is display:contents so its fields drop
   straight into the grid, which keeps the two lines' Save buttons aligned in one column (the token line
   fills the Remove column with an empty slot). The server renders it (one renderer, #586); the client
   only toggles `hidden`. Collapses to a single column on narrow / touch widths. */
.cred-creds-grid {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
  align-items: center;
  gap: 0.5rem 0.75rem;
}
.cred-creds-grid form { display: contents; margin: 0; }
@media (max-width: 767px) {
  .cred-creds-grid { grid-template-columns: 1fr; }
  .cred-creds-grid form { display: block; }
}

/* Work "Up next" row actions (#1858) — now the shared overlay (#1980). The old solid
   panel, border and drop-shadow are gone; the gradient fades over the Why-waiting / ETA
   cells instead. The row is the hover-host and sets `--row-bg` (the bg-zinc-900/40 table
   over the page, plus the cyan hover wash). `.work-next-actions` is kept as a marker only. */
.work-next-row { --row-bg: #0f0f11; }
.work-next-row:hover { --row-bg: #101b1e; }
@media (hover: none) {
  /* ≥44px tap targets on touch, matching the stacked-card Actions buttons. */
  .work-next-actions button { min-height: 44px; }
}

/* Count badges — small pills inside a nav link. Questions is red (needs you); the
 * queue count is a calmer slate so it reads as informational, not alarming. */
#nav-questions-badge,
#nav-queue-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.25rem;
  margin-left: 0.3rem;
  border-radius: 9999px;
  background: #ef4444;
  color: #fff;
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1;
  vertical-align: middle;
}
#nav-queue-badge { background: #3f3f46; color: #e4e4e7; }
#nav-questions-badge[hidden],
#nav-queue-badge[hidden] { display: none; }

/* The amber need-you COUNT badge INSIDE the Work link (#2751, replacing the old separate "N need you"
 * pill): a small amber count pill, like the red questions badge but in the "act" tone so it reads as
 * "needs a human" without the alarm-red of a failure. Hidden until notify.js fills it from
 * /work/needs/count. */
#nav-needyou-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.25rem;
  margin-left: 0.3rem;
  border-radius: 9999px;
  background: var(--tone-act);
  color: var(--tone-act-fg);
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1;
  vertical-align: middle;
}
#nav-needyou-badge[hidden] { display: none; }

/* The logo's live alert indicator (epic #1298): a red dot + count, shown only when ≥1 system-wide
 * alert is active. The wrapper is always present (so the morph can toggle it in place); with no
 * alerts it has data-alert-count="0" and renders nothing. */
.nav-alerts {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-left: 0.35rem;
}
.nav-alerts[data-alert-count="0"] { display: none; }
.nav-alert-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 9999px;
  background: #ef4444;
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.25);
}
.nav-alert-count {
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1;
  color: #fca5a5;
}

/* Credential-pool usage gauge in the header (epic #2183 part 4; two-line aligned table #2346).
 * The wrapper is a 5-column inline grid — in-use dot · label · bar · % · reset — and each credential
 * is ONE subgrid row (grid-column:1/-1 + grid-template-columns:subgrid) carrying data-active/title.
 * Because every row shares the parent's `auto` column tracks, the two stacked subscriptions line their
 * columns up like a table however different the label widths or whether a reset time is present (the
 * ragged independent-chip layout #2281 used is gone). The wrapper collapses when the pool is empty or
 * unwired (data-credential-count="0"), mirroring .nav-alerts; colour + text size ride on Tailwind
 * utilities in the markup. The `display:none` below outranks the `inline-grid` utility on specificity
 * (attribute selector), so an empty pool still collapses. */
.cred-gauge {
  grid-template-columns: auto auto auto auto auto;
}
.cred-gauge[data-credential-count="0"] { display: none; }
/* The whole gauge is a link to the Subscriptions page (#2761, moved from Settings → Claude Credentials
 * #2496), wrapped OUTSIDE the gauge's fragment root so the live morph is unaffected — for EVERY
 * logged-in user, since the Subscriptions page is readable by all (no admin gate). An empty/unwired
 * pool (data-credential-count="0") must collapse the link AND its trailing divider, else it leaves an
 * empty clickable box plus a dangling rule before the Statistics icon. :has() keys the link and its
 * trailing divider off the inner gauge's live data-credential-count, so they collapse together. */
.nav-cred-link:has(> .cred-gauge[data-credential-count="0"]) { display: none; }
.nav-cred-link:has(> .cred-gauge[data-credential-count="0"]) + .nav-cred-divider { display: none; }
/* One credential's row: a subgrid spanning the five explicit columns so its cells align to the shared
 * tracks (`1 / -1` ends at the explicit grid's last line, so the side box's implicit sixth column is
 * never spanned).
 * Equal padding on every row keeps the rows aligned to each other; a tiny inset stops the active
 * row's cyan tint crowding the text. The vertical padding (2 px top/bottom) + the wrapper's `gap-y-2`
 * (8 px) give the active row's cyan ring ~8 px of clearance so it reads as its own badge, not touching
 * the adjacent subscription row — #2350's 1 px + `gap-y-1` (4 px) still looked too close (#2385). */
.cred-gauge-row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  border-radius: 0.375rem;
  padding: 0.125rem 0.25rem;
}
/* The in-use dot's cell: empty on inactive rows, but still present so the label column aligns. */
.cred-gauge-dot { display: inline-flex; align-items: center; min-width: 0.375rem; }

/* The header question bell is a <details> summary now (#2751): it opens the dropdown menu. Drop the
 * native disclosure triangle so only the bell glyph + count badge show. Its box/colour come from the
 * Tailwind utilities in nav-bar.ts (w-10, min-h-44px, hover tones). */
#nav-questions-bell { list-style: none; cursor: pointer; }
#nav-questions-bell::-webkit-details-marker { display: none; }
#nav-questions-bell::marker { content: ""; }

/* The in-menu "Question notifications" toggle (#2751; moved out of the bell itself into its dropdown).
 * It turns cyan when active, so the operator sees popups are on at a glance. */
#nav-questions-toggle[data-bell-active="1"] { color: #22d3ee; }

/* Shared header dropdown menus (#2751): the user menu and the question bell are native
 * <details class="nav-menu">. The summary is the trigger; the panel is absolutely positioned under the
 * right edge so it never widens the one-row header. No client JS opens them — the browser does. */
.nav-menu { position: relative; }
.nav-menu > summary { list-style: none; }
.nav-menu > summary::-webkit-details-marker { display: none; }
.nav-menu > summary::marker { content: ""; }
.nav-menu[open] > summary { color: #e4e4e7; background: rgba(39, 39, 42, 0.6); }
.nav-menu-panel {
  position: absolute;
  top: calc(100% + 0.35rem);
  right: 0;
  z-index: 60;
  min-width: 12rem;
  max-width: 20rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.35rem;
  border-radius: 0.5rem;
  border: 1px solid #3f3f46;
  background: #18181b;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
}
.nav-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.4rem 0.6rem;
  border-radius: 0.375rem;
  font-size: 0.8rem;
  color: #d4d4d8;
  text-decoration: none;
  background: transparent;
  border: none;
  cursor: pointer;
}
.nav-menu-item:hover { background: rgba(39, 39, 42, 0.8); color: #fafafa; }
.nav-menu-item-btn { font: inherit; }
.nav-menu-sep { height: 1px; margin: 0.25rem 0; background: #27272a; }

/* The question bell's dropdown list (#2751): the open questions (title + project, newest first), then
 * an "All questions →" link. The title truncates to one line; the project rides muted beneath it. */
.nav-questions-panel { width: 20rem; }
.nav-questions-list { display: flex; flex-direction: column; gap: 0.1rem; }
.nav-questions-row {
  display: block;
  padding: 0.4rem 0.6rem;
  border-radius: 0.375rem;
  text-decoration: none;
}
.nav-questions-row:hover { background: rgba(39, 39, 42, 0.8); }
.nav-questions-text {
  display: block;
  font-size: 0.8rem;
  color: #e4e4e7;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav-questions-project { display: block; font-size: 0.68rem; color: #71717a; }
.nav-questions-empty { padding: 0.5rem 0.6rem; font-size: 0.8rem; color: #71717a; }
.nav-questions-all {
  display: block;
  margin-top: 0.15rem;
  padding: 0.4rem 0.6rem;
  border-top: 1px solid #27272a;
  font-size: 0.78rem;
  color: #22d3ee;
  text-decoration: none;
}
.nav-questions-all:hover { background: rgba(39, 39, 42, 0.8); }

/* The gauge's side box (Inbox/Work split PR 3): runway `⧗ 31h` and the `◆ +N` note, stacked in a
 * bordered box right of the rows. It takes the implicit sixth column and spans the shown rows (the
 * `grid-row: 1 / span N` rides inline on the element, as N is 1 or 2); box/colours are Tailwind utilities
 * in the markup. Omitted when empty, so a two-credential idle pool has no sixth column. */
.cred-gauge-side { grid-column: 6; }

/* Question arrival toast — fixed bottom-right, auto-dismisses after 8 s. */
.question-popup {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  z-index: 100;
  width: 18rem;
  background: #18181b;
  border: 1px solid #3f3f46;
  border-left: 3px solid #ef4444;
  border-radius: 0.5rem;
  padding: 0.875rem 1rem;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
  animation: popup-in 0.2s ease;
}
@keyframes popup-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to   { opacity: 1; transform: translateY(0); }
}
.question-popup-text {
  color: #e4e4e7;
  font-size: 0.8rem;
  margin: 0 1.25rem 0.5rem 0;
  line-height: 1.45;
  word-break: break-word;
}
.question-popup-link {
  color: #22d3ee;
  font-size: 0.8rem;
  text-decoration: none;
}
.question-popup-link:hover { text-decoration: underline; }
.question-popup-close {
  position: absolute;
  top: 0.4rem;
  right: 0.5rem;
  background: none;
  border: none;
  color: #71717a;
  font-size: 0.75rem;
  cursor: pointer;
  padding: 0.1rem 0.25rem;
  line-height: 1;
}
.question-popup-close:hover { color: #e4e4e7; }

/* "Updates available — refresh" banner (sse.js safeReload). Shown when a live
   update wants to reload the page but the operator is mid-edit; clicking it
   reloads. Bottom-centre so it doesn't cover the question popup (bottom-right). */
.cp-reload-banner {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 101;
  background: #0e7490;
  color: #ecfeff;
  border: 1px solid #22d3ee;
  border-radius: 999px;
  padding: 0.5rem 1.1rem;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
  animation: popup-in 0.2s ease;
}
.cp-reload-banner:hover { background: #155e75; }

/* New-session modal. The backdrop dims the page and centers the card; toggled
   open/closed via the [hidden] attribute (base reset sets display:none). */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 6rem 1rem 1rem;
  background: rgba(0, 0, 0, 0.6);
  /* Taller cards (the New-project form) must stay fully reachable on short
     viewports — scroll the backdrop rather than clipping the submit button. */
  overflow-y: auto;
}
/* display:flex above would otherwise override the base [hidden]{display:none}
   (equal specificity, later rule wins), leaving the modal open on load. */
.modal-backdrop[hidden] { display: none; }
.modal-card {
  width: 100%;
  max-width: 32rem;
}

/* Color palette picker (session/project) and the color dot shown in lists +
   detail. Colors are applied inline (never a Tailwind class — see the
   tailwind-coverage test), so these rules are shape/state only. */
.color-picker { display: flex; flex-wrap: wrap; gap: 6px; }
.color-swatch { position: relative; display: inline-flex; cursor: pointer; }
.color-swatch input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.color-swatch > span { display: block; width: 20px; height: 20px; border-radius: 9999px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.15); }
.color-swatch input:checked + span { box-shadow: 0 0 0 2px #0a0a0a, 0 0 0 4px #06b6d4; }
.color-swatch input:focus-visible + span { box-shadow: 0 0 0 2px #0a0a0a, 0 0 0 4px #38bdf8; }
.color-dot { display: inline-block; width: 10px; height: 10px; border-radius: 9999px; vertical-align: middle; }
.color-dot--none { background: #3f3f46; }
/* Spinning twin of .color-dot (Work → Running head): same 10px footprint, tinted by color. */
.color-spinner { width: 10px; height: 10px; vertical-align: middle; }
.color-spinner--none { color: #71717a; }
/* Amber pause glyph (Work → Running head) shown when no member is live — same 10px footprint. */
.work-issue-pause { display: inline-flex; width: 10px; height: 10px; vertical-align: middle; }
.work-issue-pause svg { display: block; width: 10px; height: 10px; fill: #fbbf24; }

/* Work → Running issue head (#1931): a deliberate two-line layout. A light accent tint in the
   project colour (--proj on the .work-issue block, blue fallback) sits under the header, and
   deepens on hover so a row lifts under the pointer. The tint mixes over an OPAQUE zinc-900 base
   (not `transparent`) so a sticky header (#1934) masks the rows that scroll under it. */
.work-issue-head { background: color-mix(in srgb, var(--proj, #3b82f6) 4%, #18181b); transition: background 0.12s ease; }
.work-issue-head:hover { background: color-mix(in srgb, var(--proj, #3b82f6) 9%, #18181b); }
/* The right-hand pills stack in ONE column (merge-train / batch label above the detail pill),
   right-aligned; the cost chip is top-aligned beside the column (the row is items-start). */
.work-issue-pillcol { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }

/* The live height of the two-row site header (#2281), published by public/nav.js from the REAL
   rendered <nav> (it wraps TALLER on a phone, so a hardcoded value would hide rows behind it). The
   Work sticky stack (below) pins relative to this. A static fallback keeps a no-JS / pre-nav.js
   paint roughly right (~row1 56px + row2 37px + border); nav.js corrects it to the exact height. */
:root { --site-header-h: 94px; }

/* Work → Running sticky header stack (#1934; the resizable/scroll box of #1935/#1949 was removed in
   #2297). PURE CSS — no scroll listeners, no position maths — so it survives a fragment morph. The
   Running wrapper is a plain bordered block that grows with its content: NO inner scroll, NO
   `overflow` (any value other than visible on an ancestor breaks sticky). The rows therefore pin
   relative to the PAGE: the shared label row sits just below the site header, each group header just
   below the labels, and the next group's header pushes it out. `--thead-h` is the label-row height. */
.work-running-box { --thead-h: 46px; }
/* The ONE shared column-label row pins just below the site header; opaque so cells pass under it.
   (`.group-tree`/`.tv` already border-top this row, so no seam.) */
.work-sticky-thead { position: sticky; top: var(--site-header-h); z-index: 10; background: #18181b; }
/* Each group header pins just below the labels and releases when its own issue block ends — the next
   block's header pushes it out. z below the labels so it slides beneath. */
.work-sticky-ghead { position: sticky; top: calc(var(--site-header-h) + var(--thead-h)); z-index: 9; }

/* Live-stream state in the footer (#2751, moved out of the header). The whole #sse-indicator is a
   dot + "Live" label; sse.js sets data-sse-state on it (live / reconnecting / closed) and the dot's
   colour follows. Only present on an authenticated page (the footer renders with a session). */
#sse-indicator {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  vertical-align: middle;
}
.footer-live-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 9999px;
  background: #52525b; /* zinc-600 — connecting/unknown */
  flex-shrink: 0;
  transition: background 0.4s ease;
}
#sse-indicator[data-sse-state="live"] .footer-live-dot { background: #4ade80; } /* green-400 */
#sse-indicator[data-sse-state="reconnecting"] .footer-live-dot {
  background: #fbbf24; /* amber-400 */
  animation: sse-pulse 1.2s ease-in-out infinite;
}
#sse-indicator[data-sse-state="closed"] .footer-live-dot {
  /* Terminal: the session is logged out, so sse.js is about to send the operator to /login (see
     probeThenReconnect) — a steady red, no pulse. A CLOSED-but-authenticated stream is recovered
     automatically and shows "reconnecting". */
  background: #f87171; /* red-400 */
}
@keyframes sse-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.2; }
}

/* The live-stream state ALWAYS rides the logo as a ring now (#2751) — not only below lg (#2340). The
   header no longer carries a standalone dot; the logo ring is the at-a-glance indicator at every
   width, the footer shows the "● Live" label. sse.js mirrors data-sse-state onto #nav-dashboard; these
   rules paint the ring in the same colours as the footer dot. */
#nav-dashboard img {
  border-radius: 6px;
  box-shadow: 0 0 0 2px #52525b; /* zinc-600 — connecting/unknown */
  transition: box-shadow 0.4s ease;
}
#nav-dashboard[data-sse-state="live"] img { box-shadow: 0 0 0 2px #4ade80; } /* green-400 */
#nav-dashboard[data-sse-state="reconnecting"] img {
  box-shadow: 0 0 0 2px #fbbf24; /* amber-400 */
  animation: sse-pulse 1.2s ease-in-out infinite;
}
#nav-dashboard[data-sse-state="closed"] img { box-shadow: 0 0 0 2px #f87171; } /* red-400 */

/* The shared page footer (#2751): one quiet line — brand · version · build time · live state. */
.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  border-top: 1px solid #27272a;
  margin-top: 1rem;
}
.site-footer .footer-sep { color: #3f3f46; }
.site-footer .footer-brand { font-weight: 600; color: #52525b; }

/* In-flight tool call banner below the transcript pane. Injected by
   transcript.js when a 🔧 tool call has no matching result yet; hidden when
   all calls are matched or the session is terminal. The left border matches
   the amber color used for "in progress" chips. */
.inflight-tool {
  padding: 0.35rem 0.75rem;
  border: 1px solid #3f3f46;
  border-top: none;         /* merges visually with the output pane above */
  border-left: 3px solid #fbbf24; /* amber — tool in progress */
  border-radius: 0 0 8px 8px;
  background: rgba(251, 191, 36, 0.04);
  font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #fbbf24;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.inflight-tool[hidden] { display: none; }

/* Top progress bar driven by nav.js during a client-side navigation. Gives an
   instant "the UI responded" cue on click; the ::before bar creeps toward 90%
   while the fetch is in flight, then snaps to 100% and fades on swap. */
#cp-progress { position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 100; pointer-events: none; background: transparent; }
#cp-progress::before { content: ""; display: block; height: 100%; width: 0; background: #06b6d4; } /* cyan-500 */
#cp-progress.active::before { width: 90%; transition: width 10s cubic-bezier(0.1, 0.7, 0.1, 1); }
#cp-progress.done::before { width: 100%; transition: width 0.15s ease; }

/* Stale health cell: the sample is older than ~2 heartbeat intervals. Grey the
   values out so a dead VM doesn't show last-known healthy numbers forever.
   tickHealthStale in sse.js toggles this class every second. */
.health.health-stale > span { opacity: 0.35; } /* direct children only: meter rows nest spans and opacity compounds */

/* Health meter row (sse.js healthLine): label | bar of the share in use | value.
   The bar colour follows the level health.ts assigned to the reading. */
.hm {
  display: grid;
  grid-template-columns: 2.25rem 3.5rem auto;
  align-items: center;
  column-gap: 0.5rem;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.hm-k { text-transform: uppercase; letter-spacing: 0.05em; font-size: 10px; color: #71717a; }
.hm-bar { position: relative; height: 4px; border-radius: 9999px; background: #27272a; overflow: hidden; }
.hm-bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: #22d3ee; }
.hm[data-level="amber"] .hm-bar > span { background: #fbbf24; }
.hm[data-level="red"] .hm-bar > span { background: #f87171; }
.hm-v { overflow: hidden; text-overflow: ellipsis; }

/* ── Session groups (Issue 7) — collapsed summary + framed tree ─────────── */
.session-group { border: 1px solid #27272a; border-radius: 8px; background: #18181b; }
.session-group + .session-group { margin-top: 6px; }
.session-group[data-group-open="1"] { border-color: #3f3f46; background: #141418; }
/* Desktop: 6-column grid — caret | title | count | runtime | cost | pill+action.
   The pill and action share ONE right-aligned cell (`.group-end`) spanning the
   old two trailing tracks, sized to its content (`minmax(0,max-content)`) and
   placed flush against the row's right padding; the title's `minmax(0,1fr)` track
   absorbs the slack. The old template reserved a FIXED 136px pill track + 120px
   action track: an empty action still held 120px (so every pill stopped ~230px
   short of the edge, #1005 follow-up), and a pill wider than 136px (the merge-train
   label `MERGE TRAIN n/m · CATCH-UP CHECK`) overflowed its fixed track and drew over
   cost/context. A content-sized cell can't overflow a fixed width, and the
   `.group-end`/`.group-pill .chip` overflow guards below clamp an extreme label.
   Mobile: the merged cell drops to its own row (see the breakpoint below); the 3
   hidden middle items (count, rt×2) are display:none so they vacate their cells. */
.group-head { display: grid; grid-template-columns: 18px minmax(0,1fr) 96px 104px 96px minmax(0,max-content); gap: 12px; align-items: center; padding: 10px 14px; cursor: pointer; }
.group-head:focus-visible { outline: 2px solid #22d3ee; outline-offset: -2px; border-radius: 8px; }
.group-caret { color: #71717a; font-size: 11px; }
.group-title { display: flex; flex-direction: column; min-width: 0; }
.group-title .t { font-weight: 600; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.group-title .num { color: #71717a; font-family: ui-monospace, monospace; font-weight: 500; margin-right: 6px; }
.group-title .meta { font-size: 11.5px; color: #71717a; display: flex; gap: 6px; align-items: center; }
/* Park reason line for a parked PR group (#953): one truncated line while collapsed
   (the full text rides in the title tooltip), wrapping untruncated once the group is
   expanded — keyed off data-group-open like the collapse labels above. */
.park-reason { font-size: 11.5px; color: #a1a1aa; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.session-group[data-group-open="1"] .park-reason { white-space: normal; overflow: visible; }
.park-infra { color: #fbbf24; font-weight: 600; margin-right: 6px; }
.group-count { font-size: 12px; color: #a1a1aa; }
.group-rt { font: 12.5px ui-monospace, monospace; color: #f4f4f5; font-variant-numeric: tabular-nums; }
.group-rt small { display: block; color: #71717a; font-size: 10.5px; }
.group-rt small .reap-countdown { margin-left: 0; }
.group-action { text-align: right; }
/* Runtime and cost values sit inline so their " runtime"/" cost" collapse labels
   can trail them on the same line; each figure's sub (active/countdown, ctx) is a
   block <small> that drops below (see `.group-rt small`). */
.group-runtime { display: inline; }
.group-rt-sub { display: block; color: #71717a; font-size: 10.5px; }
.group-cost { display: inline; font-weight: 600; }
/* Accumulated-context line under the cost: the sum of the members' CONTEXT values.
   `.group-rt small` already makes it a gray block; `.group-ctx-val` is the value span
   sse.js re-sums, kept a distinct selector from its " ctx" label sibling. */
.group-ctx { display: block; }
.group-ctx-val { color: inherit; }
.group-label { color: #71717a; font-size: 10.5px; }
/* Labels — and the runtime's active/countdown sub — only when the group is
   collapsed: then the header row is all that's visible so its figures need naming.
   Expanded, the tree's RUNTIME/CONTEXT/COST column headers name them, so the header
   shows bare values. Keyed off data-group-open, which sse.js toggles live, so the
   labels follow the client-side collapse toggle, not only the server render. */
.session-group[data-group-open="1"] .group-label,
.session-group[data-group-open="1"] .group-rt-sub { display: none; }
/* Pill + action share one right-aligned flex cell (#1005 follow-up): the action
   button, when present, sits to the RIGHT of the pill; with no action (most train
   members) the pill alone slides flush to the row's right padding. `min-width:0` +
   `overflow:hidden` on the cell, plus the chip's `max-width:100%` + ellipsis below,
   are the last-ditch guard so an extreme label is clipped (full text stays in the
   chip's `title`) instead of bleeding left over cost/context. */
.group-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; overflow: hidden; }
.group-pill { display: flex; align-items: center; min-width: 0; }
.group-pill .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.group-tree { display: flex; flex-direction: column; border-top: 1px solid #27272a; padding: 8px 14px 12px; overflow-x: auto; }
.state-cell { display: flex; align-items: center; }
.line-cost { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; color: #a1a1aa; }
.tv { display: flex; flex-direction: column; border-top: 1px solid #27272a; padding: 8px 14px 12px; overflow-x: auto; }
/* The display:flex above has the same specificity as the base [hidden]{display:none}
   (Tailwind preflight) but app.css loads last, so it would win and a collapsed
   group's tree (sse.js sets .group-tree.hidden) would stay visible. This override
   is more specific, so it hides the tree whenever the hidden attribute is set. */
.group-tree[hidden], .tv[hidden] { display: none; }
.tr { display: grid; grid-template-columns: auto minmax(0,1fr) 110px 96px 96px 90px 70px; column-gap: 12px; align-items: stretch; min-height: 38px; border-radius: 6px; min-width: 760px; }
/* The admin delete action no longer owns a reserved trailing column (#1980): it rides the
   shared `.hover-actions` overlay (absolute, out of this grid), so a row with a delete lays
   out identically to one without — no permanent empty 32px gap on the right — and the trash
   fades in over the row's right edge on hover. The old 8-column admin template and the tree
   header's spacer cell are gone with it. */
.tr:hover:not(.th) { background: rgba(34,211,238,.06); }
.tr.is-current { background: rgba(34,211,238,.10); }
/* Batch-merge members (#1907): a subtle left-edge accent marks the running rows that belong to
   ONE merge batch (tested + merged together through a single CI run), so they read as one unit
   rather than as unrelated serial merges. An inset shadow, not a border, so it adds the cue
   without nudging the grid columns. */
.session-row.batch-member { box-shadow: inset 3px 0 0 0 #8b5cf6; }
.tr > *:not(.gut) { display: flex; align-items: center; min-width: 0; }
.tr.th { min-height: 26px; font-size: 10.5px; color: #71717a; text-transform: uppercase; letter-spacing: .06em; }
.tr .name { gap: 8px; white-space: nowrap; overflow: hidden; }
.tr .name a { color: #f4f4f5; font-weight: 600; text-decoration: none; }
.tr .name a:hover { color: #22d3ee; }
.tr .small { font-size: 11.5px; color: #71717a; }
/* Outcome note colour (sessionGroup.ts noteFor / sse.js twin). Text only — no
   chip, no background — so it stays subtle. A note with no data-outcome (a
   follow-up title, a pending row) keeps the muted grey above. Higher specificity
   than `.tr .small` so it wins regardless of source order. */
.tr .small.outcome-note[data-outcome="ok"] { color: var(--tone-ok); }
.tr .small.outcome-note[data-outcome="act"] { color: var(--tone-act); }
.tr .small.outcome-note[data-outcome="fail"] { color: var(--tone-fail); }
.tr .c { color: #a1a1aa; font-family: ui-monospace, monospace; font-size: 11.5px; font-variant-numeric: tabular-nums; }
/* Runtime cell stacks the suspend countdown under the runtime value (like the
   group header), so "⏸ 2m 46s" sits on its own line instead of running past the
   96px column into Context/Cost. Equal specificity to `.tr > *:not(.gut)` above,
   so it must come after it in source order to win the tie. */
.tr .rt-cell { flex-direction: column; align-items: flex-start; justify-content: center; gap: 1px; }
/* The 9.5rem reservation on .reap-countdown (sized for the wide "shutdown in 23h 59m"
   string it still carries in the session detail header; the server "Age" column no
   longer renders one since #979 moved billing there) overruns the narrow session-table
   columns — 96px Runtime on rows, the
   104px runtime cell in the group header — and drew over Context/Cost. In those
   contexts the label is the compact "⏸ …" form, so drop the reservation and the
   ml-2 and let it size to its short content; tabular-nums (base rule) still holds
   the width steady across ticks, and nowrap keeps it on one line. */
.tr .reap-countdown, .group-rt .reap-countdown { min-width: 0; margin-left: 0; white-space: nowrap; }
/* Gutter: flat rows that abut (no vertical gap) so borders join into continuous lines. */
.gut { display: flex; align-self: stretch; padding-left: 6px; }
.g { position: relative; width: 22px; flex: none; }
.g::before, .g::after { content: ""; position: absolute; border-color: #52525b; border-style: solid; border-width: 0; }
.g.v::before, .g.tee::before { left: 10px; top: 0; bottom: 0; border-left-width: 2px; }
.g.tee::after, .g.end::after { left: 10px; top: calc(50% - 1px); width: 12px; border-top-width: 2px; }
.g.end::before { left: 10px; top: 0; height: 50%; border-left-width: 2px; }
.g.root::before, .g.stub::before { left: 10px; top: 50%; bottom: 0; border-left-width: 2px; }
.g.root.leaf::before { display: none; }
.g.stub::after { left: 0; top: calc(50% - 1px); width: 12px; border-top-width: 2px; }
/* The root node's circle is the project colour (--proj, set on the .session-group;
   falls back to the old blue for a project without one) — fill AND border both. The
   session state is carried by the state pill (RUNNING etc.), not the dot, so the dot
   stays a solid project-coloured dot even while live (#2097): a live ring in a
   different hue (sky-blue --tone-work) over a small green fill read as blue/teal, not
   as the project colour. */
.nd { position: absolute; left: 5px; top: calc(50% - 6px); width: 12px; height: 12px; border-radius: 50%; background: var(--proj, #3b82f6); border: 2px solid var(--proj, #3b82f6); z-index: 1; }

/* ── Projects-page issue list — lane sections + one-line expandable rows (redesign, #1145) ──
   The list is five collapsible lanes (issue-lanes.ts); every issue is one fixed-height line that
   unfolds a labelled detail block. Collapse/expand is attribute-driven (data-lane-open on
   .issue-lane, data-row-open on .issue-line), server-rendered as the default and then owned by the
   viewer — morph.js preserves both so a live morph never clobbers the viewer's choice. */
/* #754: keep the issue panel out of the auto-layout projects table's intrinsic width. The
   panel lives in a <td colspan> of a `table-auto` table, which sizes its columns from each
   cell's min-content width; nowrap lane heads / row titles would otherwise set that cell — and so
   the whole table and card — to their full width and scroll horizontally (#757). Size containment
   drops the panel's contents from that intrinsic-width contribution. */
.js-issues-body { contain: inline-size; }

/* Status dot: a lane's default cue and a per-row override. Filled = acting (working / needs you);
   ring = waiting (queued / parked); red (filled) = a failed / errored row. */
.issue-dot, .lane-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid transparent; box-sizing: border-box; }
.issue-dot[data-dot="working"], .lane-dot[data-dot="working"] { background: var(--tone-work); border-color: var(--tone-work); }
.issue-dot[data-dot="queued"], .lane-dot[data-dot="queued"] { background: transparent; border-color: #a78bfa; }
.issue-dot[data-dot="needs_you"], .lane-dot[data-dot="needs_you"] { background: #f59e0b; border-color: #f59e0b; }
.issue-dot[data-dot="parked"], .lane-dot[data-dot="parked"] { background: transparent; border-color: #52525b; }
.issue-dot[data-dot="errored"], .lane-dot[data-dot="errored"] { background: var(--tone-fail); border-color: var(--tone-fail); }

/* Lane section. */
.issue-lane { border: 1px solid #27272a; border-radius: 8px; background: #18181b; margin: 6px 0; }
.issue-lane-head { display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; }
.issue-lane-head:focus-visible { outline: 2px solid #22d3ee; outline-offset: -2px; border-radius: 8px; }
.lane-caret { color: #71717a; font-size: 11px; transition: transform .12s ease; }
.lane-name { font-size: 13px; font-weight: 600; color: #e4e4e7; }
.lane-count { font-size: 11px; color: #a1a1aa; padding: 0 6px; border-radius: 999px; border: 1px solid #3f3f46; background: rgba(9,9,11,.5); }
.lane-hint { font-size: 11.5px; color: #71717a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.issue-lane[data-lane-open="1"] .lane-caret { transform: rotate(90deg); }
.issue-lane[data-lane-open="0"] .issue-lane-body { display: none; }
.issue-lane-body { padding: 2px 8px 8px; }
.issue-lane-empty { font-size: 12px; color: #52525b; padding: 4px 8px 8px; }

/* Issue-panel styles (lanes / toolbar / rows). The Projects issue panel was removed in #1298
   part 6 (the /work view owns the pipeline now); these rules are no longer rendered against and
   can be dropped in a later CSS sweep. */
.issue-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 6px 4px 2px; }
.issue-toolbar-pills { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.issue-lane-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: #d4d4d8; padding: 2px 9px; border-radius: 999px; border: 1px solid #27272a; background: #18181b; cursor: pointer; white-space: nowrap; }
.issue-lane-pill:hover { border-color: #3f3f46; background: #1f1f23; }
.issue-lane-pill:focus-visible { outline: 2px solid #22d3ee; outline-offset: 1px; }
.issue-lane-pill .pill-count { font-variant-numeric: tabular-nums; color: #a1a1aa; }
.issue-lane-pill[data-empty="1"] { opacity: .5; }
.issue-search { margin-left: auto; min-width: 11ch; max-width: 22ch; font-size: 12px; color: #e4e4e7; background: #09090b; border: 1px solid #27272a; border-radius: 6px; padding: 3px 8px; }
.issue-search:focus { outline: none; border-color: #22d3ee; }

/* Client-side filter: hide non-matching rows + empty blocker groups; while a filter is active,
   reveal matching rows even inside a collapsed lane and drop lanes with no match. */
.issue-line[data-filtered="1"] { display: none; }
.issue-blocker-group[data-filtered="1"] { display: none; }
[data-searching="1"] .issue-lane[data-lane-open="0"] .issue-lane-body { display: block; }
[data-searching="1"] .issue-lane[data-lane-hit="0"] { display: none; }

/* One fixed-height issue line + its unfolding detail block. */
.issue-line { border-radius: 6px; }
.issue-line + .issue-line { border-top: 1px solid rgba(39,39,42,.6); }
.issue-line-head { display: grid; grid-template-columns: 14px 8px auto minmax(0,1fr) auto; align-items: center; gap: 8px; min-height: 40px; padding: 0 6px; cursor: pointer; }
.issue-line-head:hover { background: rgba(34,211,238,.06); }
.issue-line-head:focus-visible { outline: 2px solid #22d3ee; outline-offset: -2px; border-radius: 6px; }
.issue-caret { color: #71717a; font-size: 10px; transition: transform .12s ease; }
.issue-line[data-row-open="1"] .issue-caret { transform: rotate(90deg); }
.issue-line-num { color: #71717a; font-family: ui-monospace, monospace; font-size: 11.5px; white-space: nowrap; }
.issue-line-title { min-width: 0; font-size: 13px; color: #f4f4f5; font-weight: 500; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.issue-line-title:hover { color: #22d3ee; }
.issue-line-ind { display: flex; align-items: center; justify-content: flex-end; gap: 6px; white-space: nowrap; }
.issue-group-line.is-closed .issue-line-title { color: #a1a1aa; }
.issue-group-closed { color: #71717a; font-weight: 500; }

/* Sub-issue tree inside an unfolded epic / split row: one connector row per child (open +
   closed). Connectors reuse the session tree's .gut/.g.* classes (1px CSS absolute lines in a
   fixed gutter column, not text glyphs). Rows abut (no vertical gap) so the tee/end borders join
   into continuous lines, exactly as the gutter comment above requires. */
.issue-child-tree { margin-top: 4px; }
.issue-child-row { display: flex; align-items: center; min-height: 26px; gap: 8px; }
.issue-child-row .gut { padding-left: 0; align-self: stretch; }
.issue-child-title { min-width: 0; font-size: 12.5px; color: #d4d4d8; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
a.issue-child-title:hover { color: #22d3ee; }
/* A closed child reads as done: greyed and struck through. */
.issue-child-title.is-closed { color: #6b7280; text-decoration: line-through; }

/* Detail block — shown only when the row is unfolded. Labelled fields + the action buttons. */
.issue-detail { display: none; padding: 4px 8px 12px 36px; }
.issue-line[data-row-open="1"] .issue-detail { display: block; }
.issue-field { display: flex; gap: 8px; align-items: baseline; font-size: 12px; padding: 2px 0; }
.issue-field-label { flex: none; width: 92px; color: #71717a; }
.issue-field-val { min-width: 0; color: #d4d4d8; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.issue-detail-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

/* Up-next blocker sub-header: the issues behind one blocker, under one heading. */
.issue-blocker-group { margin: 2px 0; }
.issue-blocker-sub { font-size: 11.5px; color: #a1a1aa; padding: 6px 6px 2px; }

/* ── Servers card — merged Servers + Slots, collapsed head + slot detail ─────── */
.server-group { border: 1px solid #27272a; border-radius: 8px; background: #18181b; transition: background-color 0.12s ease, box-shadow 0.12s ease; --row-bg: #18181b; }
.server-group + .server-group { margin-top: 6px; }
.server-group[data-server-open="1"] { border-color: #3f3f46; background: #141418; --row-bg: #141418; }
/* Hovering the card gets the same faint cyan tint + inset ring as a hovered table row
   (the app-wide `tbody tr:hover` tint at the top of this file), so it reads as interactive
   and the hover-revealed Kill action has an obvious host (#1905). Placed AFTER the
   open-state background so the equal-specificity hover tint wins by source order. */
/* On hover the opaque card background is REPLACED by this translucent cyan (background-color only),
   so it composites over the page (bg-zinc-950 #09090b), NOT over the card's own #18181b/#141418 —
   the same blend for an open or closed card. The overlay gradient matches that computed colour so it
   blends into the hovered card with no seam (#1980): rgba(34,211,238,0.06) over #09090b ≈ #0b1519. */
.server-group:hover { background-color: rgba(34, 211, 238, 0.06); box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.18); --row-bg: #0b1519; }
/* caret | id+badge | state | ip | age | slot dots+busy.
   The title column grows to fit the full server name (minmax(max-content,…)) rather than
   being pinned to a shrink-to-zero fraction (minmax(0,1fr)) that ellipsised the name — the
   empty trailing room goes to the name so it reads in full at desktop widths (#1905). */
/* There is NO trailing action track any more (#1980): #1968's reserved `max-content` column
   squeezed the slot-dots / "N/M busy · %" slot (minmax(0,1.4fr)), which then shone through
   behind the hover-revealed Kill. The Kill / Decommission buttons now ride the shared
   `.hover-actions` overlay (absolute, out of this grid), so the slots column keeps its full
   width and nothing shows through. The head is position:relative so the overlay anchors to
   it (vertically centred on the head, not the expanded card). */
.server-head { display: grid; grid-template-columns: 18px minmax(max-content,1.6fr) 128px 140px 132px minmax(0,1.4fr); gap: 12px; align-items: center; padding: 10px 14px; cursor: pointer; position: relative; }
.server-head:focus-visible { outline: 2px solid #22d3ee; outline-offset: -2px; border-radius: 8px; }
.server-caret { color: #71717a; font-size: 11px; }
.server-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* At desktop widths the name/id are shown in full (no ellipsis) — the head's title column
   grows to fit them (#1905). Below ~900px the column is tight again, so the ellipsis is
   restored in the narrow media block below. */
.server-title .srv-id { font-size: 13px; white-space: nowrap; }
/* Friendly display name leads the block; the raw id sits beside it as a smaller,
   copyable secondary label (the id stays the key). */
.server-title .srv-name { font-size: 13px; font-weight: 600; white-space: nowrap; }
.server-title .srv-name + .srv-id { font-size: 11px; }
/* The badge never shrinks, so a long VM name truncates (see .srv-name) while the badge
   stays fully visible beside it (#1592). */
.server-badge { font-size: 10.5px; color: #a1a1aa; white-space: nowrap; padding: 1px 6px; border-radius: 999px; border: 1px solid #3f3f46; background: rgba(9,9,11,.5); flex-shrink: 0; }
.server-badge[data-kind="pinned"] { color: #fbbf24; border-color: rgba(251,191,36,.35); }
/* Image-builder VM (#1594): a build VM, the parked warm one, or one lent to the fleet — all the
   one 🏗 builder kind, rendered with the overflow-VM row design. Violet so it stands apart from a
   plain overflow VM at a glance. */
.server-badge[data-kind="builder"] { color: #c4b5fd; border-color: rgba(196,181,253,.4); }
/* Stale-image VM (#1425): still running the pre-deploy worker image, so it takes no new
   sessions and drains when idle — flagged orange so an operator spots it at a glance. */
.server-badge[data-kind="stale-image"] { color: #fb923c; border-color: rgba(251,146,60,.4); }
/* Decommissioning VM (#1906, soft kill): takes no new sessions and auto-deletes once its running
   sessions finish — flagged red so an operator sees the pending teardown and the running count. */
.server-badge[data-kind="decommissioning"] { color: #f87171; border-color: rgba(248,113,113,.4); }
/* CI-runner pool VM (#2141/#3223): an elastic self-hosted GitHub Actions runner, monitoring-only for
   the worker fleet. Teal so it stands apart from a worker VM — its row carries no slots/cost/Kill. */
.server-badge[data-kind="ci-runner"] { color: #5eead4; border-color: rgba(94,234,212,.4); }
/* The owner-project marker (#2219): a dot in the OWNING project's colour (--proj, the palette guard)
   plus the project name, so an operator sees which project a VM BELONGS to — distinct from the
   per-slot task tint (#2153), which shows the project whose task RUNS in a slot. The warning state
   (owner over its maxOverflowVms allowance, incl. an allowance of 0) is outlined amber so a VM that
   should not exist stands out. */
.server-owner { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; color: #a1a1aa; white-space: nowrap; padding: 1px 6px; border-radius: 999px; border: 1px solid #3f3f46; background: rgba(9,9,11,.5); flex-shrink: 0; }
.server-owner .server-owner-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--proj, #71717a); flex-shrink: 0; }
.server-owner--over { color: #fbbf24; border-color: rgba(251,191,36,.5); background: color-mix(in srgb, #fbbf24 10%, transparent); }
/* A ci-runner pool VM renders as an ORDINARY expandable server row (#2276/#3223): same clickable head,
   caret, two-column (slot-list | health) detail grid and health column as a worker VM, so it lines up
   column for column. Its current-job slot row and boot-latency/job labels use shared utility classes, so
   no row-specific CSS remains. (The fixed plan2x-runner box card's per-instance tiles + 24 h sparkline
   CSS was removed with the box in #3264.) */
/* The friendly VM name is shown in full at desktop widths (the title column grows to fit it,
   #1905); below ~900px it truncates with an ellipsis again (narrow media block below) so a long
   name can't overflow its cell and overlap the badge (#1592). The full name is in the title attr. */
.server-title .srv-name { min-width: 0; }
.srv-image { white-space: nowrap; }
.srv-ip, .srv-age { font-size: 12px; color: #a1a1aa; }
/* Billing-hour line for an overflow VM, rendered under the age inside the head's
   Age cell (#979: moved out of the .server-detail grid, where it was a 3rd child
   that broke the two-column slot/health layout). Its own line below the age; each
   phrase (paid-until / reaps-at) holds together and the line wraps between them only
   when the narrow 132px column can't fit both. Compact so the collapsed head stays
   short. */
.srv-age .srv-billing { display: block; margin-top: 2px; line-height: 1.25; }
.srv-paid-until, .srv-reaps-at { white-space: nowrap; }
/* Slot indicators: one dot per slot, tone-coloured, followed by the busy summary. */
.server-slots { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; min-width: 0; }
.slot-dot { font-size: 12px; line-height: 1; color: var(--tone-done); }
.slot-dot[data-tone="work"] { color: var(--tone-work); }
.slot-dot[data-tone="act"] { color: var(--tone-act); }
.slot-dot[data-tone="fail"] { color: var(--tone-fail); }
.slot-dot[data-tone="pause"] { color: var(--tone-pause); }
.slot-dot--pending { opacity: .4; }
.server-busy { margin-left: 6px; font-size: 11.5px; color: #71717a; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Slot-utilisation % in the head, right after the busy summary. Muted by default; amber
   when under 30% — we are paying for idle slots. Wraps WITH the busy summary on phone
   widths (it lives in the same .server-slots cell, adds no column). */
.srv-util { margin-left: 6px; font-size: 11.5px; color: #71717a; font-variant-numeric: tabular-nums; white-space: nowrap; }
.srv-util[data-util-tone="amber"] { color: #fbbf24; }
/* An image-builder VM shows its build in the slots cell instead of slot dots (#1594): the sha being
   built, the phase and the ticking elapsed, truncating within the cell rather than overflowing. */
.server-slots .srv-build { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
/* The muted note folding a VM's superseded predecessor shas into its single row (#1594). */
.server-slots .srv-superseded { margin-left: 8px; font-size: 11.5px; white-space: nowrap; flex-shrink: 0; }
/* The state chip truncates within the fixed state column rather than overflowing a long phase label
   (e.g. "installing toolchain") into the next cell (#1592); the full phase is in the chip's title. */
.srv-state { min-width: 0; }
.srv-state .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* Kill + Decommission (#1906) sit side by side on one row, right-aligned, never wrapping at desktop
   width — they ride the shared `.hover-actions` overlay (#1980), which floats them at the head's
   right edge on the fade gradient. `.server-action` carries only the flex layout; `.hover-actions`
   owns the absolute positioning, gradient and hover-reveal. The colour transition on the button
   stays so Kill tints on hover. */
.server-action { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: nowrap; align-items: center; }
.server-kill { transition: color 0.1s ease; }
/* Armed state of the two-step Kill / Decommission confirm (#2075, confirm.js). Once armed
   the button's label flips to "Confirm …?" and this paints it as an UNMISTAKABLE filled-red
   button — not the quiet red OUTLINE of the resting state — so a single click is never read
   as a completed action. The two-class selector beats the Tailwind utility colours. */
.server-kill.confirm-armed {
  background: #dc2626;
  border-color: #dc2626;
  color: #fff;
  font-weight: 600;
}
.server-kill.confirm-armed:hover { background: #b91c1c; }
/* The "click again to confirm" hint confirm.js appends beside an armed button. */
.confirm-hint { margin-left: 6px; font-size: 0.7rem; color: #fca5a5; white-space: nowrap; }
/* Transient status toast shown when a confirm arm lapses ("Server not decommissioned …")
   or any other one-off action note. Built client-side in confirm.js (createElement, no
   fragment markup); bottom-centre so it clears the bottom-right question popup. */
.cp-toast {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 101;
  max-width: 24rem;
  background: #18181b;
  border: 1px solid #3f3f46;
  border-left: 3px solid #ef4444;
  border-radius: 0.5rem;
  padding: 0.6rem 1rem;
  color: #e4e4e7;
  font-size: 0.8rem;
  line-height: 1.4;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
  animation: popup-in 0.2s ease;
}
/* Expanded server detail: two columns — the slot list takes the remaining width,
   the health block sits in a fixed-width right column, right-aligned and top-aligned
   with the first slot row. The `[hidden]` reset (Tailwind preflight) still wins to
   collapse it. Below 640 px the two children stack (see the mobile block). */
.server-detail { border-top: 1px solid #27272a; padding: 8px 14px 12px; display: grid; grid-template-columns: minmax(0,1fr) minmax(16rem, auto); column-gap: 24px; align-items: start; }
.server-detail[hidden] { display: none; }
/* The container-name column is a fixed width that snugly fits the longest real id
   (`S-xxxxxxxx-N`, ~12 mono chars at 11.5px → ~5.75rem), not a fraction of the
   flexible width: a fraction gave it a third of the row and left a wide empty
   band before the session (#822-follow). Fixed keeps the names aligned across
   rows while the session column takes ALL the remaining width; the name still
   ellipsises (with a title attr for the full id) if a font renders it wider. */
/* The container column grows to fit the full `friendly-name · N` slot label (every slot row in
   one card shares the same friendly name, so max-content sizes them identically and keeps the
   session column aligned) while never dropping below the old 5.75rem floor for short/provisioning
   rows (#1905). Below ~900px it reverts to the fixed 5.75rem + ellipsis (narrow media block). */
.slot-row { display: grid; grid-template-columns: 132px minmax(5.75rem,max-content) minmax(0,1fr); column-gap: 12px; align-items: center; min-height: 34px; }
.slot-row + .slot-row { border-top: 1px solid #1f1f23; }
.slot-state { display: flex; align-items: center; }
.slot-container { font-size: 11.5px; color: #a1a1aa; white-space: nowrap; }
.slot-session { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 11.5px; }
/* The session id link never wraps: it holds its intrinsic width; the title beside it
   is the flex item that shrinks and ellipsises when the row runs out of room. */
.slot-session-id { flex: none; white-space: nowrap; }
.slot-session-role { flex: none; color: #a1a1aa; text-transform: uppercase; font-size: 10px; letter-spacing: 0.04em; }
/* The issue number sits between the role chip and the title; a mono #N that scroll-links
   to the session's pipeline group (public/sse.js intercepts the click). */
.slot-session-issue { flex: none; white-space: nowrap; font-family: ui-monospace, monospace; }
.slot-session-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.slot-session-rt { flex: none; color: #a1a1aa; font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.srv-health { justify-self: end; }

/* The ci-runner drawer's finished-job history (#3375): "N jobs done" + the last few jobs, beneath the
   current-job slot row in the detail's left column. A faint top rule separates it from that row. */
.ci-runner-jobs-done { padding-top: 6px; margin-top: 6px; border-top: 1px solid #1f1f23; }
.ci-done-count { color: #a1a1aa; }
.ci-done-list { margin: 2px 0 0; padding: 0; list-style: none; }
.ci-done-row { display: flex; align-items: center; gap: 4px; min-width: 0; line-height: 1.5; }
.ci-done-job { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ci-done-mark { flex: none; font-variant-numeric: tabular-nums; }

/* Narrow viewports (below ~900px, before the 767px mobile reflow): the head's title column
   and the slot container column are tight again, so the full-width desktop treatment above would
   overflow. Restore the ellipsis truncation here — the full name is still in each element's
   `title` tooltip (#1905). */
@media (max-width: 900px) {
  .server-title .srv-id,
  .server-title .srv-name { overflow: hidden; text-overflow: ellipsis; }
  .slot-row { grid-template-columns: 132px 5.75rem minmax(0,1fr); }
  .slot-container { overflow: hidden; text-overflow: ellipsis; }
}

/* Provisioning panel: one panel for a whole booting VM (step track + a single
   VM-level progress), replacing the N identical broken placeholder rows. Nothing
   here wraps per word — the step track flows as flex items, the byte line is
   tabular + nowrap (via .slot-progress-text / .prov-bar-text). */
.provisioning { display: flex; flex-direction: column; gap: 8px; padding: 2px 0; min-width: 0; }
.prov-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; list-style: none; margin: 0; padding: 0; }
.prov-step { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
/* Separator between steps (prep › docker › image › start). */
.prov-step + .prov-step::before { content: "›"; margin-right: 8px; color: #52525b; }
.prov-step-label { font-size: 12px; color: #71717a; text-transform: uppercase; letter-spacing: 0.04em; }
.prov-step[data-status="done"] .prov-step-label { color: #52525b; }
.prov-step[data-status="current"] .prov-step-label { color: #e4e4e7; }
.prov-step[data-status="done"] .prov-step-mark { color: var(--tone-ok); }
.prov-step[data-status="current"] .prov-step-mark { color: var(--tone-work); }
.prov-step[data-status="todo"] .prov-step-mark { color: #3f3f46; }
.prov-step-mark { display: inline-flex; align-items: center; justify-content: center; width: 0.9em; font-size: 12px; }
/* The image-pull bar spans the panel width (not the squeezed 7rem slot bar). */
.prov-progress { display: flex; align-items: center; gap: 8px; min-width: 0; }
.prov-bar { flex: 1 1 auto; min-width: 0; height: 5px; background: #3f3f46; border-radius: 9999px; overflow: hidden; }
.prov-bar-fill { display: block; height: 100%; background: #fbbf24; transition: width 0.3s linear; }
.prov-bar-text { flex: none; font-size: 0.75rem; color: #a1a1aa; font-variant-numeric: tabular-nums; white-space: nowrap; }
.prov-note { margin: 0; }

/* ── Permission-question tool detail (views/questionControls.ts) ─────────────
   Shows what a gated tool call would do — command / diff / content / JSON —
   above Allow/Deny. All text comes from untrusted agent output and is HTML-
   escaped in the renderer; these rules only style it. */
.qc-detail { margin-top: 10px; }
.qc-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.qc-tool {
  font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: #a5b4fc; /* indigo-300, matches the card's violet accent */
}
.qc-target {
  font-family: ui-monospace, monospace; font-size: 12px; color: #e4e4e7;
  overflow-wrap: anywhere;
}
.qc-note { font-size: 12px; color: #a1a1aa; margin: 4px 0; }
.qc-pre {
  font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere; margin: 0;
  padding: 8px 10px; border-radius: 6px;
  background: #18181b; border: 1px solid #27272a; color: #d4d4d8;
}
/* Inline (not block): the diff lines already sit on their own line via the
   preserved "\n" between spans in the <pre>; block display would double-space. */
.qc-del { color: #f87171; }
.qc-add { color: #4ade80; }
.qc-more { margin-top: 4px; }
.qc-more > summary {
  cursor: pointer; font-size: 12px; color: #71717a;
}
.qc-more > summary:hover { color: #a1a1aa; }
.qc-more > .qc-pre { margin-top: 4px; }

/* ── Mobile responsive (below Tailwind's md = 768 px) ───────────────────── */
@media (max-width: 767px) {

  /* Group header: 3-column grid (caret | title | action).
     Row 1 holds the title spanning cols 2-3 (up to 2 lines, then ellipsis).
     Row 2 holds the single merged .group-end cell spanning cols 2/-1 (pill
     left, action right via space-between). The three desktop-only cells
     (count, rt×2) are display:none so they don't consume grid cells at all. */
  .group-head {
    grid-template-columns: 18px 1fr auto;
    grid-template-rows: auto auto;
    row-gap: 6px;
    column-gap: 8px;
    padding: 10px 10px;
    align-items: start;
    min-height: 44px;
  }
  .group-caret { grid-column: 1; grid-row: 1; align-self: center; }
  .group-title { grid-column: 2 / span 2; grid-row: 1; }
  .group-title .t {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .group-count { display: none; }
  .group-head > .group-rt { display: none; }
  /* The merged pill+action cell drops to row 2, spanning the title's columns. The
     pill sits left and the action (when present) trails right via space-between, so
     the chip never shares a track with cost/context (those rt cells are hidden here
     anyway) — matching the pre-merge row-2 layout (pill flex-start, action right). */
  .group-end { grid-column: 2 / -1; grid-row: 2; justify-content: space-between; overflow: visible; }

  /* Dashboard: the Sessions pipeline is its own flex column at the bottom of the
     operations overview. Epic #1298 moved the fleet (Servers) section OUT of
     .dashboard-main to a top-level #dash-fleet-section above it, so the old mobile
     "sessions first" flex-order promotion (#259) no longer has a sibling to jump —
     the overview (alerts, KPIs, fleet) leads and Sessions follows on every width. */
  .dashboard-main { display: flex; flex-direction: column; }

  /* Session filter pills: taller for 44 px tap targets on touch devices. */
  .session-pill {
    min-height: 2.75rem;
    padding: 0.375rem 0.625rem;
  }

  /* Modal: reduce top offset so it fits on short viewports (414 px and under). */
  .modal-backdrop { padding: 0.75rem; }

  /* Question popup: full width instead of fixed 18 rem. */
  .question-popup { width: auto; left: 1rem; right: 1rem; bottom: 1rem; }

  /* IP column: no min-width reservation on mobile — the cell layout is fluid. */
  .ip { min-width: 0; }

  /* Age + reap-countdown: allow them to be narrower on mobile. */
  .age { min-width: 3rem; }
  .reap-countdown { min-width: 0; }

  /* Every plain .datetime: the time goes on its own line under the date, so
     date columns stay narrow and the rest of the row gets the width. */
  .datetime .dt-t { display: block; }

  /* Servers card: the collapsed head reflows to two rows (caret+id+badge+state on
     top, then IP · age · slot dots), the action trailing. The desktop grid columns
     are dropped so the items wrap. */
  .server-head {
    grid-template-columns: 18px 1fr auto;
    grid-template-rows: auto auto;
    row-gap: 6px;
    column-gap: 8px;
    align-items: center;
  }
  .server-caret { grid-column: 1; grid-row: 1; }
  .server-title { grid-column: 2; grid-row: 1; }
  .srv-state { grid-column: 3; grid-row: 1; justify-self: end; }
  .srv-ip { grid-column: 2 / span 2; grid-row: 2; }
  .srv-age { grid-column: 2 / span 2; grid-row: 3; }
  .server-slots { grid-column: 2 / span 2; grid-row: 4; justify-content: flex-start; }
  /* The action buttons get the full flexible width (cols 2–3) on their own row and may wrap to a
     second line only when the phone is genuinely too narrow for Kill + Decommission side by side —
     flex-wrap is flipped from the desktop nowrap so they never overflow the card (#1968). */
  .server-action { grid-column: 2 / span 2; grid-row: 5; flex-wrap: wrap; }

  /* Queue table → stacked cards (issue #494 acceptance: no horizontal scroll on a
     phone). Each row becomes a labelled card; the wrapping div's overflow-x is a
     no-op once the table is block-level and full width. */
  .queue-table thead { display: none; }
  .queue-table, .queue-table tbody { display: block; }
  /* ONE td per line. A SINGLE-column grid stacks every td vertically; the old
     `auto 1fr` gave two columns, so the tds paired up two-across (td1|td2, td3|td4,
     …) and the `auto` track grew to the widest cell (the long status chip, e.g.
     "waiting: overlaps #1092 on …"), squeezing the `1fr` column to ~1 char and
     running the card off the right edge (#494 regression). Each td already carries
     its own "LABEL value" line via ::before, so one per row is all we want. */
  .queue-table tr {
    display: grid;
    grid-template-columns: minmax(0,1fr);
    gap: 4px 12px;
    padding: 12px 14px;
  }
  /* The hidden step-panel row (`.queue-steps-row[hidden]`) must stay hidden: the
     grid display above (specificity 0,1,1) otherwise beats the `[hidden]` base
     reset (0,1,0) and leaks an empty card. Unfolded, its single colspan td is the
     one grid item, so it fills the column = the full card width. */
  .queue-table tr[hidden] { display: none; }
  /* Scope the card-cell reset + label to the data cells (which carry data-label):
     the colspan steps-row td has none, so it keeps its own padding and spans full
     width untouched. */
  .queue-table td[data-label] { padding: 0; min-width: 0; overflow-wrap: anywhere; }
  .queue-table td[data-label]::before {
    content: attr(data-label);
    display: inline-block;
    margin-right: 0.5rem;
    min-width: 6.5rem;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: #71717a;
  }
  /* Actions cell: the button group (a flex-wrap div) drops onto its own line at
     full card width, and each control is a ≥44 px touch target (the row buttons are
     only ~24 px tall by default). */
  .queue-table td[data-label="Actions"] > div { width: 100%; margin-top: 4px; }
  .queue-table td[data-label="Actions"] button {
    min-height: 44px;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
  }
}

/* Phone (≤ 640 px): the expanded server detail stacks — the health block drops
   below the slot list at full width, restoring the pre-two-column behaviour so a
   narrow viewport doesn't squeeze the slots into a sliver (no horizontal scroll). */
@media (max-width: 640px) {
  .server-detail { grid-template-columns: minmax(0, 1fr); }
  .srv-health { justify-self: stretch; margin-top: 8px; }
  /* A busy slot's session line (id, role, title, runtime) holds fixed-width items
     that do not fit beside the state and container at this width: give it its own
     line and let it wrap, instead of pushing the page wider than the viewport. */
  .slot-row { grid-template-columns: auto minmax(0, 1fr); row-gap: 2px; padding: 6px 0; }
  .slot-session { grid-column: 1 / -1; flex-wrap: wrap; }
}

/* ── Learn page (views/learn.ts) ─────────────────────────────────────────────
   Prose, the simulation player chrome, the frame animations and the page's own
   pictures (form, issue, signal cells). Examples rendered by the dashboard's view
   functions keep their own styles above. */
.learn { color: #d4d4d8; }
.learn-layout { display: block; }
.learn-body { min-width: 0; }
.learn-index ol { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 2rem; padding: 0; list-style: none; font-size: 0.875rem; }
.learn-index a { display: inline-flex; align-items: center; gap: 8px; padding: 4px 0; color: #a1a1aa; text-decoration: none; }
.learn-index a:hover { color: #22d3ee; }
.learn-num {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 1.5em; height: 1.5em; border-radius: 9999px; border: 1px solid #3f3f46;
  font-size: 0.7em; font-weight: 600; color: #a1a1aa; font-variant-numeric: tabular-nums;
}
@media (min-width: 1024px) {
  .learn-layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 2.5rem; align-items: start; }
  .learn-index { position: sticky; top: 5rem; }
  .learn-index ol { flex-direction: column; flex-wrap: nowrap; gap: 2px; margin: 0; }
}

/* The sticky nav would cover a heading reached through the index. */
.learn-section { margin-bottom: 4rem; scroll-margin-top: 5rem; }
.learn-h2 { display: flex; align-items: center; gap: 10px; margin: 0 0 0.5rem; font-size: 1.25rem; font-weight: 700; letter-spacing: -0.01em; color: #f4f4f5; }
.learn-h3 { margin: 1.5rem 0 0.5rem; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #a1a1aa; }
.learn-lead { max-width: 46rem; margin: 0 0 1.25rem; font-size: 1rem; line-height: 1.6; color: #a1a1aa; }

.learn-prose { max-width: 46rem; margin-top: 1.25rem; font-size: 0.9rem; line-height: 1.65; }
.learn-prose p { margin: 0 0 0.75rem; }
.learn-prose ul, .learn-prose ol { margin: 0 0 0.75rem; padding-left: 1.2rem; }
.learn-prose ul { list-style: disc; }
.learn-prose li { margin-bottom: 0.4rem; }
.learn-prose li::marker { color: #52525b; }
.learn-prose strong, .learn-caption strong { color: #f4f4f5; font-weight: 600; }
.learn-prose a { color: #22d3ee; }
.learn code {
  padding: 0.1em 0.35em; border-radius: 4px; background: #27272a; color: #e4e4e7;
  font: 0.85em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere;
}
.learn-prose ul.learn-legend { display: grid; gap: 8px; padding: 0; list-style: none; }
.learn-legend li { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); gap: 10px; align-items: baseline; margin: 0; }
.learn-legend .chip { justify-self: start; }
.learn-prose ol.learn-checklist { padding-left: 1.4rem; list-style: decimal; }
.learn-accuracy { padding: 0.6rem 0.8rem; border-left: 2px solid #3f3f46; border-radius: 4px; background: #18181b; color: #a1a1aa; }

/* Simulation player. */
.learn-sim { border: 1px solid #27272a; border-radius: 10px; background: #0f0f12; padding: 14px; }
.learn-sim-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.learn-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.learn-tab {
  padding: 5px 12px; border: 1px solid #3f3f46; border-radius: 9999px;
  font-size: 0.8rem; font-weight: 500; color: #a1a1aa; background: transparent; cursor: pointer;
}
.learn-tab:hover { color: #f4f4f5; border-color: #52525b; }
.learn-tab.is-active { color: #ecfeff; border-color: #06b6d4; background: rgba(6, 182, 212, 0.15); }
.learn-sim-controls { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.learn-play { min-width: 4.5rem; }
.learn-count { min-width: 3.5rem; text-align: right; font-size: 0.75rem; color: #71717a; font-variant-numeric: tabular-nums; }
.learn-tab:focus-visible, .learn-dot:focus-visible, .learn-sim-controls button:focus-visible { outline: 2px solid #22d3ee; outline-offset: 2px; }

.learn-scenario { display: none; }
.learn-scenario.is-active { display: block; }
.learn-dots { display: flex; gap: 4px; margin: 0 0 10px; padding: 0; list-style: none; }
.learn-dots li { flex: 1 1 0; max-width: 3rem; }
/* A thin bar with a taller hit area than it looks. */
.learn-dot { display: block; width: 100%; height: 14px; padding: 5px 0; border: 0; background: #3f3f46; background-clip: content-box; cursor: pointer; }
.learn-dot.is-active { background-color: #22d3ee; }

/* Every frame of a scenario sits in the same grid cell: the box is as tall as its
   tallest frame, so the page below does not jump when rows appear. Frames stretch to
   that height and are opaque, so the step fading in covers the one underneath
   completely. Moving to a step is a dissolve: the step being left stays fully
   visible (`is-was`, set by learn.js) while the next fades in on top of it. Nothing
   moves and no colour or shape is animated, so unchanged rows stay perfectly still. */
.learn-frames { display: grid; }
.learn-frame { grid-area: 1 / 1; min-width: 0; visibility: hidden; background: #0f0f12; }
.learn-frame.is-active { visibility: visible; z-index: 2; animation: learn-fade 0.6s ease-in-out both; }
.learn-frame.is-was { visibility: visible; z-index: 1; }
.learn-caption { max-width: 46rem; min-height: 4.2em; margin: 0 0 10px; font-size: 0.9rem; line-height: 1.5; color: #d4d4d8; }
.learn-caption-title { margin-right: 0.25em; font-weight: 700; color: #f4f4f5; }
/* `inert` on a captioned example already blocks interaction; pointer-events covers a
   browser without it. The .learn-scroll around it is NOT inert, so a touch falls
   through to it and a wide example can be scrolled sideways on a phone. */
.learn-mock { margin-top: 10px; pointer-events: none; }
.learn-scroll { overflow-x: auto; }
.learn-mock--inline { display: flex; align-items: center; gap: 8px; margin-top: 0; }

/* New since the previous step (views/learn-mock.ts tagChanges): a new row fades in a
   moment after the step, so it is seen arriving. The last one added also glows
   faintly and lets go. The glow is an inset shadow: it follows the row's own
   rounded corners and leaves its background alone. */
.learn-frame.is-active [data-learn-change="new"] { animation: learn-fade 0.45s ease-in-out 0.35s both; }
.learn-frame.is-active [data-learn-glow] { animation: learn-fade 0.45s ease-in-out 0.35s both, learn-glow 2.2s ease-in-out 0.5s both; }
.learn-frame.is-active tr[data-learn-glow] > td { animation: learn-glow 2.2s ease-in-out 0.5s both; }
@keyframes learn-fade { from { opacity: 0; } }
@keyframes learn-glow {
  0% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0); }
  20% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0.16); }
  55% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0.1); }
  100% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .learn-frame.is-active, .learn-frame.is-active [data-learn-change], .learn-frame.is-active [data-learn-glow],
  .learn-frame.is-active tr[data-learn-glow] > td { animation: none; }
}

/* Brief glow when a slot's issue `#N` link scrolls its pipeline group into view
   (public/sse.js adds .is-scroll-target, then removes it when the animation ends).
   An inset shadow so it follows the group's rounded corners and leaves layout alone. */
.session-group.is-scroll-target { animation: group-scroll-glow 1.8s ease-in-out both; }
@keyframes group-scroll-glow {
  0% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0); }
  18% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0.18); }
  55% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0.1); }
  100% { box-shadow: inset 0 0 0 999px rgba(34, 211, 238, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .session-group.is-scroll-target { animation: none; }
}

/* Pipeline stage track. */
.learn-track { display: flex; flex-wrap: wrap; gap: 8px 0; margin: 0; padding: 0; list-style: none; }
.learn-stage {
  display: inline-flex; align-items: center; padding: 6px 14px; border: 1px solid #3f3f46; border-radius: 9999px;
  font-size: 0.85rem; font-weight: 600; color: #71717a; background: #18181b;
}
.learn-stage:not(:last-child) { margin-right: 22px; position: relative; }
.learn-stage:not(:last-child)::after { content: "→"; position: absolute; right: -18px; color: #52525b; font-weight: 400; }
.learn-stage.is-past { color: #a1a1aa; border-color: #52525b; }
.learn-stage.is-active { color: #ecfeff; border-color: #06b6d4; background: rgba(6, 182, 212, 0.18); }
.learn-track--compact .learn-stage { padding: 2px 9px; font-size: 0.7rem; }
.learn-track--compact .learn-stage:not(:last-child) { margin-right: 16px; }
.learn-track--compact .learn-stage:not(:last-child)::after { right: -13px; }

/* Session state diagram. */
.learn-state { display: grid; gap: 16px; align-items: center; }
@media (min-width: 768px) { .learn-state { grid-template-columns: minmax(0, 520px) minmax(0, 1fr); } }
.learn-diagram { display: block; width: 100%; max-width: 520px; height: auto; }
/* Every frame's diagram defines the same marker id, so a line may resolve to the
   marker of a hidden frame; keep the arrowhead visible regardless of that frame. */
.learn-diagram marker path { fill: #52525b; visibility: visible; }
.learn-edge { stroke: #3f3f46; stroke-width: 1.2; }
.learn-edge--dashed { stroke-dasharray: 4 3; }
.learn-edge.is-active { stroke: #22d3ee; stroke-width: 2; }
.learn-node rect { fill: #18181b; stroke: #3f3f46; stroke-width: 1; }
.learn-node text { fill: #71717a; font-size: 11px; font-weight: 600; }
.learn-node[data-tone="act"] { --learn-tone: var(--tone-act); }
.learn-node[data-tone="work"] { --learn-tone: var(--tone-work); }
.learn-node[data-tone="pause"] { --learn-tone: var(--tone-pause); }
.learn-node[data-tone="fail"] { --learn-tone: var(--tone-fail); }
.learn-node[data-tone="done"] { --learn-tone: #a1a1aa; }
.learn-node.is-active rect { stroke: var(--learn-tone); stroke-width: 2; fill: #27272a; }
.learn-node.is-active text { fill: var(--learn-tone); }
.learn-node.is-active[data-tone="act"] rect { fill: var(--tone-act); }
.learn-node.is-active[data-tone="act"] text { fill: var(--tone-act-fg); }
.learn-facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 14px; margin: 0; font-size: 0.85rem; }
.learn-facts dt { color: #71717a; }
.learn-facts dd { margin: 0; color: #d4d4d8; }

.learn-waiting { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; min-height: 2rem; margin: 0 0 4px; font-size: 0.85rem; color: #a1a1aa; }
.learn-waiting-item { display: inline-flex; align-items: center; gap: 8px; color: #e4e4e7; }

/* Tables: reference, wait reasons, signals. */
.learn-table-wrap { overflow-x: auto; border: 1px solid #27272a; border-radius: 8px; }
.learn-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; line-height: 1.5; }
.learn-table th { padding: 8px 12px; text-align: left; font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #71717a; background: #18181b; }
.learn-table td { padding: 8px 12px; vertical-align: top; border-top: 1px solid #27272a; }
.learn-table tr:first-child td { border-top: 0; }
.learn-table thead + tbody tr:first-child td { border-top: 1px solid #27272a; }
.learn-table td:first-child { white-space: nowrap; }
.learn-table--signals td { color: #a1a1aa; }
.learn-table--signals td:nth-child(3) { color: #d4d4d8; }
.learn-table a { color: #22d3ee; }
.learn-ref-chip { width: 1%; }
.learn-ref-id { width: 1%; white-space: nowrap; }
.learn-signal { width: 1%; }

/* Project settings example, start cards, issue example. */
.learn-form { display: grid; gap: 14px; padding: 14px; border: 1px solid #27272a; border-radius: 8px; background: #18181b; }
@media (min-width: 768px) { .learn-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.learn-field { display: block; min-width: 0; }
.learn-field-value {
  display: block; padding: 8px 12px; border: 1px solid #3f3f46; border-radius: 6px; background: #27272a;
  color: #f4f4f5; overflow-wrap: anywhere;
}
.learn-field-label { display: block; margin-bottom: 4px; font-size: 0.875rem; color: #a1a1aa; }
.learn-field-hint { display: block; margin-top: 5px; font-size: 0.78rem; line-height: 1.45; color: #71717a; }
.learn-field-hint b { color: #a1a1aa; font-weight: 600; }
.learn-cards { display: grid; gap: 12px; }
@media (min-width: 768px) { .learn-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.learn-card { padding: 14px; border: 1px solid #27272a; border-radius: 8px; background: #18181b; font-size: 0.875rem; line-height: 1.55; }
.learn-card .learn-h3 { margin-top: 0; }
.learn-card p { margin: 0; }
.learn-card strong { color: #f4f4f5; font-weight: 600; }
.learn-issue { max-width: 46rem; padding: 14px; border: 1px solid #27272a; border-radius: 8px; background: #18181b; }
.learn-issue-title { margin: 0 0 8px; font-size: 1rem; font-weight: 600; color: #f4f4f5; }
.learn-issue-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font: 0.8rem/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #a1a1aa; }

/* Phone: three-column tables stack, one fact per line. */
@media (max-width: 640px) {
  .learn-table thead { display: none; }
  .learn-table tr { display: block; padding: 10px 12px; border-top: 1px solid #27272a; }
  .learn-table tr:first-child { border-top: 0; }
  .learn-table td, .learn-table thead + tbody tr:first-child td { display: block; width: auto; padding: 2px 0; border: 0; white-space: normal; }
  .learn-legend li { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .learn-mock--inline { flex-wrap: wrap; }
}
/* Below md a session row keeps its 760px minimum inside the group tree, which an
   inert example cannot scroll. Widen the whole group instead and let .learn-scroll
   scroll it, so every column stays reachable. */
@media (max-width: 767px) {
  .learn-scroll .session-group { min-width: 790px; }
  .learn-scroll .group-tree { overflow-x: visible; }
  .learn-sim { padding: 10px; }
  .learn-sim-controls { margin-left: 0; }
}

/* Deploy panel head row (#1969): the status text on the left (wraps, min-width:0) and a FIXED
   action area on the right (the countdown chip + the Deploy now / Hold deploy buttons). flex-wrap
   lets the action area drop below the text on a phone with no horizontal overflow. */
.deploy-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.4rem 0.75rem;
}
.deploy-status { flex: 1 1 16rem; min-width: 0; }
/* `flex: 0 1 auto` + `flex-wrap` so that when the action area drops below the text on a phone it
   can SHRINK to the row width and wrap its own contents (chip above, buttons below) instead of
   forcing horizontal overflow at 360px. On a wide screen there is room, so it neither shrinks nor
   wraps and the buttons stay put. `justify-content: flex-end` keeps it right-aligned. */
.deploy-actions-area { flex: 0 1 auto; flex-wrap: wrap; justify-content: flex-end; display: flex; align-items: center; gap: 0.5rem; }
/* The prominent countdown chip: an accent-coloured, tabular-nums pill with a FIXED min-width, so
   the ticking digits (sse.js tickDeployCountdown patches its textContent) never change its width
   and the buttons beside it never shift. The empty variant reserves the same width when no
   deploy / revert is scheduled, so the buttons also stay put as the countdown appears / disappears. */
.deploy-countdown-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 9rem;
  padding: 0.15rem 0.7rem;
  border-radius: 9999px;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #67e8f9;
  background: #164e5b33;
  border: 1px solid #0e7490;
}
.deploy-countdown-chip--empty { background: transparent; border-color: transparent; }

/* Deploy panel phase checklist (epic #807, 2/6). The whole block is server-rendered inside the
   `deploy:status` fragment (views/dashboard.ts deployChecklist) and morphed in place; app.css
   carries the styles the fragment relies on. */
.deploy-run { margin-top: 0.5rem; }
.deploy-run-head {
  display: flex; align-items: center; gap: 0.4rem; cursor: pointer;
  user-select: none; font-variant-numeric: tabular-nums;
}
.deploy-run-head:focus-visible { outline: 2px solid #52525b; outline-offset: 2px; border-radius: 3px; }
.deploy-run-caret { display: inline-block; width: 1em; color: #71717a; flex: none; }
/* The header status emoji gets its own fixed cell, the SAME 1.2em as .deploy-phase-icon, so it sits
   in one vertical column with every phase icon below (which the checklist is indented to line up on). */
.deploy-run-icon { flex: none; width: 1.2em; text-align: center; }
.deploy-run-summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Smooth collapse/expand with no instant snap: a single-row grid whose row animates
   0fr <-> 1fr together with opacity, driven by data-open. Because idiomorph patches data-open in
   place (it is not a preserved attribute), the transition runs when the SERVER collapses the panel
   on completion too, not only on a viewer click. The inner element needs overflow:hidden and
   min-height:0 for the 0fr row to actually clip it to zero. */
.deploy-checklist-wrap {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows 300ms ease, opacity 300ms ease;
}
.deploy-checklist-wrap[data-open="1"] { grid-template-rows: 1fr; opacity: 1; }
/* One grid shared by every row, read as a log line: start time · icon · text cell. Each li is a
   subgrid row spanning all three columns (display:grid + grid-template-columns:subgrid), so times
   and icons each form one vertical column and the flexible text cell (label + inline elapsed +
   detail/strip/links) takes the rest. The elapsed rides INSIDE that text cell right after the label
   (not a far-right column — #1129's "elapsed last" pushed it to the panel edge), and a skipped step
   is an ordinary row (a real ⊘ icon + inline "· skipped (reason)"), never a dash sub-item. The whole
   checklist is indented by the header caret's width + gap (calc(1em + 0.4rem)) so it hangs under the
   header. overflow:hidden + min-height:0 keep the 0fr collapse clipping the grid to zero. */
.deploy-checklist-inner {
  overflow: hidden; min-height: 0; list-style: none;
  margin: 0.25rem 0 0 calc(1em + 0.4rem); padding: 0;
  display: grid;
  grid-template-columns: 3rem 1.2em minmax(0, 1fr);
  column-gap: 0.5rem;
}
.deploy-phase {
  display: grid; grid-column: 1 / -1; grid-template-columns: subgrid;
  align-items: baseline; padding: 0.1rem 0;
  font-variant-numeric: tabular-nums;
}
.deploy-phase-icon { width: 1.2em; text-align: center; }
.deploy-phase-time { color: #71717a; }
.deploy-elapsed { color: #a1a1aa; flex: none; }
/* The single text cell: label, then the inline elapsed, then the detail + its optional build-step
   strip and ↗ / log links, all left-aligned on one line. The detail text alone truncates with an
   ellipsis at the cell's right edge; the label, elapsed and links stay visible (flex:none). */
.deploy-phase-text { display: flex; align-items: baseline; gap: 0.35rem; min-width: 0; }
.deploy-phase-label { flex: none; }
.deploy-phase-detail { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #71717a; }
.deploy-phase-link { flex: none; }
/* A step that did not run (pending or skipped) dims to the same tone — a skipped step is an ordinary
   row this deploy type skipped, NOT a dimmer sub-item, so it carries no extra opacity. */
.deploy-phase[data-state="pending"],
.deploy-phase[data-state="skipped"] { opacity: 0.55; }

/* Self-built image build progress strip (#1078 part 2): create → build → test → push, shown in
   the Building phase. The current step is highlighted, earlier ones dimmed-but-done, later ones
   faint. */
.deploy-build-steps { display: inline-flex; gap: 0.25rem; flex: none; }
.deploy-build-step {
  font-size: 0.72em; line-height: 1.4; padding: 0 0.3rem; border-radius: 3px;
  border: 1px solid #3f3f46; color: #71717a; text-transform: lowercase;
}
.deploy-build-step[data-state="done"] { color: #a1a1aa; border-color: #52525b; }
.deploy-build-step[data-state="current"] { color: #67e8f9; border-color: #0e7490; background: #164e5b33; }
.deploy-build-step[data-state="pending"] { opacity: 0.5; }
.deploy-phase-log { font-size: 0.85em; }

/* Running-phase spinner (the checklist's "spinner running" glyph). Mirrors .spinner but sized
   for the row's inline icon cell. */
.deploy-spinner {
  display: inline-block; width: 0.75em; height: 0.75em; vertical-align: -0.02em;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 9999px;
  animation: spin 0.6s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .deploy-checklist-wrap { transition: none; }
  .deploy-spinner { animation: none; }
}

/* Work Up-next "+ N more" in-place reveal (#3531). Every queued entry is server-rendered into the
   Up-next table; the ones past the first page carry .work-next-extra and stay hidden until the viewer
   expands the .work-next-more button. Visibility is driven PURELY by that button's aria-expanded via
   :has() — no client markup is built (#586) — and aria-expanded is in morph's PRESERVE_ATTRS, so a
   live work-next morph (a queue tick) keeps the reveal open instead of re-collapsing it. The
   :not([hidden]) guard leaves a row the search/project filter hid filtered-out even while expanded. */
.work-next-extra { display: none; }
.work-next-table:has(.work-next-more[aria-expanded="true"]) .work-next-extra:not([hidden]) { display: table-row; }
.work-next-more { cursor: pointer; background: none; border: 0; padding: 0; }
.work-next-more[aria-expanded="false"] .work-next-more-hide { display: none; }
.work-next-more[aria-expanded="true"] .work-next-more-show { display: none; }

/* Inbox status cell (views/inbox/row.ts): a long reason chip wraps inside its fixed track instead of
   running into the action column. Unlayered on purpose — `.chip { nowrap }` above would beat a
   layered Tailwind utility. */
.inbox-cell-status .chip { white-space: normal; }

/* Learn "When it needs you" pictures render a real Inbox row (views/inbox/row.ts) inside a
   shrink-to-fit `.learn-signal` cell. The six fixed desktop tracks would leave the title track 0px
   there, so pin these pictures to the narrow layout: project · # · title, the rest under the title. */
.learn-signal .work-needs-row { grid-template-columns: 88px 44px minmax(12rem, 1fr); align-items: start; }
.learn-signal .work-needs-row > :nth-child(n+4) { grid-column-start: 3; justify-content: flex-start; text-align: left; }
.learn-signal .work-needs-row > :nth-child(n+4):empty { display: none; }
.learn-signal .work-needs-row .inbox-cell-title { white-space: normal; }
