@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

/* Quality ConTroll's control panel.
   Chrome is the Focus Toolbox App Layout Standard (control-center wiki); the
   token values below are lifted from Smarty rather than re-derived, because
   two sibling apps on one box that are almost the same shade of blue is worse
   than either shade on its own. */

:root {
  /* FoE brand (smarty/focus_branding.md) */
  --foe-blue: #034ea2;
  /* The same hue, lightened, for the second series on the trend chart: the two
     bars are the same kind of thing, so they should not be two different
     colours arguing with each other. */
  --foe-blue-soft: #8fb4dd;
  --foe-green: #00ab4e;
  --foe-body: #3c3c3c;
  --font-family: 'Inter', sans-serif;

  --surface: #faf8ff;
  --surface-low: #f2f3ff;
  --on-surface-variant: #45464d;
  --outline-variant: #c4c6d0;
  --card-radius: 0.5rem;
  --inner-radius: 0.375rem;
  --card-shadow: 0 2px 12px rgba(19, 27, 46, 0.08);
}

* { box-sizing: border-box; }
html, body { max-width: 100%; overflow-x: hidden; }
body { margin: 0; font-family: var(--font-family); color: var(--foe-body); background: var(--surface); }

/* ── Header: white logo bar / blue app bar / pill tab strip ────────────── */
.app-header { position: sticky; top: 0; z-index: 40; }

.logo-bar {
  height: 80px; background: #fff; border-bottom: 1px solid var(--outline-variant);
  display: flex; align-items: center; padding: 0 1.25rem;
}
.logo-bar .logo { height: 64px; width: auto; }

.app-bar {
  height: 34px; background: var(--foe-blue);
  display: flex; align-items: center; padding: 0 1.25rem;
}
.app-bar-title {
  color: #fff; font-size: .72rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;
}
.app-bar-viewer { margin-left: auto; color: rgba(255,255,255,.7); font-size: .72rem; white-space: nowrap; }

.tab-strip {
  height: 40px; background: #fff; border-bottom: 1px solid var(--outline-variant);
  display: flex; align-items: center; padding: 0 1.25rem; gap: .25rem;
}
.tab-strip a {
  padding: .3rem .7rem; border-radius: var(--inner-radius);
  color: var(--on-surface-variant); text-decoration: none;
  font-size: .8125rem; font-weight: 500; white-space: nowrap;
}
.tab-strip a.active { background: #d7f3e3; color: #0f6b3a; font-weight: 600; }
.tab-strip a:not(.active):hover { background: var(--surface-low); color: var(--foe-body); }

main { padding: .9rem 1.25rem 3.5rem; min-width: 0; }
.hidden { display: none !important; }

.card {
  background: #fff; border: 1px solid var(--outline-variant);
  border-radius: var(--card-radius); box-shadow: var(--card-shadow);
  padding: 1rem 1.25rem; margin-bottom: 1rem;
}

.search-row { display: flex; gap: .5rem; margin-bottom: .6rem; }
.search-wrap { position: relative; flex: 1; display: flex; min-width: 0; }
.search-wrap .search-ic {
  position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; color: #9aa0a6; pointer-events: none;
}
.search-wrap input[type=text] {
  flex: 1; padding: .42rem .75rem .42rem 2rem;
  border: 1px solid var(--outline-variant); border-radius: 999px;
  font-family: inherit; font-size: .8125rem; min-width: 0; background: #fff;
}
.search-row select {
  flex: none; width: 190px; padding: .35rem .6rem;
  border: 1px solid var(--outline-variant); border-radius: var(--inner-radius);
  font-family: inherit; font-size: .8125rem; color: var(--foe-body); background: #fff;
}

/* Table action buttons: right-aligned, inside the table card, above the
   header row — the standard's sixth element. */
.toolbar {
  display: flex; gap: .25rem; align-items: center; flex-wrap: wrap;
  padding: .45rem .6rem; border-bottom: 1px solid var(--outline-variant);
}
.toolbar .spacer { flex: 1; }
.toolbar .role-note { font-size: .75rem; color: var(--on-surface-variant); padding-left: .35rem; }
.runs-toolbar { background: #fff; border: 1px solid var(--outline-variant);
  border-radius: var(--card-radius); box-shadow: var(--card-shadow); margin-bottom: .8rem; }
.inline-toggle { font-size: .75rem; color: var(--on-surface-variant); display: inline-flex;
  align-items: center; gap: .35rem; padding-left: .35rem; cursor: pointer; }

.ic { width: 14px; height: 14px; flex: none; }

.tool-btn {
  display: inline-flex; align-items: center; gap: .35rem;
  background: transparent; border: none; cursor: pointer;
  color: #4b5563; font-family: inherit; font-weight: 500; font-size: .78rem;
  padding: .35rem .6rem; border-radius: var(--inner-radius); white-space: nowrap;
}
.tool-btn:hover:not(:disabled) { background: var(--surface-low); color: var(--foe-blue); }
.tool-btn:disabled { opacity: .45; cursor: not-allowed; }
#toll-delete:hover:not(:disabled) { background: #fdeceb; color: #93000a; }

button.primary, button.secondary, button.danger {
  font-family: inherit; font-weight: 600; font-size: .85rem;
  border: none; border-radius: var(--inner-radius);
  padding: .5rem .9rem; cursor: pointer; white-space: nowrap;
}
button.primary { background: var(--foe-blue); color: #fff; }
button.primary:hover { background: #023d80; }
button.secondary { background: var(--surface-low); color: var(--foe-blue); }
button.danger { background: #ffdad6; color: #93000a; }
button:disabled { opacity: .5; cursor: not-allowed; }

/* ── Tables ────────────────────────────────────────────────────────────── */
.table-card { padding: 0; }
table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: .8125rem; }
th, td { text-align: left; padding: .38rem .7rem; border-bottom: 1px solid #e8eaf0;
         overflow: hidden; text-overflow: ellipsis; }
td.wrap { white-space: normal; word-break: break-word; }
th { color: var(--on-surface-variant); font-weight: 600; font-size: .7rem;
     letter-spacing: .02em; background: #fff; border-bottom: 1px solid var(--outline-variant);
     position: sticky; top: 0; user-select: none; white-space: nowrap; }
th[data-sort] { cursor: pointer; }
th[data-sort]:hover { color: var(--foe-blue); }
th[data-sort].sorted[data-sort-dir="asc"]::after { content: '\2191'; margin-left: .25em; }
th[data-sort].sorted[data-sort-dir="desc"]::after { content: '\2193'; margin-left: .25em; }

#tab-tolls th:nth-child(1) { width: 130px; }
#tab-tolls th:nth-child(3) { width: 115px; }
#tab-tolls th:nth-child(4) { width: 115px; }
#tab-tolls th:nth-child(5) { width: 190px; }

#tab-reassignments th:nth-child(2) { width: 160px; }
#tab-reassignments th:nth-child(3) { width: 32%; }
#tab-reassignments th:nth-child(4) { width: 160px; }

#tab-runs th:nth-child(2) { width: 110px; }
#tab-runs th:nth-child(3) { width: 130px; }
#tab-runs th:nth-child(4) { width: 80px; }
#tab-runs th:nth-child(5) { width: 80px; }
#tab-runs th:nth-child(6) { width: 30%; }

tbody tr:last-child td { border-bottom: none; }
tbody tr { cursor: pointer; }
tr:hover td { background: #f7f9fc; }
tr.selected td { background: #e6f6ec; }
td a { color: var(--foe-blue); }
#tab-runs tbody tr, #tab-reassignments tbody tr { cursor: default; }
#tab-runs tbody tr:hover td { background: inherit; }

.empty-row td { color: var(--on-surface-variant); font-style: italic; padding: 1.1rem .7rem; }

.tag { display: inline-block; padding: .08rem .5rem; border-radius: 999px;
       font-size: .7rem; font-weight: 600; white-space: nowrap; }
.tag-running { background: #d7f3e3; color: #0f6b3a; }
.tag-inactive { background: #f1f5f9; color: #475569; }
.tag-dev { background: #ffe4c9; color: #8a4b00; }
.tag-failed { background: #ffdad6; color: #93000a; }
.tag-missing { background: #fff3bf; color: #7a5b00; }

/* Every toll that flagged an approval gets its own chip. `why_flagged` is
   newline-joined names; the Retool table showed it as one truncated line, so
   the second reason was invisible. */
.chip {
  display: inline-block; padding: .08rem .45rem; margin: .1rem .15rem .1rem 0;
  border-radius: 999px; background: var(--surface-low); color: #334155;
  font-size: .7rem; font-weight: 500; white-space: normal;
}

.pagination { display: flex; align-items: center; gap: .6rem; padding: .5rem .7rem;
  font-size: .78rem; color: var(--on-surface-variant); border-top: 1px solid var(--outline-variant); }
.pagination button { padding: .25rem .6rem; font-size: .78rem; }
.pagination .spacer { flex: 1; }

.footnote { font-size: .75rem; color: var(--on-surface-variant); margin: .2rem .2rem 0; }

.state-card { text-align: center; padding: 3rem 1rem; max-width: 560px; margin: 3rem auto; }
.state-card button { margin-top: 1rem; }

/* ── Run Health ────────────────────────────────────────────────────────── */
.nav-badge {
  display: inline-block; min-width: 1.15rem; padding: .05rem .35rem; margin-left: .3rem;
  border-radius: 999px; background: #ba1a1a; color: #fff;
  font-size: .68rem; font-weight: 700; text-align: center; vertical-align: text-top;
}
.banner {
  border-radius: var(--card-radius); padding: .7rem .9rem; margin-bottom: .8rem;
  font-size: .85rem; border: 1px solid transparent;
}
.banner strong { display: block; margin-bottom: .15rem; }
.banner-ok { background: #e7f6ec; border-color: #b7e2c6; color: #0f6b3a; }
.banner-warn { background: #fff8e1; border-color: #f4dfa0; color: #7a5b00; }
.banner-bad { background: #ffdad6; border-color: #f3b9b2; color: #93000a; }
.banner + .banner { margin-top: -.4rem; }

.run-error {
  font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: .72rem;
  color: var(--on-surface-variant); word-break: break-word; white-space: pre-wrap;
}

/* ── Metrics (qc_troll#10) ─────────────────────────────────────────────────
   Same tokens, same card treatment and the same tile/rank/trend vocabulary as
   Smarty's Metrics screen. Two sibling apps answering the same question with
   two different visual languages is how a reader learns to distrust both. */

.metrics-toolbar { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; flex-wrap: wrap; }
.metrics-toolbar .spacer { flex: 1; }
.metrics-toolbar .range-label { font-size: .8rem; color: var(--on-surface-variant); }
.metrics-toolbar select {
  padding: .3rem .5rem; border: 1px solid var(--outline-variant);
  border-radius: var(--inner-radius); background: #fff;
  font-family: var(--font-family); font-size: .8rem; color: var(--foe-body);
}
.metrics-toolbar .role-note { font-size: .75rem; color: var(--on-surface-variant); }

.tile-row { display: flex; gap: .75rem; flex-wrap: wrap; margin-bottom: 1rem; }
/* Centred, and centred *as a block*: one tile carries a third line naming the
   run it is describing and the others do not, so left-aligned tops left the row
   looking like four different components. `justify-content: center` sits each
   tile's content in the middle of the shared height, so the extra line reads as
   detail on one tile rather than as a tile of a different kind (qc_troll#27). */
.tile {
  flex: 1 1 9rem; background: #fff; border: 1px solid var(--outline-variant);
  border-radius: var(--card-radius); box-shadow: var(--card-shadow);
  padding: .8rem .9rem; text-align: center; font-family: var(--font-family);
  display: flex; flex-direction: column; justify-content: center;
  min-height: 4.75rem;
}
.tile-value { font-size: 1.5rem; font-weight: 700; color: var(--foe-body); line-height: 1.15; }
.tile-label { font-size: .72rem; color: var(--on-surface-variant); margin-top: .25rem; }
/* Which run the number describes, under the label it belongs to. */
.tile-sub {
  font-size: .62rem; color: var(--on-surface-variant); margin-top: .3rem;
  line-height: 1.3;
}
.tile-click { cursor: pointer; }
.tile-click:hover { border-color: var(--foe-blue); }
/* A check that has never caught anything is the one number here that means
   "go and look" — it is either dead weight or quietly broken. */
.tile-alert { border-color: #f3b9b2; background: #fff5f5; }
.tile-alert .tile-value { color: #93000a; }

/* Two panels to a row, not four (qc_troll#27). At four-up every card was about
   350px wide, and check names run past sixty characters — so every name in
   both check panels was cut off, and two different checks rendered as the
   identical string `Application Received Date Exceeds Requir...`. The panel
   was unreadable because of the grid, not because of the label.

   **Two stacks, not four cells.** A plain grid was the first fix and it left
   two holes: a grid row is as tall as its tallest card, so the eight-row
   "never fired" panel and the three-row assignee panel each sat above a block
   of nothing, waiting for the fifteen-row panel beside them to finish.
   `align-items: start` stops the *card* stretching but cannot pull the next
   row up. Each column is its own stack now, so a card sits directly under the
   one above it and no panel's height reaches across the gap.

   CSS multi-column would also do it and needs no wrappers, but it lets the
   browser choose which panel lands where, and the pairing is deliberate — the
   two check panels belong side by side. */
.metrics-grid {
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-items: start; gap: 1rem; margin-bottom: 1rem;
}
@media (min-width: 62rem) {
  .metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.metrics-col { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
.metrics-card { padding: 1rem; }
.metrics-card h3 { margin: 0 0 .75rem; font-size: .9rem; font-weight: 600; color: var(--foe-body); }
/* The caveat rides with the heading rather than sitting at the foot of the
   page, so a panel can't be read — or screenshotted — without it. */
.panel-sub {
  display: block; font-size: .7rem; font-weight: 400; line-height: 1.45;
  color: var(--on-surface-variant); margin-top: .2rem;
  /* These run to three or four sentences. Across the full width of a wide card
     that is a 130-character measure, which nobody reads to the end of. */
  max-width: 74ch;
}

.rank-list { display: flex; flex-direction: column; gap: .25rem; }
/* Every row shares one set of columns, so the whole list reads as a chart.
   Each `.rank-row` is its own grid, and an `auto` track sizes to *that row's*
   content — so with `auto` on the name the bars each began at a different x,
   and with `auto` on the count they each ended at a different x. Nothing lined
   up with anything, which is why a sorted list still looked like noise.

   A percentage and a fixed width fix that: percentages resolve against the row,
   every row is the same width, so the name column, the rail and the number all
   land on the same three verticals in every row. The rail is then a constant
   length and the fill is a true proportion of it, which is the thing that makes
   a descending list read top to bottom as a graph (qc_troll#27). */
.rank-list { --rank-name: 52%; }
/* Three short people's names do not need the room sixty-character check names
   do; the alignment guarantee is the same either way. */
.rank-list-compact { --rank-name: 11rem; }
.rank-row {
  display: grid;
  grid-template-columns: minmax(0, var(--rank-name)) minmax(4rem, 1fr) 3.5rem 2.75rem;
  align-items: center; gap: .6rem; width: 100%;
  padding: .3rem .35rem; border: 0; background: none; text-align: left;
  font-family: var(--font-family); font-size: .8rem; color: var(--foe-body);
  border-radius: var(--inner-radius);
}
.rank-click { cursor: pointer; }
.rank-click:hover { background: var(--surface-low); }
/* Names wrap; they are never shortened. The `title` attribute used to be
   the only way to read the rest of one, and a tooltip is nothing on touch
   and nothing to a keyboard user (qc_troll#27). */
.rank-name { overflow-wrap: anywhere; }
.rank-bar { background: var(--surface-low); border-radius: 999px; height: .45rem; overflow: hidden; }
.rank-bar > span { display: block; height: 100%; background: var(--foe-blue); border-radius: 999px; }
/* Right-aligned in fixed columns and tabular, so the digits stack. */
.rank-count {
  font-variant-numeric: tabular-nums; color: var(--foe-body);
  text-align: right;
}
/* The share of the column's own total. Quieter than the count — it is context
   for the number, not a second number competing with it. */
.rank-pct {
  font-variant-numeric: tabular-nums; color: var(--on-surface-variant);
  text-align: right; font-size: .72rem;
}
.id-flag {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .03em;
  background: var(--surface-low); color: var(--on-surface-variant);
  border-radius: 3px; padding: 0 .25rem; margin-left: .25rem; white-space: nowrap;
}

/* Two columns on a wide screen. This panel sits full-width at the foot of the
   page now, and a single column stretched a name and its date to opposite ends
   of 1700px — a long eye-trip for the least important list here. Two columns
   keeps each name near its own date and halves the height. */
.idle-list { display: flex; flex-direction: column; gap: .25rem; }
@media (min-width: 62rem) {
  .idle-list { display: block; columns: 2; column-gap: 2.5rem; }
  .idle-list .idle-row {
    break-inside: avoid; -webkit-column-break-inside: avoid; margin-bottom: .25rem;
  }
}
.idle-row {
  display: flex; align-items: baseline; gap: .6rem;
  padding: .3rem .35rem; font-size: .8rem; border-radius: var(--inner-radius);
}
.idle-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.idle-when { font-size: .72rem; color: var(--on-surface-variant); white-space: nowrap; }
/* A check that has never even run is a different problem from one that runs
   every night and finds nothing. */
.idle-never { color: #93000a; font-weight: 600; }

/* Three numeric columns of the same shape, each a count and its share, read as
   one undifferentiated block of digits — "142 33% 378 32% 378 31%" gives the
   eye nothing to stop at. A rule down each column boundary tells you which
   number belongs to which heading without adding any ink to the numbers
   themselves (qc_troll#27).

   The rule has to be continuous to read as a column edge rather than as a mark
   on each row, so the vertical spacing moved off the list and the row and into
   the cells: `gap: 0` between rows and between tracks, `align-items: stretch`
   so every cell is full row height, and the padding that used to separate rows
   now sits inside each cell, on both sides of the border. */
.program-list { display: flex; flex-direction: column; gap: 0; }
.program-row {
  /* 7.5rem, not 6rem: on the longer ranges the counts reach five digits and
     "12,345" plus its share no longer fitted, so the percent wrapped onto a
     second line underneath the number it belonged to. The cells also refuse to
     wrap now, so the column widens or scrolls rather than silently restacking
     (qc_troll#27). */
  display: grid; grid-template-columns: minmax(6rem, 1fr) 7.5rem 7.5rem 7.5rem;
  gap: 0; align-items: stretch; font-size: .8rem;
}
.program-head {
  font-size: .68rem; font-weight: 600; letter-spacing: .02em;
  color: var(--on-surface-variant); border-bottom: 1px solid var(--outline-variant);
}
.program-head span:not(:first-child), .program-num { text-align: right; }
.program-name {
  min-width: 0; overflow-wrap: anywhere;
  padding: .28rem .8rem .28rem .35rem;
}
.program-row > span + span {
  border-left: 1px solid var(--outline-variant);
  padding: .28rem .35rem .28rem .8rem;
}
.program-num { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The share rides in the same cell as the number it belongs to, so the table
   stays four columns instead of seven. */
.num-pct {
  display: inline-block; min-width: 2.4rem; text-align: right;
  margin-left: .4rem; font-size: .72rem; color: var(--on-surface-variant);
  font-variant-numeric: tabular-nums;
}

/* The reassignments chart (qc_troll#27). It was a row of bars floating in
   white space — no baseline, no scale, no gridlines — so the only way to
   compare two weeks was to read the number printed under each bar.

   The bars now stand on a real baseline against a labelled scale. Heights and
   gridlines are both percentages of the same plot height, so a bar meets its
   gridline exactly. */
.trend-chart {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: start; gap: .5rem;
}
/* The 2px of headroom is the top gridline's: it sits at `bottom: 100%`, which
   is one pixel above the plot, and `.trend-scroll` clips it away without it —
   `overflow-x: auto` forces the vertical axis to scroll too. The axis carries
   the same offset so its labels stay level with their lines. */
.trend-yaxis { position: relative; height: 11rem; min-width: 2rem; margin-top: 2px; }
.trend-ytick {
  position: absolute; right: 0; transform: translateY(50%);
  font-size: .62rem; line-height: 1; color: var(--on-surface-variant);
  font-variant-numeric: tabular-nums;
}
/* The bucket labels hang below the baseline, inside this box's padding rather
   than outside it: `overflow-x: auto` forces the vertical axis to scroll too,
   so anything sticking out of the top or bottom would be clipped. */
.trend-scroll { overflow-x: auto; padding-top: 2px; padding-bottom: 1.7rem; }
/* `space-evenly` because the bars are capped: five weekly buckets in a
   full-width card would otherwise huddle against the left-hand edge with the
   gridlines running off into empty space. Twelve monthly buckets fill the
   width on their own and the rule costs them nothing. */
.trend-cols {
  position: relative; display: flex; align-items: flex-end;
  justify-content: space-evenly; gap: .4rem;
  height: 11rem; min-width: 100%;
}
.trend-gridline {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px dashed var(--outline-variant); pointer-events: none;
}
.trend-baseline { border-top-style: solid; }
/* Two bars to a bucket now (qc_troll#27): approvals reviewed, and the subset of
   them that were reassigned. Side by side rather than stacked, because the
   question is how the two compare — and a stacked pair of a set and its own
   subset would double-count the reassignments into the total height. */
.trend-col {
  position: relative; z-index: 1; flex: 1 1 0;
  min-width: 3rem; max-width: 6rem; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center;
}
.trend-pair {
  display: flex; align-items: flex-end; justify-content: center;
  gap: .18rem; width: 100%; height: 100%;
}
.trend-stack {
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center; height: 100%; flex: 1 1 0; max-width: 2.4rem;
}
.trend-bar { width: 100%; border-radius: 3px 3px 0 0; }
.trend-bar-reviewed { background: var(--foe-blue-soft); }
.trend-bar-reassigned { background: var(--foe-blue); }

.trend-legend {
  display: flex; gap: 1.1rem; align-items: center;
  font-size: .7rem; color: var(--on-surface-variant); margin-bottom: .5rem;
}
.trend-legend span { display: inline-flex; align-items: center; gap: .35rem; }
.swatch {
  width: .7rem; height: .7rem; border-radius: 2px; display: inline-block;
}
.swatch-reviewed { background: var(--foe-blue-soft); }
.swatch-reassigned { background: var(--foe-blue); }
.trend-value {
  font-size: .6rem; color: var(--on-surface-variant);
  font-variant-numeric: tabular-nums; line-height: 1.1; margin-bottom: .15rem;
}
.trend-label {
  position: absolute; top: calc(100% + .35rem); left: -.25rem; right: -.25rem;
  text-align: center; font-size: .6rem; color: var(--on-surface-variant);
  white-space: nowrap;
}

.empty-note { font-size: .8rem; color: var(--on-surface-variant); margin: .5rem 0; font-style: italic; }

/* The no-access page's confirmation / error line (qc_troll#20). */
.request-status { font-size: .85rem; margin: .6rem 0 0; min-height: 1.2em; }
.request-status.ok { color: #0f6b3a; }
.request-status.bad { color: #93000a; }

/* ── The Control Center dock ───────────────────────────────────────────── */
/*
 * Element 5 of the Focus Toolbox layout standard: fixed bottom-left, a solid
 * opaque white card. Values are the standard's, and match Smarty, which is the
 * reference app for this element.
 *
 * This rule was simply missing. `index.html` has carried
 * `<div class="cc-dock">` since #5, but nothing ever styled it, so the dock was
 * an ordinary block element at the end of <body> and sat wherever the document
 * ended — under the page rather than pinned to the corner. On a long tab like
 * Reassignments you had to scroll past every row to reach the account menu.
 *
 * `main` already reserves 3.5rem of bottom padding, so pinning this does not
 * cover the last row of a table.
 */
.cc-dock {
  position: fixed; left: 16px; bottom: 16px;
  /* Above the sticky header (40), deliberately below the modal backdrop (50):
     the account menu should float over the page, but never over an open
     dialog. Smarty uses 60 and does sit over its own modals; that is the one
     place this diverges, and the standard does not specify a layer. */
  z-index: 45;
  background: #fff; border: 1px solid #e5e7eb;
  border-radius: var(--inner-radius);
  box-shadow: 0 4px 16px rgba(0,0,0,.10);
  padding: 6px 8px;
}

/* ── Modal ─────────────────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(19,27,46,0.4);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal {
  position: relative; background: #fff; border-radius: var(--card-radius);
  box-shadow: 0 8px 32px rgba(19,27,46,0.14);
  width: 560px; max-width: 92vw; max-height: 85vh; overflow: auto; padding: 1.5rem;
}
.modal.modal-wide { width: 820px; }
.modal h3 { margin-top: 0; padding-right: 2.25rem; }
.modal label { display: block; font-weight: 600; font-size: .85rem; margin: .75rem 0 .25rem; }
.modal input[type=text], .modal select, .modal textarea {
  width: 100%; padding: .5rem .6rem; border: 1px solid var(--outline-variant);
  border-radius: var(--inner-radius); font-family: inherit; font-size: .9rem;
}
.modal .actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: 1.25rem; }
.modal .hint { font-size: .75rem; color: var(--on-surface-variant); margin: .25rem 0 0; }

.modal-tabs { display: flex; gap: .35rem; border-bottom: 1px solid var(--outline-variant); margin: .75rem 0 0; }
.modal-tab {
  background: transparent; border: none; cursor: pointer; font-family: inherit;
  color: var(--on-surface-variant); font-weight: 600; font-size: .85rem;
  padding: .5rem .9rem; border-radius: 999px 999px 0 0; margin-bottom: -1px;
}
.modal-tab.active { background: #d7f3e3; color: #0f6b3a; }
.modal-tab-body { padding-top: .25rem; }

/* The SQL check's answer lands next to the query, not in an alert() that
   vanishes on OK. */
.sql-check { margin-top: .6rem; font-size: .8rem; white-space: pre-wrap; }
.sql-check.good { color: #0f6b3a; }
.sql-check.warn { color: #7a5b00; }
.sql-check.bad { color: #93000a; }
.sql-check:empty { display: none; }

.quill-host, .quill-host .ql-editor, .quill-host .ql-toolbar { font-family: var(--font-family) !important; }
.quill-host { background: #fff; }
.quill-host .ql-toolbar { border-radius: var(--inner-radius) var(--inner-radius) 0 0; border-color: var(--outline-variant); }
.quill-host .ql-container {
  border-radius: 0 0 var(--inner-radius) var(--inner-radius); border-color: var(--outline-variant);
  min-height: 280px; max-height: 55vh; overflow-y: auto; font-size: .92rem;
}
.quill-host .ql-editor.quill-readonly { padding: 1rem; }
.quill-host .ql-editor pre.ql-syntax {
  background: #1e1e1e; color: #d4d4d4; border-radius: var(--inner-radius);
  font-family: 'JetBrains Mono', ui-monospace, monospace !important; font-size: .82rem; padding: .85rem;
}

/* ── Narrow viewports ──────────────────────────────────────────────────── */
@media (max-width: 720px) {
  .logo-bar { height: 64px; }
  .logo-bar .logo { height: 48px; }
  main { padding: .75rem .75rem 4rem; }
  .tab-strip { overflow-x: auto; }
  .search-row { flex-wrap: wrap; }
  .search-row select { width: 100%; }
}

/* ---- Starting a run (qc_troll#23) --------------------------------------- */

/* Element 7 of the Focus Toolbox layout standard: a control that fires a
   request shows motion the instant it is clicked and goes un-clickable until
   the answer arrives. Drawn in `currentColor` and defined here rather than
   pulled from a CDN or an icon font — the moment the network is slow enough
   for a busy state to matter is the moment a remote asset has not arrived. */
.spin {
  display: inline-block;
  width: .85em; height: .85em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin .6s linear infinite;
  vertical-align: -.1em;
  flex: none;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* The wider in-progress verb must not wrap the button. */
.tool-btn, button.primary, button.secondary, button.danger { white-space: nowrap; }

.runs-picker {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .6rem; padding: .45rem .7rem;
  background: #fff; border: 1px solid var(--outline-variant); border-radius: 10px;
  font-size: .8rem;
}
.runs-picker label { color: var(--on-surface-variant); }
.runs-picker select { padding: .2rem .4rem; font-size: .8rem; max-width: 24rem; }

.run-live {
  margin: 0 0 .6rem;
  border: 1px solid var(--outline-variant); border-radius: 10px;
  background: #fff; overflow: hidden;
}
.run-live-head {
  display: flex; align-items: center; gap: .5rem;
  padding: .45rem .7rem; border-bottom: 1px solid var(--outline-variant);
  font-size: .8rem; font-weight: 600;
}
.run-live-head .spacer { flex: 1; }
.run-output {
  margin: 0; padding: .6rem .7rem;
  max-height: 20rem; overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .74rem; line-height: 1.45;
  white-space: pre-wrap; word-break: break-word;
  background: #0f172a; color: #e2e8f0;
}
.run-output:empty::before { content: "Waiting for output…"; color: #94a3b8; }

/* The gate panel inside the Run now dialog. Four rows, one per required Night
   Shift task, because "which one is stale and by how much" is the whole
   question — a single red sentence cannot answer it. */
.gate-list { list-style: none; margin: .4rem 0 0; padding: 0; font-size: .78rem; }
.gate-list li {
  display: flex; gap: .5rem; align-items: baseline;
  padding: .22rem 0; border-top: 1px solid var(--outline-variant);
}
.gate-list li:first-child { border-top: 0; }
.gate-list .gate-task { flex: 1; min-width: 0; word-break: break-word; }
.gate-list .gate-age { color: var(--on-surface-variant); white-space: nowrap; }

/* `.modal label` sets display/weight/margins for the editor's fields, so these
   are scoped the same way to win — a choice card is a label, not a field. */
.modal label.run-choice {
  display: block; padding: .5rem .6rem; border: 1px solid var(--outline-variant);
  border-radius: 8px; margin: .5rem 0 0; cursor: pointer; font-weight: 400;
}
.modal label.run-choice:hover { background: var(--surface-low); }
.modal label.run-choice input { margin-right: .45rem; }
.modal label.run-choice strong { font-size: .85rem; font-weight: 600; }
.modal label.run-choice .run-choice-note {
  display: block; margin: .15rem 0 0 1.35rem;
  font-size: .76rem; font-weight: 400; color: var(--on-surface-variant);
}
.modal label.run-choice.run-choice-selected { border-color: var(--foe-blue); background: #f3f8ff; }
.modal label.inline-toggle { display: inline-flex; font-weight: 400; margin: .6rem 0 0; }
.modal label.footnote { display: block; font-weight: 400; margin: .75rem 0 .2rem; }

.confirm-type { width: 100%; margin-top: .4rem; }
.modal-error { margin-top: .6rem; }

/* ---- Which copy of the app is this? (qc_troll#24) ----------------------- */

/* Sits in the blue app-name bar. Amber against that blue is the loudest thing
   on the page without being an error state, which is right: a practice copy is
   not broken, it is just not the one you can be careless in. */
.env-chip {
  margin-left: .6rem;
  padding: .12rem .5rem;
  border-radius: 999px;
  background: #ffd54f;
  color: #4a3200;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The Real run choice on a copy that may not do one. Deliberately styled as a
   disabled card rather than removed: "you cannot do this here, and here is
   why" beats a dialog that silently has one option. */
.modal .run-choice-off {
  display: block; padding: .5rem .6rem;
  border: 1px dashed var(--outline-variant);
  border-radius: 8px; margin: .5rem 0 0;
  background: var(--surface-low); color: var(--on-surface-variant);
}
.modal .run-choice-off strong { font-size: .85rem; font-weight: 600; }
.modal .run-choice-off .run-choice-note {
  display: block; margin: .15rem 0 0; font-size: .76rem;
}
