/* Pico v2 doesn't expose --pico-color-amber-* / --pico-color-red-* variables,
   so they silently resolve to nothing. Use literal Tailwind-style palette values
   with highly-contrasting foreground/background pairs instead. */
.dashboard-error {
  border: 1px solid #dc2626;       /* red-600 */
  background: #fee2e2;             /* red-100 */
  color: #7f1d1d;                  /* red-900 — readable on light bg */
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
}

/* DEV MODE badge (.todo/172): outlined chip with high-contrast amber on white,
   so it stays readable in both light and dark themes (previous amber-on-amber
   washed out, particularly in dark mode where pico's chrome is near-black). */
kbd {
  background: #ffffff;
  color: #92400e;                  /* amber-800 — reads black-ish on white */
  border: 1px solid #92400e;
  padding: 0.05rem 0.4rem;
  border-radius: 0.25rem;
  font-size: 0.8em;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Prevent long outbound_bot bodies (and any other <pre> content) from
   overflowing horizontally out of the card layout. Pico gives bare <pre> its
   grey background + radius but NO padding (only `pre > code` is padded), so
   every grey box app-wide sat flush against its edges except /templates, which
   nested <pre><code> and inherited Pico's 1rem inner-code padding by accident.
   Adding `--pico-spacing` (1rem) here converges every grey box on the one
   style without per-site edits; /templates now uses bare <pre> too (no
   double-padding). .todo/397. */
pre {
  white-space: pre-wrap;
  word-wrap: break-word;
  padding: var(--pico-spacing, 1rem);
}

/* Colour-code the recipe unified diff (recipes/detail.html). Recipe prose is
   stored as paragraph-length lines, so a one-word edit shows as a whole
   paragraph removed + re-added; pre-wrap then wraps each into a block. Without
   colour the +/- markers get buried and the diff reads as an undifferentiated
   wall. rgba tints + mid-tone text stay legible in pico's light and dark
   themes. */
.recipe-diff .diff-add  { color: #2da44e; background: rgba(46, 164, 78, 0.12); }
.recipe-diff .diff-del  { color: #cf222e; background: rgba(207, 34, 46, 0.12); }
.recipe-diff .diff-hunk { color: #8250df; font-weight: 600; }

/* List-view filter rows: a few narrow selects, not the default `.grid`'s
   one-per-row 50% stretch. Capped widths keep multi-filter forms compact.
   Each control is wrapped in a <label> (the filter_form macros associate a
   visually-hidden caption for a11y), so the width caps target both the bare
   control and the label-wrapped one. */
.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.filter-row > label {
  margin-bottom: 0;
}
.filter-row > select,
.filter-row > input:not([type="checkbox"]),
.filter-row > label > select,
.filter-row > label > input:not([type="checkbox"]) {
  width: auto;
  max-width: 14rem;
  margin-bottom: 0;
}

/* Accessible label hidden from sighted users (kept for screen readers): the
   compact .filter-row search box associates a <label> by id without showing a
   visible caption that would unbalance the single-row filter layout. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Sortable column headers (the `sort_th` macro). A sortable header reads as a
   clickable control: pointer cursor + hover underline. The active column gets
   a heavier weight + accent colour so the sorted-by column is obvious. The
   faint `.sort-hint` glyph marks inactive sortable columns before any click. */
.sort-header {
  cursor: pointer;
  text-decoration: none;
  color: inherit;
}
.sort-header:hover {
  text-decoration: underline;
}
.sort-header--active {
  font-weight: 700;
  color: var(--pico-primary, #1095c1);
}
.sort-hint {
  opacity: 0.35;
  font-size: 0.85em;
}

/* Tester running-state stage indicator. Each <li> shows pending / active /
   complete based on which SSE events have landed so far. */
.stage-list {
  list-style: none;
  padding-left: 0;
  margin: 0.5rem 0;
}
.stage-list li {
  padding: 0.25rem 0.5rem;
  border-left: 3px solid #d4d4d8;        /* zinc-300 — pending */
  color: #71717a;                        /* zinc-500 */
  margin-bottom: 0.25rem;
}
.stage-list li::before {
  content: "○ ";
  font-family: monospace;
}
.stage-list li[data-status="active"] {
  border-left-color: #2563eb;            /* blue-600 */
  color: #1e3a8a;                        /* blue-900 */
  font-weight: 600;
}
.stage-list li[data-status="active"]::before {
  content: "● ";
}
.stage-list li[data-status="complete"] {
  border-left-color: #16a34a;            /* green-600 */
  color: #14532d;                        /* green-900 */
}
.stage-list li[data-status="complete"]::before {
  content: "✓ ";
}

/* "Not recorded" and similar empty-state notes — dim, italicised inline
   text. Used on the decisions detail page for the empty prompt-hash slot
   instead of a bare em-dash <code> pill that looked like a glitch. */
.muted {
  color: #71717a;                        /* zinc-500 */
  font-style: italic;
}

/* Inbox thread nav (.todo/483): "back to Inbox" on the left, "Next message" on
   the right, so sequential triage has a fixed forward target that doesn't move
   as the thread's own links change. */
.thread-nav {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

/* Keep short, ID-like table tokens on one line so they don't wrap mid-value
   (e.g. the eval runs list's "default #1" recipe version or "2026-06-v1"
   eval version splitting at the hyphen). .todo/347. */
.nowrap { white-space: nowrap; }

/* Clamp a long table-cell value to N lines so a full customer message stored
   as a subject can't blow a /decisions row to ~20 lines. The full text stays
   available via the element's title attribute (hover). .todo/409. */
.cell-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Wide tables (decisions, evals per-case, tester runs) scroll horizontally
   inside their own wrapper instead of forcing the whole page body to scroll
   and clipping the rightmost columns at desktop widths. The mobile
   `main table { display:block; overflow-x:auto }` fallback below already does
   this <768px; this wrapper brings the same behaviour to desktop. .todo/403. */
.table-scroll {
  overflow-x: auto;
}

/* Overview segmented window picker — three buttons (7d/30d/90d) in a row,
   replacing a full-width <select>. aria-current="page" marks the active one
   so it reads correctly even without colour. Pico's `[role=group]` rule
   sets `width: 100%`, which would stretch the strip across the page; we
   pin width to auto + flex:0 on the children so the control is only as
   wide as its labels. */
.window-picker {
  display: inline-flex;
  width: auto;
  margin-bottom: 1rem;
}
.window-picker > a {
  flex: 0 0 auto;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--pico-muted-border-color, #cbd5e1);
  text-decoration: none;
  color: inherit;
  background: var(--pico-background-color, transparent);
}
.window-picker > a + a {
  border-left: none;
}
.window-picker > a:first-child {
  border-top-left-radius: 0.25rem;
  border-bottom-left-radius: 0.25rem;
}
.window-picker > a:last-child {
  border-top-right-radius: 0.25rem;
  border-bottom-right-radius: 0.25rem;
}
.window-picker > a[aria-current="page"] {
  background: #2563eb;                  /* blue-600 */
  color: #ffffff;
  border-color: #2563eb;
}

/* Overview KPI tile row. Pico's `.grid` forces all seven tiles onto one row at
   equal fractions, so at mid widths (~970px) each column gets too narrow and
   the content breaks mid-word ("Forwar d", "$0.4 5", "(50%)" on its own line).
   Override with auto-fit columns: tiles keep a sensible minimum width and the
   row wraps instead of shrinking columns below it. 7.5rem (not the old 9rem)
   because Pico scales the root font-size up to ~20px at ≥1280px, so 9rem became
   ~183px and only six of the seven tiles fit per row — the Reviewed tile
   orphaned onto a second row (.todo/410). 7.5rem fits all seven at 1280px while
   staying wide enough to keep the .todo/273 mid-word-break guard. */
#tiles {
  grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
}

/* Overview action tiles wrapped in <a> for click-through to filtered
   /decisions. Keep the link block-level so the whole card is the hit area,
   and strip the default anchor underline so the card layout stays clean. */
.tile-link {
  text-decoration: none;
  color: inherit;
  display: block;
}

/* Equal-height tiles (.todo/410). The .tile-link grid items stretch to the
   row height (grid default align-items:stretch), but the Pico <article> inside
   keeps its own content height and picks up a default bottom margin, so linked
   tiles rendered shorter and ragged next to the bare-<article> tiles. Fill the
   link and drop the margin so every tile is the same height. */
#tiles > article,
#tiles .tile-link > article {
  height: 100%;
  margin: 0;
}

/* Hover affordance for the (now all-linked) tiles: a lift + shadow so they
   read as clickable. Box-shadow (not a border) avoids a 1px layout shift. */
.tile-link > article {
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.tile-link:hover > article {
  box-shadow: 0 4px 14px rgba(37, 99, 235, 0.22);  /* blue-600 tint */
  transform: translateY(-2px);
}

/* Pair with a row-level background hover so the operator sees the affordance.
   No `cursor: pointer` — rows are no longer whole-row clickable (.todo/301
   PR11 moved navigation to the single Customer-cell <a>); a row-wide pointer
   would imply clickability the row no longer has (.todo/344). */
table tbody tr:hover {
  background: #f4f4f5;                   /* zinc-100 */
}

/* Star toggle button: unstyled, just the glyph character. No border, no
   background — looks like plain text but is a real submit target. The
   star-cell column is kept narrow so it doesn't crowd the data columns. */
.star-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  color: #ca8a04;                        /* amber-600 — fills in warm gold */
}
.star-btn:hover {
  opacity: 0.7;
}
/* On detail pages the star toggle is a <button type="submit"> inside a POST
   form, so Pico's default `button[type=submit] { width:100% }` stretches it to
   the full content width — an invisible 1200px click target with the ☆ glyph
   centred on its own row. The `button[type="submit"]` qualifier matches Pico's
   specificity (the .feedback-thumb trick at ~line 694) so `width:auto` actually
   wins and the hit area shrinks to the glyph. .todo/404. */
button[type="submit"].star-btn {
  width: auto;
}
td.star-cell {
  width: 2rem;
  text-align: center;
  padding-left: 0.25rem;
  padding-right: 0.25rem;
}

/* Bulk-action bar (.todo/301 §5). Sits above a list table; lays the action
   select + Apply + live "(N selected)" count out in one wrapping row. The flash
   is the always-on success feedback shown after an apply. The leading checkbox
   column mirrors .star-cell: narrow and centred, outside any linked cell. */
.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.bulk-bar select,
.bulk-bar button {
  width: auto;
  margin-bottom: 0;
}
.bulk-status {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
}
.bulk-flash {
  color: #15803d;                        /* green-700 — success */
  font-style: normal;
}
td.bulk-check-cell,
th.bulk-check-cell {
  width: 2rem;
  text-align: center;
  padding-left: 0.25rem;
  padding-right: 0.25rem;
}

/* Shared badge component used for both DecisionAction and ConversationState.
   Bare `.badge` is the visible neutral fallback for unknown enum values.
   Colour modifiers are shared across vocabs — forwarded_to_groove (state)
   and forward_groove (action) both map to amber for visual consistency. */
.badge {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.85em;
  font-weight: 600;
  background: #e5e7eb;                   /* zinc-200 — neutral fallback */
  color: #374151;                        /* zinc-700 */
  border: 1px solid #d1d5db;             /* zinc-300 */
}
/* DecisionAction modifiers */
.badge--auto-reply {
  background: #dcfce7;                   /* green-100 */
  color: #14532d;                        /* green-900 */
  border-color: #86efac;                 /* green-300 */
}
.badge--forward-groove {
  background: #fef3c7;                   /* amber-100 */
  color: #78350f;                        /* amber-900 */
  border-color: #fcd34d;                 /* amber-300 */
}
/* drop = "no reply needed" — a correct, benign outcome, NOT an error (.todo/296).
   Was red (#fee2e2); recoloured to the neutral zinc grey so the audit log reads
   it as "handled, nothing to send" rather than flagging it like a failure. */
.badge--drop {
  background: #e5e7eb;                   /* zinc-200 — neutral */
  color: #374151;                        /* zinc-700 */
  border-color: #d1d5db;                 /* zinc-300 */
}
.badge--re-forward {
  background: #e5e7eb;                   /* zinc-200 */
  color: #374151;                        /* zinc-700 */
  border-color: #d1d5db;                 /* zinc-300 */
}
.badge--forward-groove-spam {
  background: #ede9fe;                   /* violet-100 — distinct from amber forward */
  color: #4c1d95;                        /* violet-900 */
  border-color: #c4b5fd;                 /* violet-300 */
}
/* send_pending = a genuine auto-reply waiting out its randomised send jitter
   (.todo/346). Indigo so it reads as its OWN "scheduled / waiting to send"
   state — distinct from green (answered), amber (forwarded), and blue (open). */
.badge--send-pending {
  background: #e0e7ff;                   /* indigo-100 */
  color: #312e81;                        /* indigo-900 */
  border-color: #a5b4fc;                 /* indigo-300 */
}
/* ConversationState modifiers */
.badge--open {
  background: #dbeafe;                   /* blue-100 */
  color: #1e3a8a;                        /* blue-900 */
  border-color: #93c5fd;                 /* blue-300 */
}
.badge--bot-replied {
  background: #dcfce7;                   /* green-100 — matches auto-reply action */
  color: #14532d;                        /* green-900 */
  border-color: #86efac;                 /* green-300 */
}
.badge--forwarded-to-groove {
  background: #fef3c7;                   /* amber-100 — same as forward_groove action */
  color: #78350f;                        /* amber-900 */
  border-color: #fcd34d;                 /* amber-300 */
}
.badge--closed {
  background: #e5e7eb;                   /* zinc-200 */
  color: #374151;                        /* zinc-700 */
  border-color: #d1d5db;                 /* zinc-300 */
}

/* Eval pass/fail + the enabled/disabled lifecycle, folded into the one badge
   system (.todo/401). Own semantic colours — a FAIL must NOT reuse
   `.badge--drop`, which .todo/296 recoloured to neutral grey for the benign
   "no reply needed" outcome. Green = pass / enabled / active, red = fail /
   error; disabled / deactivated / off keep the bare `.badge` neutral grey.
   These replace the `.eval-badge-pass` / `.eval-badge-fail` classes that lived
   in per-template inline <style> blocks. */
.badge--pass,
.badge--enabled {
  background: #dcfce7;                   /* green-100 */
  color: #14532d;                        /* green-900 */
  border-color: #86efac;                 /* green-300 */
}
.badge--fail {
  background: #fee2e2;                   /* red-100 */
  color: #7f1d1d;                        /* red-900 */
  border-color: #fca5a5;                 /* red-300 */
}

/* Eval per-case score table + judge-comment reading column (.todo/347),
   relocated here from evals/detail.html's inline <style> (.todo/401 — no
   <style> blocks in templates). */
.score-cell {
  text-align: center;
  min-width: 2.5rem;
}
.judge-review { margin: 0.75rem 0; }
.judge-review > header {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}
.judge-comment {
  margin: 0;
  color: #374151;                        /* zinc-700 */
  max-width: 72ch;
}

/* Conversations list — Status cell step history (.todo/296). Multi-decision
   threads get a quiet, collapsed-by-default disclosure beneath the status
   badge; single/zero-decision rows render only the badge, so the common case
   reads exactly like a plain status cell. Everything reuses the existing
   `.badge` classes — the only new chrome is the disclosure itself, kept
   deliberately faint (no card, no spine, no new colours). */
td.status-cell {
  vertical-align: top;
}
.step-toggle {
  margin-top: 0.2rem;
}
.step-toggle > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  list-style: none;                      /* hide native marker; we draw a caret */
  cursor: pointer;
  width: max-content;                    /* tight hit-target, not the full cell */
  font-size: 0.8em;
  color: #71717a;                        /* zinc-500 — matches .muted */
}
.step-toggle > summary::-webkit-details-marker {
  display: none;
}
/* Suppress Pico's float-right accordion chevron — our own ▸ caret (below) is
   the affordance, so Pico's would be redundant clutter on the right. */
.step-toggle > summary::after {
  display: none;
}
.step-toggle > summary::before {
  content: "\25B8";                      /* ▸ */
  font-size: 0.85em;
  transition: transform 0.12s ease;
}
.step-toggle[open] > summary::before {
  transform: rotate(90deg);
}
.step-list {
  list-style: none;
  margin: 0.3rem 0 0;
  padding-left: 0.75rem;                 /* quiet indent — no rule/spine */
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.step-list li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
  font-size: 0.85em;
}
.step-conf {
  color: #71717a;                        /* zinc-500 */
}
.step-flag {
  font-size: 0.9em;
  color: #71717a;                        /* zinc-500 */
  border: 1px solid #e4e4e7;             /* zinc-200 */
  border-radius: 0.2rem;
  padding: 0 0.3rem;
}

/* Tester rows in the decisions list. A subtle blue-tinted background
   distinguishes them from production decisions at a glance. */
tr.tester-row {
  background: #eff6ff;                    /* blue-50 */
}
tr.tester-row mark {
  background: #dbeafe;                    /* blue-100 */
  color: #1e40af;                         /* blue-800 */
  border: 1px solid #93c5fd;             /* blue-300 */
  padding: 0.1rem 0.35rem;
  border-radius: 0.25rem;
  font-size: 0.75em;
  font-weight: 600;
  margin-left: 0.25rem;
}

/* Decisions detail copy-to-clipboard buttons (.todo/157 item 2). Inline,
   small, sits beside the H1 / H2 it copies — single-click ergonomics for
   the most-copied values on the page (decision UUID, raw model JSON). */
.copy-btn {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.1rem 0.5rem;
  font-size: 0.7em;
  font-weight: 500;
  background: #f3f4f6;                   /* zinc-100 */
  color: #374151;                        /* zinc-700 */
  border: 1px solid #d1d5db;             /* zinc-300 */
  border-radius: 0.25rem;
  cursor: pointer;
  width: auto;
  vertical-align: middle;
}
.copy-btn:hover {
  background: #e5e7eb;                   /* zinc-200 */
}

/* Pico's default ``button[type=submit]`` carries ``width:100%``; single-shot
   actions (the tester "Run"; the eval run/filter/compare and freeze-version
   submits via ``.action-btn``) read disproportionately wide as a full row.
   Override with the same selector specificity so the rule actually wins. A
   min-width keeps the button comfortably clickable. */
button[type="submit"].tester-run-button,
button[type="submit"].action-btn {
  width: auto;
  min-width: 6rem;
}

/* Compact per-row action buttons in list "Actions" cells (recipes manager,
   eval runs). Without this, pico's full-width submit turns each inline
   <form> into a stacked full-width slab that triples the row height. Lay the
   row's Edit link and action submits on one line, each sized to its label. */
.row-actions {
  display: flex;
  /* Column, not row: in a narrow table "Actions" cell (the recipes manager's
     7-column layout) three horizontal items don't fit and wrap raggedly, so
     stack them into a clean left-aligned action list. Single-action cells
     (the eval-runs "Remove") look identical either way. */
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
}
.row-actions form {
  margin: 0;
}
.row-actions button[type="submit"] {
  width: auto;
  margin: 0;
  padding: 0.2rem 0.6rem;
  font-size: 0.85em;
  /* Grow the box to the label instead of breaking a two-word action
     ("Set default") mid-phrase when the Actions column is narrow. */
  white-space: nowrap;
}

/* The inline link row (brand + 11 links + Sign out) needs ~1195px to fit on one
   line, but the mobile "Menu" disclosure below only takes over under 768px — so
   between 768 and ~1194px (iPad, small laptops, split-screen) the row overflowed
   and pushed the page into horizontal scroll, clipping the rightmost links.
   Letting it wrap keeps every link clickable at every width with no breakpoint to
   tune; the nav isn't sticky, so the extra row height is harmless. .todo/405. */
ul.nav-links { flex-wrap: wrap; }

/* /users per-row role <select>: Pico sizes selects width:100% and reserves
   chevron padding, so in the narrow Actions cell the longest option
   ("superuser") clipped to "superus". Let it size to its content instead.
   .todo/411. */
select.users-role-select {
  width: auto;
  margin: 0;
}

/* ---- Mobile nav disclosure (.todo/172 #1 + .todo/174 #2) -----------------
   Default state (>=768px) is the inline link row. The `<details>` ("Menu")
   sibling is hidden. Below 768px the inline row is hidden and the disclosure
   takes its place — keeps the brand on one line and pushes overflowing
   items into a vertical list that the operator opens on demand. */
.nav-disclosure { display: none; }

@media (max-width: 767.98px) {
  /* The outer nav is a flex row by default (brand + links). Switch to
     column so the disclosure can claim its own row beneath the brand
     instead of overlapping it when expanded. */
  body > nav {
    flex-direction: column;
    align-items: stretch;
  }
  .nav-links { display: none; }
  .nav-disclosure {
    display: block;
    width: 100%;
  }
  .nav-disclosure > summary {
    cursor: pointer;
    padding: 0.25rem 0.75rem;
    list-style: none;
  }
  .nav-disclosure > summary::-webkit-details-marker { display: none; }
  .nav-disclosure > ul {
    list-style: none;
    padding-left: 0;
    margin: 0.5rem 0 0;
    /* Pico's `nav ul` rule sets `display: flex; flex-direction: row` which
       would lay the menu out horizontally and re-clip. Override here. */
    display: flex !important;
    flex-direction: column !important;
    align-items: flex-start !important;
  }
  .nav-disclosure > ul > li { padding: 0.35rem 0; }

  /* Stack the chart row vertically; three half-height articles fit the
     viewport without the canvas spilling outside its card. */
  .chart-row { grid-template-columns: 1fr !important; }
  .chart-row > article { height: 16rem; }

  /* Wide tables (templates, decisions, evals lists) become horizontally
     scrollable on mobile instead of squeezing every cell down to single
     characters wrapping vertically. `display: block` + `overflow-x: auto`
     is the same trick pico applies to `<figure>` table wrappers; we apply
     it to bare `<table>` because the templates don't use figure wrappers.
     Headers and rows stay aligned because `display:block` on the table
     hoists scroll to the table itself. */
  main table {
    display: block;
    overflow-x: auto;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
    /* nowrap so cells scroll horizontally as one row instead of collapsing
       into one-word-per-line towers (and silently clipping the right-edge
       columns) at narrow widths. */
    white-space: nowrap;
  }

  /* Inbox list (.todo/311): the generic `main table` horizontal-scroll
     fallback above leaves the Inbox unreadable on a phone — its five fixed
     columns force a wider-than-viewport scroll. The Inbox is the primary
     mobile view, so it gets a real treatment instead: collapse each row into
     a stacked card. The desktop fixed column widths + When nowrap are scoped to
     a `min-width: 768px` query (see below), so they never reach this block — the
     cells flow full-card-width here with no width reset needed. `white-space:
     normal` IS still needed: it undoes the generic `main table` mobile
     fallback's `white-space: nowrap` (which the cells inherit), so a long
     subject/email wraps inside its card instead of forcing horizontal scroll. */
  table.inbox,
  table.inbox tbody,
  table.inbox tr,
  table.inbox td {
    display: block;
    white-space: normal;
  }
  /* The header row labels each desktop column; on the stacked card the labels
     move inline onto the cells (below), so the thead is redundant — hide it. */
  table.inbox thead { display: none; }
  /* Each row is a card: a bordered, padded block separated from the next. */
  table.inbox tr {
    border: 1px solid var(--pico-muted-border-color, #e5e7eb);
    border-radius: 0.375rem;
    padding: 0.5rem 0.75rem;
    margin-bottom: 0.6rem;
  }
  /* Each cell pads vertically so When/Customer/Subject/Bot-did read as a short
     stack. (No width reset: the desktop widths are min-width-gated, not global.) */
  table.inbox td {
    padding: 0.15rem 0;
  }
  /* Lang is the lowest-priority column — drop it on mobile (the detail page
     still shows language). The customer email keeps Pico's link colour and is
     the card's lede, so promote it; the When/Subject cells take a small inline
     label so the stacked values stay self-describing. */
  table.inbox td.lang-cell { display: none; }
  table.inbox td.customer-cell { font-weight: 600; }
  table.inbox td.when-cell::before { content: "When: "; color: #6b7280; }
  table.inbox td.subject-cell::before { content: "Subject: "; color: #6b7280; }
}

/* Email-routing diagram (.todo/265). One row per inbound address laid out as
   inbound → {auto-reply | escalate} so an admin reads the flow direction
   left-to-right. The bot stage is not drawn — every row runs through it and
   the whole dashboard is the bot. A grid (not wrapping flex) keeps the three
   rows column-aligned like a table; the earlier flex-wrap layout broke into
   a ragged stack as soon as the addresses didn't fit one line. Palette
   mirrors the badge/reply-status colours already used: blue for the inbound
   stage, green for the customer auto-reply, amber for the escalate-to-human
   branch (same amber as the forward_groove badge). */
.routing-flow > header {
  font-weight: 600;
}
.routing-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) min-content minmax(0, 1.4fr);
  align-items: center;
  gap: 0.5rem 0.75rem;
  padding: 0.75rem 0;
}
.routing-row + .routing-row {
  border-top: 1px solid var(--pico-muted-border-color, #e5e7eb);
}
.routing-node {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.2rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid #d1d5db;             /* zinc-300 */
  border-radius: 0.375rem;
  background: #f9fafb;                   /* zinc-50 */
}
/* Long addresses wrap inside their box instead of widening the column and
   pushing the row into overflow. */
.routing-node code {
  overflow-wrap: anywhere;
}
.routing-node--inbound {
  align-self: stretch;                   /* match the two-branch column's height */
  border-color: #93c5fd;                 /* blue-300 */
  background: #eff6ff;                   /* blue-50 */
}
.routing-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.7em;
  font-weight: 600;
  color: #6b7280;                        /* zinc-500 */
}
.routing-node--reply {
  border-color: #86efac;                 /* green-300 — matches auto-reply badge */
  background: #f0fdf4;                   /* green-50 */
}
.routing-node--escalate {
  border-color: #fcd34d;                 /* amber-300 — matches forward_groove badge */
  background: #fffbeb;                   /* amber-50 */
}
.routing-branches {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.routing-arrow {
  font-size: 1.25rem;
  color: #6b7280;                        /* zinc-500 */
}
@media (max-width: 575.98px) {
  /* Stack the stages vertically on phones; the arrow adds nothing once the
     flow reads top-to-bottom. */
  .routing-row { grid-template-columns: minmax(0, 1fr); }
  .routing-arrow { display: none; }
}

/* Generated reply ("draft") shown for operator debugging on the decision /
   tester detail pages. When the draft was NOT the action actually taken
   — forwarded to a human, dropped, or (on a tester run) a routing the live
   pipeline would forward — dim the body and label it amber so staff never
   mistake the draft for what the customer actually received. The label
   itself stays full-opacity (only the <pre> dims) so the verdict reads
   clearly. Palette mirrors the literal-Tailwind values used above. */
.reply-status {
  margin: 0 0 0.25rem;
  font-size: 0.85em;
  font-weight: 600;
}
.reply-status--sent {
  color: #15803d;                        /* green-700 — sent to the customer */
}
.reply-status--unused {
  color: #b45309;                        /* amber-700 — draft, not sent */
}
.reply-draft--unused pre {
  opacity: 0.55;
}
/* Red-flag forward (.todo/287): the draft often carries the flagged
   hallucination, so it's collapsed behind a disclosure rather than shown
   inline. The summary reads amber to match the not-sent caption; the body
   keeps the dimmed .reply-draft--unused pre styling once expanded. */
.reply-draft-collapse > summary {
  color: #b45309;                        /* amber-700 — draft, not sent */
  font-size: 0.85em;
  cursor: pointer;
}

/* Operator feedback card (.todo/263): thumbs rating + free-form note on the
   decision / tester-run detail page. Thumbs are unstyled glyph buttons (the
   .star-btn analog); the active one fills amber to match the star palette.

   The `button[type=submit]` qualifier matches Pico's default
   `button[type=submit] { width:100% }` specificity (the .tester-run-button
   trick) so two thumbs sit side-by-side instead of stacking full-width. The
   `button[type=button]` twin covers the eval-review deck, whose thumbs select
   client-side (no per-thumb POST) and so aren't submit buttons. */
button[type="submit"].feedback-thumb,
button[type="button"].feedback-thumb {
  background: none;
  border: 1px solid transparent;
  padding: 0.1rem 0.35rem;
  cursor: pointer;
  font-size: 1.15rem;
  line-height: 1;
  width: auto;
  filter: grayscale(1);                  /* inactive: muted, not coloured */
  opacity: 0.6;
}
.feedback-thumb:hover {
  opacity: 0.85;
}
/* Same `button[type=submit|button]` qualifier as the base rule so the active
   state actually wins (a bare `.feedback-thumb--active` would lose specificity). */
button[type="submit"].feedback-thumb--active,
button[type="button"].feedback-thumb--active {
  filter: none;                          /* active: full-colour emoji */
  opacity: 1;
  background: #fef3c7;                   /* amber-100 — matches forward badge */
  border-color: #fcd34d;                 /* amber-300 */
  border-radius: 0.25rem;
}
.feedback-label {
  font-weight: 600;
  margin-right: 0.25rem;
}
/* Pico's default ``button[type=submit]`` carries ``width:100%``; match that
   selector specificity (as .tester-run-button does) so a content-width Save
   button actually wins instead of stretching the full card. */
button[type="submit"].feedback-save-btn {
  width: auto;
  margin-top: 0.25rem;
}
.feedback-saved {
  margin-left: 0.5rem;
  color: #15803d;                        /* green-700 — matches reply "sent" */
  font-weight: 600;
}

/* ── Inbox (Variant A) — .todo/296 ──────────────────────────────────────────
   Aligned table: When | Lang | Customer | Subject | Bot did. Tightened column
   widths so the inbound (customer/subject) reads first and the bot's outcome
   sits at the row end like a status. Subject takes the slack; the others stay
   compact. Rows align top so a multi-step "Bot did" cell doesn't centre the
   neighbours against its taller content. */
table.inbox {
  table-layout: fixed;
  width: 100%;
}
table.inbox th,
table.inbox td {
  vertical-align: top;
}
/* Cosmetics that apply at every width (the stacked mobile cards want them too). */
table.inbox td.when-cell { color: #6b7280; }
table.inbox td.lang-cell { text-transform: uppercase; }
table.inbox td.customer-cell { overflow-wrap: anywhere; }
table.inbox td.subject-cell { overflow-wrap: anywhere; }
/* Fixed column widths, the When nowrap, AND the cell padding belong to the
   desktop `table-layout: fixed` grid ONLY. They live in a min-width query so
   they never reach the <768px stacked cards. The cell padding (`table.inbox td`,
   specificity 0,1,1) MUST be gated for the same reason as the widths: left
   global it sits later in source than the mobile `table.inbox td` reset (also
   0,1,1), so it would win on source order and the stacked cards' tighter
   `0.15rem 0` vertical-stack padding would be dead. The per-cell width rules
   (specificity 0,2,2) need the gate even harder — they'd out-specify the mobile
   reset regardless of source order and pin the cards to desktop widths (bleeding
   a 16rem customer cell past its card on the narrowest phones). Gating by
   breakpoint — not a specificity bump — keeps the two layouts cleanly separate. */
@media (min-width: 768px) {
  table.inbox th,
  table.inbox td { padding: 0.45rem 0.5rem; }
  table.inbox .star-col { width: 1.6rem; }
  table.inbox td.star-cell { width: 1.6rem; }
  table.inbox td.when-cell { width: 8rem; white-space: nowrap; }
  table.inbox td.lang-cell { width: 3rem; }
  table.inbox td.customer-cell { width: 16rem; }
  table.inbox td.botdid-cell { width: 13rem; }
}

/* ── Thread detail: inbound/outbound message lanes + inline decision card ──
   The decision card sits directly under the inbound it answered. A quiet left
   spine + faint card chrome reads it as "the bot's response to the message
   above" without shouting. Inbound and outbound messages get a subtle tint so
   the chain alternates legibly (blue inbound, green outbound), matching the
   badge/reply palette already in use. */
.thread-msg { border-left: 3px solid transparent; }
.thread-msg--inbound { border-left-color: #93c5fd; }   /* blue-300 */
.thread-msg--outbound { border-left-color: #86efac; }  /* green-300 */

/* "Original HTML" disclosure (.todo/375): the sandboxed iframe can't be sized
   from the parent, so pin it to full width with a fixed max-height and let the
   frame scroll internally. */
.thread-html { margin: 0.4rem 0; }
.thread-html-frame {
  width: 100%;
  height: 32rem;                         /* fixed: a sandboxed frame can't self-size, so give it an explicit height + internal scroll */
  border: 1px solid #e4e4e7;             /* zinc-200 */
  border-radius: 0.375rem;
  background: #fff;
}

.decision-card {
  margin: 0.4rem 0 1.1rem 1.25rem;       /* nudged right: nested under its msg */
  border: 1px solid #e4e4e7;             /* zinc-200 */
  border-left: 3px solid #d4d4d8;        /* zinc-300 spine */
  border-radius: 0.375rem;
  background: #fafafa;                    /* zinc-50 */
  padding: 0.6rem 0.85rem;
}
.decision-card > header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.9rem;
  margin-bottom: 0.35rem;
}
.decision-card-action { font-weight: 600; }
.decision-card-conf { font-size: 0.9em; color: #6b7280; }   /* zinc-500 */
.decision-card-why { margin: 0.25rem 0; }
.decision-card-reasoning > summary,
.decision-card-flags > summary {
  cursor: pointer;
  font-size: 0.9em;
}
.decision-card-flags ul { margin: 0.3rem 0 0; }
.decision-card > footer {
  margin-top: 0.4rem;
}

@media (max-width: 767.98px) {
  /* On phones the decision card un-indents (the spine alone marks the nesting)
     so it isn't squeezed against the right edge. */
  .decision-card { margin-left: 0; }
}

/* ── Review deck (.todo/353) ────────────────────────────────────────────
   The /review loop: one decision per card, htmx-swapped in place. Panes reuse
   the badge / reply-status palette already defined above so the card reads
   consistently with the audit page. */
.review-card {
  border: 1px solid var(--pico-muted-border-color, #e4e8ec);
  border-radius: 10px;
  padding: 1rem 1.15rem;
  box-shadow: 0 2px 10px rgba(16, 21, 27, 0.06);
}
.review-progress {
  display: flex;
  gap: 0.6rem;
  font-size: 0.8rem;
  margin: 0 0 0.5rem;
}
.review-head {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
}
.review-headline { font-size: 1.05rem; }
.review-en-tag { font-size: 0.72rem; }
.review-head-spacer { flex: 1; }
/* Clamp the question so a long body (avg ~1.9k chars) doesn't push the reply
   and feedback controls off-screen; it scrolls inside its own box. */
/* Human-written email text — customer questions and agent/bot replies —
   rendered as PROSE, not code: a padded grey box in the proportional body font
   with line breaks preserved. Machine content (JSON, YAML, prompts, diffs,
   tracebacks) stays in a monospace <pre>. One treatment app-wide so the
   most-read object in the app stops rendering four different ways (.todo/399). */
.prose-box,
.review-qpane {
  background: var(--pico-code-background-color, #f6f8fa);
  border-radius: 0.25rem;
  padding: 0.5rem 0.75rem;
  margin: 0.2rem 0 0.4rem;
  font-size: 0.9rem;
  white-space: pre-wrap;
  word-break: break-word;
}
/* On the review card only, clamp the question height so a long question can't
   push the reply below the fold — with a stable scrollbar gutter so the clip is
   visibly scrollable, never a silent mid-sentence cut (.todo/399). */
.review-qpane {
  max-height: 140px;
  overflow-y: auto;
  scrollbar-gutter: stable;
}
/* "EN ⇄ original" translation toggle (.todo/355) — a compact inline button on
   the question-pane header, not a full-width block button. */
.review-translate-btn {
  width: auto;
  padding: 0.15rem 0.5rem;
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.2;
}
.review-translate-error {
  font-size: 0.78rem;
  margin: 0.2rem 0;
  color: #b45309;                                /* amber-700 */
}
/* Amber "was forwarding right?" heading — mirrors the not-sent caption colour. */
.review-forward-head { color: #b45309; }        /* amber-700 */
.review-reason { font-size: 0.9rem; margin: 0.2rem 0 0.4rem; }
.review-context { margin: 0.4rem 0; }
.review-context > summary { cursor: pointer; }

/* ── Eval-review deck tweaks ─────────────────────────────────────────────────
   Scoped to #eval-review-body so the shared /review deck is untouched. */
/* Give the question + gold-reply panes ~3× the review clamp (140px → 420px) so
   a whole eval case reads without scrolling inside the box on most cards. */
#eval-review-body .review-qpane { max-height: 420px; }
/* The notes/source captions are secondary metadata — drop them a notch below
   the body prose so they don't compete with the question and gold reply. */
#eval-review-body .review-note,
#eval-review-body .review-source { font-size: 0.72rem; }
/* Inline reason shown when a 👎 is submitted without an explanation. */
.eval-error { color: #b91c1c; font-size: 0.85rem; margin: 0.2rem 0; }  /* red-700 */
.eval-verdict-q { margin-bottom: 0.4rem; }
.eval-down-comment { margin: 0.2rem 0 0.4rem; }

/* Customer-context human summary + bounded raw-JSON disclosure (.todo/414). The
   summary is compact prose; the raw JSON — kept for debugging — is capped so it
   can't push the question and reply below the fold like the old unbounded dump
   (measured 1280px tall on one live card). */
.context-summary { margin: 0.3rem 0; }
.context-order { margin: 0.3rem 0; }
.context-letter { font-size: 0.85rem; margin: 0.1rem 0; }
.context-raw > pre { max-height: 400px; overflow: auto; }
.review-feedback { margin-top: 0.8rem; }
.review-rate-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 0.5rem;
}
button.review-skip { width: auto; padding: 0.15rem 0.7rem; margin: 0; }
.review-card-footer {
  display: flex;
  gap: 0.7rem;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 0.5rem;
  font-size: 0.85rem;
}
.review-done { text-align: center; padding: 2rem 1rem; }
.review-cta { display: inline-block; }
.review-queue-count { margin: 0.6rem 0; }

/* ── Review deck polish (.todo/354) ─────────────────────────────────────── */
/* Thin session-progress bar under the tally; fills as the backlog drains. */
.review-progressbar {
  height: 4px;
  border-radius: 2px;
  background: var(--pico-muted-border-color, #e4e8ec);
  overflow: hidden;
  margin: 0 0 0.7rem;
}
.review-progressbar > span {
  display: block;
  height: 100%;
  background: #2563eb;              /* blue-600 — matches the overview line chart */
  transition: width 0.2s ease;
}
.review-note-label {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.85rem;
}

/* "Save as eval" disclosure (.todo/371). It sits next to the one-click "Save as
   exemplar" checkbox but opens a correction form instead of saving, so it needs
   to *look* expandable. Same caret treatment as .step-toggle — the house
   disclosure pattern — rather than a second bespoke one. */
.feedback-eval {
  margin-top: 0.4rem;
}
.feedback-eval > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  list-style: none;                      /* hide native marker; we draw a caret */
  cursor: pointer;
  width: max-content;                    /* tight hit-target, not the full row */
  font-size: 0.85rem;
}
.feedback-eval > summary::-webkit-details-marker {
  display: none;
}
/* Suppress Pico's float-right accordion chevron — our ▸ caret is the affordance. */
.feedback-eval > summary::after {
  display: none;
}
.feedback-eval > summary::before {
  content: "\25B8";                      /* ▸ */
  font-size: 0.85em;
  transition: transform 0.12s ease;
}
.feedback-eval[open] > summary::before {
  transform: rotate(90deg);
}

/* ── Shared EN-translation panes (.todo/368) ──────────────────────────────
   One pattern across review, evals, decisions, conversations, templates: a
   compact inline toggle button on the pane head; the body renders in a <pre>
   (or the shared reply_block). Reuses the review-toggle sizing so the button
   stays inline, not a full-width block. */
.translate-head {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.3rem;
}
.translate-btn {
  width: auto;
  padding: 0.15rem 0.5rem;
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.2;
}
.translate-en-tag { font-size: 0.72rem; }
.translate-error {
  font-size: 0.78rem;
  margin: 0.2rem 0;
  color: #b45309;                                /* amber-700 */
}

/* ── Conversation thread: translate toolbar + enriched decision card ──────── */
.thread-toolbar {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.decision-card-feedback {
  margin: 0.4rem 0;
  font-size: 0.9rem;
}
/* Keep the inline note compact — Pico gives textareas a roomy default
   margin/height that overwhelms a thread with several decision cards. */
.decision-card-feedback textarea {
  margin-bottom: 0.25rem;
}
.decision-card-context { margin: 0.4rem 0; }

/* Staff-only rating strip on the public share view. The page has no decision
   card to sit inside, so the dashed rule is what marks the block as internal
   and keeps it visually separate from the customer-facing thread above it. */
.public-staff-feedback {
  margin: 0.25rem 0 1.25rem;
  padding-top: 0.4rem;
  border-top: 1px dashed var(--muted-border-color);
}
