/* ============================================================
   Todo++ — Warm Industrial Dark
   Deep brown-blacks, amber copper, aged earth tones.
   A dark UI that feels warm, not cold.
   ============================================================ */

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  color-scheme: dark;

  /* Warm dark palette — brown-tinted, never blue-gray */
  --bg:              oklch(0.145 0.01 55);
  --surface:         oklch(0.185 0.012 55);
  --surface-hover:   oklch(0.22 0.014 55);
  --surface-active:  oklch(0.26 0.016 52);
  --border:          oklch(0.27 0.014 55);
  --border-strong:   oklch(0.35 0.018 55);

  --text:            oklch(0.88 0.012 70);
  --text-secondary:  oklch(0.68 0.02 65);
  --text-muted:      oklch(0.56 0.018 60);

  /* Accent — warm amber copper */
  --accent:          oklch(0.74 0.17 65);
  --accent-hover:    oklch(0.79 0.18 65);
  --accent-dim:      oklch(0.74 0.17 65 / 0.14);
  --accent-text:     oklch(0.15 0.02 55);

  /* Status — vivid pigments */
  --status-backlog:       oklch(0.58 0.06 60);
  --status-todo:          oklch(0.68 0.16 250);
  --status-in-progress:   oklch(0.75 0.18 75);
  --status-blocked:       oklch(0.65 0.20 28);
  --status-done:          oklch(0.65 0.16 155);
  --status-rejected:      oklch(0.55 0.18 0);

  /* A merged PR (TODO-214). Its own variable rather than a reused status colour
     because it means something none of them do: not done, not blocked, landed.
     Purple is the convention GitHub itself uses, and nothing else in this
     palette occupies that hue. Defined in exactly the two blocks the status
     colours are — here and the shared light-theme block — since the five dark
     themes never override status colours and inherit this unchanged. */
  --pr-merged:            oklch(0.68 0.16 305);

  --status-backlog-bg:    oklch(0.58 0.06 60 / 0.08);
  --status-todo-bg:       oklch(0.68 0.16 250 / 0.08);
  --status-progress-bg:   oklch(0.75 0.18 75 / 0.08);
  --status-blocked-bg:    oklch(0.65 0.20 28 / 0.08);
  --status-done-bg:       oklch(0.65 0.16 155 / 0.08);
  --status-rejected-bg:   oklch(0.55 0.18 0 / 0.08);

  /* Inline error / danger text, readable on dark surfaces */
  --danger-text: oklch(0.68 0.17 27);

  /* A FILLED danger surface with text on top of it (TODO-106's YOLO send).
     Distinct from --status-blocked, which at L 0.65 is a fine border and glyph
     colour but far too light to carry white text. Only two blocks define these:
     here and the shared light-theme block — the five dark themes never override
     status colours, so they inherit this pair unchanged. */
  --danger-fill:       oklch(0.58 0.19 27);
  --danger-fill-hover: oklch(0.64 0.19 27);
  --danger-fill-text:  oklch(0.97 0.01 70);

  /* Priority */
  --priority-high:   oklch(0.65 0.22 25);
  --priority-medium: oklch(0.74 0.17 70);
  --priority-low:    oklch(0.55 0.04 60);

  /* Shadows & overlays */
  --dropdown-shadow: 0 8px 24px oklch(0.05 0.01 55 / 0.45),
                     0 2px 8px oklch(0.05 0.01 55 / 0.25);
  --modal-shadow:    0 20px 60px oklch(0.05 0.01 55 / 0.5),
                     0 4px 20px oklch(0.05 0.01 55 / 0.3);
  --modal-backdrop:  oklch(0.08 0.01 55 / 0.65);
  /* Docked-panel edge: a RECESSED SEAM — two hard shadow rules laid just outside
     the panel carve a dark trench into the board, and the panel's own fill is
     lifted above it (see --panel-* below). The old treatment was an inner bevel
     plus a soft cast, which had almost nothing to land on: --bg (0.145) and
     --surface (0.185) sit 0.04 apart, so a blurred shadow between them was
     invisible and the 1px --border-strong line was doing all the work alone.

     The trench is ALPHA, not opaque. It paints over whatever the board has at
     that x — usually --bg, but a card can sit flush against the gutter at some
     column widths, and an opaque trench would band across it at the wrong
     lightness. Alpha darkens both by the same ratio instead. */
  --panel-shadow:    -1px 0 0 oklch(0.05 0.01 55 / 0.55),
                     -4px 0 0 oklch(0.05 0.01 55 / 0.30),
                     -24px 0 48px oklch(0.05 0.01 55 / 0.55);

  /* The other half of the seam: the docked panel's own surface ramp, lifted one
     step above the board's so the chat reads as a plane resting on it rather
     than a region of it. This is the ROOT CAUSE fix — `.chat-threads` paints
     `var(--bg)`, the board's exact background colour, so the panel's leftmost
     176px was literally indistinguishable from what it sits on. No amount of
     shadow tuning reaches that; the fill has to move.

     Derived with color-mix rather than written out per theme: six dark themes
     define their own --bg/--surface ramp, and hand-tuning six more copies of it
     is six more chances to drift. Custom properties substitute at the element
     they are DECLARED on, so these resolve here against whichever theme block
     last set --bg — and .chat-shell inherits the already-substituted result,
     which is why remapping --bg there is not a cycle.

     7% white is +0.06 L at the dark end, the measured distance at which the
     board stops reading through the panel. Applied to the whole ramp, not just
     --bg: lift the fills alone and --surface-active (0.26) lands BELOW the
     lifted --surface-hover (0.285), so pressing a row would make it darker than
     hovering it. */
  --panel-bg:             color-mix(in oklab, var(--bg), oklch(1 0 0) 7%);
  --panel-surface:        color-mix(in oklab, var(--surface), oklch(1 0 0) 7%);
  --panel-surface-hover:  color-mix(in oklab, var(--surface-hover), oklch(1 0 0) 7%);
  --panel-surface-active: color-mix(in oklab, var(--surface-active), oklch(1 0 0) 7%);
  --panel-border:         color-mix(in oklab, var(--border), oklch(1 0 0) 7%);
  --panel-border-strong:  color-mix(in oklab, var(--border-strong), oklch(1 0 0) 7%);

  /* The lit edge itself, brighter than the lifted ramp so the trench has a
     highlight to read against. */
  --panel-edge:           color-mix(in oklab, var(--border-strong), oklch(1 0 0) 15%);
  --accent-dim-hover: oklch(0.74 0.17 65 / 0.22);

  /* Typography */
  --font-display: 'Newsreader', Georgia, 'Times New Roman', serif;
  --font-body: 'Instrument Sans', -apple-system, 'Segoe UI', Helvetica, sans-serif;
  --font-mono: 'Consolas', 'SF Mono', ui-monospace, monospace;

  /* Layout */
  --header-h: 56px;
  /* Height of the preview strip. Goes into header's sticky top and into
     applyFit()'s --header-actual-h sum, and NOWHERE else — writing it into
     --header-h is exactly the latching feedback loop --header-actual-h exists
     to avoid. */
  --preview-h: 24px;
  /* What the header ACTUALLY measures, republished by applyFit() — normally
     --header-h, but taller whenever a narrow window wraps it onto more rows.
     Deliberately a SECOND variable: `header` sizes itself from --header-h, so
     writing a measurement back into that one is a feedback loop. It latches —
     wrap the header once at a narrow width and the measured height becomes the
     height, which measures the same, forever. Anything positioned BELOW the
     header reads this; the header itself must never read it (TODO-169). */
  --header-actual-h: var(--header-h);

  /* Spacing */
  --space-2xs: 2px;
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 40px;

  /* Motion */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
  --dur-fast: 100ms;
  --dur-normal: 180ms;
  --dur-slow: 350ms;
}

body {
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

::selection {
  background: var(--accent-dim);
}

/* ============================================================
   Custom Scrollbars — Global
   ============================================================ */

/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

/* Webkit / Blink */
::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-track {
  background: transparent;
  border-radius: 3px;
}

::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
  transition: background 0.2s;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--border-strong);
}

::-webkit-scrollbar-corner {
  background: transparent;
}

/* ============================================================
   Header
   ============================================================ */

/* Ephemeral per-PR preview builds carry an unmissable strip so the board is
   never mistaken for the real one. IN FLOW and sticky, never fixed: applyFit()
   adds its height to --header-actual-h, which .board's height and
   .chat-panel's top are both offsets from, so a strip that took no flow space
   would push the board a strip's height past the bottom of the window and slide
   the chat panel under the header. Hidden entirely unless the server said this
   is a preview. */
.preview-banner { display: none; }
body.preview-mode.preview-dismissed .preview-banner { display: none; }
/* Dismissing the strip must not dismiss the WARNING (TODO-327). What is left is
   a hairline in the same accent along the very top edge — no height worth
   speaking of, no hit area, nothing to lay out around, and still impossible to
   mistake a preview build for production at a glance. It is fixed rather than
   sticky because with the strip gone there is no flow slot to sit in, and it
   has to stay put over a full-screen sheet. */
body.preview-mode.preview-dismissed::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--accent);
  z-index: 3000;
  pointer-events: none;
}
body.preview-mode .preview-banner {
  display: flex;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  position: sticky;
  top: 0;
  /* Above the header's 95: the strip is chrome over app chrome. */
  z-index: 96;
  height: var(--preview-h);
  background: var(--accent);
  color: #1c1814;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The header sticks BELOW the strip rather than underneath it. Sticky top
   only — the header's own height, and therefore --header-h, is untouched. */
body.preview-mode header { top: var(--preview-h); }
/* The header escapes the docked-chat gutter with a negative margin (see
   body.chat-docked header); the strip is chrome at the same level and needs the
   identical escape, or it stops dead at the panel's edge. */
body.preview-mode.chat-docked .preview-banner {
  margin-right: calc(-1 * var(--chat-dock-w, 0px));
  transition: margin-right var(--dur-normal) var(--ease-out-expo);
}
/* A drag sets the width every frame; a transition would fight it all the way. */
body.chat-resizing .preview-banner { transition: none; }
/* The pop-out window IS the chat, and applyFit() returns early for it, so
   nothing would compensate for a strip taking flow space there. */
body.is-popout .preview-banner { display: none !important; }

header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-lg);
  height: var(--header-h);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* Pinned to the top of the window, on every view (TODO-174). It got away
     with being in normal flow only while NO view scrolled the document: .board
     is capped at 100dvh - --header-actual-h and its columns scroll inside. The
     `stack` tier broke that — body.board-stacked .m-board has no height cap, so
     the document scrolls — and .chat-panel is position: fixed, so the nav bar
     slid away while the panel stayed, leaving an empty band exactly one header
     tall above it. Projects, project detail and Settings scroll the document
     too, so the fix belongs here rather than on any one view.

     STICKY, NOT FIXED. Sticky keeps the header in flow, so applyFit()'s
     measurement of offsetHeight into --header-actual-h, .board's height and
     .chat-panel's top all keep working untouched, and on the views that do not
     scroll it is a visual no-op. position: fixed would take it out of flow and
     make every one of those an offset from an element that no longer occupies
     any space.

     z-index 95: above .chat-panel (90) — the header is meant to span the whole
     window over the top of the panel (see body.chat-docked header) — and below
     .modal (100). This does make the header a stacking context, so the project
     filter's dropdown (z-index 200) is now scoped to it: still above the board
     and the panel, below modals and toasts, which is the right order. */
  position: sticky;
  top: 0;
  z-index: 95;
}

.header-left {
  display: flex;
  align-items: center;
  gap: var(--space-xl);
}

.brand {
  font-family: var(--font-display);
  font-size: clamp(18px, 2vw, 22px);
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--text);
  font-style: italic;
}

.brand-accent {
  color: var(--accent);
  font-style: normal;
  font-weight: 700;
}

.header-sep {
  width: 1px;
  height: 20px;
  background: var(--border);
  flex-shrink: 0;
}

.header-left .custom-select {
  width: auto;
  min-width: 130px;
}

/* ============================================================
   Project picker — the board's multi-select project filter (TODO-133)

   Deliberately its own control rather than a CustomSelect variant: that class
   is keyed off a <select>'s single value. The visual language is borrowed on
   purpose, so the header still reads as one family of controls.
   ============================================================ */

.project-picker {
  position: relative;
  display: inline-flex;
  flex: none;
}

.project-picker-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 150px;
  max-width: 240px;
  padding: 8px 12px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: var(--font-body);
  font-size: 14px;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  transition: border-color var(--dur-fast), background var(--dur-fast);
}

.project-picker-trigger:hover {
  border-color: var(--border-strong);
  background: var(--surface);
}

.project-picker.open .project-picker-trigger {
  border-color: var(--accent);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--accent-dim);
  transition: border-color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-normal) var(--ease-out);
}

.project-picker-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.project-picker-chevron {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  transition: transform var(--dur-normal) var(--ease-out);
}

.project-picker.open .project-picker-chevron {
  transform: rotate(180deg);
  color: var(--accent);
}

/* Stacked colour dots. They are what makes "3 projects" mean something without
   opening the panel, so they overlap slightly rather than eating trigger width. */
.project-picker-dots {
  display: inline-flex;
  flex: none;
  align-items: center;
}

.project-picker-dots:empty { display: none; }

.project-picker-dots .project-picker-dot {
  margin-right: -3px;
  border: 1.5px solid var(--bg);
}

.project-picker.open .project-picker-dots .project-picker-dot,
.project-picker-trigger:hover .project-picker-dot {
  border-color: var(--surface);
}

.project-picker-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}

/* position: fixed for the same reason CustomSelect's dropdown is — the header
   is a stacking context (z-index 95) and the panel has to escape it. */
.project-picker-panel {
  position: fixed;
  display: flex;
  flex-direction: column;
  max-height: 60vh;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 4px;
  z-index: 200;
  box-shadow: var(--dropdown-shadow);
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
  pointer-events: none;
  transition: opacity var(--dur-normal) var(--ease-out),
              transform var(--dur-normal) var(--ease-out);
}

.project-picker.open .project-picker-panel {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

.project-picker-search {
  position: relative;
  flex: none;
  padding: 4px 4px 6px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}

.project-picker-search svg {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-60%);
  color: var(--text-muted);
  pointer-events: none;
}

/* input[type="text"] rather than plain `input`: the shared form-control rule
   further down this file is `select, input[type="text"], textarea`, which ties
   this selector on specificity and would win on source order — silently
   restoring its own 8px 12px padding and dropping the icon on top of the text. */
.project-picker-search input[type="text"] {
  width: 100%;
  padding: 7px 10px 7px 32px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 13px;
  outline: none;
  transition: border-color var(--dur-fast), box-shadow var(--dur-normal) var(--ease-out);
}

.project-picker-search input[type="text"]:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}

.project-picker-list {
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}

.project-picker-option {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--text-secondary);
  border-radius: 4px;
  cursor: pointer;
  outline: none;
  transition: background var(--dur-fast), color var(--dur-fast);
}

.project-picker-option:hover,
.project-picker-option:focus {
  background: var(--surface-hover);
  color: var(--text);
}

.project-picker-option.selected {
  color: var(--text);
}

.project-picker-option.selected:hover,
.project-picker-option.selected:focus {
  background: var(--surface-active);
}

.project-picker-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Narrow to this one project without clearing first (TODO-291). Hidden at rest
   so a resting list stays a plain list of names, and revealed by hover/focus on
   the row — `opacity`, not `display`, because a control that appears and takes
   up width would shove the label sideways under the cursor. Where there is no
   hover to reveal it — touch, and the mobile sheet's own row action — it stays
   visible. */
.project-picker-only {
  flex: none;
  background: none;
  border: none;
  padding: 2px 5px;
  margin-right: -3px;
  border-radius: 3px;
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-fast), background var(--dur-fast), color var(--dur-fast);
}

.project-picker-option:hover .project-picker-only,
.project-picker-option:focus .project-picker-only,
.project-picker-option:focus-within .project-picker-only,
.project-picker-only:focus-visible {
  opacity: 1;
}

.project-picker-only:hover {
  background: var(--accent-dim);
  color: var(--accent-hover);
}

@media (hover: none) {
  .project-picker-only { opacity: 1; }
}

.project-picker-box {
  width: 15px;
  height: 15px;
  flex: none;
  border-radius: 4px;
  border: 1px solid var(--border-strong);
  display: grid;
  place-items: center;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}

.project-picker-box svg {
  width: 10px;
  height: 10px;
  opacity: 0;
  color: var(--accent-text);
}

.project-picker-option.selected .project-picker-box {
  background: var(--accent);
  border-color: var(--accent);
}

.project-picker-option.selected .project-picker-box svg { opacity: 1; }

/* "All Projects" has no colour dot, so its label needs the dot's width back to
   stay in the same text column as every project below it. */
.project-picker-all .project-picker-name { margin-left: 18px; }

.project-picker-sep {
  height: 1px;
  background: var(--border);
  margin: 5px 6px;
}

.project-picker-empty {
  padding: 12px 10px;
  font-size: 13px;
  color: var(--text-muted);
  text-align: center;
}

.project-picker-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex: none;
  gap: var(--space-md);
  padding: 7px 10px 3px;
  margin-top: 3px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
}

.project-picker-clear {
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--accent);
  cursor: pointer;
}

.project-picker-clear:hover { color: var(--accent-hover); }
.project-picker-clear.hidden { display: none; }

.header-right {
  display: flex;
  gap: 6px;
  align-items: center;
}

/* ============================================================
   Tabs
   ============================================================ */

.view-tabs {
  display: flex;
  gap: 0;
}

.tab {
  position: relative;
  padding: 6px 16px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: color var(--dur-normal) var(--ease-out);
}

.tab::after {
  content: '';
  position: absolute;
  bottom: -1px;
  left: 14px;
  right: 14px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out-expo);
}

.tab:hover { color: var(--text-secondary); }
.tab.active { color: var(--text); }
.tab.active::after { transform: scaleX(1); }

/* ============================================================
   Buttons
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  padding: 7px 16px;
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
  white-space: nowrap;
}

.btn:active {
  transform: scale(0.97);
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}
.btn:disabled:active {
  transform: none;
}

.btn-accent {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}
.btn-accent:hover:not(:disabled) {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
  border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
  color: var(--text);
  background: var(--surface-hover);
}

.btn-danger {
  background: transparent;
  color: var(--status-blocked);
  border-color: var(--status-blocked);
}
.btn-danger:hover:not(:disabled) {
  background: oklch(0.65 0.20 28 / 0.15);
  border-color: oklch(0.70 0.22 28);
  color: oklch(0.72 0.22 28);
}
.btn-danger:active {
  transform: scale(0.95);
  background: oklch(0.65 0.20 28 / 0.22);
}

/* ============================================================
   Form Controls
   ============================================================ */

select,
input[type="text"],
textarea {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 8px 12px;
  font-family: var(--font-body);
  font-size: 14px;
  width: 100%;
  transition: border-color var(--dur-fast);
}

/* Hide native selects once custom dropdowns are active */
select.custom-select-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
}

select:focus,
input:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}

input:focus,
textarea:focus {
  transition: border-color var(--dur-fast), box-shadow var(--dur-normal) var(--ease-out);
}

/* ============================================================
   Custom Select Dropdown
   ============================================================ */

.custom-select {
  position: relative;
  display: inline-flex;
  width: 100%;
}

.list-toolbar .custom-select {
  width: auto;
  min-width: 130px;
}

.btn-icon {
  padding: 7px 8px;
  line-height: 0;
}

.btn-icon svg {
  display: block;
}

.btn-icon.active {
  color: var(--accent);
  background: var(--accent-dim);
}

.custom-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 8px 12px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: var(--font-body);
  font-size: 14px;
  cursor: pointer;
  transition: border-color var(--dur-fast), background var(--dur-fast);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
}

.custom-select-trigger:hover {
  border-color: var(--border-strong);
  background: var(--surface);
}

.custom-select.open .custom-select-trigger {
  border-color: var(--accent);
  background: var(--surface);
  box-shadow: 0 0 0 3px var(--accent-dim);
  transition: border-color var(--dur-fast), background var(--dur-fast), box-shadow var(--dur-normal) var(--ease-out);
}

.custom-select-value {
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1;
  min-width: 0;
}

.custom-select-chevron {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  transition: transform var(--dur-normal) var(--ease-out);
}

.custom-select.open .custom-select-chevron {
  transform: rotate(180deg);
  color: var(--accent);
}

.custom-select-dropdown {
  position: fixed;
  max-height: 220px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 4px;
  z-index: 120;
  box-shadow: var(--dropdown-shadow);
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
  pointer-events: none;
  transition: opacity var(--dur-normal) var(--ease-out),
              transform var(--dur-normal) var(--ease-out);
}

.custom-select.open .custom-select-dropdown {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

/* Drop-up direction handled via JS with fixed positioning */

.custom-select-option {
  padding: 7px 10px;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--text-secondary);
  border-radius: 4px;
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.custom-select-option:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.custom-select-option.selected {
  background: var(--accent-dim);
  color: var(--accent);
  font-weight: 500;
}

/* Optional second column / second line (TODO-106). Only options carrying
   data-meta or data-desc get these; every other select in the app still renders
   a plain text row. */
.custom-select-option:has(.custom-select-option-meta) {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
}

.custom-select-option-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.custom-select-option-meta {
  flex: none;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  color: var(--text-muted);
}

/* A description needs to wrap, so this row stops being a single nowrap line. */
.custom-select-option-rich {
  display: flex;
  flex-direction: column;
  gap: 2px;
  white-space: normal;
  max-width: 268px;
}

.custom-select-option-desc {
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--text-muted);
}

.custom-select-option:hover .custom-select-option-meta,
.custom-select-option:hover .custom-select-option-desc {
  color: var(--text-secondary);
}

.custom-select-option.selected:hover {
  background: var(--accent-dim-hover);
}

.custom-select-dropdown {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}

textarea { resize: vertical; }

label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: var(--space-xs);
  margin-top: var(--space-md);
  transition: color var(--dur-normal) var(--ease-out);
}

/* Label highlights when its next sibling input/select/editor is focused */
label:has(+ input:focus),
label:has(+ .rte-wrap:focus-within),
label:has(+ select:focus),
label:has(+ .custom-select .custom-select-trigger:focus) {
  color: var(--accent);
}

/* ============================================================
   View System
   ============================================================ */

main { display: none; }
main.view-active { display: grid; }

/* ============================================================
   Board
   ============================================================ */

.board {
  /* One track definition, four :has() arrangements, and the fit tiers below
     retune the track alone — so a collapsed-column layout cannot fall out of
     sync with a narrower board (TODO-169). */
  --col-track: 1fr;
  grid-template-columns: repeat(5, var(--col-track));
  gap: 0;
  /* --header-actual-h, not --header-h: a narrow window wraps the header onto
     more rows, and a hardcoded 56px would overflow the viewport by exactly the
     extra rows (TODO-169). */
  height: calc(100vh - var(--header-actual-h));
  height: calc(100dvh - var(--header-actual-h));
  overflow: hidden;
}

/* One collapsible column collapsed */
.board:has(.column[data-status="backlog"].collapsed):not(:has(.column[data-status="blocked"].collapsed)) {
  grid-template-columns: 44px repeat(4, var(--col-track));
}
.board:has(.column[data-status="blocked"].collapsed):not(:has(.column[data-status="backlog"].collapsed)) {
  grid-template-columns: repeat(3, var(--col-track)) 44px var(--col-track);
}
/* Both collapsed */
.board:has(.column[data-status="backlog"].collapsed):has(.column[data-status="blocked"].collapsed) {
  grid-template-columns: 44px repeat(2, var(--col-track)) 44px var(--col-track);
}

/* Fit tiers (TODO-169). Keyed to body[data-fit] — the AVAILABLE width, which a
   media query cannot see once the chat panel is docked beside the board. The
   columns each tier auto-collapses live in FIT_AUTO_COLLAPSE in app.js; these
   rules only size what is left. */
body[data-fit="tight"] .board {
  --col-track: minmax(170px, 1fr);
  overflow-x: auto;
}
body[data-fit="narrow"] .board {
  --col-track: minmax(150px, 1fr);
  overflow-x: auto;
}

.column {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: var(--space-md) 0 var(--space-md);
  position: relative;
  overflow: hidden;
}

/* Thin warm dividers between columns */
.column + .column::before {
  content: '';
  position: absolute;
  left: 0;
  top: var(--space-xl);
  bottom: var(--space-xl);
  width: 1px;
  background: var(--border);
}

.column-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: 0 var(--space-md) var(--space-md);
}

.column-header h2 {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 500;
  font-style: italic;
  letter-spacing: 0.01em;
}

/* Tint column headers by status */
.column[data-status="backlog"] .column-header h2     { color: var(--status-backlog); }
.column[data-status="todo"] .column-header h2        { color: var(--status-todo); }
.column[data-status="in-progress"] .column-header h2 { color: var(--status-in-progress); }
.column[data-status="blocked"] .column-header h2     { color: var(--status-blocked); }
.column[data-status="done"] .column-header h2        { color: var(--status-done); }

/* Collapse toggle button */
.collapse-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm, 4px);
  flex-shrink: 0;
  transition: color 0.15s, background 0.15s, transform 0.2s;
}
.collapse-toggle:hover {
  color: var(--text);
  background: var(--border);
}
.collapse-toggle svg {
  transition: transform 0.2s;
}

/* Collapsed backlog column */
.column.collapsed .card-list {
  display: none;
}
.column.collapsed .column-header {
  flex-direction: column;
  align-items: center;
  padding: var(--space-sm) 0;
  gap: var(--space-xs, 4px);
  height: 100%;
}
.column.collapsed .column-header h2 {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  font-size: 13px;
  white-space: nowrap;
}
.column.collapsed .column-header .count {
  font-size: 11px;
}
.column.collapsed .collapse-toggle svg {
  transform: rotate(90deg);
}
.column:not(.collapsed) .collapse-toggle svg {
  transform: rotate(0deg);
}

/* Per-column sort control — injected into each header by initColumnSorts(),
   then decorated by decorateColumnSorts(). At rest it is only the sort glyph;
   the label unfurls on hover, focus, or while the menu is open. */
.column-header .custom-select {
  width: auto;
  margin-left: auto;
  align-self: center;
}
.column-header .custom-select-trigger {
  gap: 5px;
  padding: 3px 6px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: transparent;
  border-color: transparent;
  border-radius: 4px;
}
.column-header .custom-select-trigger:hover {
  color: var(--text-secondary);
  background: var(--surface-hover);
  border-color: transparent;
}
.column-header .custom-select.open .custom-select-trigger {
  color: var(--text);
  background: var(--surface);
  border-color: var(--accent);
}
/* The chevron is redundant once the glyph carries the meaning. */
.column-header .custom-select-chevron {
  display: none;
}
.column-header .sort-glyph {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}
.column-header .sort-glyph.asc {
  transform: scaleY(-1);
}
/* Glyph alone at rest; the label appears only on hover/focus/open. */
.column-header .custom-select-value {
  display: none;
  flex: 0 1 auto;
  max-width: 130px;
}
.column-header .custom-select-trigger:hover .custom-select-value,
.column-header .custom-select-trigger:focus .custom-select-value,
.column-header .custom-select.open .custom-select-value {
  display: block;
}
/* The collapsed header is a vertical stack — there is no room for it there. */
.column.collapsed .column-header .custom-select {
  display: none;
}

.count {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.card-list {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--space-sm);
  scroll-behavior: smooth;
  contain: layout style;
}

/* scrollbar inherited from global styles */

/* Drop target */
.column.drag-over {
  background: var(--accent-dim);
}

/* ============================================================
   Cards
   ============================================================ */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 12px 14px;
  margin-bottom: 6px;
  cursor: grab;
  transition: transform var(--dur-normal) var(--ease-out),
              border-color var(--dur-fast),
              opacity var(--dur-normal);
  contain: layout style;
}

.card:hover {
  border-color: var(--border-strong);
  transform: translateY(-1px);
}

.card:active {
  cursor: grabbing;
  transform: scale(0.98);
}

.card.dragging { opacity: 0.25; }

.card-title {
  font-size: 14px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--text);
}

.card-title-row,
.cell-title-row {
  display: flex;
  align-items: flex-start;
  gap: 4px;
}

/* The card's hover controls: the rename pencil, and the agent handoff
   (TODO-171). One set of rules rather than two near-identical ones, so the
   pair cannot drift out of alignment in the title row. */
.inline-edit-btn,
.card-handoff-btn {
  flex-shrink: 0;
  margin-top: -1px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s, background 0.15s, color 0.15s;
}

.inline-edit-btn svg,
.card-handoff-btn svg {
  width: 13px;
  height: 13px;
}

.card:hover .inline-edit-btn,
tr:hover .inline-edit-btn,
.card:hover .card-handoff-btn,
tr:hover .card-handoff-btn {
  opacity: 0.6;
}

.inline-edit-btn:hover,
.card-handoff-btn:hover {
  opacity: 1 !important;
  background: var(--accent-dim);
  color: var(--accent);
}

/* Keyboard parity: the pair is reachable by Tab, and a focused control that
   stayed at opacity 0 would be invisible while it had focus. */
.inline-edit-btn:focus-visible,
.card-handoff-btn:focus-visible {
  opacity: 1;
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/* The task modal's handoff button (TODO-171). Sits in the title row beside the
   id badge, so it borrows that row's quiet chip treatment rather than reading
   as a primary action competing with the form below. */
.task-agent-handoff {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}

.task-agent-handoff svg {
  width: 12px;
  height: 12px;
}

.task-agent-handoff:hover {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: var(--accent);
}

.inline-edit-input {
  width: 100%;
  font-size: inherit;
  font-weight: inherit;
  font-family: inherit;
  line-height: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 1px 4px;
  margin: -2px -4px;
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-dim);
}

.card:has(.inline-edit-input) {
  cursor: default;
  transform: none !important;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

.card-project {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* Task id chip — board cards, all-tasks rows, mobile cards */
.card-id {
  font-family: monospace;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  flex-shrink: 0;
  line-height: 1;
}
.cell-title-row .card-id { margin-top: 4px; }

.card-priority-dot {
  font-weight: 700;
  font-size: 13px;
  font-family: monospace;
  flex-shrink: 0;
  line-height: 1;
}
.card-priority-dot.high   { color: var(--priority-high); }
.card-priority-dot.medium { color: var(--priority-medium); }
.card-priority-dot.low    { color: var(--priority-low); }

/* Status-colored card top accent */
.column[data-status="backlog"] .card     { border-top: 2px solid var(--status-backlog); }
.column[data-status="todo"] .card        { border-top: 2px solid var(--status-todo); }
.column[data-status="in-progress"] .card { border-top: 2px solid var(--status-in-progress); }
.column[data-status="blocked"] .card     { border-top: 2px solid var(--status-blocked); }
.column[data-status="done"] .card        { border-top: 2px solid var(--status-done); }

/* Done — fade back */
.column[data-status="done"] .card { opacity: 0.45; }
.column[data-status="done"] .card:hover { opacity: 1; }

/* Blocked — subtle left rule */
.column[data-status="blocked"] .card {
  border-left: 2px solid var(--status-blocked);
}

/* ============================================================
   List Views
   ============================================================ */

.list-view.view-active {
  display: flex;
  flex-direction: column;
  /* --header-actual-h, not a hardcoded 56px — same reason as .board above: a
     narrow window wraps the header onto more rows and this view would overflow
     the viewport by exactly those rows. Missed when TODO-169 fixed .board. */
  height: calc(100vh - var(--header-actual-h));
  height: calc(100dvh - var(--header-actual-h));
  padding: var(--space-lg) var(--space-xl);
}

.list-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--space-md);
  flex-shrink: 0;
}

.search-wrap {
  position: relative;
  max-width: 280px;
  flex: 1;
}

.search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}

input.search-input { padding-left: 34px; }

.list-toolbar select {
  width: auto;
  min-width: 130px;
}

.list-result-count {
  font-size: 13px;
  color: var(--text-muted);
  margin-left: auto;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.list-table-wrap {
  flex: 1;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
}

/* scrollbar inherited from global styles */

/* ============================================================
   Table
   ============================================================ */

.task-table {
  width: 100%;
  border-collapse: collapse;
}

.task-table thead {
  position: sticky;
  top: 0;
  z-index: 1;
}

.task-table th {
  background: var(--surface);
  text-align: left;
  padding: 12px 18px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
  user-select: none;
}

.task-table th.sortable { cursor: pointer; }
.task-table th.sortable:hover { color: var(--text-secondary); }

.task-table th.sort-asc::after {
  content: ' \2191';
  font-size: 11px;
  color: var(--accent);
}
.task-table th.sort-desc::after {
  content: ' \2193';
  font-size: 11px;
  color: var(--accent);
}

.task-table td {
  padding: 13px 18px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  font-size: 14px;
}

.task-table tbody tr {
  cursor: pointer;
  transition: background var(--dur-fast);
}
.task-table tbody tr:hover { background: var(--surface-hover); }
.task-table tbody tr:last-child td { border-bottom: none; }

.cell-title {
  font-weight: 500;
  color: var(--text);
}

.cell-desc {
  color: var(--text-muted);
  font-size: 13px;
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 380px;
}

.cell-date {
  color: var(--text-muted);
  font-size: 13px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.cell-muted { color: var(--text-muted); }

/* Status dot + label */
.status-dot {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 500;
}

.status-dot::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.status-dot.backlog::before     { background: var(--status-backlog); }
.status-dot.todo::before        { background: var(--status-todo); }
.status-dot.in-progress::before { background: var(--status-in-progress); }
.status-dot.blocked::before     { background: var(--status-blocked); }
.status-dot.done::before        { background: var(--status-done); }
.status-dot.rejected::before    { background: var(--status-rejected); }

.status-dot.backlog     { color: var(--status-backlog); }
.status-dot.todo        { color: var(--status-todo); }
.status-dot.in-progress { color: var(--status-in-progress); }
.status-dot.blocked     { color: var(--status-blocked); }
.status-dot.done        { color: var(--status-done); }
.status-dot.rejected    { color: var(--status-rejected); }

/* ============================================================
   Empty States
   ============================================================ */

.list-empty {
  text-align: center;
  padding: var(--space-xl) var(--space-lg);
}
.list-empty.hidden { display: none; }

.empty-title {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 400;
  font-style: italic;
  color: var(--text-secondary);
  margin-bottom: var(--space-xs);
}

.empty-sub {
  font-size: 14px;
  color: var(--text-muted);
}

kbd {
  display: inline-block;
  padding: 2px 6px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--surface-hover);
  border: 1px solid var(--border);
  border-radius: 3px;
  box-shadow: 0 1px 0 var(--border-strong);
}

/* ============================================================
   Modals
   ============================================================ */

.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal.hidden { display: none; }

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: var(--modal-backdrop);
  backdrop-filter: blur(3px);
}

.modal-content {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: var(--space-lg);
  width: 460px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: var(--modal-shadow);
}

.modal-content.modal-wide { width: 620px; }

.modal-content.modal-task {
  width: clamp(520px, 55vw, 740px);
}

/* Full-size image view (TODO-112). Sized to the image rather than to a column
   width, and padded down to a frame so the picture is the content. */

/* Above the chat panel (z 2000), not merely above the board: thumbnails are
   clicked from inside that panel, so the default .modal z-index of 1100 would
   open this viewer behind the very thing it was opened from. */
#image-modal {
  z-index: 2100;
}

.modal-content.modal-image {
  width: auto;
  max-width: 92vw;
  max-height: 92vh;
  padding: var(--space-sm);
  overflow: visible;
  display: flex;
}

/* The frame is also the viewport the zoom moves inside (TODO-255): it clips
   what the transform pushes past the edges, and it claims every touch axis —
   body.is-mobile is touch-action: manipulation, which would otherwise hand a
   two-finger drag to the browser before the pinch handler ever saw it. */
.image-modal-frame {
  display: flex;
  min-width: 0;
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.image-modal-frame img {
  max-width: 100%;
  /* The frame's own padding is the only chrome, so subtract it rather than
     letting a tall image push the close button off-screen. */
  max-height: calc(92vh - var(--space-md));
  object-fit: contain;
  border-radius: 6px;
  display: block;
  /* transform-origin stays centred on purpose — see initImageZoom. Duration is
     written inline per gesture: 0ms while a finger is down, a settle otherwise. */
  transition: transform 0ms var(--ease-out-expo);
  -webkit-user-drag: none;
}

/* Only while a gesture is live — a permanent will-change would keep every
   viewed image on its own layer for as long as the modal is open. */
.image-modal-frame.is-zooming img {
  will-change: transform;
}

.image-modal-close {
  position: absolute;
  top: -14px;
  right: -14px;
  z-index: 1;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
}

/* Carousel chrome (TODO-331). Same circular treatment as the close button —
   these three are one set of controls floating over a picture, and a second
   visual language for the arrows would only invite the question of why. */
.image-modal-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: 50%;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}

.image-modal-nav:hover:not(:disabled) { background: var(--surface-hover); }

/* Disabled rather than hidden at the ends: an arrow that vanishes moves the
   other one under the cursor mid-click. Kept visible enough to stay legible
   as a control, dim enough to read as spent. */
.image-modal-nav:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Outside the card on desktop, mirroring how the close button hangs off the
   corner — the picture is the content and the chrome stays off it. */
.image-modal-prev { left: -18px; }
.image-modal-next { right: -18px; }

.image-modal-count {
  position: absolute;
  bottom: -14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  padding: 2px var(--space-sm);
  border-radius: 999px;
  font-size: 11px;
  font-variant-numeric: tabular-nums; /* the count must not jitter as it steps */
  color: var(--text-secondary);
  background: var(--surface);
  border: 1px solid var(--border-strong);
}

/* Only offered once there is overhang to drag at — a grab cursor on an image
   that cannot move is a promise the viewer does not keep. */
.image-modal-frame.can-pan img { cursor: grab; }
.image-modal-frame.is-panning img { cursor: grabbing; }

/* Full-screen plan review (TODO-168). Above the chat panel for the same reason
   as #image-modal: Expand is clicked from inside that panel, which sits at
   z 2000 on mobile, so a default .modal would open behind its own trigger. */
#plan-modal {
  z-index: 2100;
}

/* The diff viewer (TODO-265). Third member of the same club as #image-modal
   and #plan-modal above, and for the identical reason: its trigger is the
   status strip's +/- chip, which lives INSIDE the chat panel — and that panel
   sits at z 2000 on mobile. At the default .modal z-index of 1100 the viewer
   opened behind the very thing that opened it, which on a phone reads as the
   chip simply not working. Caught by screenshotting it, not by reading it. */
#diff-modal {
  z-index: 2100;
}

/* Wider than the form modals — a plan is prose with headings, lists and code,
   and the point of this view is not having to read it through a 400px column. */
.modal-content.modal-plan {
  width: clamp(560px, 70vw, 900px);
  max-width: 92vw;
  max-height: 86vh;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  overflow: hidden; /* the frame scrolls, not the shell — the head stays put */
}

.plan-modal-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-shrink: 0;
}

.plan-modal-label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  flex-shrink: 0;
}

/* min-width: 0 + ellipsis so a long plan title cannot push the close button
   out of the row (same fix as .chat-title). */
.plan-modal-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.plan-modal-head .modal-close {
  flex-shrink: 0;
  align-self: center;
}

.plan-modal-frame {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-right: 4px; /* keeps text off the scrollbar */
  font-size: 14px;
  line-height: 1.6;
  color: var(--text);
}

/* `.modal-content h2` is the form-modal title style — display serif, italic,
   22px. `.chat-md` overrides its size/weight/margin (it is later in the file at
   equal specificity) but sets no font-family or font-style, so those two leak
   into an `##` heading and render it as a modal title. Shut that off here: a
   heading inside a plan is prose, not the dialog's own title. */
.plan-modal-frame .chat-md h1,
.plan-modal-frame .chat-md h2,
.plan-modal-frame .chat-md h3,
.plan-modal-frame .chat-md h4,
.plan-modal-frame .chat-md h5,
.plan-modal-frame .chat-md h6 {
  font-family: var(--font-body);
  font-style: normal;
}

/* Reading view, not a chat column — headings get a little more room to lead. */
.plan-modal-frame .chat-md h1 { font-size: 19px; }
.plan-modal-frame .chat-md h2 { font-size: 17px; }
.plan-modal-frame .chat-md h3 { font-size: 15px; }

/* marked/DOMPurify-unavailable fallback, mirroring .chat-exit-plan-body. */
.plan-modal-plain {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}

.modal-content h2 {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 500;
  font-style: italic;
  color: var(--text);
  margin-bottom: var(--space-xs);
}

.modal-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.modal-title-row h2 { margin-bottom: 0; }
.modal-title-row { margin-bottom: var(--space-xs); }

.task-id-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-family: monospace;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.task-id-badge:hover { color: var(--text); border-color: var(--text-muted); }
.task-id-badge svg { flex-shrink: 0; }

.task-agent-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.task-agent-chip:hover { color: var(--text); border-color: var(--text-muted); }
.task-agent-chip:hover .task-agent-chip-x { color: var(--clr-danger, #d44); }
.task-agent-chip-x { font-size: 13px; line-height: 1; }

/* Edit-modal autosave indicator (TODO-150) — quiet by design: no per-field
   toast, just this pill fading in/out next to the id badge and agent chip. */
.task-save-status {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
  transition: opacity 0.4s;
}
.task-save-status.is-saving { color: var(--text-muted); }
.task-save-status.is-saved { color: var(--accent); }
.task-save-status.is-error { color: var(--danger-text); cursor: pointer; text-decoration: underline dotted; }

/* ============================================================
   Task Links
   ============================================================ */

.task-links-section {
  margin-bottom: var(--space-md);
}

.task-links-section > label {
  display: block;
  margin-bottom: var(--space-xs);
}

.link-add-row {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}

.link-add-row select {
  flex: 0 0 auto;
  width: 130px;
}

.link-add-row .custom-select {
  flex: 0 0 130px;
  width: 130px;
}

.link-search-wrap {
  flex: 1;
  position: relative;
}

.link-search-wrap input {
  width: 100%;
}

.link-search-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 200;
  max-height: 180px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  margin-top: 2px;
  box-shadow: 0 6px 20px oklch(0 0 0 / 0.35);
}

.link-search-dropdown.hidden { display: none; }

.link-search-option {
  padding: 6px 10px;
  cursor: pointer;
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  transition: background var(--dur-fast);
}

.link-search-option:hover,
.link-search-option.focused {
  background: var(--surface-hover);
}

.link-search-option .link-opt-status {
  font-size: 0.7rem;
  padding: 1px 6px;
  border-radius: 3px;
  flex-shrink: 0;
}

.task-links-list {
  max-height: 160px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.task-link-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px;
  border-radius: 5px;
  background: var(--surface);
  font-size: 0.85rem;
  transition: background var(--dur-fast);
}

.task-link-item:hover {
  background: var(--surface-hover);
}

.link-type-label {
  font-size: 0.78rem;
  color: var(--text-muted);
  flex-shrink: 0;
  min-width: 72px;
}

.link-task-id {
  font-family: monospace;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 7px;
  flex-shrink: 0;
}

.link-task-title {
  flex: 1;
  color: var(--accent);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.link-task-title:hover {
  text-decoration: underline;
}

.link-task-status {
  font-size: 0.7rem;
  padding: 1px 6px;
  border-radius: 3px;
  flex-shrink: 0;
}

.link-remove-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px 4px;
  font-size: 1rem;
  line-height: 1;
  border-radius: 3px;
  transition: color var(--dur-fast), background var(--dur-fast);
  flex-shrink: 0;
}

.link-remove-btn:hover {
  color: var(--danger);
  background: oklch(0.65 0.20 28 / 0.12);
}

.link-remove-btn:active {
  transform: scale(0.85);
  transition-duration: 50ms;
}

/* ===== Task Chats (TODO-158) =====
   The chats that claimed, edited or mentioned this ticket. Same shape as the
   linked-tasks list above — one compact row each — but every row is a button,
   because the whole row does exactly one thing: open that conversation. */

.task-chats-section {
  margin-bottom: var(--space-md);
}

.task-chats-section > label {
  display: block;
  margin-bottom: var(--space-xs);
}

.task-chats-list {
  max-height: 140px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.task-chat-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 5px 8px;
  border: none;
  border-radius: 5px;
  background: var(--surface);
  color: inherit;
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  transition: background var(--dur-fast);
}

.task-chat-row:hover {
  background: var(--surface-hover);
}

/* The row became a div in TODO-311 (it hosts a PR link, and an <a> cannot nest
   in a <button>), so the focus ring a <button> got for free has to be asked
   for. Same treatment as the sidebar's thread row, which is a div for the same
   reason. */
.task-chat-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.task-chat-via {
  flex-shrink: 0;
  font-size: 0.78rem;
  line-height: 1;
  width: 1.1rem;
  text-align: center;
  color: var(--text-muted);
}

/* A claim is the deliberate one — the same signal the board card's 🤖 chip
   carries — so it is the only via that gets the accent. */
.task-chat-row[data-via="claim"] .task-chat-via {
  color: var(--accent);
}

.task-chat-title {
  flex: 1;
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.task-chat-row:hover .task-chat-title {
  text-decoration: underline;
}

.task-chat-when {
  flex-shrink: 0;
  font-size: 0.72rem;
  color: var(--text-muted);
}

/* ===== Task Comments ===== */

.task-comments-section {
  margin-bottom: var(--space-md);
}

.task-comments-section > label {
  display: block;
  margin-bottom: var(--space-xs);
}

.task-comments-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}

.comments-empty {
  color: var(--text-muted);
  font-size: 0.82rem;
  padding: 4px 0;
}

.comment-item {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  padding: 8px 12px 10px;
}

.comment-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.comment-author {
  color: var(--text-secondary);
  font-weight: 600;
}

.comment-author-badge {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--accent-dim);
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: lowercase;
}

.comment-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 2px;
  opacity: 0;
  transition: opacity var(--dur-fast);
}

.comment-item:hover .comment-actions,
.comment-actions:focus-within {
  opacity: 1;
}

.comment-edit-btn,
.comment-delete-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px 4px;
  line-height: 1;
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  transition: color var(--dur-fast), background var(--dur-fast);
}

.comment-edit-btn:hover {
  color: var(--text);
  background: var(--surface-hover);
}

.comment-delete-btn {
  font-size: 1rem;
}

.comment-delete-btn:hover {
  color: var(--danger);
  background: oklch(0.65 0.20 28 / 0.12);
}

/* An image in a rendered comment opens the full-size viewer (TODO-255). Only
   in the read-only rendering: in a live editor a click still selects the image,
   which is how it gets deleted. */
.rte-editor.rte-readonly img {
  cursor: zoom-in;
}

/* Floating "Expand" chip over an image in the task description (TODO-255).
   The description is contenteditable, so it cannot spend its click on the
   viewer; this is the affordance that gets one without taking the other away.
   Positioned on the body like .rte-table-toolbar, for the same reason — it has
   to escape the editor's overflow. */
.rte-img-expand {
  position: fixed;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow: 0 4px 16px oklch(0 0 0 / 0.4);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.1s, border-color 0.1s;
  white-space: nowrap;
}

.rte-img-expand.visible {
  opacity: 1;
  pointer-events: auto;
}

.rte-img-expand:hover {
  color: var(--text-primary);
  border-color: var(--accent);
}

/* Read-only comment bodies reuse .rte-editor content styling without the
   input-surface chrome. */
.rte-editor.rte-readonly {
  min-height: 0;
  max-height: none;
  overflow-y: visible;
  padding: 0;
  cursor: default;
}

/* Compact editor variant for the comment composer / inline comment editing */
.rte-wrap.rte-compact .rte-editor {
  min-height: 64px;
  max-height: 30vh;
  padding: 10px 12px;
}

.comment-composer-actions,
.comment-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}

.card-links {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--text-muted);
  font-size: 0.72rem;
}

.card-links svg {
  width: 12px;
  height: 12px;
}

/* Agent-claimed chip (🤖) — pulses while the task is in-progress */
.card-agent {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.78rem;
  color: var(--text-muted);
  flex-shrink: 0;
}

.card-agent.active { color: var(--status-in-progress); }

.agent-pulse {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--status-in-progress);
  animation: agentPulse 1.6s ease-in-out infinite;
}

@keyframes agentPulse {
  0%, 100% { opacity: 0.35; transform: scale(0.8); }
  50%      { opacity: 1; transform: scale(1.15); }
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
}

.form-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: var(--space-lg);
  gap: var(--space-sm);
}

.form-actions-right {
  display: flex;
  gap: var(--space-sm);
  margin-left: auto;
}

/* ============================================================
   Rich Text Editor
   ============================================================ */

.rte-wrap {
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg);
  transition: border-color var(--dur-fast);
}

.rte-wrap:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
  transition: border-color var(--dur-fast), box-shadow var(--dur-normal) var(--ease-out);
}

.rte-toolbar {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  flex-wrap: wrap;
  position: sticky;
  top: 0;
  z-index: 2;
}

.rte-group {
  display: flex;
  align-items: center;
  gap: 1px;
}

.rte-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast);
}

.rte-btn svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.rte-btn:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.rte-btn.active {
  background: var(--accent-dim);
  color: var(--accent);
}

.rte-btn:active {
  transform: scale(0.90);
  transition-duration: 50ms;
}

/* Tooltip on hover */
.rte-btn::after {
  content: attr(title);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 4px 8px;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  color: var(--text);
  background: var(--surface-active);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast), transform var(--dur-fast);
  z-index: 10;
}

.rte-btn:hover::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.rte-sep {
  width: 1px;
  height: 18px;
  background: var(--border);
  margin: 0 2px;
  flex-shrink: 0;
}

.rte-editor {
  min-height: 180px;
  max-height: 45vh;
  overflow-y: auto;
  padding: 14px 16px;
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.6;
  color: var(--text);
  outline: none;
  cursor: text;
}

/* scrollbar inherited from global styles */

/* Placeholder */
.rte-editor:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  pointer-events: none;
  font-style: italic;
}

/* Rich text content styles */
.rte-editor h2 {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  font-style: italic;
  color: var(--text);
  margin: 18px 0 6px 0;
}

.rte-editor h2:first-child { margin-top: 0; }

.rte-editor h3 {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 600;
  font-style: italic;
  color: var(--text);
  margin: 16px 0 6px 0;
}

.rte-editor h3:first-child { margin-top: 0; }

.rte-editor h4 {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 600;
  font-style: italic;
  color: var(--text-secondary);
  margin: 14px 0 4px 0;
}

.rte-editor h4:first-child { margin-top: 0; }

.rte-editor p {
  margin: 0 0 8px 0;
}

.rte-editor ul,
.rte-editor ol {
  margin: 4px 0 10px 0;
  padding-left: 22px;
}

.rte-editor li {
  margin-bottom: 3px;
}

/* Nested lists — distinct bullet styles per depth */
.rte-editor ul ul,
.rte-editor ol ul {
  list-style-type: circle;
  margin: 2px 0 2px 0;
  padding-left: 20px;
}

.rte-editor ul ul ul,
.rte-editor ol ul ul,
.rte-editor ul ol ul,
.rte-editor ol ol ul {
  list-style-type: square;
}

.rte-editor ol ol,
.rte-editor ul ol {
  list-style-type: lower-alpha;
  margin: 2px 0 2px 0;
  padding-left: 20px;
}

.rte-editor ol ol ol,
.rte-editor ul ol ol,
.rte-editor ol ul ol,
.rte-editor ul ul ol {
  list-style-type: lower-roman;
}

.rte-editor code {
  font-family: 'Consolas', 'SF Mono', monospace;
  font-size: 13px;
  background: var(--surface-hover);
  color: var(--accent);
  padding: 2px 6px;
  border-radius: 3px;
  border: 1px solid var(--border);
}

.rte-editor pre {
  background: oklch(0.13 0.008 55);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 12px 14px;
  margin: 10px 0;
  overflow-x: auto;
}

.rte-editor pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: 13px;
  color: var(--text-secondary);
  white-space: pre;
}

.rte-editor mark {
  background: oklch(0.70 0.13 70 / 0.22);
  color: var(--text);
  padding: 1px 4px;
  border-radius: 2px;
}

/* Task list checkboxes */
.rte-task-item {
  list-style: none;
  margin-left: -22px;
  padding-left: 4px;
}

.rte-task-checkbox {
  width: 14px;
  height: 14px;
  margin-right: 8px;
  vertical-align: middle;
  accent-color: var(--accent);
  cursor: pointer;
  position: relative;
  top: -1px;
}

.rte-task-done {
  text-decoration: line-through;
  color: var(--text-muted);
}

.rte-editor a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.rte-editor a:hover {
  color: var(--accent-hover);
}

/* Tables */
.rte-editor table.rte-table {
  width: 100%;
  border-collapse: collapse;
  margin: 12px 0;
  font-size: 14px;
}

.rte-editor table.rte-table th,
.rte-editor table.rte-table td {
  border: 1px solid var(--border-strong);
  padding: 8px 12px;
  text-align: left;
  vertical-align: top;
  min-width: 60px;
}

.rte-editor table.rte-table th {
  background: var(--surface);
  font-weight: 600;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}

.rte-editor table.rte-table td {
  background: transparent;
}

.rte-editor table.rte-table tr:hover td {
  background: var(--surface-hover);
}

.rte-editor table.rte-table td:focus,
.rte-editor table.rte-table th:focus {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Table Floating Toolbar */
.rte-table-toolbar {
  position: fixed;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px 6px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 4px 16px oklch(0 0 0 / 0.4);
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
  white-space: nowrap;
}

.rte-table-toolbar.visible {
  opacity: 1;
  pointer-events: auto;
}

.rte-tt-group {
  display: flex;
  gap: 1px;
}

.rte-tt-btn {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 7px;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}

.rte-tt-btn:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.rte-tt-btn.active {
  background: var(--accent-dim);
  color: var(--accent);
}

.rte-tt-btn svg {
  flex-shrink: 0;
}

.rte-tt-danger:hover {
  background: oklch(0.55 0.20 25 / 0.15);
  color: oklch(0.70 0.20 25);
}

.rte-tt-sep {
  width: 1px;
  height: 18px;
  background: var(--border);
  margin: 0 3px;
}

.rte-editor blockquote {
  border-left: 3px solid var(--border-strong);
  margin: 8px 0;
  padding: 4px 0 4px 14px;
  color: var(--text-secondary);
  font-style: italic;
}

.rte-editor hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 14px 0;
}

.rte-editor img {
  max-width: 100%;
  border-radius: 6px;
  margin: 10px 0;
  display: block;
  border: 1px solid var(--border);
}

.rte-editor img:hover {
  border-color: var(--border-strong);
}

/* Image entrance animation */
.rte-editor img.rte-img-enter {
  animation: rteImgIn 350ms var(--ease-out-expo) both;
}

@keyframes rteImgIn {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}

/* Upload placeholder */
.rte-upload-placeholder {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 80px;
  margin: 10px 0;
  border-radius: 6px;
  border: 1px dashed var(--border-strong);
  background: var(--surface);
  overflow: hidden;
  user-select: none;
}

.rte-upload-shimmer {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    oklch(0.70 0.145 70 / 0.06) 40%,
    oklch(0.70 0.145 70 / 0.10) 50%,
    oklch(0.70 0.145 70 / 0.06) 60%,
    transparent 100%
  );
  background-size: 200% 100%;
  animation: rteShimmer 1.5s ease-in-out infinite;
}

@keyframes rteShimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.rte-upload-label {
  position: relative;
  z-index: 1;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
}

/* Upload error state */
.rte-upload-placeholder.rte-upload-error {
  border-color: var(--status-blocked);
  background: var(--status-blocked-bg);
  flex-direction: column;
  gap: 8px;
  padding: 14px;
}

.rte-upload-error .rte-upload-label {
  color: var(--status-blocked);
}

.rte-upload-retry {
  position: relative;
  z-index: 1;
  padding: 4px 14px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast);
}

.rte-upload-retry:hover {
  background: var(--surface-hover);
  color: var(--text);
}

/* Rendered description in table/card views */
.desc-rendered {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 380px;
}

.desc-rendered img {
  display: none; /* hide images in truncated preview */
}

/* ============================================================
   Projects Page View
   ============================================================ */

.projects-view {
  padding: var(--space-lg) var(--space-xl);
  max-width: 1200px;
  margin: 0 auto;
  display: none;
}

.projects-view.view-active {
  display: block;
}

/* --- Hero / Create Section --- */

.projects-hero {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--border);
}

.projects-hero-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(28px, 3vw, 38px);
  font-weight: 400;
  color: var(--text);
  letter-spacing: -0.01em;
  line-height: 1.1;
}

.projects-hero-sub {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 4px;
}

.project-create-wrap {
  flex-shrink: 0;
}

.project-create-row {
  display: flex;
  gap: var(--space-sm);
}

.project-create-error {
  text-align: right;
}

.project-create-input {
  width: 240px;
  font-size: 14px;
  padding: 8px 14px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-body);
  transition: border-color var(--dur-fast);
}

.project-create-input:focus {
  outline: none;
  border-color: var(--accent);
}

.project-create-input::placeholder {
  color: var(--text-muted);
}

/* --- Filter tabs --- */

.projects-filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  flex-wrap: wrap;
  margin-bottom: var(--space-md);
}

.projects-toolbar-right {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.projects-search-wrap .search-input {
  width: 200px;
}

.tab-count {
  font-size: 10px;
  font-weight: 500;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  margin-left: 2px;
}

.projects-filter-tab.active .tab-count {
  color: var(--text-secondary);
}

.projects-filter-tabs {
  display: flex;
  gap: 2px;
  background: var(--surface);
  border-radius: 8px;
  padding: 3px;
  border: 1px solid var(--border);
}

.projects-filter-tab {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  padding: 5px 14px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  background: transparent;
  color: var(--text-muted);
  transition: background var(--dur-fast), color var(--dur-fast);
}

.projects-filter-tab:hover {
  color: var(--text-secondary);
  background: var(--surface-hover);
}

.projects-filter-tab.active {
  background: var(--surface-active);
  color: var(--text);
  font-weight: 600;
}

.projects-count {
  font-size: 12px;
  color: var(--text-muted);
}

/* --- Projects Grid --- */

.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--space-md);
}

/* --- Project Dossier Card --- */

.project-dossier {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--dur-normal), box-shadow var(--dur-normal), transform var(--dur-normal);
  animation: cardIn 300ms var(--ease-out-expo) both;
}

.project-dossier:hover {
  border-color: var(--border-strong);
  box-shadow: 0 3px 12px oklch(0 0 0 / 0.18);
  transform: translateY(-1px);
}

.project-dossier:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.project-dossier-main {
  padding: 14px 16px 12px;
}

.project-dossier-header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.project-dossier-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.project-dossier-name {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 18px;
  font-weight: 500;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  letter-spacing: -0.01em;
}

.project-dossier-badge {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
  flex-shrink: 0;
}

.project-dossier-badge.badge-paused::before {
  content: "❚❚";
  margin-right: 5px;
  font-size: 8px;
  letter-spacing: 0;
}

.project-dossier-badge.badge-archived {
  font-style: italic;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
}

.project-card-icon-btn {
  background: none;
  border: 1px solid transparent;
  color: var(--text-muted);
  cursor: pointer;
  padding: 5px;
  border-radius: 5px;
  line-height: 0;
  transition: color var(--dur-fast), background var(--dur-fast), border-color var(--dur-fast);
  flex-shrink: 0;
}

.project-card-icon-btn:hover:not(:disabled) {
  color: var(--text);
  background: var(--surface-hover);
  border-color: var(--border);
}

.project-card-icon-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.project-dossier-desc {
  font-size: 13px;
  color: var(--text-secondary);
  margin-top: 6px;
  margin-left: 20px;
  line-height: 1.45;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Progress + meta --- */

.project-progress-row {
  display: flex;
  align-items: baseline;
  gap: 16px;
  margin-top: 16px;
}

.project-progress-wrap {
  flex: 1;
  min-width: 0;
}

.project-progress-bar {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-active);
  overflow: hidden;
  display: flex;
}

.project-progress-seg {
  height: 100%;
}

.project-progress-seg.seg-done     { background: var(--status-done); }
.project-progress-seg.seg-progress { background: var(--status-in-progress); }
.project-progress-seg.seg-todo     { background: var(--status-todo); }
.project-progress-seg.seg-blocked  { background: var(--status-blocked); }
.project-progress-seg.seg-backlog  { background: var(--status-backlog); }

.project-percent {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 26px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--text);
  flex-shrink: 0;
  min-width: 56px;
  text-align: right;
}

.project-percent-empty {
  color: var(--text-muted);
  font-size: 14px;
  font-style: italic;
}

.project-dossier .project-progress-row {
  margin-top: 12px;
}

.project-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  font-size: 11px;
  color: var(--text-muted);
}

.project-card-meta {
  font-variant-numeric: tabular-nums;
}

.project-activity {
  flex-shrink: 0;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.project-activity.is-idle {
  color: oklch(0.62 0.10 60);
}

.project-cta-btn {
  background: var(--surface-hover);
  border: 1px solid var(--border);
  color: var(--text);
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 6px;
  font-size: 12px;
  font-family: var(--font-body);
  font-weight: 600;
  letter-spacing: 0.01em;
  transition: color var(--dur-fast), background var(--dur-fast), border-color var(--dur-fast);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.project-cta-btn:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

.project-cta-btn svg {
  width: 12px;
  height: 12px;
}

/* --- Archived compact variant --- */

.project-dossier.is-archived {
  background: transparent;
  border-style: dashed;
  border-color: oklch(0.27 0.014 55 / 0.6);
}

.project-dossier.is-archived .project-dossier-main {
  padding: 12px 22px;
}

.project-dossier.is-archived .project-dossier-name {
  font-size: 16px;
  color: var(--text-muted);
}

.project-dossier.is-archived .project-dossier-dot {
  width: 7px;
  height: 7px;
  opacity: 0.6;
}

.project-dossier.is-archived .project-progress-row,
.project-dossier.is-archived .project-meta-row,
.project-dossier.is-archived .project-dossier-desc,
.project-dossier.is-archived .project-card-icon-btn,
.project-dossier.is-archived .project-dossier-archived-meta > span {
  display: none;
}

.project-dossier.is-archived .project-dossier-archived-meta {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
}

.project-dossier-archived-meta {
  display: none;
}

.project-dossier-archived-meta .restore-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-muted);
  cursor: pointer;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-family: var(--font-body);
  font-weight: 500;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

.project-dossier-archived-meta .restore-btn:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.delete-project-btn.confirming {
  background: oklch(0.50 0.20 27);
  border-color: oklch(0.50 0.20 27);
  color: oklch(0.98 0.005 60);
  animation: pulseFade 4000ms linear forwards;
}

@keyframes pulseFade {
  0%   { box-shadow: 0 0 0 0 oklch(0.50 0.20 27 / 0.45); }
  20%  { box-shadow: 0 0 0 6px oklch(0.50 0.20 27 / 0); }
  100% { box-shadow: 0 0 0 0 oklch(0.50 0.20 27 / 0); }
}

/* --- Empty State --- */

.projects-empty {
  text-align: center;
  padding: var(--space-xl) 0;
}

.projects-empty-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 20px;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.projects-empty-sub {
  font-size: 13px;
  color: var(--text-muted);
}

.projects-empty-action {
  margin-top: 14px;
  background: none;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  cursor: pointer;
  padding: 6px 16px;
  border-radius: 6px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}

.projects-empty-action:hover {
  color: var(--text);
  background: var(--surface-hover);
  border-color: var(--border-strong);
}

.projects-empty-action.hidden { display: none; }

/* Shared segmented status control (project modal + detail page) */
.project-status-options {
  display: flex;
  gap: 4px;
  background: var(--surface);
  border-radius: 6px;
  padding: 3px;
  border: 1px solid var(--border);
  width: fit-content;
}

.project-status-opt {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  padding: 5px 14px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background: transparent;
  color: var(--text-muted);
  transition: background var(--dur-fast), color var(--dur-fast);
}

.project-status-opt:hover {
  color: var(--text-secondary);
  background: var(--surface-hover);
}

.project-status-opt.selected {
  font-weight: 600;
}

.project-status-opt.selected[data-status="active"] {
  background: var(--status-done-bg);
  color: var(--status-done);
}

.project-status-opt.selected[data-status="paused"] {
  background: var(--status-progress-bg);
  color: var(--status-in-progress);
}

.project-status-opt.selected[data-status="archived"] {
  background: oklch(0.56 0.018 60 / 0.12);
  color: var(--text-muted);
}

/* Color swatch grid (shared between project dossier and other uses) */

.color-swatch-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.color-swatch {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform var(--dur-fast), border-color var(--dur-fast), box-shadow var(--dur-fast);
  position: relative;
  padding: 0;
}

.color-swatch:hover {
  transform: scale(1.2);
  box-shadow: 0 2px 8px oklch(0 0 0 / 0.3);
}

.color-swatch.selected {
  border-color: var(--text);
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px currentColor;
}

.color-swatch.selected::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: oklch(1 0 0 / 0.85);
  box-shadow: 0 1px 2px oklch(0 0 0 / 0.3);
}

/* (Old project modal styles removed — now in Projects Page View section) */

.btn-sm {
  font-size: 12px;
  padding: 4px 12px;
}

/* ============================================================
   Project Detail View
   ============================================================ */

.project-detail {
  padding: var(--space-lg) var(--space-xl);
  max-width: 960px;
  margin: 0 auto;
  display: none;
}

.project-detail.view-active {
  display: block;
}

.project-detail-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.project-back-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px 6px 4px;
  border-radius: 6px;
  transition: color var(--dur-fast), background var(--dur-fast);
}

.project-back-btn:hover {
  color: var(--text);
  background: var(--surface-hover);
}

.project-detail-topbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.project-detail-topbar-actions.hidden { display: none; }
.project-detail-body.hidden { display: none; }

.project-detail-header {
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-md);
}

.project-detail-title-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.project-detail-title-row .project-dossier-dot {
  width: 12px;
  height: 12px;
}

.project-detail-name {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(26px, 3vw, 34px);
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1.15;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-detail-desc {
  font-size: 14px;
  color: var(--text-secondary);
  margin-top: 8px;
  max-width: 65ch;
  line-height: 1.5;
}

.project-detail-desc.hidden { display: none; }

.project-detail .project-progress-row {
  margin-top: var(--space-md);
}

.project-detail-meta {
  margin-top: 10px;
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.project-detail-blocked {
  color: var(--status-blocked);
  font-weight: 600;
}

.project-detail-quickadd {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.project-detail-quickadd .project-create-input {
  flex: 1;
  width: auto;
}

.project-detail .task-table .cell-id {
  width: 90px;
  white-space: nowrap;
}

/* Inline validation (project modal + quick create) */
.project-modal-error {
  font-size: 12px;
  color: var(--danger-text);
  margin-top: 6px;
}

/* Sub-label under an optional field, explaining what filling it in buys you. */
.project-field-hint {
  font-size: 11px;
  color: var(--text-secondary);
  margin: 4px 0 0;
}

#project-repo-input {
  font-family: var(--font-mono);
  font-size: 12px;
}

/* Repo link on the project detail page — reads as metadata, not a CTA. */
.project-detail-repo {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  font-size: 12px;
  font-family: var(--font-mono);
  color: var(--text-secondary);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.project-detail-repo:hover {
  color: var(--text);
  border-bottom-color: var(--border);
}
.project-detail-repo.hidden { display: none; }

input.is-invalid,
input.is-invalid:focus {
  border-color: var(--danger-text);
}

/* Visually hidden, accessible label */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================
   Settings View
   ============================================================ */

.settings-view {
  padding: var(--space-lg) var(--space-xl);
  max-width: 1120px;
  margin: 0 auto;
  display: none;
}

.settings-view.view-active {
  display: block;
}

/* --- Sidebar layout ---
   A 232px rail of sections beside a single visible pane. Before this the view
   was one 720px column ~1650px tall, of which the twelve theme cards were more
   than half; Export/Import and the agent token only existed below the fold.
   The rail is sticky under the header, so switching sections never asks the
   user to scroll back up to reach the nav. */

.settings-layout {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: var(--space-xl);
  align-items: start;
}

.settings-rail {
  position: sticky;
  /* --header-actual-h, not --header-h: a header wrapped by a narrow window is
     taller than 56px, and applyFit() measures the real value. */
  top: calc(var(--header-actual-h) + var(--space-lg));
  align-self: start;
}

.settings-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.settings-nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 12px;
  border: none;
  border-radius: 8px;
  background: none;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--text-secondary);
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.settings-nav-item svg {
  width: 16px;
  height: 16px;
  flex: none;
  opacity: 0.75;
}

.settings-nav-item:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.settings-nav-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.settings-nav-item.active {
  background: var(--accent-dim);
  color: var(--accent);
  font-weight: 600;
}

.settings-nav-item.active svg { opacity: 1; }

/* One pane at a time. Capped so hint text keeps a readable measure in the
   wide content column -- the pane is a form, not a canvas. */
.settings-pane { display: none; max-width: 780px; }
.settings-pane.active { display: block; }
.settings-pane:focus { outline: none; }

.settings-pane-head {
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--border);
}

.settings-pane-title {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: 24px;
  color: var(--text);
  letter-spacing: -0.01em;
  line-height: 1.2;
}

.settings-pane-sub {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 2px;
}

/* --- Narrow: rail becomes a horizontal strip ---
   Keyed off body[data-fit] rather than a media query, because the docked chat
   panel narrows the content without narrowing the viewport (see applyFit). */

body[data-fit="narrow"] .settings-layout,
body[data-fit="stack"] .settings-layout {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
}

body[data-fit="narrow"] .settings-rail,
body[data-fit="stack"] .settings-rail {
  position: static;
}

body[data-fit="narrow"] .settings-nav,
body[data-fit="stack"] .settings-nav {
  flex-direction: row;
  gap: var(--space-xs);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--border);
}

body[data-fit="narrow"] .settings-nav::-webkit-scrollbar,
body[data-fit="stack"] .settings-nav::-webkit-scrollbar { display: none; }

body[data-fit="narrow"] .settings-nav-item,
body[data-fit="stack"] .settings-nav-item {
  width: auto;
  flex: none;
  white-space: nowrap;
  border-radius: 8px 8px 0 0;
}

/* The pane head repeats the section name the rail already shows at this width. */
body[data-fit="narrow"] .settings-pane-head,
body[data-fit="stack"] .settings-pane-head {
  border-bottom: none;
  padding-bottom: 0;
  margin-bottom: var(--space-md);
}

body[data-fit="narrow"] .settings-pane-title,
body[data-fit="stack"] .settings-pane-title { display: none; }

body[data-fit="stack"] .settings-view {
  padding: var(--space-lg) var(--space-md);
}

.settings-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(28px, 3vw, 38px);
  font-weight: 400;
  color: var(--text);
  letter-spacing: -0.01em;
  line-height: 1.1;
}

/* --- Groups within a pane (TODO-357) ---
   Admin carries three former tabs at once — Agent, Desktop sessions, Data &
   Backup — so the pane needs a rung between its title and the fields, or the
   five field labels read as one undifferentiated list. Quiet by design: this
   The display italic is deliberate. Field labels here inherit the global
   `label` treatment — 15px, uppercase, letter-spaced — so a SMALLER uppercase
   heading reads as the quieter of the two and inverts the hierarchy. A
   different kind of type outranks them at a smaller size than they use. */

.settings-group {
  margin-bottom: var(--space-xl);
}

.settings-group:last-child {
  margin-bottom: 0;
}

.settings-group-title {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: 17px;
  letter-spacing: -0.01em;
  color: var(--text);
  margin-bottom: var(--space-md);
  padding-bottom: var(--space-xs);
  border-bottom: 1px solid var(--border);
}

/* The desktop group's two halves: controls when the launcher answers, this
   note when it does not. Never both, and never neither. */
.settings-field-hint.desktop-unavailable-note {
  margin-bottom: 0;
  /* Louder than a field hint on purpose. --text-muted measures 4.24:1 here,
     under AA, and this is not decorative small print -- it is the whole
     explanation for why the controls under this heading are not there. */
  color: var(--text-secondary);
}

.settings-field {
  margin-bottom: var(--space-lg);
}

.settings-field-label {
  display: block;
  font-size: 15px;
  font-weight: 500;
  color: var(--text);
  margin-bottom: 2px;
}

.settings-field-hint {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: var(--space-md);
}

/* --- Agent access --- */

.agent-enabled-btn.active {
  color: var(--accent);
  border-color: var(--accent);
}

.agent-token-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}

.agent-token-status {
  font-size: 13px;
  color: var(--text-muted);
  margin-right: var(--space-xs);
}

/* The notifications row carries up to three buttons after its status text,
   which is one more than fits beside it on a phone. Only this row wraps —
   the token row above it is a status and a single button and must not. */
.push-row {
  flex-wrap: wrap;
  row-gap: var(--space-sm);
}

.push-error {
  color: var(--danger-text);
  margin-top: var(--space-sm);
  margin-bottom: 0;
}

.agent-token-reveal {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}

.agent-token-value {
  flex: 1;
  min-width: 0;
  padding: var(--space-sm) 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12px;
  color: var(--text);
  word-break: break-all;
  user-select: all;
}

.agent-token-note {
  color: var(--danger-text);
  margin-top: var(--space-sm);
  margin-bottom: 0;
}

/* --- Desktop Remote Control sessions (TODO-263) --- */

.desktop-start-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  flex-wrap: wrap;
}

/* initCustomSelects() upgrades every <select> on the page, so the thing sitting
   in this flex row is the wrapper it built — the raw #desktop-dir is hidden
   inside it and styling that would do nothing. */
.desktop-start-row .custom-select {
  width: 220px;
  flex-shrink: 0;
}

.desktop-name {
  flex: 1;
  min-width: 140px;
  padding: var(--space-sm) 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 13px;
  color: var(--text);
  font-family: inherit;
}

.desktop-error {
  color: var(--danger-text);
  margin-top: var(--space-sm);
  margin-bottom: 0;
}

.desktop-list {
  list-style: none;
  margin: var(--space-sm) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.desktop-empty {
  font-size: 13px;
  color: var(--text-dim);
}

.desktop-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.desktop-row-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
}

.desktop-row-name:hover {
  text-decoration: underline;
}

.desktop-row-meta {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.desktop-row-stop {
  flex-shrink: 0;
}

/* The ticket-level button is present in the markup but pointless without a
   launcher, so it collapses rather than offering an action that cannot work. */
.task-desktop-session.desktop-unavailable {
  display: none;
}

/* The session toast persists — the deep link is the point of the click, so it
   must not time out before it is used. */
.toast-desktop {
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.toast-desktop-msg {
  font-size: 13px;
}

.toast-desktop-prompt {
  display: block;
  padding: 6px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-size: 12px;
  color: var(--text-muted);
  word-break: break-word;
  user-select: all;
}

.toast-desktop-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.toast-desktop-btn {
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 500;
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}

.toast-desktop-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.toast-desktop-dismiss {
  margin-left: auto;
}

/* --- Theme Cards --- */

.theme-grid {
  display: grid;
  /* 200px lands on 3 columns in the pane, so the six themes in each group
     fill two even rows rather than leaving a ragged 4+2. Drops to 2 and then
     1 as the docked chat narrows the pane. */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-md);
}

.theme-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: 0;
  border: 2px solid var(--border);
  border-radius: 10px;
  background: transparent;
  cursor: pointer;
  transition: border-color var(--dur-normal) var(--ease-out),
              box-shadow var(--dur-normal) var(--ease-out);
  overflow: hidden;
  font-family: var(--font-body);
}

.theme-card:hover {
  border-color: var(--border-strong);
}

.theme-card.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.theme-card-preview {
  background: var(--p-bg);
  aspect-ratio: 16 / 9;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.theme-card-bar {
  height: 3px;
  flex-shrink: 0;
  background: var(--p-ac);
}

.theme-card-cols {
  display: flex;
  gap: 4px;
  padding: 8px 10px;
  flex: 1;
  align-items: stretch;
}

.theme-card-cols span {
  flex: 1;
  background: var(--p-sf);
  border-radius: 3px;
  border: 1px solid var(--p-bd);
}

.theme-card-name {
  display: block;
  padding: 6px 10px 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  text-align: left;
}

.theme-card.active .theme-card-name {
  color: var(--accent);
  font-weight: 600;
}

.theme-group {
  margin-bottom: var(--space-lg);
}

.theme-group:last-child {
  margin-bottom: 0;
}

.theme-group-label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: var(--space-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

@media (max-width: 560px) {
  .theme-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ============================================================
   Project Colors (applied via inline styles from project.color)
   ============================================================ */

/* ============================================================
   Utility
   ============================================================ */

.hidden { display: none !important; }

/* ---- Shared swipe-to-dismiss state (TODO-247) ----
   Written by attachSheetDismiss, worn by every sheet that can be dragged down:
   the chat, the picker sheet, the thread slide-over, the two mobile modals.

   While a finger is down the transform is written inline every frame and a
   transition would fight it — hence `none` rather than a shorter duration. On
   release JS swaps to `.sheet-settling` and sets `transition-duration` inline
   from the distance still to travel, so this rule supplies only the curve.
   Backdrops carry `.sheet-dragging` for the same reason their sheet does: the
   drag writes their opacity per frame too. */
.sheet-dragging { transition: none !important; }

.sheet-settling {
  transition: transform 200ms cubic-bezier(0.32, 0.72, 0, 1);
}


/* ============================================================
   Toast Notifications
   ============================================================ */

/* ---- Add to Home Screen coach (TODO-359) ----------------------------------
   A non-modal hint, so it deliberately does NOT participate in overlay locking:
   it is hidden around real overlays by the CSS at the end of this block, which
   means there is no JS state that can fall out of sync with what is on screen.

   The container spans a whole edge so the card can be centred without a
   transform (a transform here would fight attachSheetDismiss, which writes one
   onto .install-card every frame of a dismiss drag) and is pointer-events:none
   so it cannot swallow clicks on the board it floats over. */
.install-coach {
  position: fixed;
  z-index: 150; /* over the page, under toasts (200); modals are handled below */
  display: flex;
  pointer-events: none;
}

.install-coach.hidden { display: none !important; }

.install-card {
  position: relative;
  pointer-events: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px 18px 14px;
  box-shadow: 0 12px 32px oklch(0.05 0.01 55 / 0.45);
  max-width: 360px;
  animation: m-slide-up 240ms cubic-bezier(0.32, 0.72, 0, 1);
}

.install-coach-close {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  padding: 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.install-coach-close:hover { background: var(--surface-hover); color: var(--text); }

.install-card-title {
  margin: 0 0 10px;
  padding-right: 24px; /* clear of the close button */
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}

.install-steps {
  margin: 0;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.65;
  color: var(--text-secondary);
}
/* Prev-sibling test, not a plain li + li: step one has two variants and one of
   them is always .hidden, which would otherwise leave the visible first step
   carrying a stray top margin on iPad. */
.install-steps li:not(.hidden) + li:not(.hidden) { margin-top: 4px; }
.install-steps b { color: var(--text); font-weight: 600; }

/* The glyphs carry the instruction — the user matches them by sight against a
   real toolbar button — so they sit on the text baseline at reading size and
   inherit currentColor so they theme with all twelve palettes. */
.install-glyph {
  display: inline-flex;
  width: 18px;
  height: 18px;
  vertical-align: -4px;
  color: var(--text);
}
.install-glyph svg { width: 100%; height: 100%; }

.install-card-line {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.install-card-line.hidden { display: none; }
.install-steps.hidden { display: none; }

.install-card-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  margin-top: 14px;
}

/* A rotated square with two borders — the theme-safe tooltip arrow, since it
   inherits --surface and --border rather than restating a colour. */
.install-tail {
  position: absolute;
  width: 12px;
  height: 12px;
  background: var(--surface);
  transform: rotate(45deg);
}
.install-coach[data-arrow="none"] .install-tail { display: none; }

.install-coach[data-arrow="down"] .install-tail {
  left: 50%;
  bottom: -7px;
  margin-left: -6px;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.install-coach[data-arrow="up-right"] .install-tail {
  top: -7px;
  right: 28px;
  border-left: 1px solid var(--border);
  border-top: 1px solid var(--border);
}

/* Anchors. "down" is the iPhone case and gets re-pinned to the VISUAL viewport
   in style.mobile.css; this is its desktop fallback. Centred with justify rather
   than a translate for the reason in the block comment above. */
.install-coach[data-arrow="down"] {
  left: 0;
  right: 0;
  bottom: 20px;
  justify-content: center;
}
/* iPad Safari keeps Share at the top right, beside the address bar. iPad is
   768px+ wide, so body.is-mobile never applies here and this is the only rule
   that runs — no --vvh involved, and no keyboard geometry to get wrong. */
.install-coach[data-arrow="up-right"] {
  top: calc(env(safe-area-inset-top, 0px) + 12px);
  right: 12px;
}
.install-coach[data-arrow="none"] {
  right: 20px;
  bottom: 20px;
}
/* The docked chat reserves the right gutter; a card anchored to the window edge
   would sit on top of it. */
body.chat-docked .install-coach[data-arrow="none"],
body.chat-docked .install-coach[data-arrow="up-right"] {
  right: calc(20px + var(--chat-dock-w, 0px));
}

.install-coach-out .install-card {
  animation: m-slide-down 200ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}

/* Suppressed rather than layered: a hint must never sit over the thing you
   opened. Pure CSS on purpose — nothing to keep in sync. On mobile
   body.overlay-locked already covers every sheet, modal and the chat panel;
   on desktop the board is not locked, so the sibling test does that job. */
body.overlay-locked .install-coach,
.modal:not(.hidden) ~ .install-coach,
.m-sheet:not(.hidden) ~ .install-coach,
.chat-panel:not(.hidden) ~ .install-coach {
  display: none !important;
}

.toast-container {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  pointer-events: none;
}

.toast {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 18px;
  font-size: 14px;
  color: var(--text);
  box-shadow: 0 8px 24px oklch(0.05 0.01 55 / 0.4);
  pointer-events: auto;
  animation: toastIn 350ms var(--ease-out-expo) both;
  max-width: 320px;
}

.toast.toast-error {
  border-color: oklch(0.55 0.18 27 / 0.6);
  color: var(--danger-text);
}

/* Undo toasts carry a message + button + count; give them room */
.toast.toast-undo { max-width: 440px; }

.toast.toast-out {
  animation: toastOut 250ms var(--ease-out) both;
}

.toast-muted {
  color: var(--text-muted);
  font-size: 12px;
  margin-left: 4px;
}

@keyframes toastIn {
  from { opacity: 0; transform: translateY(12px) scale(0.95); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toastOut {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(-8px) scale(0.95); }
}

/* Undo toast variant */
.toast-undo {
  padding: 0;
  overflow: hidden;
  max-width: 360px;
}

.toast-undo-body {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 14px;
}

.toast-undo-msg {
  font-size: 14px;
  color: var(--text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.toast-undo-msg strong {
  font-weight: 600;
}

.toast-undo-btn {
  flex-shrink: 0;
  padding: 4px 12px;
  border: none;
  border-radius: 4px;
  background: var(--accent-dim);
  color: var(--accent);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast);
}

.toast-undo-btn:hover {
  background: var(--accent);
  color: var(--accent-text);
}

.toast-undo-bar {
  height: 3px;
  background: var(--border);
}

.toast-undo-progress {
  height: 100%;
  width: 100%;
  background: var(--accent);
  border-radius: 0 3px 3px 0;
}

/* ============================================================
   Column Empty States
   ============================================================ */

.column-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl) var(--space-sm);
  text-align: center;
}

.column-empty-text {
  font-family: var(--font-display);
  font-size: 13px;
  font-style: italic;
  color: var(--text-muted);
  opacity: 0.7;
  line-height: 1.5;
}

/* ============================================================
   Confetti
   ============================================================ */

.confetti-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 300;
  overflow: hidden;
}

.confetti-piece {
  position: absolute;
  width: 8px;
  height: 8px;
  animation: confettiFall 1.2s var(--ease-out) both;
}

.confetti-piece:nth-child(odd) { border-radius: 50%; }
.confetti-piece:nth-child(even) { border-radius: 1px; }

@keyframes confettiFall {
  0% {
    opacity: 1;
    transform: translateY(0) rotate(0deg) scale(1);
  }
  100% {
    opacity: 0;
    transform: translateY(120px) rotate(720deg) scale(0.3);
  }
}

/* ============================================================
   Drag Ghost Enhancement
   ============================================================ */

.card.drag-lift {
  transform: scale(1.04) rotate(1.5deg);
  box-shadow: 0 8px 24px oklch(0.05 0.01 55 / 0.35);
  z-index: 50;
  transition: none;
}

/* ============================================================
   Easter Egg — Party Mode
   ============================================================ */

body.party-mode {
  animation: partyBg 2s linear infinite;
}

@keyframes partyBg {
  0%   { background: oklch(0.145 0.02 0); }
  16%  { background: oklch(0.145 0.02 60); }
  33%  { background: oklch(0.145 0.02 120); }
  50%  { background: oklch(0.145 0.02 180); }
  66%  { background: oklch(0.145 0.02 240); }
  83%  { background: oklch(0.145 0.02 300); }
  100% { background: oklch(0.145 0.02 360); }
}

body.party-mode header {
  animation: partyHeader 2s linear infinite;
}

@keyframes partyHeader {
  0%   { background: oklch(0.185 0.02 0); }
  16%  { background: oklch(0.185 0.02 60); }
  33%  { background: oklch(0.185 0.02 120); }
  50%  { background: oklch(0.185 0.02 180); }
  66%  { background: oklch(0.185 0.02 240); }
  83%  { background: oklch(0.185 0.02 300); }
  100% { background: oklch(0.185 0.02 360); }
}

/* ============================================================
   Entrance Motion
   ============================================================ */

@keyframes cardIn {
  from { opacity: 0; transform: translateY(5px); }
  to   { opacity: 1; transform: translateY(0); }
}

.card-enter {
  animation: cardIn 250ms var(--ease-out-expo) both;
}

@keyframes rowIn {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: translateY(0); }
}

.row-enter {
  animation: rowIn 180ms var(--ease-out-expo) both;
}

/* ============================================================
   Agent Chat Panel — docked right on desktop; mobile bottom-sheet
   overrides live in style.mobile.css
   ============================================================ */

.chat-panel {
  position: fixed;
  top: var(--header-actual-h); /* below the header; the board stays visible + interactive */
  right: 0;
  bottom: 0;
  /* --chat-w is set inline by the resize handle; the default leaves room for
     the thread sidebar (176px) plus the classic 400px message column. The
     panel keeps this width when the sidebar collapses — the conversation
     expands into the freed space instead. */
  width: var(--chat-w, min(576px, 92vw));
  /* The drag's only stop is BOARD_MIN_W, enforced in chatMaxWidth() — a second,
     narrower ceiling here would silently cap the drag short of it (TODO-169). */
  max-width: 100vw;
  z-index: 90; /* below .modal (100): task modals still open above */
}

.chat-panel.hidden { display: none; }

/* The panel docks rather than floats: it stays position:fixed in the gutter,
   and the BODY reserves that gutter, so the board and every other view reflow
   into what is left instead of being covered (TODO-169). */
body.chat-docked {
  padding-right: var(--chat-dock-w, 0);
  transition: padding-right var(--dur-normal) var(--ease-out-expo);
}

/* ...but the header spans the WHOLE window, over the top of the panel, and
   pulls itself back out of the gutter to do it. The panel already starts at
   top: var(--header-h), so it docks BENEATH the header rather than beside it:
   the nav bar is app-level chrome, not part of the board, and squeezing it into
   the same strip as the board is what made it wrap to two rows the moment the
   chat got wide. Its width is the window's, so its own wrapping stays a plain
   media query.

   The panel being fixed is also why the header is `position: sticky` (TODO-174):
   a docked panel that never moves, above a view that scrolls the document, will
   separate from a header that does — see the header rule at the top of this
   file. */
body.chat-docked header {
  margin-right: calc(-1 * var(--chat-dock-w, 0px));
  transition: margin-right var(--dur-normal) var(--ease-out-expo);
}

/* A drag sets the width every frame; a transition would fight it the whole way
   down. Same reason .sheet-dragging carries none. Both the gutter and the
   header's escape from it have to be exempt, or they drift apart mid-drag. */
body.chat-resizing,
body.chat-resizing header {
  transition: none;
}

/* Mobile-only backdrop (see style.mobile.css) */
.chat-backdrop { display: none; }

.chat-shell {
  position: relative; /* anchors the resize handle + mobile threads slide-over */
  height: 100%;
  display: flex;
  flex-direction: row; /* full-height sidebar | header + conversation column */
  /* The lifted ramp, remapped onto the ordinary token names so every descendant
     picks it up without knowing it is in a panel — `.chat-threads` is the one
     that matters (it paints var(--bg), the board's own background, which is why
     the seam had nothing to separate), but bubbles, popovers and rows all ride
     the same lift and stay in step with each other.

     Remapped HERE rather than on .chat-panel for two reasons: .chat-shell is
     what actually paints a background, and .chat-backdrop is a sibling inside
     .chat-panel that must NOT lift — it is the mobile scrim over the board.
     Not a cycle: --panel-* are declared on :root and substituted there, so what
     inherits down to this element is already a colour, not a var() chain. */
  --bg:             var(--panel-bg);
  --surface:        var(--panel-surface);
  --surface-hover:  var(--panel-surface-hover);
  --surface-active: var(--panel-surface-active);
  --border:         var(--panel-border);
  --border-strong:  var(--panel-border-strong);

  background: var(--surface);
  border-left: 1px solid var(--panel-edge);
  box-shadow: var(--panel-shadow);
  animation: chat-slide-in var(--dur-normal) var(--ease-out-expo);
}

@keyframes chat-slide-in {
  from { transform: translateX(24px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

/* ...and back out the same way (TODO-247). The panel slid in from the right and
   then vanished on close; this is the missing half. The mobile rule in
   style.mobile.css is more specific and overrides this with a downward slide,
   because there the panel is a bottom sheet rather than a docked column. */
@keyframes chat-slide-out {
  from { transform: translateX(0); opacity: 1; }
  to   { transform: translateX(24px); opacity: 0; }
}

.chat-panel.chat-panel-out .chat-shell {
  animation: chat-slide-out var(--dur-normal) var(--ease-out-expo) forwards;
}

/* Left-edge drag handle (desktop only; mobile hides it). The hit area is
   wider than the visible affordance, which appears on hover/focus/drag. */
.chat-resize-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -3px;
  width: 8px;
  cursor: ew-resize;
  z-index: 5;
  touch-action: none;
}
.chat-resize-handle::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 3px;
  width: 2px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--dur-fast);
}
.chat-resize-handle:hover::after,
.chat-resize-handle:focus-visible::after,
body.chat-resizing .chat-resize-handle::after {
  opacity: 0.7;
}

body.chat-resizing,
body.chat-threads-resizing {
  cursor: ew-resize;
  -webkit-user-select: none;
  user-select: none;
}

/* Header + messages + composer; sits beside the full-height sidebar. */
.chat-main {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* 1fr auto 1fr rather than flex + space-between: the two side groups hold
   different numbers of buttons, and space-between would centre the title on
   whatever gap was left over — which moves as the sides change. The grid centres
   it on the header itself, so it is still centred when a side group grows. */
.chat-header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-sm);
  padding: 10px var(--space-md);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

/* No min-width:0 on the two side groups, unlike the title. An fr track floors at
   its content, which is what keeps the buttons at full size while the title
   absorbs the shrinking — zero it here and a long title crushes them instead. */
.chat-header-left {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Holds a thread name now, not the static word "Agent" — so it must survive a
   200-char title in a panel the user can drag narrow. min-width:0 is what lets
   a grid item shrink below its content and actually ellipsise. */
/* The header's middle grid track (TODO-220): the thread name plus its copyable
   id badge. min-width:0 sits here now rather than on .chat-title, because the
   flex child is what has to be allowed to shrink for the ellipsis to engage. */
.chat-title-group {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs, 6px);
  min-width: 0;
}

/* Same vocabulary as .task-id-badge — a chat is addressable the way a ticket
   is, so it should look it. Monospace and muted: it is an identifier, not a
   heading, and must not compete with the italic display-font title beside it. */
.chat-id-badge {
  flex: none;
  font-family: monospace;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}

.chat-id-badge:hover { color: var(--text); border-color: var(--text-muted); }

/* The same id on the sidebar row's meta line. flex:none so the date/count text
   beside it is what gets ellipsised in a narrow sidebar — the id is short and
   fixed, and truncating it would defeat the point of showing it. */
.chat-thread-meta-id {
  flex: none;
  font-family: monospace;
  font-weight: 600;
  opacity: 0.85;
}

/* A 'TODO-42' / 'RUN-42' written into a task description or comment body
   (TODO-220). Underlined rather than chipped: this is prose, and a pill per id
   would turn a paragraph that mentions three tickets into a row of buttons.
   Decoration only — stripped again before the HTML is saved. */
.board-ref {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}

.board-ref:hover {
  text-decoration-style: solid;
}

.chat-title {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 500;
  font-style: italic;
  color: var(--text);
  min-width: 0;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  justify-content: flex-end;
}

/* The two side groups are equal fr tracks, so the title is centred on the header
   whenever there is slack; past that it ellipsises in place rather than pushing
   a button off the row. Nothing in the header may shrink except the title. */
.chat-header .btn-icon {
  flex-shrink: 0;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  /* Inter-run gap deliberately larger than intra-run: turns group by proximity */
  gap: var(--space-md);
}

.chat-run {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.chat-bubble {
  max-width: 88%;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 14px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-user {
  align-self: flex-end;
  background: var(--accent-dim);
  border: 1px solid var(--accent-dim-hover); /* accent-tinted edge separates roles */
  color: var(--text);
  border-bottom-right-radius: 3px;
}

.chat-assistant {
  align-self: flex-start;
  background: var(--surface-hover);
  /* Transparent, not none: keeps box metrics and lets .chat-bubble-error
     recolor it. The quiet fill is the assistant's whole voice. */
  border: 1px solid transparent;
  color: var(--text);
  border-bottom-left-radius: 3px;
}

.chat-bubble-error {
  border-color: var(--status-blocked);
  color: var(--danger-text);
}

.chat-thinking {
  align-self: flex-start;
  max-width: 88%;
  font-size: 12px;
  color: var(--text-muted);
}

.chat-thinking-summary {
  cursor: pointer;
  font-style: italic;
  -webkit-user-select: none;
  user-select: none;
}
.chat-thinking-summary:hover { color: var(--text-secondary); }

.chat-thinking-body {
  margin-top: 4px;
  padding: 4px 10px;
  border-left: 2px solid var(--border-strong);
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-tool {
  align-self: flex-start;
  max-width: 88%;
  min-width: 0;
}

.chat-tool-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-hover);
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-secondary);
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  list-style: none;
}
.chat-tool-chip::-webkit-details-marker { display: none; }
.chat-tool-chip:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.chat-tool-chip .chat-tool-icon {
  display: inline-flex;
  flex-shrink: 0;
}

.chat-tool-chip .chat-tool-name { flex-shrink: 0; }

.chat-tool-preview {
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
}

.chat-tool-chip.chat-tool-done::after {
  content: '\2713';
  color: var(--status-done);
}

/* The Auto-mode gate refused this call. Must NOT wear the done ✓ — a denial is
   not a completed call, and reading it as one was the whole problem. Static
   glyph, so it needs no reduced-motion entry. */
.chat-tool-chip.chat-tool-denied {
  border-color: var(--status-blocked);
  background: var(--status-blocked-bg);
  color: var(--danger-text);
}
.chat-tool-chip.chat-tool-denied:hover {
  border-color: var(--status-blocked);
  color: var(--danger-text);
}
.chat-tool-chip.chat-tool-denied::after {
  content: '\2298';
  color: var(--danger-text);
}

/* In-flight chip: same pulse as the composer's working dot. */
.chat-tool-chip.chat-tool-running::after {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--accent);
  animation: chat-pulse 1.2s ease-in-out infinite;
}

.chat-tool-body {
  margin-top: var(--space-xs);
  padding: var(--space-xs) 0 var(--space-xs) 10px;
  border-left: 2px solid var(--border-strong);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.chat-tool-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.chat-tool-pre {
  margin: 0;
  padding: 6px 8px;
  max-height: 240px;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-secondary);
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-tool-pre.chat-tool-pending {
  font-style: italic;
  color: var(--text-muted);
}

/* ---- Rich tool cards (TODO-139) ----
   Bash / Edit / Write / Read render as cards rather than chips. Deliberately
   built from tokens that ALREADY exist and are already re-tuned per theme — a
   new token costs seven edits (:root, five dark themes, the shared light block),
   and the gate-denial work established that the status palette is enough:

     --bg                  the recessed plane (darker than --surface on light
                           themes too, so the terminal reads as inset everywhere)
     --status-todo         Read      --status-in-progress  Edit
     --status-done         Write, added lines, the ok dot
     --accent              Bash, the $ prompt, the running state
     --status-blocked      removed lines, the failed/denied card */

.chat-tool-card {
  align-self: flex-start;
  width: 100%;
  max-width: 88%;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  overflow: hidden;
}

.chat-card-summary {
  display: block;
  padding: 0;
  cursor: pointer;
  list-style: none;
  -webkit-user-select: none;
  user-select: none;
}
.chat-card-summary::-webkit-details-marker { display: none; }
.chat-card-summary::marker { content: ''; }

.chat-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  background: var(--surface-hover);
  font-size: 12px;
  line-height: 1.4;
}
.chat-card-summary:hover .chat-card-head { background: var(--surface-active); }

.chat-card-badge {
  flex: none;
  padding: 1px 6px;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.chat-card-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  color: var(--text);
}
/* A description is prose, not a path — the one header title that isn't code. */
.chat-card-has-desc .chat-card-title {
  font-family: var(--font-body);
  font-size: 12.5px;
}

.chat-card-tag {
  flex: none;
  color: var(--text-muted);
  font-size: 11px;
}
.chat-card-tag:empty { display: none; }

.chat-card-gap { flex: 1 1 auto; min-width: 6px; }

.chat-card-metric {
  flex: none;
  display: inline-flex;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}
.chat-card-plus  { color: var(--status-done); }
.chat-card-minus { color: var(--status-blocked); }

/* Status as a MARK, never a number to decode. */
.chat-card-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
}
.chat-card-running .chat-card-dot {
  background: var(--accent);
  animation: chat-pulse 1.2s ease-in-out infinite;
}
.chat-card-done .chat-card-dot { background: var(--status-done); }
/* Failure is a diamond as well as a colour, so it survives a colour-blind read. */
.chat-card-failed .chat-card-dot,
.chat-card-denied .chat-card-dot {
  border-radius: 0;
  background: var(--status-blocked);
  transform: rotate(45deg);
}
.chat-card-stranded .chat-card-dot { background: var(--border-strong); }

/* min-width, not width: the cell reserves enough for a typical "0.2s" so
   stacked cards line up, but a long-running call reads "4m 23s" on one line
   rather than wrapping into two. */
.chat-card-time {
  flex: none;
  min-width: 36px;
  text-align: right;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

.chat-card-chevron {
  flex: none;
  display: inline-flex;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-out);
}
.chat-tool-card[open] .chat-card-chevron { transform: rotate(180deg); }

/* A call that failed or was refused wears it on the frame, not just the dot. */
.chat-card-failed,
.chat-card-denied { border-color: var(--status-blocked); }
.chat-card-failed .chat-card-head,
.chat-card-denied .chat-card-head { background: var(--status-blocked-bg); }
.chat-card-failed .chat-card-title,
.chat-card-denied .chat-card-title { color: var(--danger-text); }

/* In-flight border. Reduced-motion drops to a static accent edge below. */
.chat-card-running { border-color: var(--accent); }

.chat-card-body {
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.65;
}

/* Rows: fixed-width gutters so the text column can't shift line to line. */
.chat-card-row {
  display: flex;
  padding: 0 10px;
}
.chat-card-gutter {
  flex: none;
  width: 38px;
  padding-right: 10px;
  text-align: right;
  color: var(--text-muted);
  opacity: 0.7;
  -webkit-user-select: none;
  user-select: none;
}
.chat-card-text {
  white-space: pre;
  min-width: 0;
}

/* -- Bash -- */
.chat-card-bash .chat-card-badge { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); }

/* The command line and the output each own ONE scrollbar, not two: .chat-card-out
   already scrolls itself, so .chat-card-term must not also scroll horizontally —
   with both wide, that used to draw a second bar directly under the output's. */
.chat-card-term {
  padding: 9px 10px;
  background: var(--bg);
  overflow-x: hidden;
}
.chat-card-cmd {
  display: flex;
  gap: 8px;
  margin-bottom: 6px;
  overflow-x: auto;
  padding-bottom: 2px;
}
.chat-card-prompt {
  flex: none;
  color: var(--accent);
  font-weight: 600;
}
/* flex: 0 0 auto, not the inherited min-width: 0 — a shrinkable flex item never
   grows past its container, so scrollWidth never exceeds clientWidth and the
   bar above would never appear for a genuinely long command. */
.chat-card-cmd .chat-card-text { color: var(--text); flex: 0 0 auto; }

.chat-card-out {
  margin: 0;
  max-height: 320px;
  overflow: auto;
  white-space: pre;
  color: var(--text-secondary);
}
.chat-card-cursor {
  display: inline-block;
  width: 7px;
  height: 13px;
  vertical-align: -2px;
  background: var(--accent);
  animation: chat-card-blink 1.1s step-end infinite;
}
@keyframes chat-card-blink {
  0%, 45% { opacity: 1; }
  50%, 95% { opacity: 0; }
  100% { opacity: 1; }
}

.chat-card-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 10px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  font-size: 11px;
  color: var(--text-muted);
}
.chat-card-foot-status { color: var(--status-done); }
.chat-card-failed .chat-card-foot-status,
.chat-card-denied .chat-card-foot-status,
.chat-card-stranded .chat-card-foot-status { color: var(--danger-text); }

/* -- Edit -- */
.chat-card-edit .chat-card-badge {
  color: var(--status-in-progress);
  border-color: var(--status-in-progress);
  background: var(--status-progress-bg);
}

.chat-card-diff,
.chat-card-newfile,
.chat-card-source {
  padding: 7px 0;
  max-height: 340px;
  overflow: auto;
}

.chat-card-diff .chat-card-gutter { width: 34px; }
/* Hunk-relative numbers (chatEditLineBase couldn't prove an absolute offset) —
   italic marks them as counted from the shown diff, not the file, so they are
   never mistaken for real line numbers. */
.chat-card-gutter-rel .chat-card-gutter { font-style: italic; }
.chat-card-ctx { color: var(--text-muted); }
.chat-card-add {
  background: var(--status-done-bg);
  color: var(--text);
  box-shadow: inset 2px 0 0 var(--status-done);
}
.chat-card-del {
  background: var(--status-blocked-bg);
  color: var(--text-secondary);
  box-shadow: inset 2px 0 0 var(--status-blocked);
}

/* The diff shows what was PROPOSED. When it didn't land, dim it and say so —
   a refused edit that reads as applied is the failure mode worth preventing. */
.chat-card-diff-void { opacity: 0.55; }
.chat-card-note {
  padding: 6px 10px;
  border-top: 1px solid var(--border);
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--danger-text);
}

/* -- Write -- */
.chat-card-write .chat-card-badge {
  color: var(--status-done);
  border-color: var(--status-done);
  background: var(--status-done-bg);
}
/* A faint wash over the whole pane, not per-row: every line of a Write is new,
   so striping each one would be noise dressed as information. */
.chat-card-newfile { background: var(--status-done-bg); }
.chat-card-wrote { color: var(--text); }

/* -- Read -- */
.chat-card-read .chat-card-badge {
  color: var(--status-todo);
  border-color: var(--status-todo);
  background: var(--status-todo-bg);
}
.chat-card-src { color: var(--text-secondary); }

/* -- ApplyPatch -- */
.chat-card-patch .chat-card-badge {
  color: var(--status-in-progress);
  border-color: var(--status-in-progress);
  background: var(--status-progress-bg);
}
.chat-card-patch-list {
  max-height: 420px;
  overflow: auto;
}
.chat-card-patch-file + .chat-card-patch-file { border-top: 1px solid var(--border); }
.chat-card-patch-head {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: max-content;
  padding: 6px 10px;
  background: var(--bg);
  color: var(--text-secondary);
}
.chat-card-patch-kind {
  flex: none;
  min-width: 44px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.chat-card-patch-add { color: var(--status-done); }
.chat-card-patch-update,
.chat-card-patch-move { color: var(--status-in-progress); }
.chat-card-patch-delete { color: var(--status-blocked); }
.chat-card-patch-path { white-space: nowrap; color: var(--text); }
.chat-card-patch-arrow { color: var(--text-muted); }
.chat-card-patch-lines { padding: 4px 0; overflow-x: auto; }
.chat-card-patch-lines .chat-card-row { min-width: max-content; }
.chat-card-patch-lines .chat-card-gutter { width: 24px; }
.chat-card-patch-note,
.chat-card-patch-warning {
  padding: 5px 10px;
  font-family: var(--font-body);
  font-size: 11.5px;
  color: var(--text-muted);
}
.chat-card-patch-warning {
  border-top: 1px solid var(--border);
  color: var(--danger-text);
}

/* ---- Board tool cards — "Rail" (TODO-147) ----
   The sixteen mcp__todopp__ board tools, in the same frame as the four above.
   Quietest available content surface: hairline rows, a status rail down the
   left, field changes as inline swaps.

   Badge hue says what the call DOES, not which table it touched — and every hue
   is a token that already exists and is already re-tuned across all twelve
   themes, so there are no per-theme rules here to keep in sync:

     read     --status-todo          get_task, list_tasks, list_projects
     mutate   --status-in-progress   update_*, claim, release, unlink
     create   --status-done          create_task, add_comment, create_project
     relate   --accent               link_tasks
     destroy  --status-blocked       delete_*                                  */

/* Board tool names are long (`update_project`, `delete_comment`) and the chat
   panel is ~300px, so the badge gives up tracking to leave the title readable —
   the header cell that actually says which record was touched. */
.chat-card-board .chat-card-badge { letter-spacing: 0.04em; padding: 1px 5px; }

.chat-card-fam-read    .chat-card-badge { color: var(--status-todo);        border-color: var(--status-todo);        background: var(--status-todo-bg); }
.chat-card-fam-mutate  .chat-card-badge { color: var(--status-in-progress); border-color: var(--status-in-progress); background: var(--status-progress-bg); }
.chat-card-fam-create  .chat-card-badge { color: var(--status-done);        border-color: var(--status-done);        background: var(--status-done-bg); }
.chat-card-fam-relate  .chat-card-badge { color: var(--accent);             border-color: var(--accent);             background: var(--accent-dim); }
.chat-card-fam-destroy .chat-card-badge { color: var(--status-blocked);     border-color: var(--status-blocked);     background: var(--status-blocked-bg); }

/* A refused or conflicting board call gets a DASHED frame and a struck target
   on top of the shared red treatment. A stopped call must never be mistakable
   for a finished one, and on a card whose whole body is small records the dot
   alone is too quiet to carry that. */
.chat-card-board.chat-card-failed,
.chat-card-board.chat-card-denied { border-style: dashed; }
.chat-card-board.chat-card-failed .chat-card-title,
.chat-card-board.chat-card-denied .chat-card-title {
  text-decoration: line-through;
  text-decoration-color: var(--status-blocked);
}

.chat-card-rail { max-height: 360px; overflow: auto; }

/* -- The CLI's own todo list (TODO-157) --

   The panel is the run's plan, not a call in flight, so it drops the running
   border in JS and keeps a plain frame. No new theme tokens: the three statuses
   are the same --status-todo / --status-in-progress / --status-done the board
   columns and every other card already use, so the light themes need no rules
   of their own. */
.chat-todo-panel .chat-card-badge {
  color: var(--status-in-progress);
  border-color: var(--status-in-progress);
  background: var(--status-progress-bg);
}
.chat-todo-list { max-height: 360px; overflow: auto; padding: 4px 0; }

.chat-todo-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 10px;
}
.chat-todo-glyph {
  flex: none;
  width: 12px;
  text-align: center;
  color: var(--text-muted);
  -webkit-user-select: none;
  user-select: none;
}
/* The one row worth finding at a glance, so it is the only one that is not
   muted or struck — the same reason the board's in-progress column is tinted. */
.chat-todo-in_progress .chat-todo-glyph { color: var(--status-in-progress); }
.chat-todo-in_progress .chat-todo-text { color: var(--text); font-weight: 600; }
.chat-todo-completed .chat-todo-glyph { color: var(--status-done); }
.chat-todo-completed .chat-todo-text { color: var(--text-muted); text-decoration: line-through; }
.chat-todo-text { min-width: 0; overflow-wrap: anywhere; }
/* A TaskUpdate for an item this panel never saw created: the id is all the
   payload proves, so it reads as a placeholder rather than as a real subject. */
.chat-todo-bare { color: var(--text-muted); font-style: italic; }
/* Touched by THIS run — a marker on the row, not a colour, so it composes with
   the three status treatments above instead of fighting them. */
.chat-todo-changed { box-shadow: inset 2px 0 0 var(--accent); }

/* -- Background subagents --
   The roster of agents a run spawned, plus the marking that says which of the
   transcript's bubbles and cards are theirs rather than the orchestrator's.
   Reuses the same --status-* tokens as the todo panel above for the same reason:
   these are the board's existing states (working / done / blocked), so the light
   themes need no rules of their own. */
.chat-agents-panel .chat-card-badge {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--surface);
}
.chat-agent-list { max-height: 360px; overflow: auto; padding: 4px 0; }

.chat-agent-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 3px 10px;
  cursor: pointer;
  border-radius: 4px;
}
.chat-agent-row:hover { background: var(--surface-hover); }
.chat-agent-glyph {
  flex: none;
  width: 12px;
  text-align: center;
  color: var(--text-muted);
  -webkit-user-select: none;
  user-select: none;
}
.chat-agent-main { display: flex; min-width: 0; flex-direction: column; gap: 1px; }
.chat-agent-desc { min-width: 0; overflow-wrap: anywhere; }
.chat-agent-meta { font-size: 11px; color: var(--text-muted); }

/* -- What a subagent is running on --
   Its own line under the meta, because "what it is doing" and "what it is doing
   it with" are different questions — and because half the answer is marked as
   inherited rather than proven, which needs room to say so. */
.chat-agent-cfg {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  font-size: 11px;
  color: var(--text-muted);
}
.chat-agent-cfg-part { display: inline-flex; align-items: baseline; gap: 4px; }
.chat-agent-cfg-label { opacity: 0.75; }
.chat-agent-cfg-value { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
/* The unproven half. Marked, never asserted: the event stream carries no
   subagent effort at all and only sometimes carries a model, so this says the
   value came from the run rather than from the agent. */
.chat-agent-cfg-inherited {
  font-style: italic;
  opacity: 0.8;
  cursor: help;
  border-bottom: 1px dotted var(--border-strong);
}
.chat-agent-cfg-card { padding: 6px 10px 0; }

/* A question raised by a subagent. The card itself stays in the run's flow —
   it is answered through the composer, which is addressed to the run — so this
   row is the only thing that says the question is not the orchestrator's. */
.chat-agent-asker {
  margin-bottom: 4px;
  font-size: 11px;
  color: var(--text-muted);
  font-style: italic;
}

/* The prompt a subagent was handed. Bounded because this card opens by default
   and a delegation prompt runs to hundreds of lines. */
.chat-agent-prompt {
  max-height: 220px;
  overflow: auto;
  padding: 8px 10px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.chat-card-agent .chat-card-badge {
  color: var(--accent);
  border-color: var(--accent);
}
/* The launch acknowledgement. Its own class because .chat-card-note is the
   failure surface — an agent that started fine must not read in danger red. */
.chat-agent-launched {
  padding: 6px 10px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
}
.chat-agent-count { margin-left: auto; font-size: 11px; color: var(--text-muted); }
.chat-agent-running .chat-agent-glyph { color: var(--status-in-progress); }
.chat-agent-running .chat-agent-desc { color: var(--text); font-weight: 600; }
.chat-agent-done .chat-agent-glyph { color: var(--status-done); }
.chat-agent-done .chat-agent-desc { color: var(--text-muted); }
.chat-agent-blocked .chat-agent-glyph { color: var(--status-blocked); }
.chat-agent-stranded .chat-agent-glyph { color: var(--text-muted); }
/* Stranded AND refused: the refusal is the likelier explanation of the silence,
   so the row keeps the blocked tint rather than fading out with the rest. */
.chat-agent-stranded.chat-agent-was-blocked .chat-agent-glyph { color: var(--status-blocked); }
.chat-agent-stranded .chat-agent-desc { color: var(--text-muted); font-style: italic; }
/* The row whose agent is open in the drill view. Marked, not re-rendered — the
   roster lives in the transcript, and rebuilding it on every navigation would
   collapse the panel the reader just scrolled to. */
.chat-agent-open { box-shadow: inset 2px 0 0 var(--accent); background: var(--surface-hover); }

/* Work that belongs to a subagent is NOT part of the run's transcript. A
   delegated run reads as one interleaved wall otherwise — the orchestrator's
   narration and two subagents' tool calls in a single column — so it is hidden
   outright and reachable only through the drill view below.
   It still LIVES where it happened, marked rather than moved, which is what
   keeps two subagents genuinely interleaved once you are inside one. The main
   transcript keeps the three things that are ABOUT the delegation rather than
   inside it: the Agent launch card, the roster panel, and the chip strip.
   No gutter rule any more: this is only ever on screen inside the drill view,
   where everything visible is that one agent's and a gutter said nothing. */
.chat-messages:not(.chat-drilled) .chat-from-agent { display: none; }

/* -- The chip strip (TODO-215) --
   One chip per background agent, above the transcript, for as long as a run has
   any. Its own row rather than something inside .chat-header: the header is a
   three-column grid centred on the thread title, and a variable-width strip in
   any of those tracks would drag the title off centre as agents came and went. */
.chat-agent-bar {
  flex-shrink: 0;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.chat-agent-chips {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px var(--space-md);
  /* Scrolls rather than wraps or collapses into a count: a wrapping strip
     changes the transcript's height as agents finish, and a count hides the one
     thing the strip is for. */
  overflow-x: auto;
  scrollbar-width: none;
}
.chat-agent-chips::-webkit-scrollbar { display: none; }

.chat-agent-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 13px;
  line-height: 1.5;
  white-space: nowrap;
  cursor: pointer;
}
.chat-agent-chip:hover { background: var(--surface-hover); border-color: var(--border-strong); }
.chat-agent-chip-name { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; }
/* Tabular so a ticking counter cannot change the chip's width every second and
   nudge every chip to its right. */
.chat-agent-chip-time { color: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.chat-agent-chip.chat-agent-running { color: var(--text); border-color: var(--border-strong); }
.chat-agent-chip.chat-agent-running .chat-agent-glyph { animation: chat-pulse 1.6s ease-in-out infinite; }
.chat-agent-chip.chat-agent-done,
.chat-agent-chip.chat-agent-cancelled,
.chat-agent-chip.chat-agent-stranded { color: var(--text-muted); }
.chat-agent-chip.chat-agent-open {
  border-color: var(--accent);
  background: var(--surface-hover);
  color: var(--text);
  box-shadow: none; /* the roster row's gutter rule does not belong on a pill */
}

/* -- The drill bar --
   Shown while the transcript is filtered to one agent. Independent of the chips
   above it: the roster panel of a finished run opens the same view, and by then
   there is no live run to draw chips for. */
.chat-agent-drill {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: 7px var(--space-md);
  min-width: 0;
}
.chat-agent-chips:not(.hidden) + .chat-agent-drill { border-top: 1px solid var(--border); }
.chat-agent-back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  align-self: center;
  padding: 3px 9px 3px 7px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
}
.chat-agent-back:hover { background: var(--surface-hover); color: var(--text); border-color: var(--border-strong); }
/* min-width: 0 on both, or a flex item floors at its content and an agent
   described in a full sentence pushes the bar wider than the panel. */
.chat-agent-drill-name {
  min-width: 0;
  flex: 0 1 auto;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The name is what identifies the view, so the meta gives up its width first —
   which is what the lopsided shrink factor buys: the meta absorbs effectively
   all of the shrinking, and the name only starts losing characters once the
   meta has none left to give. */
.chat-agent-drill-meta {
  min-width: 0;
  flex: 1 100 auto;
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-agent-drill.chat-agent-running .chat-agent-glyph { animation: chat-pulse 1.6s ease-in-out infinite; }

/* -- The drill view --
   Every rule here only ever HIDES. Nothing is moved and no `display` is forced,
   so a bubble, a card and a flex row all come back exactly as they were when the
   reader steps out — and the interleaving of two subagents, which re-parenting
   into a panel would have destroyed, is still the run that actually happened.
   CSS cannot match data-agent against the drilled key (it cannot compare two
   attribute values), so the visible set is marked in JS and this only says what
   the unmarked rest looks like: gone. */
.chat-messages.chat-drilled > :not(.chat-drill-host):not(.chat-drill-keep) { display: none; }
.chat-messages.chat-drilled > .chat-drill-host > :not(.chat-drill-show) { display: none; }
.chat-drill-empty { color: var(--text-muted); font-style: italic; }

/* -- Field rows: label gutter, then the value -- */
.chat-card-field {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
}
.chat-card-field:last-child { border-bottom: none; }
.chat-card-field-label {
  flex: none;
  width: 78px;
  color: var(--text-muted);
}
.chat-card-field-value {
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  color: var(--text);
  overflow-wrap: anywhere;
}
.chat-card-void { color: var(--text-muted); font-style: italic; }

.chat-card-pill {
  padding: 0 7px;
  border-radius: 4px;
  background: var(--surface-hover);
  color: var(--text-secondary);
}
.chat-card-pill[data-status="backlog"]     { color: var(--status-backlog);     background: var(--status-backlog-bg); }
.chat-card-pill[data-status="todo"]        { color: var(--status-todo);        background: var(--status-todo-bg); }
.chat-card-pill[data-status="in-progress"] { color: var(--status-in-progress); background: var(--status-progress-bg); }
.chat-card-pill[data-status="blocked"]     { color: var(--status-blocked);     background: var(--status-blocked-bg); }
.chat-card-pill[data-status="done"]        { color: var(--status-done);        background: var(--status-done-bg); }
.chat-card-pill[data-status="rejected"]    { color: var(--status-rejected);    background: var(--status-rejected-bg); }

/* Priority as a heat mark: three characters read faster than a word, and the
   word is still on the title attribute for anyone who wants it. */
.chat-card-prio { flex: none; font-size: 11px; letter-spacing: 0.06em; color: var(--text-muted); }
.chat-card-prio[data-priority="medium"] { color: var(--status-done); }
.chat-card-prio[data-priority="high"]   { color: var(--status-blocked); }

/* -- The swap: what a field WAS, and what it is now -- */
.chat-card-was {
  color: var(--text-secondary);
  text-decoration: line-through;
  text-decoration-color: var(--status-blocked);
}
.chat-card-arrow { flex: none; color: var(--text-muted); }
/* The landed value carries the pill so the eye finds it without reading: a
   status brings its own colour, everything else borrows the "this is the new
   one" green. */
.chat-card-now > span:not(.chat-card-pill) {
  padding: 0 7px;
  border-radius: 4px;
  background: var(--status-done-bg);
  color: var(--text);
}

/* -- Ticket rows: a status rail, an id, a title, a priority -- */
.chat-card-tick {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 12px 6px 11px;
  border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--border-strong);
}
.chat-card-tick:last-child { border-bottom: none; }
.chat-card-tick[data-status="backlog"]     { box-shadow: inset 2px 0 0 var(--status-backlog); }
.chat-card-tick[data-status="todo"]        { box-shadow: inset 2px 0 0 var(--status-todo); }
.chat-card-tick[data-status="in-progress"] { box-shadow: inset 2px 0 0 var(--status-in-progress); }
.chat-card-tick[data-status="blocked"]     { box-shadow: inset 2px 0 0 var(--status-blocked); }
.chat-card-tick[data-status="done"]        { box-shadow: inset 2px 0 0 var(--status-done); }
.chat-card-tick[data-status="rejected"]    { box-shadow: inset 2px 0 0 var(--status-rejected); }
.chat-card-tick-id { flex: none; color: var(--text-muted); font-size: 11px; }
.chat-card-tick-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 12.5px;
  color: var(--text);
}
.chat-card-gone { opacity: 0.6; }
.chat-card-gone .chat-card-tick-title,
.chat-card-gone-text {
  text-decoration: line-through;
  text-decoration-color: var(--status-blocked);
}

/* -- A record that did not exist a moment ago -- */
.chat-card-fresh {
  padding: 8px 12px 9px;
  border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--status-done);
  background: var(--status-done-bg);
}
.chat-card-fresh-head { display: flex; align-items: baseline; gap: 8px; }
/* inline-block, not block: an id that is also a link must underline the TEXT,
   not stripe the full width of its column. */
.chat-card-fresh-id { display: inline-block; color: var(--text-muted); font-size: 11px; }
.chat-card-fresh-tag {
  flex: none;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--status-done);
}
.chat-card-fresh-title {
  margin-top: 3px;
  font-family: var(--font-body);
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--text);
}
.chat-card-fresh-note { margin-top: 3px; font-size: 11px; color: var(--text-muted); }

/* -- Two tickets and the relation between them -- */
.chat-card-bridge {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px 12px;
}
.chat-card-bridge-end {
  flex: 1 1 0;
  min-width: 0;
  padding-left: 10px;
  box-shadow: inset 2px 0 0 var(--status-in-progress);
}
.chat-card-bridge-target { box-shadow: inset 2px 0 0 var(--status-todo); }
.chat-card-bridge-id { display: inline-block; color: var(--text-muted); font-size: 11px; }
.chat-card-bridge-title {
  margin-top: 2px;
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.35;
  color: var(--text);
}
.chat-card-bridge-mid {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--accent);
}
.chat-card-bridge-rel { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; }
.chat-card-bridge-line { font-size: 12px; }
.chat-card-bridge-broken { color: var(--status-blocked); }
.chat-card-bridge-broken .chat-card-bridge-rel { text-decoration: line-through; }

/* -- Comments -- */
.chat-card-cmt {
  padding: 8px 12px 9px;
  border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--status-done);
}
.chat-card-cmt:last-child { border-bottom: none; }
.chat-card-cmt-was { box-shadow: inset 2px 0 0 var(--status-blocked); }
.chat-card-cmt-head { display: flex; align-items: baseline; gap: 8px; font-size: 11px; }
.chat-card-cmt-author { color: var(--text-secondary); }
.chat-card-cmt-when { color: var(--text-muted); }
.chat-card-cmt-body {
  margin-top: 3px;
  font-family: var(--font-body);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.chat-card-cmt-was .chat-card-cmt-body {
  color: var(--text-secondary);
  text-decoration: line-through;
  text-decoration-color: var(--status-blocked);
}

/* -- Projects -- */
.chat-card-proj {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
}
.chat-card-proj:last-child { border-bottom: none; }
.chat-card-proj-ident { display: inline-flex; align-items: baseline; gap: 8px; }
.chat-card-proj-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--border-strong);
}
.chat-card-proj-name { color: var(--text); }
.chat-card-proj-state { color: var(--text-muted); font-size: 11px; }
.chat-card-proj-repo {
  flex: none;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: 11px;
}

/* -- What the list is, and is not, showing -- */
.chat-card-listfoot {
  padding: 6px 12px 7px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-muted);
}

/* -- Refusals and conflicts, on the recessed plane the terminal uses -- */
.chat-card-stop-head,
.chat-card-stop-why {
  padding: 3px 12px;
  background: var(--bg);
  line-height: 1.6;
  overflow-wrap: anywhere;
}
.chat-card-stop-head { padding-top: 9px; color: var(--danger-text); }
.chat-card-stop-why  { padding-bottom: 10px; color: var(--text-secondary); }

/* A result that could not be parsed is shown as-is rather than as a widget
   invented around it — the same bytes the generic chip would have shown. */
.chat-card-raw {
  margin: 0;
  padding: 9px 12px;
  max-height: 260px;
  overflow: auto;
  background: var(--bg);
  color: var(--text-secondary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Ticket ids go somewhere: #task/TODO-42 opens that task. */
.chat-card-ref { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--border-strong); }
.chat-card-ref:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* Markdown-rendered assistant bubbles (TODO-100). Fallback plain-text bubbles
   keep the .chat-bubble pre-wrap; markdown handles its own block spacing. */
.chat-bubble.chat-md { white-space: normal; }
.chat-md p { margin: 0 0 8px; }
.chat-md > :last-child { margin-bottom: 0; }
.chat-md ul, .chat-md ol { margin: 0 0 8px; padding-left: 20px; }
.chat-md li { margin: 2px 0; }
.chat-md li > p { margin: 0; }
.chat-md h1, .chat-md h2, .chat-md h3,
.chat-md h4, .chat-md h5, .chat-md h6 {
  margin: 10px 0 6px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
}
.chat-md h1 { font-size: 17px; }
.chat-md h2 { font-size: 16px; }
.chat-md code {
  font-family: var(--font-mono);
  font-size: 13px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 4px;
}
.chat-md pre {
  margin: 0 0 8px;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  overflow-x: auto;
}
.chat-md pre code {
  display: block;
  background: none;
  border: none;
  padding: 0;
  line-height: 1.45;
}
.chat-md blockquote {
  margin: 0 0 8px;
  padding: 2px 10px;
  border-left: 2px solid var(--border-strong);
  color: var(--text-secondary);
}
.chat-md a { color: var(--accent); }
.chat-md hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 10px 0;
}
.chat-md table {
  display: block;
  border-collapse: collapse;
  margin: 0 0 8px;
  font-size: 13px;
  overflow-x: auto;
}
.chat-md th, .chat-md td {
  border: 1px solid var(--border);
  padding: 4px 8px;
}
.chat-md th { background: var(--surface); }
.chat-md img { max-width: 100%; }

.chat-notice {
  align-self: center;
  max-width: 92%;
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  padding: 2px 10px;
  word-break: break-word;
}

.chat-notice-error { color: var(--danger-text); }
.chat-notice-warn { color: var(--status-in-progress); }

/* First-run welcome (and the empty thread list): centered, quiet, actionable. */
.chat-empty-notice {
  align-self: center;
  margin: auto 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  text-align: center;
  padding: var(--space-md);
  color: var(--text-muted);
}

.chat-empty-icon {
  display: inline-flex;
  color: var(--text-muted);
  opacity: 0.7;
}

.chat-empty-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 17px;
  font-weight: 500;
  color: var(--text-secondary);
}

/* The 176px sidebar needs a quieter empty state than the message column. */
.chat-threads-list .chat-empty-title { font-size: 14px; }
.chat-threads-list .chat-empty-notice { padding: var(--space-sm); }

.chat-empty-sub {
  font-size: 12px;
  max-width: 32ch;
}

.chat-suggestions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
  margin-top: var(--space-xs);
}

.chat-suggestion {
  padding: 5px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.chat-suggestion:hover {
  background: var(--surface-hover);
  color: var(--text);
  border-color: var(--border-strong);
}

/* Thread sidebar: persistent on desktop, recessed against the panel surface.
   Collapses to zero width via .chat-sidebar-collapsed on the panel — instantly,
   in the same frame the panel narrows, so the message column never squeezes.
   Inner content keeps its natural width so nothing rewraps at width 0. */
.chat-threads {
  /* --chat-threads-w is set inline by the sidebar resize handle */
  width: var(--chat-threads-w, 176px);
  flex-shrink: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border-right: 1px solid var(--border);
  overflow: hidden;
}

.chat-sidebar-collapsed .chat-threads {
  width: 0;
  border-right: none;
}

/* Drag handle on the sidebar's right edge: a zero-net-width flex child that
   straddles the border, same reveal behavior as the panel's edge handle. */
.chat-threads-resize {
  position: relative;
  width: 8px;
  margin: 0 -4px;
  flex-shrink: 0;
  cursor: ew-resize;
  z-index: 5;
  touch-action: none;
}
.chat-threads-resize::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 3px;
  width: 2px;
  background: var(--accent);
  opacity: 0;
  transition: opacity var(--dur-fast);
}
.chat-threads-resize:hover::after,
.chat-threads-resize:focus-visible::after,
body.chat-threads-resizing .chat-threads-resize::after {
  opacity: 0.7;
}

.chat-sidebar-collapsed .chat-threads-resize { display: none; }

@keyframes chat-threads-in {
  from { transform: translateX(15%); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

/* Same vertical padding as .chat-header, and both rows contain a .btn-icon —
   the bottom borders of sidebar header and panel header stay level. */
.chat-threads-header {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 10px var(--space-sm);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  min-width: var(--chat-threads-w, 176px);
}

.chat-threads-title {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 15px;
  font-weight: 500;
  color: var(--text);
}

/* Mobile-only back affordance; desktop collapses via the sidebar's own
   panel-toggle button instead. */
.chat-threads-back { display: none; }

.chat-threads-collapse { margin-left: auto; }

/* One visible toggle at a time on desktop: the sidebar carries it while open,
   the panel header only while collapsed. */
body:not(.is-mobile) .chat-panel:not(.chat-sidebar-collapsed) #chat-threads-btn {
  display: none;
}

/* Thread search (TODO-103): between the header that names the list and the list
   itself — above the thing it filters. Carries the same min-width as its two
   neighbours so the field does not rewrap while the sidebar animates to width 0.
   Built entirely from existing tokens, so the light themes need no rules of
   their own. */
.chat-threads-search {
  position: relative;
  display: flex;
  align-items: center;
  flex-shrink: 0;
  padding: var(--space-sm) var(--space-sm) 0;
  min-width: var(--chat-threads-w, 176px);
}

.chat-threads-search-input {
  flex: 1 1 auto;
  min-width: 0;
  /* Right padding is the ✕'s lane: the button overlays the field rather than
     sitting beside it, so the input keeps the sidebar's full width. */
  padding: 6px 26px 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
}

.chat-threads-search-input::placeholder { color: var(--text-muted); }

.chat-threads-search-input:focus {
  outline: none;
  border-color: var(--accent);
}

/* WebKit draws its own clear glyph inside type="search"; ours is themed and
   keyboard-reachable, and two of them side by side reads as a bug. */
.chat-threads-search-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

.chat-threads-search-clear {
  position: absolute;
  right: calc(var(--space-sm) + 5px);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.chat-threads-search-clear:hover {
  color: var(--text);
  background: var(--surface-hover);
}

.chat-threads-list {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: var(--chat-threads-w, 176px);
}

.chat-thread-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--text);
  cursor: pointer;
  font: inherit;
}

.chat-thread-item:hover {
  background: var(--surface-hover);
}

.chat-thread-current {
  background: var(--surface-active);
  border-color: var(--border);
}

.chat-thread-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
}

.chat-thread-title {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-thread-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  font-size: 11px;
  color: var(--text-muted);
}

/* The one elastic item on the row: the id, the queued chip (TODO-217) and the
   status badge are all flex: none, so without this the text refuses to give up
   its width and the badge is clipped off the end of a narrow dock instead. */
.chat-thread-meta-text {
  flex: 1 1 auto;
  /* A floor, not zero (TODO-311). The PR chip is one more flex: none item on
     this line, and at the sidebar's default 176px a bare min-width: 0 let the
     date be crushed to the single letter "A" — an ellipsis with nothing in
     front of it. 4.5ch keeps at least "Aug 2…" readable; past that the chip
     and the badge are the things worth the pixels. */
  min-width: 4.5ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Row actions surface on hover/keyboard focus, same voice as the comment
   buttons. display, not opacity: hidden buttons must give their layout space
   back so the title runs to the edge. :focus (the row has tabindex) makes
   them reachable by Tab; :focus-within keeps them shown while focused. */
.chat-thread-actions {
  display: none;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}
.chat-thread-item:hover .chat-thread-actions,
.chat-thread-item:focus .chat-thread-actions,
.chat-thread-item:focus-within .chat-thread-actions {
  display: flex;
}

/* While renaming, the actions get out of the way too — the input takes the
   full row. Same specificity as the show rules; later in the sheet wins. */
.chat-thread-item.chat-thread-renaming .chat-thread-actions {
  display: none;
}

.chat-thread-action {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px 4px;
  line-height: 1;
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  transition: color var(--dur-fast), background var(--dur-fast);
}
.chat-thread-action:hover {
  color: var(--text);
  background: var(--surface-hover);
}
.chat-thread-action-delete:hover {
  color: var(--danger-text);
  background: oklch(0.65 0.20 28 / 0.12);
}
.chat-thread-action.confirming {
  opacity: 1;
  color: var(--danger-text);
  background: oklch(0.65 0.20 28 / 0.18);
}

.chat-thread-rename {
  width: 100%;
  font-size: 13px;
  padding: 1px 6px;
}

/* Latest-run badge on a thread row: live runs pulse, failures name themselves. */
.chat-thread-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  font-size: 11px;
}

.chat-thread-badge-live { color: var(--accent); }
.chat-thread-badge-paused { color: var(--status-in-progress); }
.chat-thread-badge-error { color: var(--danger-text); }

.chat-thread-badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: chat-pulse 1.2s ease-in-out infinite;
}

/* Messages waiting on a thread's run (TODO-217). Muted and static: what is
   queued is not activity, and the badge beside it is already pulsing. */
.chat-thread-queued {
  flex: none;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

/* --- PR chips in the two chat lists (TODO-311) ---
   The thread's pull request, on the sidebar row's meta line and on a ticket's
   Chats row. Quieter than .chat-status-pr on purpose: that chip sits alone in
   a strip and can afford a frame, while these sit in a line that already
   carries an id, a date, a message count and possibly a badge. So: no border,
   no background, just the glyph, the number and the state tint. Same four
   colours as the strip's chip, because it is the same fact. */
.chat-thread-pr,
.task-chat-pr {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex: none;
  color: var(--text-muted);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  border-radius: 3px;
  padding: 0 2px;
}

.chat-thread-pr { font-size: 11px; }
.task-chat-pr { font-size: 0.72rem; }

.chat-thread-pr:hover,
.task-chat-pr:hover {
  background: var(--surface-hover);
  text-decoration: underline;
}

.chat-thread-pr:focus-visible,
.task-chat-pr:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Same rule as .chat-status-pr: an open PR is a thing to go and look at, a
   merged one is a thing that landed, a closed one is neither, and a draft is
   not asking for attention yet. */
.chat-thread-pr[data-state='open'],
.task-chat-pr[data-state='open'] { color: var(--status-done); }
.chat-thread-pr[data-state='draft'],
.task-chat-pr[data-state='draft'] { color: var(--text-muted); }
.chat-thread-pr[data-state='merged'],
.task-chat-pr[data-state='merged'] { color: var(--pr-merged); }
.chat-thread-pr[data-state='closed'],
.task-chat-pr[data-state='closed'] { color: var(--status-blocked); }

/* The overflow marker is a count, not a link — it has no single PR to point at.
   Muted and un-hoverable so it does not read as something that was clicked and
   did nothing; the tooltip lists what it stands for. */
.chat-thread-pr-more,
.task-chat-pr-more {
  color: var(--text-muted);
  cursor: default;
}
.chat-thread-pr-more:hover,
.task-chat-pr-more:hover {
  background: none;
  text-decoration: none;
}

/* Unread signal (TODO-153). The title row, not the meta line the status badge
   above lives on — unread and "currently running" are orthogonal, a thread can
   be re-running while an earlier reply is still unread. Deliberately NOT
   animated: .chat-thread-badge-dot already pulses on the meta line below, and
   two pulsing accent dots on one row would read as one thing. */
.chat-thread-unread-dot {
  display: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}

.chat-thread-item.chat-thread-unread .chat-thread-unread-dot {
  display: inline-block;
}

.chat-thread-item.chat-thread-unread .chat-thread-title {
  font-weight: 600;
}

/* Launcher badges: a corner dot on the chat button and the mobile tab, so a
   finished background thread is visible without opening the panel at all. */
#chat-btn,
#m-chat-tab {
  position: relative;
}

#chat-btn.has-unread::after,
#m-chat-tab.has-unread::after {
  content: '';
  position: absolute;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
}

#chat-btn.has-unread::after {
  top: 2px;
  right: 2px;
}

#m-chat-tab.has-unread::after {
  top: 2px;
  right: calc(50% - 18px);
}

/* --- The running light (TODO-333) ----------------------------------------
   While ANY thread has a run in flight, an accent light travels around the
   launcher — the same statement .chat-console-ring makes about the composer,
   and deliberately the same 2.6s @keyframes chat-orbit, so the two surfaces
   say "a run is in flight" in one voice.

   Why this is a layer of its own rather than the corner dot animated: running
   and unread are different facts and a thread is routinely both. Sharing the
   dot would have collapsed them into one pixel — and under reduced motion,
   where the rule below stops the movement but keeps the state, into one
   IDENTICAL pixel.

   THE MASK IS THE DIFFERENCE FROM .chat-console-ring. That one masks its disc
   with a solid var(--surface) fill, which it can because the composer's
   background is always exactly that. A launcher's is not: .btn-icon.active is
   --accent-dim for as long as the panel is open, and .m-tab is transparent over
   the tab bar. So the ring band is CUT with mask-composite instead — the centre
   stays genuinely transparent, whatever is behind it survives, and no theme
   needs a rule of its own. `padding` is the ring's thickness: the two mask
   layers differ by exactly the padding box, so excluding one from the other
   leaves the band and nothing else. */
.chat-btn-ring {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  overflow: hidden; /* the disc below is wider than the button */
  padding: 1.5px;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-normal) var(--ease-out), visibility var(--dur-normal);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

.has-running .chat-btn-ring {
  opacity: 1;
  visibility: visible;
}

/* Same two constraints as the composer's disc: it must cover the button's
   diagonal at every angle (190% of a near-square button is ample), and it spins
   about the button's centre — `translate` centres it, `rotate` turns it, and
   the two individual transform properties compose without fighting. */
.chat-btn-ring::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 190%;
  aspect-ratio: 1;
  translate: -50% -50%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    transparent 250deg,
    var(--accent) 340deg,
    transparent 360deg);
  animation: chat-orbit 2.6s linear infinite;
}

/* Header button: the ring sits just outside the 5px button radius. The icon has
   to be lifted over it — the ring is a later sibling and would otherwise paint
   across the glyph's top-left corner. */
#chat-btn .chat-btn-ring {
  inset: -1px;
  border-radius: 6px;
}

#chat-btn svg {
  position: relative;
  z-index: 1;
}

.chat-composer {
  position: relative; /* anchors the jump-to-latest pill */
  flex-shrink: 0;
  padding: var(--space-sm) var(--space-md) var(--space-md);
}

/* Floats above the composer while the user reads backlog mid-stream.
   Accent-filled like Send: it exists to be noticed over bubble content. */
.chat-jump-btn {
  position: absolute;
  top: -40px;
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-text);
  cursor: pointer;
  box-shadow: var(--dropdown-shadow);
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.chat-jump-btn:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

@keyframes chat-pulse {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50%      { opacity: 1; transform: scale(1); }
}

/* --- The console card (TODO-106) ------------------------------------------
   One bordered surface instead of five stacked strips. Three layers:
     .chat-console       the card and its border
     .chat-console-ring  the running light, at z-index 0
     .chat-console-body  everything else, at z-index 1
   The body wrapper is not cosmetic — without it the orbiting gradient paints
   over the controls. It also carries the corner clipping, so the status strip
   and the controls row need no radius of their own and the card stays square
   with them however many rows are hidden. Clipping the BODY rather than the
   card is deliberate: the ring lives at inset -1px and would be cut off. */
.chat-console {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  transition: border-color var(--dur-fast);
}

.chat-console:focus-within {
  border-color: var(--border-strong);
}

.chat-console-body {
  position: relative;
  z-index: 1;
  border-radius: 11px;
  overflow: hidden;
}

/* A conic-gradient disc spins behind the card; an inner layer filled with the
   card's own background masks all but a 1px ring, so what shows is a light
   travelling around the border. Two constraints, both load-bearing:

   - The disc must cover the card's diagonal at every angle. 150% of the width
     at a 1:1 ratio does it for any card at least as wide as it is tall
     (diagonal <= w*sqrt2 ~ 1.42w), which the composer always is. The mock's
     fixed 900px would show the disc's own edge on a wide desktop panel.
   - The mask must be the same SOLID token as the card. A gradient or a
     translucent fill would let the disc show through the middle. */
.chat-console-ring {
  position: absolute;
  inset: -1px;
  z-index: 0;
  border-radius: 13px;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-normal) var(--ease-out), visibility var(--dur-normal);
}

.chat-console-running .chat-console-ring {
  opacity: 1;
  visibility: visible;
}

.chat-console-ring::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 150%;
  aspect-ratio: 1;
  translate: -50% -50%;
  background: conic-gradient(from 0deg,
    transparent 0deg,
    transparent 260deg,
    var(--accent) 340deg,
    transparent 360deg);
  animation: chat-orbit 2.6s linear infinite;
}

.chat-console-ring::after {
  content: '';
  position: absolute;
  inset: 1px;
  border-radius: 12px;
  background: var(--surface);
}

/* Rotation only — `translate` above centres the disc and the two individual
   transform properties compose without fighting, so the disc spins about the
   card's centre rather than drifting. */
@keyframes chat-orbit {
  to { rotate: 360deg; }
}

/* --- Status strip ---
   --bg rather than a new token: it is recessed relative to --surface in BOTH
   modes (0.145 vs 0.185 dark, 0.94 vs 0.97 on paper-ink), so the strip reads as
   inset either way instead of inverting the way --surface-hover would. */
.chat-status {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 7px 11px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
}

.chat-status-place,
.chat-status-git {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 1 auto;
  min-width: 0;
}

/* The place segment is a button — it opens the per-repo breakdown, or the
   picker itself when there's nothing yet to break down (TODO-243) — but must
   read as part of the strip, not as a control sitting in it. Stripped to
   nothing at rest; the affordance is the hover brightening, which is enough
   for a target that already carries a tooltip naming every repo. `font:
   inherit` is load-bearing: a button does not inherit the strip's mono 11px.
   `position: relative` backs the ::after touch-target expansion below. */
.chat-status-place {
  position: relative;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

/* A strip-sized frame is a strip-sized target, which a thumb misses — this is
   also the only way into repos on mobile now that the composer's folder
   button is gone. Same trick as .chat-status-usage::after: the target grows
   past the frame, out into the strip's own height and gaps, rather than the
   control growing and putting the row's cramped width problem back. */
.chat-status-place::after {
  content: '';
  position: absolute;
  inset: -10px -6px -10px -2px;
}

.chat-status-place:hover,
.chat-status-place[aria-expanded='true'] {
  color: var(--text);
}

.chat-status-place:hover .chat-status-icon,
.chat-status-place[aria-expanded='true'] .chat-status-icon {
  color: var(--text-secondary);
}

/* Picked but not yet checked out (TODO-243): a real reading of what WILL be
   there, not yet the live one, so it is styled as a promise rather than a
   fact — dashed underline, muted text, same rule a disabled control follows
   elsewhere in this file. */
.chat-status-place-pending {
  color: var(--text-muted);
}

.chat-status-place-pending .chat-status-cwd {
  border-bottom: 1px dashed var(--text-muted);
}

.chat-status-icon {
  flex: none;
  color: var(--text-muted);
}

.chat-status-cwd,
.chat-status-branch {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Hidden together with the branch it belongs to, so there is never a divider
   with nothing on the far side of it. */
.chat-status-sep {
  flex: none;
  width: 1px;
  height: 12px;
  background: var(--border-strong);
}

.chat-status-gap {
  flex: 1 1 auto;
  min-width: var(--space-sm);
}

.chat-status-diff {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  font-size: 10.5px;
}

/* It became a <button> in TODO-265 — it opens the viewer for the very changes
   it counts. Button defaults are reset back to the span it used to be so the
   strip's layout is unchanged; only the affordances are new. */
.chat-status-diff {
  font-family: inherit;
  cursor: pointer;
  appearance: none;
}
.chat-status-diff:hover { border-color: var(--accent); }
.chat-status-diff:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.chat-status-files { color: var(--status-in-progress); }
.chat-status-plus  { color: var(--status-done); }
.chat-status-minus { color: var(--status-blocked); }

/* --- The PR chip (TODO-214) ---
   Framed like .chat-status-diff, because it belongs to the same group: these
   are the facts about the working tree, as opposed to the ctx/spend readings
   about the run. Both variants share a frame so the single- and multi-repo
   cases do not look like different kinds of thing. */
.chat-status-pr,
.chat-status-prs {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  /* font-family/line-height inherited rather than left to the UA: one of these
     two is a <button>, which does not inherit either by default, and the pair
     must be visually identical. font-size is then set explicitly for both. */
  font-family: inherit;
  font-size: 10.5px;
  line-height: inherit;
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
}

.chat-status-pr:hover,
.chat-status-prs:hover,
.chat-status-prs[aria-expanded='true'] {
  border-color: var(--border-strong);
  background: var(--surface-hover);
}

/* State is the whole point of the tint: an open PR is a thing to go and look
   at, a merged one is a thing that landed, and a closed one is neither. Draft
   stays muted deliberately — it is not yet asking for attention. */
.chat-status-pr[data-state='open'] { color: var(--status-done); }
.chat-status-pr[data-state='draft'] { color: var(--text-muted); }
.chat-status-pr[data-state='merged'] { color: var(--pr-merged); }
.chat-status-pr[data-state='closed'] { color: var(--status-blocked); }

.chat-status-pr .chat-status-icon,
.chat-status-prs .chat-status-icon { flex: none; }

.chat-status-pr-num,
.chat-status-prs-count {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-status-ctx {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
}

.chat-status-ctx-label { color: var(--text-muted); }

.chat-status-meter {
  width: 52px;
  height: 3px;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}

/* Fills by scaleX rather than width: the bar animates on every assistant
   message, and transitioning width would relayout the strip each time.
   transform-origin pins the growth to the left edge; the 2px radius squashing
   with the scale is invisible on a 3px-tall bar the track already clips. */
.chat-status-meter-fill {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: 2px;
  background: var(--status-todo);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-normal) var(--ease-out);
}

.chat-status-spend {
  flex: none;
  color: var(--text-muted);
}

/* The mobile mount of the ⚡ trigger (TODO-175). Sized to the STRIP, not to the
   composer's 44px controls row — the whole point is that it costs the row
   nothing — so it reads as a strip segment that happens to be tappable, the
   way .chat-status-place already does.

   Desktop keeps the button in the controls row, where there is room for it and
   where a mouse does not care about target size. Hidden rather than never
   rendered so one #chat-pop-usage can serve both, with no second markup. */
body:not(.is-mobile) .chat-status-usage {
  display: none;
}

.chat-status-usage {
  position: relative;
  display: inline-flex;
  align-items: center;
  flex: none;
  height: 24px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font: inherit;
  cursor: pointer;
}

/* A 24px control on a strip is a 24px target, which a thumb misses. The frame
   stays strip-sized and the TARGET grows past it — out to the strip's full
   height and into its gaps — rather than the control growing to meet the
   44px guidance and putting the row's problem back in a different place. */
.chat-status-usage::after {
  content: '';
  position: absolute;
  inset: -10px -7px;
}

.chat-status-usage:active,
.chat-status-usage[aria-expanded='true'] {
  background: var(--surface-hover);
  color: var(--text);
}

/* Narrow card — the chat panel's default width, and every phone. The strip
   carries five fields and only about three fit, so it degrades the way the mock
   degrades it at 390: the diff loses its badge frame and file count, and the
   meter bar becomes plain text. Driven by a class from a ResizeObserver rather
   than a container query: container-type would make the card a containing block
   for the pickers' position:fixed dropdowns and move them off their triggers. */
.chat-console-narrow .chat-status {
  gap: 7px;
}

.chat-console-narrow .chat-status-ctx-label,
.chat-console-narrow .chat-status-meter,
.chat-console-narrow .chat-status-files,
.chat-console-narrow .chat-status-sep {
  display: none;
}

.chat-console-narrow .chat-status-diff {
  padding: 0;
  border: none;
  background: none;
}

/* The strip now carries six fields and fits about three at the panel's default
   width, so the PR chip has to pay its way here like everything else. It sheds
   its FRAME and its ICON but keeps its number (TODO-214): "#54" is the whole
   point of the ticket and is already only three characters, whereas the icon is
   decoration and the frame is what makes it look like a control. It stays a
   link — tinted, hoverable and clickable — without the chrome. */
.chat-console-narrow .chat-status-pr,
.chat-console-narrow .chat-status-prs {
  padding: 0;
  border: none;
  background: none;
}

.chat-console-narrow .chat-status-pr .chat-status-icon,
.chat-console-narrow .chat-status-prs .chat-status-icon {
  display: none;
}

/* :not(.hidden) is load-bearing — .hidden sets display:none, which does not
   stop the element matching as an adjacent sibling. Without it a thread with
   spend but no context reading shows a dot separating nothing from $0.00. */
.chat-console-narrow .chat-status-ctx:not(.hidden) + .chat-status-spend::before {
  content: '·';
  margin-right: 5px;
}

/* --- Container-down row ---
   Persistent, not a toast: the worker being unreachable is a standing condition
   you have to go and fix, not a transient notice you can miss. */
.chat-console-alert {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 8px 12px;
  border-bottom: 1px solid var(--status-blocked);
  background: var(--status-blocked-bg);
  font-size: 12px;
  line-height: 1.45;
  color: var(--danger-text);
}

.chat-console-alert::before {
  content: '';
  flex: none;
  width: 6px;
  height: 6px;
  background: var(--status-blocked);
  rotate: 45deg;
}

/* --- Rate-limit hold row (TODO-143) ---
   Same shape as .chat-console-alert, but its own colour: a held run is
   expected to clear itself, not a standing condition to go fix, so it borrows
   the amber --status-in-progress pair rather than the danger-red one above. */
.chat-ratelimit {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 8px 12px;
  border-bottom: 1px solid var(--status-in-progress);
  background: var(--status-progress-bg);
  font-size: 12px;
  line-height: 1.45;
  color: var(--status-in-progress);
}

.chat-ratelimit svg { flex: none; }

.chat-input {
  resize: none;
  min-height: 44px;
  max-height: 160px; /* mirrored by chatAutosize(); overflow scrolls past it */
  overflow-y: auto;
  padding: 12px 13px 6px;
  border: none;
  border-radius: 0;
  background: transparent;
}

.chat-input:focus {
  outline: none;
  box-shadow: none;
}

/* --- Controls row --- */
/* Wraps rather than shrinks. The chat panel's default width leaves this row
   about ten pixels short once a run is live, and letting flex absorb that
   turned every pill into an ellipsis ("Op… h… Y…"). Two legible lines beat one
   unreadable one, and at any comfortable panel width it stays on one line. */
.chat-console-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 9px 9px;
  border-top: 1px solid var(--border);
}

/* Right-aligns the trailing group with an auto margin rather than a spacer
   element: a flex spacer only works on ONE line, and this row wraps. Exactly one
   of these two is ever visible, so the margins can never fight.
   .chat-queue-btn (TODO-217) is deliberately NOT in this list: it only appears
   while a run is live, which is exactly when .chat-working is carrying the auto
   margin — a second one would split the group across the gap. */
.chat-working,
.chat-send-btn {
  margin-left: auto;
}

/* --- Touch pickers (TODO-106) ---
   On a phone the three desktop pills are replaced by two 44px controls: model
   and effort merge into one, mode keeps its own. They drive the same <select>
   elements, so nothing about the per-mode config logic is duplicated. */
.chat-mobile-pickers {
  display: none;
  align-items: center;
  gap: 6px;
  flex: none;
}

body.is-mobile .chat-mobile-pickers {
  display: flex;
}

body.is-mobile .chat-composer-options {
  display: none;
}

body.is-mobile .chat-console-btn {
  width: 44px;
  height: 44px;
  border-radius: 10px;
}

/* The ⚡ usage trigger was a SIXTH 44px control on a row that fit five at the
   time (TODO-175; the row has since lost the repo button too, TODO-243). It
   is what pushed Send onto a second line, so on mobile it stands down here
   and reappears in the status strip as #chat-status-usage —
   a 22px segment beside the ctx and spend readings, which is where a reading
   belongs anyway. Same popover, so nothing about the meters is duplicated.

   Hidden outright rather than only while a run is live: it was already the one
   control the .chat-console-running rules below did not step aside, so the
   mid-run row read usage + config + stop. One rule settles both cases — and
   the strip, unlike this row, has room for it mid-run too. */
body.is-mobile #chat-usage-btn {
  display: none;
}

.chat-mobile-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
}

.chat-mobile-dot {
  flex: none;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--text-muted);
}

/* The same glyph the desktop pills use — a text ▾ at this size reads as a
   full stop. */
.chat-mobile-chev {
  flex: none;
  width: 12px;
  height: 12px;
  color: var(--text-muted);
}

/* Circle in every mode but YOLO, where it becomes the same red diamond the
   gate notices use. */
.chat-mobile-glyph {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.chat-console-yolo .chat-mobile-glyph {
  border-radius: 0;
  background: var(--status-blocked);
  rotate: 45deg;
}

.chat-console-yolo #chat-mode-btn {
  border-color: var(--danger-fill);
  background: var(--status-blocked-bg);
  color: var(--danger-text);
}

/* --- The row's shrink floor (TODO-175) ---
   Reclaiming the ⚡ button's 44px fixes the row at the labels in the report,
   but the labels are not fixed: lib/agentOptions.js runs up to "GPT-5.6
   Terra", some 50px past "Opus 5", and "medium" is wider than "high". At those
   the row wrapped with or without the usage button — so it was never only
   TODO-146's doing, and reclaiming space alone would leave the bug waiting for
   the next model that ships.

   The row is therefore made structurally unable to wrap. This is NOT the
   desktop compromise .chat-console-controls warns about above: that one
   squeezed all three pills at once until every one of them read "Op… h… Y…".
   Here exactly one label gives way, only once there is nothing left to give,
   in this order —

   - the 44px buttons never shrink (flex: none, .chat-console-btn)
   - the mode pill never shrinks: "YOLO" is the one word on this row that has
     to be readable before the thumb reaches Send
   - the EFFORT word gives way first, via a shrink factor an order of magnitude
     above the model's. Measured, this is what the ordering buys: at a 393px
     viewport "Sonnet 5 · medium" is 26px over, and letting both halves shrink
     in proportion spends most of that on the model name — "Son… · medi…" —
     when the name is the half that actually identifies what is about to run.
     Effort is a qualifier on a value the pill has already given you, it is
     spelled out in full in the segmented control one tap away, and mid-run
     .chat-runcfg prints the whole triple anyway.
   - the model NAME gives way last, at "GPT-5.6 Ter…", which still answers the
     question the pill is asked — which provider am I about to spend. */
/* The floor only exists if the row is FORBIDDEN to wrap. A wrapping flex line
   never shrinks its items to avoid a break — it breaks, and the items that fit
   keep their full width — so on `flex-wrap: wrap` every `flex: 0 1 auto` below
   is dead weight and Send drops to line two exactly as reported. Mobile is
   therefore nowrap, which is what turns "the row might wrap" into "the model
   label gives way".

   Desktop keeps `wrap` deliberately — see .chat-console-controls above: at the
   docked panel's default width three pills squeezed onto one line all
   ellipsized at once, and two legible lines beat one unreadable one. That
   argument holds there and not here: this row has four items now (attach,
   the two-pill wrapper, send — the repo button left for the status strip in
   TODO-243), and only one of them is allowed to give.

   The gap and side padding are tightened here for the reason spelled out under
   .chat-mobile-pill below — together they are seven of the twenty-two pixels
   the row was short even at the default model. The BOTTOM padding is left
   alone: it is what separates the row from the card's edge, and the row has no
   trouble in that direction. */
body.is-mobile .chat-console-controls {
  flex-wrap: nowrap;
  gap: 5px;
  padding-left: 7px;
  padding-right: 7px;
}

/* `flex: 0 1 auto` and not just `min-width: 0`: .chat-mobile-pickers is
   `flex: none` above, and a wrapper that refuses to shrink is a ceiling the
   pill inside it can never get under, however shrinkable that pill is. */
body.is-mobile .chat-mobile-pickers {
  flex: 0 1 auto;
  min-width: 0;
  gap: 5px;
}

/* Originally measured against a five-item row (attach + repo + send, 132px,
   plus gaps) that left two pills fighting over 203px against a natural 225px
   — 22px short even on "Opus 5 · high", the shortest pairing anyone actually
   runs. TODO-243 removed the repo button from this row (it lives in the
   status strip now), which hands back roughly 44px + a gap — likely enough on
   its own that the floor above no longer engages at the default model. Left
   in place anyway, unre-measured, as a safety net for whatever model name
   ships next rather than assumed dead: shrinking spacing that was already
   this tight costs nothing when it isn't needed, and re-tuning it back
   looser needs a real measurement, not a guess from an equation that changed.

   Original budget, unchanged, for whichever part of this still applies: padding
   12→8, the pill's internal gaps 6→4, the row's gap 6→5 and its side padding
   9→7. Nothing is removed — both chevrons stay, so the pills still read as
   pickers rather than as the plain-text .chat-runcfg readout that replaces
   them mid-run — and at a 44px tap height 9px of padding is still
   comfortable. The label ellipsis is left to do what it should: catch the
   genuinely long names, not the default. */
body.is-mobile .chat-mobile-pill {
  gap: 4px;
  padding: 0 7px;
}

/* 12→10. Worth its own rule because it is what clears 375px — the iPhone SE
   and the 13 mini — where "Opus 5 · high", the default pairing, was still
   clipping to "Opu…" by a handful of pixels with everything else already
   tightened. The chevron is a 2px-stroke glyph; at 10px it is still plainly a
   chevron, and keeping it at all is the point (see .chat-mobile-pill above). */
body.is-mobile .chat-mobile-chev {
  width: 10px;
  height: 10px;
}

body.is-mobile #chat-mp-btn {
  flex: 0 1 auto;
  min-width: 0;
}

body.is-mobile #chat-mp-model {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The high shrink factor is the whole ordering: flex distributes shrink in
   proportion to each item's base size, so equal factors would take most of the
   deficit out of the LONGER item — the model name. At 12 against the model's 1,
   the effort word absorbs the overflow until it has nothing left to give, and
   only then does the name start to go. The min-width keeps it from collapsing
   to a bare ellipsis: below about three characters "high" and "low" stop being
   tellable apart, at which point it should be showing nothing rather than
   something misleading. */
body.is-mobile #chat-mp-effort {
  flex: 0 12 auto;
  min-width: 3ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A column, and bounded (TODO-322).

   These grow UPWARD — `bottom: 100%` off the composer — so an unbounded one
   runs off the top of its own sheet rather than off the bottom of the screen,
   and the rows it loses are the first ones, silently. `#chat-pop-repos` had no
   bound at all and no scroller under it: with a few repos checked out the top
   of the list was simply unreachable on a phone.

   `max-height` is written by chatOpenPop from measured geometry rather than set
   here in `vh`, because `vh` is the LAYOUT viewport — it does not know the
   keyboard exists, and the keyboard is exactly when the space above the
   composer is scarce. The 70vh here is only the desktop-shaped fallback for a
   pop opened before that measurement can run.

   Column + the .chat-pop-list rule below is what keeps `.chat-pop-foot`
   ("Manage repositories…") pinned under the scroller instead of scrolling away
   with the rows — it is the way OUT of a list too long to read, so it is the
   one thing that must never need scrolling to. */
.chat-pop {
  position: absolute;
  left: var(--space-sm);
  right: var(--space-sm);
  bottom: 100%;
  z-index: 30;
  margin-bottom: var(--space-sm);
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--dropdown-shadow);
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-height: 70vh;
}

/* The scroller every popover was missing. `min-height: 0` is load-bearing: a
   flex item's default `min-height: auto` refuses to shrink below its content,
   which is how a list inside a capped column overflows it instead of
   scrolling. `overscroll-behavior: contain` keeps a flick that reaches the end
   of the list from continuing into the transcript behind it. */
.chat-pop-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* ...and everything ELSE in a popover is chrome around that scroller, so the
   list is the only thing allowed to give (TODO-322). Stated once as "not the
   list" rather than named per element: the pops already carry five different
   kinds of chrome between them — a search head, a footer action, two notes, an
   effort segment — and a sixth added later would otherwise silently default to
   shrink: 1 and be squashed by a long list. .chat-pop-caret is absolutely
   positioned, so it is not a flex item and this does not reach it. */
.chat-pop > :not(.chat-pop-list) {
  flex: none;
}

/* Points back at the pill that opened it. Positioned by JS from the trigger's
   own centre rather than a hardcoded offset, so it stays on target at any
   width and in any language. */
.chat-pop-caret {
  position: absolute;
  bottom: -6px;
  width: 10px;
  height: 10px;
  background: var(--surface);
  border-right: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
  rotate: 45deg;
}

.chat-pop-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 13px 10px;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}

.chat-pop-row:hover {
  background: var(--surface-hover);
}

.chat-pop-mark {
  flex: none;
  width: 6px;
  height: 6px;
  margin-top: 6px;
  border-radius: 50%;
  background: var(--border-strong);
}

.chat-pop-row.selected .chat-pop-mark {
  background: var(--accent);
}

.chat-pop-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.chat-pop-desc {
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-muted);
}

.chat-pop-meta {
  flex: none;
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

.chat-pop-rule {
  height: 1px;
  margin: var(--space-sm) var(--space-xs);
  background: var(--border);
}

/* --- The per-repo breakdown (TODO-134) ---
   Rows in the strip's popover. MUST stay after the .chat-pop-* rules above:
   .chat-repo-mark-error and .chat-pop-mark both set background at equal
   specificity, so source order is what decides which wins. Placed earlier, a
   failed clone's marker silently stays grey.

   Not selectable like the picker rows above them, so they lose the pointer and
   the hover fill: this is a readout, and something that highlights under the
   cursor without doing anything is a lie. */
.chat-repo-row {
  cursor: default;
  align-items: center;
  padding: 9px 10px;
}

.chat-repo-row:hover {
  background: transparent;
}

/* A repo that failed to clone says so where its path would be — the
   alternative is a repo that is silently just not there. */
.chat-repo-mark-error {
  background: var(--status-blocked);
}

.chat-repo-row-error .chat-pop-label {
  color: var(--danger-text);
}

.chat-repo-row-error .chat-pop-desc {
  color: var(--status-blocked);
}

/* Picked but not yet checked out (TODO-243) — the breakdown's fallback when
   chatStatus.repos is still empty. Italic rather than a new colour: it is the
   same muted grey every desc already uses, just marked as a promise instead
   of a reading. */
.chat-repo-row-pending .chat-pop-desc {
  font-style: italic;
}

.chat-repo-branch {
  color: var(--text-secondary);
}

.chat-pop-meta > * + * {
  margin-left: 6px;
}

/* Untracked reads as a question, not a change: it is the one count here that
   carries no +/- of its own. */
.chat-repo-untracked {
  color: var(--text-muted);
}

/* The per-repo PR link (TODO-214). The only interactive thing in a row that is
   otherwise a readout, so it carries an underline on hover to say so — the row
   itself does not navigate anywhere. Tints match the strip's chip exactly. */
.chat-pop-pr {
  color: var(--text-secondary);
  text-decoration: none;
}

.chat-pop-pr:hover { text-decoration: underline; }

.chat-pop-pr[data-state='open'] { color: var(--status-done); }
.chat-pop-pr[data-state='draft'] { color: var(--text-muted); }
.chat-pop-pr[data-state='merged'] { color: var(--pr-merged); }
.chat-pop-pr[data-state='closed'] { color: var(--status-blocked); }

/* A footer action on a popover that is otherwise a readout — the way from the
   breakdown to the control that changes it. */
.chat-pop-foot {
  display: block;
  width: 100%;
  margin-top: 2px;
  padding: 10px;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0 0 9px 9px;
  background: transparent;
  color: var(--accent);
  font-family: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}

.chat-pop-foot:hover {
  background: var(--surface-hover);
}

/* --- Subscription usage popover (TODO-146) ---
   A readout, like the repo breakdown above it: no hover fill, no pointer. */
.chat-usage-row {
  cursor: default;
  align-items: center;
}

.chat-usage-row:hover {
  background: transparent;
}

/* One provider's heading. Its own row rather than a nested list because the
   two providers are peers and either may be absent — OpenCode switched off
   drops the OpenAI group entirely. */
.chat-usage-group {
  padding: 10px 10px 2px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.chat-usage-group + .chat-usage-row {
  padding-top: 6px;
}

/* Deliberately independent of .chat-status-meter/-fill (the ctx meter): that
   pair carries `.chat-console-narrow .chat-status-meter { display: none }`, a
   rule aimed at the STRIP's five-field degradation at the panel's default
   width. This popover is not part of that degradation — sharing the class
   name hid the bar outright the moment the console was narrow, which is its
   usual state. Visuals are otherwise identical to the ctx meter on purpose. */
.chat-usage-meter {
  flex: none;
  width: 60px;
  height: 3px;
  margin-left: auto;
  border-radius: 2px;
  background: var(--border);
  overflow: hidden;
}

.chat-usage-meter-fill {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: 2px;
  background: var(--status-todo);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-normal) var(--ease-out);
}

/* The meter earns its colour rather than being read for it: the percentage
   next to it is the number, and these two only say "getting close" and
   "nearly gone" without needing to be read at all. */
.chat-usage-meter-warn {
  background: var(--status-in-progress);
}

.chat-usage-meter-high {
  background: var(--status-blocked);
}

.chat-usage-pct {
  flex: none;
  min-width: 32px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-secondary);
  text-align: right;
}

/* One provider failing while the other answers is a normal state here, so a
   provider's own error sits in its group rather than replacing the popover. */
.chat-usage-err {
  padding: 2px 10px 10px;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
}

/* Its own class rather than .chat-picker-note: that one is dedicated to the
   repo picker's clash warning and is red by default with no muted variant.
   This note is neutral information ("as of hh:mm") far more often than it is
   an error, so neutral is the base state and .chat-usage-note-error is the
   exception, not the reverse. */
.chat-usage-note {
  padding: 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-muted);
}

.chat-usage-note.chat-usage-note-error {
  color: var(--danger-text);
}

.chat-picker-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 2px 6px;
}

.chat-repo-search {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
}

.chat-repo-search:focus {
  outline: none;
  border-color: var(--accent);
}

.chat-repo-refresh {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text-muted);
  cursor: pointer;
}

.chat-repo-refresh:hover {
  color: var(--text);
}

/* .chat-picker-list has no rule of its own any more. The catalogue is every
   repo the token can see — hundreds on a real account — and it used to bound
   itself at `max-height: 46vh`. 46vh is the LAYOUT viewport: ~392pt of cap
   inside a 284pt sheet on a phone with the keyboard up (TODO-322). Bounding and
   scrolling now come from .chat-pop / .chat-pop-list, which every popover
   shares, and the real cap is measured in chatOpenPop. */

.chat-picker-row {
  align-items: center;
  padding: 10px;
}

/* The mention autocomplete (TODO-148) likewise. Capped at 8 results, but a long
   title still shouldn't be able to push the popover past the composer's own
   width. Its `max-height: 46vh` / `overflow-y` went the same way as the
   picker's (TODO-322) — scrolling belongs to .chat-pop-list, one layer in, or
   the pop scrolls its own padding and border along with the rows. */

.chat-mention-row {
  align-items: center;
  padding: 8px 10px;
  gap: 8px;
}

.chat-mention-row.selected {
  background: var(--surface-hover);
}

.chat-mention-row-id {
  flex: none;
  font-family: monospace;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.chat-mention-row-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
}

/* A square rather than the round dot the single-choice lists use — this is a
   multi-select, and the shape should say so before the label does. */
.chat-picker-check {
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--border-strong);
  border-radius: 3px;
  background: transparent;
}

.chat-picker-check.checked {
  border-color: var(--accent);
  background: var(--accent);
}

.chat-picker-empty,
.chat-picker-note {
  padding: 10px;
  font-size: 12.5px;
  line-height: 1.5;
}

.chat-picker-note:empty {
  display: none;
}

/* Colour stated per state rather than as a base plus an override: .chat-picker-
   error is applied ALONGSIDE .chat-picker-empty, so a shared muted default would
   need this rule to win on source order alone. */
.chat-picker-empty {
  color: var(--text-muted);
}

/* Both failure states read as failures — a catalogue that would not load, and a
   directory-name collision between two repos. Neither is an empty list. */
.chat-picker-empty.chat-picker-error,
.chat-picker-note {
  color: var(--danger-text);
}

/* Effort is a segmented control rather than a fourth list: the tiers are an
   ordered scale, and a row of radio-ish options reads as one. */
.chat-mobile-seg {
  display: flex;
  gap: 3px;
  padding: 3px;
  border-radius: 10px;
  background: var(--bg);
}

.chat-mobile-seg-btn {
  flex: 1;
  padding: 13px 0;
  border: none;
  border-radius: 7px;
  background: transparent;
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
}

.chat-mobile-seg-btn.selected {
  background: var(--surface-active);
  color: var(--text);
}

.chat-console-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast);
}

.chat-console-btn:hover:not(:disabled) {
  background: var(--surface-hover);
  color: var(--text);
}

.chat-console-btn:active:not(:disabled) {
  transform: scale(0.94);
}

.chat-attach-btn:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

/* The ⚡ usage trigger has NO tint (TODO-175). TODO-146 gave it a two-tier
   amber/red border so "should I look" would not require opening the popover,
   but at 80% it recoloured a resting control that sits between two plain grey
   ones, and the row read as something being wrong when nothing was. The
   trigger stays chrome-coloured like its neighbours; the reading itself is
   still coloured, on the meters inside the popover, where it is next to the
   number it describes. The summary also rides the trigger's title. */

.chat-send-btn {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-text);
}

.chat-send-btn:hover:not(:disabled) {
  border-color: var(--accent-hover);
  background: var(--accent-hover);
  color: var(--accent-text);
}

.chat-send-btn:disabled {
  border-color: var(--border);
  background: var(--surface);
  color: var(--text-muted);
  cursor: default;
}

/* Stop keeps the accent, in every mode. Only the ENABLED Send goes red under
   YOLO, so the colour says "this will act unchecked" rather than "this button
   is dangerous to look at". */
.chat-stop-btn {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: var(--accent);
}

.chat-stop-btn:hover:not(:disabled) {
  background: var(--accent);
  color: var(--accent-text);
}

/* Queue (TODO-217) sits next to Stop while a run is live, so it is quieter than
   Send by design: an outline rather than a fill. Queueing is a deferral, not the
   act Send is, and two filled accent buttons side by side would compete. It also
   deliberately takes no YOLO treatment below — nothing acts when you press it. */
.chat-queue-btn {
  border-color: var(--border-strong);
  background: var(--surface);
  color: var(--text-secondary);
}

.chat-queue-btn:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

.chat-console-yolo .chat-send-btn:not(:disabled) {
  border-color: var(--danger-fill);
  background: var(--danger-fill);
  color: var(--danger-fill-text);
}

.chat-console-yolo .chat-send-btn:not(:disabled):hover {
  border-color: var(--danger-fill-hover);
  background: var(--danger-fill-hover);
  color: var(--danger-fill-text);
}

/* Replaces the old .chat-working row: the orbiting ring carries "something is
   happening", this carries WHAT and for how long. The label still holds the
   queued / waiting / reconnecting / cancelling states, which the ring cannot
   express on its own. */
/* The whole row has to survive the panel at its narrowest. Order of sacrifice:
   the status LABEL ellipsizes first (the ring already says a run is live and the
   full text is on the title), then the pickers. The elapsed counter and the stop
   button never shrink — mid-run they are the two things you actually came to
   look at. */
.chat-working {
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--accent);
  white-space: nowrap;
}

#chat-working-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-elapsed {
  flex: none;
}

/* The pickers keep their content width — they wrap with the row instead of
   compressing. Only a pathologically long model label ellipsizes. */
.chat-composer-options {
  flex: none;
}

.chat-composer-options .custom-select-value {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-elapsed:not(:empty)::before {
  content: '·';
  margin: 0 5px;
  color: var(--text-muted);
}

/* --- Run config, mobile only (TODO-165) ---
   On a phone the pickers + "running · Ns" + stop button used to wrap onto a
   second row every time a run went live. While a run is active there's
   nothing to pick anyway — the composer is locked to this thread until it
   finishes — so the pickers and the attach button hide outright and this
   text takes their place: what the run is actually using, since the pickers
   underneath may have already moved on to the next message's config. Desktop
   never hides its pickers, so it never shows this. The repo control moved
   into the status strip (TODO-243) and isn't in this row any more, so it has
   nothing to hide here — it stays reachable mid-run same as the strip's other
   segments. */
.chat-runcfg {
  display: none;
}

body.is-mobile .chat-console-running .chat-console-controls > #chat-attach-btn,
body.is-mobile .chat-console-running .chat-console-controls > .chat-mobile-pickers {
  display: none;
}

body.is-mobile .chat-console-running .chat-runcfg:not(.hidden) {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-right: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
}

.chat-runcfg-sep {
  opacity: 0.5;
}

/* Colors match the per-message config divider (.chat-config-mark-plan/-auto)
   — same meaning, same palette. YOLO gets no color override there either,
   so it doesn't get one here. */
.chat-runcfg-part.chat-runcfg-mode-plan {
  color: var(--accent);
}

.chat-runcfg-part.chat-runcfg-mode-auto {
  color: var(--status-in-progress);
}

/* .chat-working normally claims the row's free space with margin-left:auto
   (shared with .chat-send-btn, see below) — here .chat-runcfg claims it
   instead, so .chat-working just sits close beside it. */
body.is-mobile .chat-console-running .chat-working {
  margin-left: 8px;
}

/* --- Image attachments (TODO-112) ---
   Thumbnails are square crops: a row of uniform tiles reads as "these are
   attached" at a glance, where preserving each image's aspect ratio would make
   the tray look like a ragged collage. The full image is one click away.

   Both trays are direct children of .chat-console-body, which has no padding
   of its own — so without the padding below they sit flush against the card's
   border. The horizontal value is the textarea's own 13px, deliberately, so a
   thumbnail and a chip line up with the draft text under them rather than
   floating on their own margin. Bottom stays 0: whatever comes next (the other
   tray, or the textarea) supplies the gap with its own 12px top, so a hidden
   tray never leaves a doubled gap behind.

   The top padding is also what keeps .chat-attachment-remove (top/right -6px)
   and .chat-attachment-badge (bottom -5px) from being clipped — they hang
   outside the tile on purpose, and .chat-console-body clips its corners with
   overflow: hidden, so a zero-padding row shears the ✕ clean off. */
.chat-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: 12px 13px 0;
}

/* --- Draft ticket mentions (TODO-155) ---
   Same row shape as the image attachments above it — a live preview of what
   '@TODO-<id>' in the draft resolves to, so a chip disappearing when the id is
   still wrong is visible before Send rather than after. */
.chat-mentions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: 12px 13px 0;
}

.chat-mention-strip {
  cursor: pointer;
}

/* Queued messages (TODO-217). Same strip shape as .chat-mentions above — a row
   above the textarea, hidden while empty — because it is the same kind of thing:
   composer-owned state that is not yet part of the transcript. */
.chat-queue {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 13px 0;
}

.chat-queue-label {
  font-size: 11px;
  color: var(--text-muted);
}

.chat-queue-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 4px 6px 4px 8px;
  font-size: 12px;
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent-dim-hover);
  border-radius: 6px;
  background: var(--surface-hover);
}

/* One line with an ellipsis: the queue is a reminder of what is waiting, not a
   second transcript. The full text is on the title attribute. */
.chat-queue-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-queue-count {
  flex-shrink: 0;
  font-size: 11px;
  color: var(--text-muted);
}

.chat-queue-remove {
  flex-shrink: 0;
  padding: 0 4px;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

.chat-queue-remove:hover {
  color: var(--danger-text);
}

.chat-attachment {
  position: relative;
}

.chat-thumb {
  display: block;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
  cursor: pointer;
}

.chat-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.chat-thumb-static {
  cursor: default;
  display: grid;
  place-items: center;
}

/* The preview sits under the state label while the upload is in flight. */
.chat-thumb-static img {
  grid-area: 1 / 1;
}

.chat-attachment-state {
  grid-area: 1 / 1;
  font-size: 10px;
  color: var(--text-muted);
  background: var(--modal-backdrop);
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}

.chat-attachment-uploading .chat-thumb,
.chat-attachment-compressing .chat-thumb { opacity: 0.6; }
.chat-attachment-error .chat-thumb { border-color: var(--status-blocked); }
.chat-attachment-error .chat-attachment-state { color: var(--danger-text); }

/* Marks an image that was downscaled to fit. Quiet, but present — the file that
   was sent is not byte-for-byte the one that was attached. */
.chat-attachment-badge {
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  padding: 0 5px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text-muted);
  font-size: 9px;
  line-height: 1.5;
}

.chat-attachment-remove,
.chat-attachment-retry {
  position: absolute;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
  line-height: 1;
}

.chat-attachment-remove {
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  font-size: 13px;
}

/* Inside the tile, not hanging off it: the tray sits directly above the input,
   and an overhanging control collides with its border. */
.chat-attachment-retry {
  bottom: 3px;
  left: 3px;
  padding: 1px 5px;
  border-radius: 999px;
  font-size: 10px;
}

.chat-attachment-remove:hover,
.chat-attachment-retry:hover {
  color: var(--text);
  border-color: var(--accent);
}

/* Drop target feedback. Border colour rather than a shadow: light-theme shadow
   alphas are ~4x weaker, so a shadow-only cue effectively vanishes there. */
.chat-composer-dropping .chat-console {
  border-color: var(--accent);
  background: var(--accent-dim);
}

/* Ticket-card drop feedback (TODO-149): the whole panel is the target, not
   just the composer, so the cue rings the whole shell — an inset outline
   rather than a border, since .chat-shell already carries a border-left. */
.chat-panel-dropping .chat-shell {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Attached images inside the sent bubble. */
.chat-bubble-images {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chat-bubble-images + .chat-bubble-text {
  margin-top: 8px;
}

/* Nothing but images: drop the text padding so the bubble hugs the thumbnails. */
.chat-user-images-only {
  padding: 6px;
}

/* A '@TODO-<id>' mention inside a sent prompt (TODO-148) or the composer's
   draft strip (TODO-155), both rendered by the shared chatMentionChip. Built
   from the same vocabulary as .link-task-id (task-detail modal) and
   .task-id-badge, but a <button> since it opens the ticket. white-space:
   normal so it doesn't inherit the bubble's pre-wrap and let the id wrap
   mid-string; inline-flex + middle keeps it seated on the text baseline
   alongside ordinary words. */
.chat-mention {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  vertical-align: middle;
  white-space: normal;
  font-size: 0.82em;
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  padding: 1px 8px 1px 7px;
  cursor: pointer;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}

.chat-mention:hover {
  background: var(--surface-hover);
  border-color: var(--text-muted);
}

.chat-mention-id {
  font-family: monospace;
}

/* Muted relative to the id, so the id — the thing you actually typed — stays
   what the eye lands on first; the title is context, not the headline. The
   ':' separator lives here rather than in markup so it can never appear
   without a title next to it (the unknown-ticket chip has neither). */
.chat-mention-title {
  font-weight: 500;
  color: var(--text-muted);
  max-width: 26ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-mention-title::before {
  content: ':';
  margin: 0 4px 0 2px;
  color: var(--text-muted);
}

/* A '@RUN-<id>' mention naming another CHAT (TODO-220). Deliberately the same
   chip as the ticket one — same size, same radius, same hover — because they
   are one feature; only the leading slot differs, a 💬 where a ticket carries
   its status dot. The glyph is the whole distinction — no new theme tokens and
   no second border colour, which would make one kind look more important than
   the other. */
.chat-mention-glyph,
.chat-mention-row-glyph {
  flex: none;
  font-size: 0.9em;
  line-height: 1;
}

/* Fixed width so a chat row's title starts on the same column as a ticket
   row's, whose .status-dot is a fixed 8px + gap. A mixed menu that jitters
   between the two kinds reads as two lists rather than one. */
.chat-mention-row-glyph {
  width: 8px;
  text-align: center;
  font-size: 0.8em;
}

/* A mention naming a ticket this board no longer has (or never did) — flat and
   inert rather than a live-looking control with nothing to open. Id-only:
   there's no title to show. */
.chat-mention-unknown {
  color: var(--text-muted);
  background: none;
  border-style: dashed;
  cursor: default;
  font-family: monospace;
}

.chat-mention-unknown:hover {
  background: none;
  border-color: var(--border-strong);
}

/* An image the agent chose to show (show_image, TODO-132). Bigger than the
   56px composer tiles and uncropped — the whole point is seeing what the agent
   produced, so the natural aspect is kept and the modal does the zooming. */
.chat-shown-image {
  align-self: flex-start;
  max-width: min(320px, 100%);
}

.chat-shown-image .chat-thumb {
  width: 100%;
  height: auto;
}

.chat-shown-image .chat-thumb img {
  height: auto;
  object-fit: contain;
}

.chat-shown-image-caption {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-muted);
}

/* Model / effort / mode pickers (TODO-102, restyled TODO-106). Native selects
   upgraded by CustomSelect. They now share the console's controls row, so they
   match .chat-console-btn's height and radius instead of floating as their own
   strip of transparent pills. */
.chat-composer-options {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.chat-composer-options .custom-select {
  width: auto;
}

.chat-composer-options .custom-select-trigger {
  gap: 5px;
  height: 28px;
  padding: 0 9px;
  font-size: 12px;
  border-radius: 7px;
  border-color: var(--border);
  color: var(--text);
  background: var(--surface);
}

.chat-composer-options .custom-select-trigger:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: var(--surface-hover);
}

.chat-composer-options .custom-select-chevron {
  width: 11px;
  height: 11px;
  color: var(--text-muted);
}

/* Plan mode reads as a state, not a preference: the mode pill fills with the
   accent so a read-only run is never mistaken for one that will act.

   Selected by :has() on the mode select itself rather than :first-child. The
   control order changed in TODO-106 and a positional selector retargets
   silently the next time it changes — this one can only ever hit the mode pill.
   YOLO gets the same treatment in red: mode is the one picker whose value
   changes what a run is allowed to do, so both extremes read as a state. */
.chat-composer-options.chat-mode-plan .custom-select:has(#chat-mode-select) .custom-select-trigger {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-dim);
  font-weight: 500;
}

.chat-composer-options.chat-mode-yolo .custom-select:has(#chat-mode-select) .custom-select-trigger {
  color: var(--danger-text);
  border-color: var(--danger-fill);
  background: var(--status-blocked-bg);
  font-weight: 500;
}

.chat-composer-options.chat-mode-yolo .custom-select:has(#chat-mode-select) .custom-select-trigger:hover {
  border-color: var(--danger-fill-hover);
  background: var(--status-blocked-bg);
}

/* Marks where the run config changed — sits above the first message that used
   it, and only when it differs from the message before. Reads as a divider
   ("from here on…"), not as a label stuck on every message. */
.chat-config-mark {
  align-self: center;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.chat-config-mark::before,
.chat-config-mark::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.chat-config-mark-plan {
  color: var(--accent);
}

.chat-config-mark-auto {
  color: var(--status-in-progress);
}

/* The gate stopped a call (TODO-113 v2). Sits between the denied chip and the
   question card it caused, so the card never reads as the agent's own idea.
   Built from the danger trio the chat already uses — --status-blocked,
   --status-blocked-bg, --danger-text — all three of which the shared light-theme
   block re-tunes, so this needs no per-theme rule. Deliberately not amber: the
   default theme's accent IS amber, so a warning built from --status-in-progress
   would not read as unusual there. */
.chat-gate-notice {
  align-self: flex-start;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  max-width: 90%;
  margin: 2px 0;
  padding: 6px 10px;
  border: 1px solid var(--status-blocked);
  border-radius: 6px;
  background: var(--status-blocked-bg);
  font-size: 12px;
  line-height: 1.45;
  color: var(--danger-text);
}

/* A gate malfunction is not a policy objection: the call was never judged at all
   (TODO-239). It drops out of the blocked palette into the neutral one, so a
   broken permission check cannot be misread as the agent having attempted
   something dangerous — the amber card means "we stopped you", this means "we
   could not look". Tokens only, so it follows all 12 themes. */
.chat-gate-notice-broken {
  border-color: var(--border);
  background: var(--surface);
  color: var(--text-secondary);
}

.chat-gate-notice-broken .chat-gate-rule {
  border-color: var(--border);
  color: var(--text-secondary);
}

.chat-gate-icon {
  flex-shrink: 0;
  font-size: 11px;
}

.chat-gate-text {
  font-weight: 600;
}

.chat-gate-target {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
  /* A long id must wrap rather than push the chat panel into a sideways scroll. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.chat-gate-rule {
  margin-left: auto;
  padding: 1px 6px;
  border: 1px solid var(--status-blocked);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.03em;
  color: var(--danger-text);
  overflow-wrap: anywhere;
}

/* ask_user: the agent stopped to ask something. Loud enough to notice on a
   scroll-back, quiet enough not to compete with the answer itself. */
.chat-question {
  align-self: flex-start;
  max-width: 90%;
  margin: 2px 0;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent-dim);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.chat-question-text {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
}

/* Numbered rows rather than pills: the numbers are the second answer route —
   typing "2" is unambiguous against a question the agent can still see. */
.chat-question-options {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none; /* the numbers are rendered as spans so they can be styled */
}

.chat-question-option {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  padding: 5px 8px;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.4;
  text-align: left;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}

.chat-question-option:hover:not(:disabled) {
  background: var(--bg);
  border-color: var(--accent);
}

.chat-question-num {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--accent);
}

.chat-question-hint {
  font-size: 11px;
  color: var(--text-muted);
}

/* Answered already — kept legible as a record of what was offered, but plainly
   no longer a live choice. */
.chat-question-option-spent,
.chat-question-option-spent .chat-question-num {
  color: var(--text-muted);
  cursor: default;
}

/* Answered card (TODO-110): the accent belongs to questions still waiting on
   you, so an answered one gives it up and folds its options away, keeping the
   question as context for the answer underneath. */
.chat-question-answered {
  border-color: var(--border);
  background: transparent;
  gap: 4px;
}

.chat-question-answered .chat-question-text {
  color: var(--text-muted);
}

.chat-question-answered:not(.chat-question-expanded) .chat-question-options {
  display: none;
}

.chat-question-expanded .chat-question-options {
  margin-top: 2px;
}

.chat-question-answer {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

.chat-question-answer-icon {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--accent);
}

.chat-question-answer-text {
  white-space: pre-wrap;
  min-width: 0;
}

/* An answer that is QUEUED rather than sent (TODO-308). It has been recorded on
   the card, but the run it is waiting on is still going and the composer strip
   can still take it back — so the card reads as spent while the answer itself
   stays visibly provisional: a dashed leading rule, and a badge that says so. */
.chat-question-queued,
.chat-exit-plan-queued {
  border-left: 2px dashed var(--border-strong);
  padding-left: 9px;
}

.chat-question-answer-pending,
.chat-exit-plan-answer-pending {
  flex-shrink: 0;
  align-self: center;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* The queued answer is the provisional half of the row, so it is the half that
   reads muted — the badge beside it is what explains why. */
.chat-question-queued .chat-question-answer-text,
.chat-exit-plan-queued .chat-exit-plan-answer-text {
  color: var(--text-muted);
}

/* Sized like .chat-question-hint it replaces — a footnote, not an action. */
.chat-question-toggle {
  align-self: flex-start;
  padding: 0;
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--text-muted);
  background: transparent;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: color var(--dur-fast);
}

.chat-question-toggle:hover {
  color: var(--accent);
}

/* ── multi-question stepper (TODO-170) ──────────────────────────────────────
   One card, one question at a time. Every step is in the DOM; only the active
   one is displayed. Deliberately sets NO `border` on .chat-question, so the
   .chat-question-gated block below still wins on source order. */

.chat-question-progress {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
}

.chat-question-count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

.chat-question-dots {
  display: flex;
  align-items: center;
  gap: 5px;
}

/* Direct jumps between questions. Kept a real button (not a bare span) because
   navigating a stepper by keyboard is the whole point of the ticket. */
.chat-question-dot {
  width: 9px;
  height: 9px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--accent);
  border-radius: 50%;
  cursor: pointer;
  transition: background var(--dur-fast), box-shadow var(--dur-fast), opacity var(--dur-fast);
}

.chat-question-dot-done {
  background: var(--accent);
}

.chat-question-dot-current {
  box-shadow: 0 0 0 3px var(--accent-dim);
}

.chat-question-dot:disabled {
  cursor: default;
  opacity: 0.5;
}

.chat-question-step {
  display: none;
}

.chat-question-step-active {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* The per-question answer box. Does double duty: annotates a picked option, or
   stands alone as the answer when nothing is picked (and when the agent supplied
   no options at all). Only ever one on screen, since only the active step shows —
   which is why it is always visible rather than hidden behind a toggle. */
.chat-question-note {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 8px;
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.45;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  resize: vertical;
  transition: border-color var(--dur-fast);
}

.chat-question-note::placeholder {
  color: var(--text-muted);
}

.chat-question-note:focus {
  outline: none;
  border-color: var(--accent);
}

/* Spent card: still readable as a record of what was typed, no longer editable. */
.chat-question-note:read-only {
  color: var(--text-muted);
  background: transparent;
  resize: none;
}

/* The answer currently chosen for THIS step. Survives navigating away and back,
   which is what makes changing an earlier answer legible. */
.chat-question-option-picked,
.chat-question-option-picked:hover:not(:disabled) {
  background: var(--accent-dim);
  border-color: var(--accent);
}

.chat-question-option-picked .chat-question-label {
  font-weight: 600;
}

.chat-question-nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap; /* the docked panel gets narrow; the two controls must not clip */
  gap: 6px;
}

.chat-question-nav-btn {
  padding: 5px 10px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

.chat-question-nav-btn:hover:not(:disabled) {
  color: var(--accent);
  border-color: var(--accent);
}

.chat-question-send {
  margin-left: auto;
  padding: 6px 12px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--bg);
  background: var(--accent);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: opacity var(--dur-fast);
}

.chat-question-send:hover:not(:disabled) {
  opacity: 0.85;
}

.chat-question-nav-btn:disabled,
.chat-question-send:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Why the controls are inert right now (TODO-170): a run is still live. Hidden
   until chatSyncQuestionCards says otherwise, so it costs nothing at rest. */
.chat-question-wait {
  display: none;
  font-size: 11px;
  color: var(--text-muted);
}

.chat-question-waiting .chat-question-wait {
  display: block;
}

/* The hint it replaces would otherwise contradict it. */
.chat-question-waiting .chat-question-hint {
  display: none;
}

/* The options used to fade here, back when a live run made them inert. They are
   answerable now (TODO-308) — the click queues behind the run — so the only
   thing liveness changes is the note below them. */

/* A stepper's controls are spent once answered; the questions stay as a record.
   Mirrors how .chat-question-answered folds the single card's options away. */
.chat-question-answered .chat-question-progress,
.chat-question-answered .chat-question-nav {
  display: none;
}

.chat-question-answered:not(.chat-question-expanded) .chat-question-steps {
  display: none;
}

/* Expanded, the record shows every question that was asked, not just the last
   step that happened to be active. */
.chat-question-expanded .chat-question-step {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-bottom: 4px;
}

/* A question the gate forced, as opposed to one the agent chose to ask.
   MUST stay after .chat-question and .chat-question-answered: both set the
   `border` shorthand, which resets border-color, and all three selectors have
   the same specificity — so source order is what decides. Placed before this
   block, the gated border silently never applied. */
.chat-question-gated,
.chat-question-gated.chat-question-answered {
  border-color: var(--status-blocked);
}

.chat-question-gate-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--status-blocked);
}

.chat-question-gate-label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--danger-text);
}

.chat-question-gate-target {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* exit_plan_mode (TODO-109): the plan agent's real exit. Same frame as
   .chat-question — it is ask_user's sibling — but its own class, since the
   body is a scrollable plan rather than a one-line prompt and the actions are
   a fixed row rather than a numbered list.

   ONE surface, at the full width of the flow (TODO-248). It used to be a
   bordered box inside a bordered box at 90% of a ~370px column, which left a
   real plan reading in a ~300px gutter under four rows of chrome. `stretch`
   rather than `flex-start` is deliberate and unlike every bubble around it:
   this is the card that stops the conversation, so it gets the column. */
.chat-exit-plan {
  align-self: stretch;
  max-width: 100%;
  margin: 2px 0;
  padding: 10px 12px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent-dim);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* The hairline under the head is what separates title from plan now that the
   plan has no box of its own. */
.chat-exit-plan-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--accent-dim-hover);
}

.chat-exit-plan-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  min-width: 0;
}

/* The plan itself — flush against the card (TODO-248): no fill, no border, no
   padding of its own, so the plan sits on the card's one surface rather than in
   a box inside it.

   Still bounded and scrollable, because the four responses sit BELOW it and
   must stay reachable without scrolling past a long plan — Expand (TODO-168) is
   the escape hatch for reading one properly. The cap is viewport-relative on
   purpose: a flat 500px is the right amount of plan in a docked panel and more
   than a phone or a short window has to spare, and there the buttons going
   off-screen is exactly the failure the cap exists to prevent.

   The mono/pre-wrap base here is the marked/DOMPurify-unavailable fallback,
   which is still a <pre>; the .chat-md variant below is the normal case. */
.chat-exit-plan-body {
  margin: 0;
  padding: 0;
  max-height: min(500px, 55vh);
  overflow: auto;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Markdown-rendered plan (TODO-168): same surface, prose inside. Drops the three
   properties that only make sense for raw text; .chat-md supplies the rest. */
.chat-exit-plan-body.chat-md {
  font-family: var(--font-body);
  font-size: 13px;
  white-space: normal;
}

/* Expand sits at the far end of the head row. Quieter than .chat-exit-plan-link
   — it is a view, not one of the four responses — and it never disables, since
   a spent card's plan is still worth reading. */
.chat-exit-plan-expand {
  margin-left: auto;
  flex-shrink: 0;
  padding: 2px 7px;
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

.chat-exit-plan-expand:hover {
  color: var(--text);
  border-color: var(--text-muted);
}

/* All four responses on one row (TODO-248). .chat-exit-plan-secondary is nested
   INSIDE .chat-exit-plan-actions and pushed to the far end: the two buttons
   that start a run and the two that don't stay visibly separate without costing
   a second row, and when the row is too narrow the quiet pair wraps as a unit
   rather than the four buttons breaking wherever they happen to land. */
.chat-exit-plan-actions,
.chat-exit-plan-secondary {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* The rule above the actions is the head's hairline mirrored, and it is doing
   real work rather than decoration: the body is a scroll container, so a plan
   longer than the cap clips at exactly this line. Without an edge there, a
   clipped sentence ran straight into the buttons and read as the end of the
   plan. (The scroll itself is signalled the way every other scrollable pane in
   the app signals it — the global 6px scrollbar at the top of this file.) */
.chat-exit-plan-actions {
  align-items: center;
  padding-top: 8px;
  border-top: 1px solid var(--accent-dim-hover);
}

.chat-exit-plan-secondary {
  margin-left: auto;
}

/* The two real actions: running the plan. --status-done for Auto (the
   checked, still-supervised path) and --status-blocked for YOLO (nothing
   between the click and a real repository) — the same pair chatApplyMode
   already paints the mode pill with, so approving here reads as the same
   choice as picking the mode by hand. */
.chat-exit-plan-btn {
  padding: 6px 12px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--bg);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: opacity var(--dur-fast);
}

.chat-exit-plan-approve-auto {
  background: var(--status-done);
}

.chat-exit-plan-approve-yolo {
  background: var(--status-blocked);
}

.chat-exit-plan-btn:hover:not(:disabled) {
  opacity: 0.85;
}

/* The two non-actions: nothing runs, so neither reads as a call to action. */
.chat-exit-plan-link {
  padding: 5px 8px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  transition: color var(--dur-fast), border-color var(--dur-fast);
}

.chat-exit-plan-link:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--text-muted);
}

.chat-exit-plan-btn:disabled,
.chat-exit-plan-link:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Resolved card: the plan and its outcome stay, the choice is spent. Mirrors
   .chat-question-answered. */
.chat-exit-plan-spent {
  border-color: var(--border);
  background: transparent;
}

/* The head's hairline goes neutral with the frame — an accent-tinted rule on an
   otherwise de-accented card was the one bit of the spent state that still
   read as live. */
.chat-exit-plan-spent .chat-exit-plan-head {
  border-bottom-color: var(--border);
}

.chat-exit-plan-spent .chat-exit-plan-actions {
  border-top-color: var(--border);
}

.chat-exit-plan-answer {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

.chat-exit-plan-answer-icon {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--accent);
}

/* Keyboard focus mirrors .project-dossier:focus-visible: accent ring, offset. */
.chat-thread-item:focus-visible,
.chat-tool-chip:focus-visible,
.chat-thinking-summary:focus-visible,
.chat-jump-btn:focus-visible,
.chat-status-place:focus-visible,
.chat-status-usage:focus-visible,
.chat-status-pr:focus-visible,
.chat-status-prs:focus-visible,
.chat-pop-pr:focus-visible,
.chat-question-toggle:focus-visible,
.chat-question-option:focus-visible,
.chat-question-dot:focus-visible,
.chat-question-nav-btn:focus-visible,
.chat-question-send:focus-visible,
.chat-suggestion:focus-visible,
.chat-resize-handle:focus-visible,
.chat-threads-resize:focus-visible,
.chat-exit-plan-btn:focus-visible,
.chat-exit-plan-expand:focus-visible,
.chat-exit-plan-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .card-enter,
  .row-enter {
    animation: none;
  }

  .chat-shell,
  .chat-threads {
    animation: none;
    transition: none;
  }

  /* dismissWithMotion skips the exit in JS under the same query, so only the
     entrance needs suppressing here. */
  .install-card { animation: none; }

  /* A dismiss drag still tracks the finger under reduced motion — that is
     direct manipulation, not decoration — but the release lands instantly
     instead of gliding (TODO-247). dismissWithMotion checks the same query in
     JS and skips the exit animations outright, so the button paths never get
     here at all. */
  .sheet-settling { transition: none; }

  .chat-tool-chip.chat-tool-running::after,
  .chat-card-running .chat-card-dot,
  .chat-thread-badge-dot {
    animation: none;
    opacity: 1;
  }

  /* The cursor stops blinking but stays drawn — it is the only thing marking a
     Bash card as still running once the dot has stopped moving. */
  .chat-card-cursor { animation: none; }
  .chat-card-chevron { transition: none; }

  /* A light continuously circling the composer is a far stronger motion cue
     than anything else on this list, so it is not optional. It is not HIDDEN
     either: "running" still has to be legible without motion. Filling the disc
     solid turns the travelling light into a still accent ring — freezing the
     gradient instead would leave one arbitrary arc, which reads as a glitch. */
  .chat-console-ring::before {
    animation: none;
    background: var(--accent);
  }

  /* The launcher's ring (TODO-333), by exactly the argument above it: frozen to
     a solid accent ring rather than hidden. "Running" is the whole point of the
     thing and has to stay legible without motion — and it is now the ONLY
     difference between a running launcher and an idle one, since the corner dot
     next to it means unread. Filling the disc solid is what turns the
     travelling light into a still ring; freezing the gradient instead would
     leave one arbitrary arc, which reads as a glitch. */
  .chat-btn-ring::before {
    animation: none;
    background: var(--accent);
  }

  .agent-pulse {
    animation: none;
    opacity: 1;
  }

  /* The meter still fills — it just snaps to the new level instead of sliding. */
  .chat-status-meter-fill {
    transition: none;
  }

  .btn:active,
  .btn-danger:active,
  .rte-btn:active,
  .link-remove-btn:active,
  .chat-console-btn:active {
    transform: none;
  }

  select:focus,
  input:focus,
  textarea:focus,
  .rte-wrap:focus-within,
  .custom-select.open .custom-select-trigger {
    box-shadow: none;
  }

  label {
    transition: none;
  }
}

/* === The cumulative diff viewer (TODO-265) ================================
   Opened from the status strip's +/- chip. Deliberately reuses the rich-card
   diff palette (.chat-card-add/-del/-ctx and .chat-card-row/-gutter/-text)
   rather than defining a second one: a hunk should look the same whether it
   arrived as one Edit call or as the sum of the whole thread. As with those,
   NO new theme tokens — everything below maps onto variables the themes
   already re-tune. */

.modal-content.modal-diff {
  width: clamp(640px, 82vw, 1180px);
  max-width: 94vw;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  overflow: hidden; /* the frame scrolls, not the shell — the head stays put */
}

.diff-modal-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-shrink: 0;
}

.diff-modal-title {
  margin: 0;
  font-size: 15px;
}

/* The base the whole viewer is measured against. Muted and in the mono face
   because it is a ref name, not prose — and present only when every repo
   agrees on one, so it is never a claim about a base some file didn't use. */
.diff-modal-base {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  margin-right: auto;
}

.diff-modal-frame {
  overflow: auto;
  flex: 1 1 auto;
  min-height: 0;
}

.chat-diff-repo + .chat-diff-repo { margin-top: var(--space-md); }

.chat-diff-repo-name {
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--text-secondary);
  font-family: var(--font-mono);
}

.chat-diff-file {
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-bottom: 6px;
  background: var(--surface);
  /* Deliberately NOT overflow: hidden. That would make this the nearest scroll
     container for the sticky header below, which would then stick to a box that
     does not scroll — i.e. not stick at all. The corners are rounded per-child
     instead. */
}

.chat-diff-file-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  cursor: pointer;
  font-size: 12px;
  list-style: none;
  border-radius: 5px;
  background: var(--surface);
  /* Pinned while its own diff scrolls past. A long file is thousands of pixels
     of hunks, and without this the only thing telling you WHICH file you are
     reading has long since scrolled away — the case that matters most on a
     phone, where the whole viewer is one column. */
  position: sticky;
  top: 0;
  z-index: 1;
}
.chat-diff-file[open] .chat-diff-file-head { border-radius: 5px 5px 0 0; }
.chat-diff-file-head::-webkit-details-marker { display: none; }
.chat-diff-file[open] .chat-diff-file-head { border-bottom: 1px solid var(--border); }

/* One letter, git's own. The tint says which kind of change without the row
   having to spell it out; the full word rides the title attribute. */
.chat-diff-badge {
  flex: none;
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-secondary);
  background: var(--bg);
  border: 1px solid var(--border);
}
.chat-diff-badge[data-kind="A"] { color: var(--status-done); border-color: var(--status-done); }
.chat-diff-badge[data-kind="D"] { color: var(--status-blocked); border-color: var(--status-blocked); }
.chat-diff-badge[data-kind="M"] { color: var(--status-in-progress); border-color: var(--status-in-progress); }
.chat-diff-badge[data-kind="R"],
.chat-diff-badge[data-kind="C"] { color: var(--accent); border-color: var(--accent); }
/* Untracked is deliberately the quietest of the set: it is a real file the
   agent made, but it is not part of the +/- the chip counted. */
.chat-diff-badge[data-kind="U"] { color: var(--text-muted); border-style: dashed; }

.chat-diff-path {
  display: flex;
  align-items: baseline;
  font-family: var(--font-mono);
  font-size: 11.5px;
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
}
/* The directory gives first and takes the ellipsis; the filename never does. */
.chat-diff-dir {
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
}
.chat-diff-name {
  flex: none;
  white-space: nowrap;
}

.chat-diff-from {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  flex: none;
}

.chat-diff-plus   { color: var(--status-done); font-size: 11px; flex: none; }
.chat-diff-minus  { color: var(--status-blocked); font-size: 11px; flex: none; }
.chat-diff-binary { color: var(--text-muted); font-size: 11px; flex: none; }

.chat-diff-pane {
  max-height: 60vh;
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre;
}

/* ONE gutter, unlike the Edit card's two: a second column of numbers costs
   ~44px of every row on a surface whose whole job is showing code. It carries a
   real file line number either way — the hunk header states both sides, so
   unlike the Edit card there is no relative variant to mark. */
.chat-diff-pane .chat-card-gutter { width: 44px; }

/* A deleted line's number is the OLD file's, and it sits in a column otherwise
   counting the new one — so between two new-side numbers it reads as the
   sequence jumping backwards. Italic marks it as a number from a different
   frame of reference, which is exactly what .chat-card-gutter-rel means on the
   Edit card. Same vocabulary, same reason: never let an honest number be read
   as one it is not. (The alternative was blanking it, which is more monotonic
   and strictly less informative — the old position of a removal is real.) */
.chat-diff-pane .chat-card-del .chat-card-gutter {
  font-style: italic;
  opacity: 0.55;
}

/* The @@ line's context hint. Separated as a band rather than a row because it
   is a position in the file, not a line of it. */
.chat-diff-hunk {
  background: var(--bg);
  color: var(--text-muted);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  font-style: italic;
}
.chat-diff-hunk .chat-card-gutter { opacity: 0.4; }

.chat-diff-note,
.chat-diff-empty,
.chat-diff-loading {
  padding: 8px 10px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--text-muted);
}
/* A cap, a failure or an unreadable repo is a statement, not decoration. */
.chat-diff-note { color: var(--danger-text); }

/* ============================================================
   Chat pop-out window (TODO-151)
   ============================================================
   The popped-out window loads the same index.html at `/?window=chat`; this
   block is the whole difference. Placed after every .chat-* rule above so
   source order carries the overrides — no !important needed except where the
   panel width is set inline by the resize handle.

   The launcher's popped-out state lives here too: the chat is elsewhere, so
   the button raises that window instead of toggling a panel. */

#chat-btn.is-popped-out {
  color: var(--accent);
  background: var(--accent-dim);
}

/* Everything that is "the board". #toast-container deliberately stays: a failed
   upload or a 409 still has to be able to say so in here. */
body.is-popout header,
body.is-popout main,
body.is-popout #m-tabbar,
body.is-popout #m-fab {
  display: none;
}

/* The window IS the panel: no header to sit below, no width to negotiate. */
body.is-popout .chat-panel {
  top: 0;
  inset: 0;
  width: 100% !important; /* beats the inline --chat-w the resize handle sets */
  max-width: none;
  z-index: 1; /* #image-modal (2100) and .modal (100) still open above */
}

body.is-popout .chat-shell {
  border-left: 0;
  box-shadow: none;
  animation: none; /* chat-slide-in is wrong for a window that IS the window */

  /* ...and the lift comes off with the edge. The lifted ramp means "this plane
     sits above the board"; in a popout there is no board behind it, so the lift
     is a claim about nothing and only leaves the window a shade off from every
     other surface in the app. Back to the theme's own tokens.

     `inherit`, NOT `initial`: for a custom property `initial` is the
     guaranteed-invalid value, so every descendant's var(--bg) would fail
     substitution rather than fall back to the theme. `inherit` takes
     .chat-panel's copy, which is the untouched :root value — the remap above
     lands on .chat-shell, so the parent still holds the original. */
  --bg:             inherit;
  --surface:        inherit;
  --surface-hover:  inherit;
  --surface-active: inherit;
  --border:         inherit;
  --border-strong:  inherit;
}

/* The OS window edge resizes now, and the titlebar closes. Neither control has
   anything left to do, and a close button that killed the window without
   docking the chat back would be a trapdoor. */
body.is-popout .chat-resize-handle,
body.is-popout #chat-close-btn {
  display: none;
}

/* One button, two directions: out of the board, or back into it. */
body.is-popout #chat-popout-btn svg {
  transform: rotate(180deg);
}

/* ============================================================
   Responsive
   ============================================================ */

/* The board's own narrow layouts moved to body[data-fit] up in the Board
   section (TODO-169): a docked chat panel makes the viewport the wrong number
   to key them off. */

/* Content that has to give way when the BOARD AREA is narrow — which since
   TODO-169 includes "the window is wide but the chat took most of it". Toggled
   by applyFit() off available width, at the same 900px it used to be a media
   query for. These live inside the docked gutter, so available width is the
   only honest number for them.

   The header is deliberately NOT here: it spans the whole window over the top
   of the panel (see body.chat-docked header), so the dock never narrows it and
   its own wrapping stays viewport-keyed in the media query below. The modal
   rules are viewport-keyed for the same reason — modals are fixed and centred,
   so the dock never pushes them either. */
body.compact-chrome .list-view.view-active {
  padding: var(--space-md);
}

body.compact-chrome .list-toolbar { flex-wrap: wrap; }
body.compact-chrome .search-wrap { max-width: none; min-width: 200px; }

@media (max-width: 900px) {
  header {
    flex-wrap: wrap;
    height: auto;
    padding: var(--space-sm) var(--space-md);
    gap: var(--space-sm);
  }

  .header-left {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }

  .modal-content { width: calc(100vw - 32px); }
  .modal-content.modal-task { width: calc(100vw - 32px); }
  /* Projects is now a page, not a modal */
}

/* ============================================================
   Touch & coarse-pointer adaptations
   Tablets and touch laptops above the is-mobile threshold get the
   desktop layout; hover-gated controls must be reachable there.
   ============================================================ */

@media (hover: none) {
  .inline-edit-btn,
  .card-handoff-btn {
    opacity: 0.55;
  }

  .comment-actions {
    opacity: 1;
  }

  /* Hover can't restore full opacity, so dim done cards less. */
  .column[data-status="done"] .card {
    opacity: 0.7;
  }

  .task-table th.sortable {
    color: var(--text-secondary);
  }
}

@media (pointer: coarse) {
  .inline-edit-btn {
    width: 32px;
    height: 32px;
  }

  .inline-edit-btn svg {
    width: 15px;
    height: 15px;
  }

  .rte-btn {
    min-width: 38px;
    min-height: 38px;
  }

  .comment-edit-btn,
  .comment-delete-btn {
    min-width: 32px;
    min-height: 32px;
  }

  .project-card-icon-btn {
    padding: 9px;
  }

  .project-status-opt {
    padding: 8px 14px;
  }

  /* Covers #chat-new-btn too, now that it is a .btn-icon rather than a text
     button with its own touch-target override. */
  .chat-header .btn-icon,
  .chat-threads-header .btn-icon {
    min-width: 38px;
    min-height: 38px;
    justify-content: center;
  }

  .chat-tool-chip {
    padding: 6px 12px;
  }

  .chat-jump-btn {
    width: 38px;
    height: 38px;
    top: -48px;
  }

  .chat-thread-item {
    padding: 10px 12px;
  }

  /* No hover on touch: keep the row actions visible and finger-sized. */
  .chat-thread-actions {
    display: flex;
  }
  .chat-thread-action {
    min-width: 30px;
    min-height: 30px;
    justify-content: center;
  }
}

/* ============================================================
   Theme Transition
   ============================================================ */

html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
  transition: background-color 350ms ease,
              color 250ms ease,
              border-color 250ms ease,
              box-shadow 250ms ease !important;
}

/* ============================================================
   Dark Themes
   ============================================================ */

[data-theme="midnight-bloom"] {
  --bg:              oklch(0.13 0.025 280);
  --surface:         oklch(0.17 0.03 278);
  --surface-hover:   oklch(0.21 0.032 278);
  --surface-active:  oklch(0.25 0.034 276);
  --border:          oklch(0.26 0.028 278);
  --border-strong:   oklch(0.34 0.032 276);
  --text:            oklch(0.90 0.01 285);
  --text-secondary:  oklch(0.70 0.02 280);
  --text-muted:      oklch(0.55 0.025 278);
  --accent:          oklch(0.72 0.17 350);
  --accent-hover:    oklch(0.77 0.18 350);
  --accent-dim:      oklch(0.72 0.17 350 / 0.15);
  --accent-text:     oklch(0.14 0.03 350);
  --accent-dim-hover: oklch(0.72 0.17 350 / 0.24);
  --dropdown-shadow: 0 8px 24px oklch(0.04 0.02 280 / 0.5), 0 2px 8px oklch(0.04 0.02 280 / 0.3);
  --modal-shadow:    0 20px 60px oklch(0.04 0.02 280 / 0.55), 0 4px 20px oklch(0.04 0.02 280 / 0.35);
  --modal-backdrop:  oklch(0.06 0.02 280 / 0.7);
  /* Recessed seam — see the :root definition. */
  --panel-shadow:    -1px 0 0 oklch(0.04 0.02 280 / 0.55),
                     -4px 0 0 oklch(0.04 0.02 280 / 0.30),
                     -24px 0 48px oklch(0.04 0.02 280 / 0.55);
}

[data-theme="verdant"] {
  --bg:              oklch(0.13 0.025 155);
  --surface:         oklch(0.17 0.03 152);
  --surface-hover:   oklch(0.21 0.032 150);
  --surface-active:  oklch(0.25 0.032 148);
  --border:          oklch(0.26 0.026 152);
  --border-strong:   oklch(0.34 0.03 150);
  --text:            oklch(0.89 0.012 145);
  --text-secondary:  oklch(0.68 0.02 148);
  --text-muted:      oklch(0.55 0.02 148);
  --accent:          oklch(0.78 0.16 90);
  --accent-hover:    oklch(0.83 0.17 90);
  --accent-dim:      oklch(0.78 0.16 90 / 0.14);
  --accent-text:     oklch(0.14 0.02 90);
  --accent-dim-hover: oklch(0.78 0.16 90 / 0.22);
  --dropdown-shadow: 0 8px 24px oklch(0.04 0.02 155 / 0.5), 0 2px 8px oklch(0.04 0.02 155 / 0.3);
  --modal-shadow:    0 20px 60px oklch(0.04 0.02 155 / 0.55), 0 4px 20px oklch(0.04 0.02 155 / 0.35);
  --modal-backdrop:  oklch(0.06 0.02 155 / 0.7);
  /* Recessed seam — see the :root definition. */
  --panel-shadow:    -1px 0 0 oklch(0.04 0.02 155 / 0.55),
                     -4px 0 0 oklch(0.04 0.02 155 / 0.30),
                     -24px 0 48px oklch(0.04 0.02 155 / 0.55);
}

[data-theme="arctic-steel"] {
  --bg:              oklch(0.14 0.012 240);
  --surface:         oklch(0.18 0.014 238);
  --surface-hover:   oklch(0.22 0.016 238);
  --surface-active:  oklch(0.26 0.018 236);
  --border:          oklch(0.27 0.015 238);
  --border-strong:   oklch(0.35 0.018 236);
  --text:            oklch(0.89 0.006 235);
  --text-secondary:  oklch(0.68 0.012 235);
  --text-muted:      oklch(0.55 0.014 235);
  --accent:          oklch(0.70 0.08 220);
  --accent-hover:    oklch(0.75 0.09 220);
  --accent-dim:      oklch(0.70 0.08 220 / 0.15);
  --accent-text:     oklch(0.14 0.01 220);
  --accent-dim-hover: oklch(0.70 0.08 220 / 0.24);
  --dropdown-shadow: 0 8px 24px oklch(0.04 0.01 240 / 0.5), 0 2px 8px oklch(0.04 0.01 240 / 0.3);
  --modal-shadow:    0 20px 60px oklch(0.04 0.01 240 / 0.55), 0 4px 20px oklch(0.04 0.01 240 / 0.35);
  --modal-backdrop:  oklch(0.06 0.01 240 / 0.7);
  /* Recessed seam — see the :root definition. */
  --panel-shadow:    -1px 0 0 oklch(0.04 0.01 240 / 0.55),
                     -4px 0 0 oklch(0.04 0.01 240 / 0.30),
                     -24px 0 48px oklch(0.04 0.01 240 / 0.55);
}

[data-theme="dusk"] {
  --bg:              oklch(0.14 0.02 320);
  --surface:         oklch(0.18 0.024 318);
  --surface-hover:   oklch(0.22 0.026 318);
  --surface-active:  oklch(0.26 0.028 316);
  --border:          oklch(0.27 0.022 318);
  --border-strong:   oklch(0.35 0.026 316);
  --text:            oklch(0.89 0.01 330);
  --text-secondary:  oklch(0.68 0.018 325);
  --text-muted:      oklch(0.55 0.018 322);
  --accent:          oklch(0.75 0.15 50);
  --accent-hover:    oklch(0.80 0.16 50);
  --accent-dim:      oklch(0.75 0.15 50 / 0.14);
  --accent-text:     oklch(0.15 0.02 50);
  --accent-dim-hover: oklch(0.75 0.15 50 / 0.22);
  --dropdown-shadow: 0 8px 24px oklch(0.04 0.02 320 / 0.5), 0 2px 8px oklch(0.04 0.02 320 / 0.3);
  --modal-shadow:    0 20px 60px oklch(0.04 0.02 320 / 0.55), 0 4px 20px oklch(0.04 0.02 320 / 0.35);
  --modal-backdrop:  oklch(0.06 0.02 320 / 0.7);
  /* Recessed seam — see the :root definition. */
  --panel-shadow:    -1px 0 0 oklch(0.04 0.02 320 / 0.55),
                     -4px 0 0 oklch(0.04 0.02 320 / 0.30),
                     -24px 0 48px oklch(0.04 0.02 320 / 0.55);
}

[data-theme="ember"] {
  --bg:              oklch(0.14 0.015 20);
  --surface:         oklch(0.18 0.018 18);
  --surface-hover:   oklch(0.22 0.02 18);
  --surface-active:  oklch(0.26 0.022 16);
  --border:          oklch(0.27 0.018 18);
  --border-strong:   oklch(0.35 0.022 16);
  --text:            oklch(0.89 0.008 20);
  --text-secondary:  oklch(0.68 0.016 18);
  --text-muted:      oklch(0.55 0.016 18);
  --accent:          oklch(0.68 0.20 22);
  --accent-hover:    oklch(0.73 0.21 22);
  --accent-dim:      oklch(0.68 0.20 22 / 0.14);
  --accent-text:     oklch(0.14 0.02 22);
  --accent-dim-hover: oklch(0.68 0.20 22 / 0.22);
  --dropdown-shadow: 0 8px 24px oklch(0.04 0.015 20 / 0.5), 0 2px 8px oklch(0.04 0.015 20 / 0.3);
  --modal-shadow:    0 20px 60px oklch(0.04 0.015 20 / 0.55), 0 4px 20px oklch(0.04 0.015 20 / 0.35);
  --modal-backdrop:  oklch(0.06 0.015 20 / 0.7);
  /* Recessed seam — see the :root definition. */
  --panel-shadow:    -1px 0 0 oklch(0.04 0.015 20 / 0.55),
                     -4px 0 0 oklch(0.04 0.015 20 / 0.30),
                     -24px 0 48px oklch(0.04 0.015 20 / 0.55);
}

/* ============================================================
   Light Themes — Shared Overrides
   Status, project, and priority colors adjusted for light bg.
   ============================================================ */

[data-theme="paper-ink"],
[data-theme="sandstone"],
[data-theme="morning-fog"],
[data-theme="sage"],
[data-theme="lavender-haze"],
[data-theme="rose-quartz"] {
  color-scheme: light;

  --status-backlog:       oklch(0.45 0.06 60);
  --status-todo:          oklch(0.50 0.18 250);
  --status-in-progress:   oklch(0.55 0.18 75);
  --status-blocked:       oklch(0.52 0.22 28);
  --status-done:          oklch(0.48 0.18 155);
  --status-rejected:      oklch(0.45 0.20 0);
  /* Darkened to carry on a light surface, same move as every status colour
     above it. */
  --pr-merged:            oklch(0.48 0.19 305);

  --status-backlog-bg:    oklch(0.45 0.06 60 / 0.10);
  --status-todo-bg:       oklch(0.50 0.18 250 / 0.08);
  --status-progress-bg:   oklch(0.55 0.18 75 / 0.10);
  --status-blocked-bg:    oklch(0.52 0.22 28 / 0.08);
  --status-done-bg:       oklch(0.48 0.18 155 / 0.08);
  --status-rejected-bg:   oklch(0.45 0.20 0 / 0.08);

  --danger-text: oklch(0.52 0.19 27);

  /* Already dark enough for white text at L 0.52, so the fill matches the
     light theme's own blocked red rather than darkening further. */
  --danger-fill:       oklch(0.52 0.22 28);
  --danger-fill-hover: oklch(0.46 0.22 28);
  --danger-fill-text:  oklch(0.98 0.005 60);

  --priority-high:   oklch(0.50 0.22 25);
  --priority-medium: oklch(0.55 0.17 70);
  --priority-low:    oklch(0.48 0.04 60);

  --dropdown-shadow: 0 8px 24px oklch(0.40 0.005 0 / 0.12), 0 2px 8px oklch(0.40 0.005 0 / 0.08);
  --modal-shadow:    0 20px 60px oklch(0.40 0.005 0 / 0.15), 0 4px 20px oklch(0.40 0.005 0 / 0.08);
  --modal-backdrop:  oklch(0.30 0.005 0 / 0.35);
  /* Recessed seam, light-theme half. Same two hard rules plus a cast as the dark
     themes, at roughly a third of the alpha: light shadows are ~4x weaker for
     the same number, but a trench dark enough to read on --bg 0.94 reads as a
     BRUISE if it is tuned by the dark themes' numbers. */
  --panel-shadow:    -1px 0 0 oklch(0.40 0.005 0 / 0.20),
                     -4px 0 0 oklch(0.40 0.005 0 / 0.09),
                     -24px 0 44px oklch(0.40 0.005 0 / 0.18);

  /* The lift, light-theme half. Deliberately NOT the dark themes' uniform 7%
     white across the whole ramp: on light themes the ramp runs the other way
     (--surface-hover is DARKER than --surface, the same asymmetry that killed
     the old inset bevel), so mixing every rung toward white compresses the
     fills and the borders into each other and the panel goes flat.

     Only the three FILLS move, and by different amounts. --surface-active,
     --border and --border-strong stay exactly where the theme put them: they
     are already dark against a near-white panel, so leaving them alone actually
     widens their contrast instead of washing them out. */
  --panel-bg:             color-mix(in oklab, var(--bg), oklch(1 0 0) 75%);
  --panel-surface:        oklch(1 0 0);
  --panel-surface-hover:  color-mix(in oklab, var(--surface-hover), oklch(1 0 0) 40%);
  --panel-surface-active: var(--surface-active);
  --panel-border:         var(--border);
  --panel-border-strong:  var(--border-strong);

  /* Darker than --border-strong, not brighter: on a light theme the edge that
     reads is a shadow line, not a highlight. */
  --panel-edge:           color-mix(in oklab, var(--border-strong), oklch(0 0 0) 15%);
}

/* ============================================================
   Light Themes
   ============================================================ */

[data-theme="paper-ink"] {
  --bg:              oklch(0.94 0.007 85);
  --surface:         oklch(0.97 0.004 80);
  --surface-hover:   oklch(0.91 0.01 80);
  --surface-active:  oklch(0.87 0.012 78);
  --border:          oklch(0.84 0.01 80);
  --border-strong:   oklch(0.74 0.015 75);
  --text:            oklch(0.22 0.015 50);
  --text-secondary:  oklch(0.40 0.02 50);
  --text-muted:      oklch(0.54 0.015 55);
  --accent:          oklch(0.52 0.20 25);
  --accent-hover:    oklch(0.47 0.22 25);
  --accent-dim:      oklch(0.52 0.20 25 / 0.10);
  --accent-text:     oklch(0.97 0.005 25);
  --accent-dim-hover: oklch(0.52 0.20 25 / 0.16);
}

[data-theme="sandstone"] {
  --bg:              oklch(0.92 0.012 75);
  --surface:         oklch(0.96 0.007 75);
  --surface-hover:   oklch(0.88 0.016 72);
  --surface-active:  oklch(0.85 0.018 70);
  --border:          oklch(0.82 0.014 72);
  --border-strong:   oklch(0.72 0.018 68);
  --text:            oklch(0.24 0.02 45);
  --text-secondary:  oklch(0.42 0.022 48);
  --text-muted:      oklch(0.55 0.018 50);
  --accent:          oklch(0.55 0.18 40);
  --accent-hover:    oklch(0.50 0.20 38);
  --accent-dim:      oklch(0.55 0.18 40 / 0.10);
  --accent-text:     oklch(0.97 0.005 40);
  --accent-dim-hover: oklch(0.55 0.18 40 / 0.16);
}

[data-theme="morning-fog"] {
  --bg:              oklch(0.93 0.005 240);
  --surface:         oklch(0.97 0.003 240);
  --surface-hover:   oklch(0.90 0.008 238);
  --surface-active:  oklch(0.86 0.01 236);
  --border:          oklch(0.83 0.007 238);
  --border-strong:   oklch(0.73 0.01 235);
  --text:            oklch(0.20 0.018 240);
  --text-secondary:  oklch(0.40 0.016 238);
  --text-muted:      oklch(0.54 0.012 236);
  --accent:          oklch(0.45 0.14 250);
  --accent-hover:    oklch(0.40 0.16 250);
  --accent-dim:      oklch(0.45 0.14 250 / 0.10);
  --accent-text:     oklch(0.97 0.004 250);
  --accent-dim-hover: oklch(0.45 0.14 250 / 0.16);
}

[data-theme="sage"] {
  --bg:              oklch(0.93 0.01 145);
  --surface:         oklch(0.96 0.006 145);
  --surface-hover:   oklch(0.89 0.014 142);
  --surface-active:  oklch(0.86 0.016 140);
  --border:          oklch(0.83 0.01 142);
  --border-strong:   oklch(0.73 0.014 140);
  --text:            oklch(0.20 0.018 150);
  --text-secondary:  oklch(0.40 0.018 148);
  --text-muted:      oklch(0.54 0.014 146);
  --accent:          oklch(0.42 0.12 160);
  --accent-hover:    oklch(0.38 0.14 160);
  --accent-dim:      oklch(0.42 0.12 160 / 0.10);
  --accent-text:     oklch(0.97 0.005 160);
  --accent-dim-hover: oklch(0.42 0.12 160 / 0.16);
}

[data-theme="lavender-haze"] {
  --bg:              oklch(0.93 0.012 300);
  --surface:         oklch(0.96 0.007 300);
  --surface-hover:   oklch(0.89 0.015 298);
  --surface-active:  oklch(0.86 0.018 296);
  --border:          oklch(0.83 0.012 298);
  --border-strong:   oklch(0.73 0.016 295);
  --text:            oklch(0.20 0.018 290);
  --text-secondary:  oklch(0.40 0.02 292);
  --text-muted:      oklch(0.54 0.016 294);
  --accent:          oklch(0.48 0.18 300);
  --accent-hover:    oklch(0.43 0.20 300);
  --accent-dim:      oklch(0.48 0.18 300 / 0.10);
  --accent-text:     oklch(0.97 0.005 300);
  --accent-dim-hover: oklch(0.48 0.18 300 / 0.16);
}

[data-theme="rose-quartz"] {
  --bg:              oklch(0.93 0.012 350);
  --surface:         oklch(0.96 0.007 350);
  --surface-hover:   oklch(0.89 0.015 348);
  --surface-active:  oklch(0.86 0.018 346);
  --border:          oklch(0.83 0.012 348);
  --border-strong:   oklch(0.73 0.016 345);
  --text:            oklch(0.20 0.018 340);
  --text-secondary:  oklch(0.40 0.02 342);
  --text-muted:      oklch(0.54 0.016 344);
  --accent:          oklch(0.50 0.20 340);
  --accent-hover:    oklch(0.45 0.22 340);
  --accent-dim:      oklch(0.50 0.20 340 / 0.10);
  --accent-text:     oklch(0.97 0.005 340);
  --accent-dim-hover: oklch(0.50 0.20 340 / 0.16);
}
