:root {
  --bg: #14161a;
  --panel: #1b1e24;
  --panel-2: #252932;
  --border: #2c313b;
  --border-strong: #3b414d;
  --text: #d4d7dd;
  --muted: #8a919c;
  --accent: #8ab4f8;
  --accent-soft: rgba(138, 180, 248, 0.12);
  --ok: #7ec699;
  --warn: #e0b05c;
  --danger: #e0685f;
  --radius: 4px;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Tell the UA the page is dark so native widget chrome (select popups,
     number spinners, scrollbars) follows the theme instead of rendering
     white-on-dark (site picker, expiry bar). */
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body { min-height: 100%; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  line-height: 1.45;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Top bar */
:root { --topbar-h: 44px; --sidebar-w: 208px; }
.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 16px;
  height: var(--topbar-h);
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 50;
}
.brand { font-weight: 600; font-size: 14px; color: var(--text); }
.brand span { color: var(--accent); }
.topbar .user { margin-left: auto; color: var(--muted); }

/* Sidebar */
.sidebar {
  position: fixed;
  top: var(--topbar-h);
  left: 0;
  bottom: 0;
  width: var(--sidebar-w);
  overflow-y: auto;
  background: var(--panel);
  border-right: 1px solid var(--border);
  padding: 10px 8px 12px;
  display: flex;
  flex-direction: column;
}
.nav-home {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 13px;
  margin-bottom: 6px;
  white-space: nowrap;
  overflow: hidden;
}
.nav-home:hover { background: var(--panel-2); text-decoration: none; color: var(--text); }
.nav-home.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.nav-icon { width: 16px; height: 16px; flex: none; color: inherit; }
.nav-group { margin-bottom: 2px; }
.nav-group > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  padding: 6px 10px 4px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary::after { content: "\25be"; float: right; font-size: 10px; color: var(--muted); }
.nav-group:not([open]) > summary::after { content: "\25b8"; }
.nav-group > summary:hover { color: var(--text); }
.nav-links { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 6px; }
.nav-links a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 18px;
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
}
.nav-links a:hover { background: var(--panel-2); color: var(--text); text-decoration: none; }
.nav-links a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

/* Layout */
.container { max-width: 1200px; margin: 0 auto; padding: 16px 16px 48px; }
body.with-sidebar .container { margin-left: calc(var(--sidebar-w) + 16px); margin-right: 16px; max-width: none; }
@media (max-width: 900px) {
  .sidebar { position: static; width: auto; border-right: none; border-bottom: 1px solid var(--border); }
  body.with-sidebar .container { margin-left: 0; margin-right: 0; }
}
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.page-header .flex { flex-wrap: wrap; }
.page-header h1 { font-size: 18px; margin: 0; font-weight: 600; overflow-wrap: anywhere; }
.page-header .sub { color: var(--muted); font-size: 12px; }

/* Cards */
.grid { display: grid; gap: 10px; }
/* Long content must never blow a grid column out; let it wrap instead. */
.grid > *, .detail-grid > * { min-width: 0; }
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 1000px) { .grid.cols-4 { grid-template-columns: repeat(2, 1fr); } .grid.cols-3 { grid-template-columns: 1fr 1fr; } .grid.cols-2 { grid-template-columns: 1fr; } }
@media (max-width: 700px) {
  .grid.cols-4, .grid.cols-3 { grid-template-columns: 1fr; }
}
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}
/* `tabular-nums` and nothing else, deliberately. The report was that these figures looked
   like a different typeface from the table cells beside them, and the tempting fix -- give
   `.value` the mono stack the numeric cells use -- would align them *and* change every
   glyph, which is a different visual change from the one being asked for. Tabular figures
   give the alignment without the face swap. */
.stat .value { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat .label { color: var(--muted); font-size: 12px; }

/* Tables */
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
th { color: var(--muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
td .mono { font-family: var(--mono); font-size: 12px; }
tr:last-child td { border-bottom: none; }
/* Row-label th cells (asset Details metadata) lose the last-row separator
   too — the label side kept a border the value side dropped. tbody-scoped:
   thead rows are last-children of their own parent and must keep theirs. */
tbody tr:last-child th { border-bottom: none; }
/* Wide content must never bleed past the table/card borders. */
.card { overflow-x: auto; }
td.mono, td code { overflow-wrap: anywhere; }
td .mono { overflow-wrap: anywhere; }
/* Tables whose last column holds row actions align those to the right.
   The full-width empty-state cell must stay centered, so exclude it. */
.actions-last th:last-child, .actions-last td:last-child:not(.empty) { text-align: right; }
.actions-last td:last-child .flex { justify-content: flex-end; }
/* Opt a cell out of wrapping (timestamps, hashes, ids). */
.nowrap { white-space: nowrap; }

/* Buttons */
.btn {
  display: inline-block;
  flex-shrink: 0;
  /* Flex items shrink by default, and `white-space: nowrap` below stops the label from
     wrapping -- so a shrinking button does not reflow its text, it lets the text spill out
     of the box. That is the "tab buttons squish when the viewport shrinks" report: the
     `.wrap` utility the tab rows use was already defined, so the flex row could wrap but
     the buttons still shrank instead of wrapping to a new line. With this, a row that
     cannot fit its buttons moves them down rather than deforming them. */
  padding: 5px 11px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  line-height: 1.3;
  white-space: nowrap;
}
.btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #121212; font-weight: 600; }
.btn.primary:hover { background: #a5c5fb; color: #121212; }
.btn.danger { background: transparent; border-color: rgba(224,104,95,.45); color: var(--danger); }
.btn.danger:hover { background: rgba(224,104,95,.1); color: var(--danger); border-color: var(--danger); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--accent); border-color: transparent; }
.btn.small { padding: 3px 7px; font-size: 12px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
a.btn { text-decoration: none; }

/* Forms */
form .row { margin-bottom: 10px; }
label { display: block; color: var(--muted); font-size: 11px; font-weight: 600; margin-bottom: 3px; text-transform: uppercase; letter-spacing: .04em; }
input[type=text], input[type=url], input[type=number], input[type=password], textarea, select {
  width: 100%;
  padding: 6px 9px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
}
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
input.autocomplete:focus { box-shadow: none; }
input[type=checkbox] { accent-color: var(--accent); width: 14px; height: 14px; }
/* email/date/search were rendered as unstyled browser widgets -- white background and
   system font inside a dark panel -- because the rule above listed the types in use at
   the time and never grew. Radio needed accent-color too, or the export dialog's
   groups sat untinted in the default dark theme. */
input[type=email], input[type=date], input[type=search] {
  width: 100%;
  padding: 6px 9px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
}
input[type=radio] { accent-color: var(--accent); width: 14px; height: 14px; }
input[type=file] { font-size: 12px; color: var(--muted); }
input[type=file]::file-selector-button {
  margin-right: 8px;
  padding: 3px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
input[type=file]::file-selector-button:hover { border-color: var(--accent); color: var(--accent); }
textarea { font-family: var(--mono); font-size: 12px; }
.checkbox-row { display: flex; align-items: center; gap: 6px; }
.checkbox-row label { margin: 0; text-transform: none; letter-spacing: 0; }
.form-actions { display: flex; gap: 8px; margin-top: 12px; }

/* Modal dialogs (e.g. API key creation) */
dialog {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  padding: 14px 16px;
  width: min(420px, calc(100vw - 32px));
}
dialog::backdrop { background: rgba(10, 11, 14, 0.6); }
:root[data-theme='light'] dialog::backdrop { background: rgba(36, 41, 47, 0.35); }
dialog h2 { margin-top: 0; }
/* Standard dialog header: title left, exit control top-right. Shared by every
   panel dialog (see _dialog_head.html) so the exit lives in one place. The
   header is pinned and the body scrolls, so Exit stays reachable on a dialog
   taller than the viewport. `flex-shrink: 0` on the button for the reason
   .btn carries it everywhere else: it would otherwise shrink and spill its
   label, since .btn sets white-space: nowrap. */
.dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
/* Capability picker inside the create-key dialog: one standard list. */
#key-dialog { width: min(560px, calc(100vw - 32px)); }
/* Export dialog: statistic checkboxes as one tight column. */
#export-dialog { width: min(460px, calc(100vw - 32px)); }
#export-dialog .export-views { display: grid; gap: 6px; margin-top: 6px; }
/* Site Details action popups reuse the shared detail dialog; the assets
   popup hosts the Add/Remove split and needs the extra room (:has() is a
   progressive enhancement — without it the dialog just stays narrower). */
#detail-dialog:has(.dialog-wide) { width: min(960px, calc(100vw - 32px)); }
/* Add/Remove split inside a popup: two halves, visible center divider. */
.split-grid { display: grid; grid-template-columns: 1fr 1fr; }
.split-grid > :first-child { padding-right: 18px; }
.split-grid > :last-child { border-left: 1px solid var(--border); padding-left: 18px; }
@media (max-width: 800px) {
  .split-grid { grid-template-columns: 1fr; }
  .split-grid > :first-child { padding-right: 0; padding-bottom: 14px; }
  .split-grid > :last-child { border-left: 0; border-top: 1px solid var(--border); padding-left: 0; padding-top: 14px; }
}
.dialog-rule { border: 0; border-top: 1px solid var(--border); margin: 14px 0; }
/* Master action bar on the site Details tab: buttons wrap instead of
   overflowing on narrow screens. */
.actions-row { display: flex; flex-wrap: wrap; gap: 8px; }
/* Site Configuration: one setting per row — input column anchored left,
   plain-English description filling the rest. */
.config-row {
  display: grid;
  grid-template-columns: 15rem 1fr;
  gap: 2px 16px;
  align-items: start;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}
.config-row:last-of-type { border-bottom: 0; }
.config-row > label.field { display: flex; flex-direction: column; gap: 4px; margin: 0; }
.config-row > label.field > span { margin: 0; }
.config-row > label.field input[type="text"],
.config-row > label.field input[type="number"] { width: 100%; }
.config-row .desc { color: var(--muted); font-size: 12px; margin: 0; padding-top: 17px; }
/* Checkbox rows have no stacked caption above the control; its description
   lines up with the checkbox instead of the input baseline. */
.config-row:has(> label.checkbox-row) .desc { padding-top: 2px; }
.config-row > label.field.checkbox-row { flex-direction: row; align-items: center; padding-top: 0; }
.config-row > label.field.checkbox-row > span { margin: 0; }
@media (max-width: 700px) {
  .config-row { grid-template-columns: 1fr; gap: 2px; }
  .config-row .desc { padding-top: 0; }
}
/* Long source paths stay on one line; the tail truncates with a tooltip. */
.asset-path { display: inline-block; max-width: 24rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
/* Owner site (wide) + expiry (narrow) share one line in the create-key dialog. */
.key-context-grid { display: grid; grid-template-columns: 4fr 1fr; gap: 10px; }
@media (max-width: 560px) { .key-context-grid { grid-template-columns: 1fr; } }
.key-caps { display: flex; flex-direction: column; gap: 10px; }
.key-cap-group { display: flex; flex-direction: column; gap: 4px; }
.key-cap { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.key-cap input { accent-color: var(--accent); }
/* Expiry bar: one control holding a number and its unit, select anchored
   right; blank number means "never expires". */
.input-bar {
  display: flex;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg);
}
/* `min-width: 0` plus `flex: 1` lets the field shrink until the native spinner's up/down
   arrows are clipped, which is what made the key-expiry control look too short for its
   stepper. `flex-basis: 0` with an `auto` minimum lets it shrink only as far as the
   control genuinely needs. */
.input-bar input {
  flex: 1 1 auto;
  min-width: auto;
  border: none;
  background: transparent;
}
.input-bar select {
  flex: 0 0 auto;
  width: auto;
  border: none;
  border-left: 1px solid var(--border-strong);
  border-radius: 0;
  background: transparent;
  cursor: pointer;
}
.input-bar input:focus, .input-bar select:focus { outline: none; border-color: transparent; box-shadow: none; }
.input-bar:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
/* Job-details popup: roomier than the key-creation dialog. */
#detail-dialog { width: min(680px, calc(100vw - 32px)); }
#detail-dialog pre { margin: 0; }
/* Manage-assets popup: tree | selection side by side, the tag bar pinned at
   the bottom (without :has() support the whole dialog body scrolls). */
#detail-dialog:has(.manage-layout) #detail-dialog-body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.manage-layout {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 14px;
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 10px;
}
.manage-col { display: flex; flex-direction: column; gap: 6px; min-height: 0; }
.manage-pane {
  flex: 1;
  min-height: 0;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  padding: 4px;
  /* Theme-following thin scrollbar on both panes (explorer, selection). */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.manage-bar {
  flex: 0 0 auto;
  border-top: 1px solid var(--border);
  margin-top: 12px;
  padding-top: 10px;
}
@media (max-width: 800px) { .manage-layout { grid-template-columns: 1fr; } }
/* Logs/Workers: finished-run history beside the live queue (4:1). */
.jobs-split {
  display: grid;
  grid-template-columns: 4fr 1fr;
  gap: 14px;
  align-items: start;
}
.jobs-split > .card { min-width: 0; }
.queue-list { list-style: none; margin: 0; padding: 0; }
.queue-list li {
  display: flex;
  align-items: center;
  /* Wrap rather than scroll. The report was a horizontal scrollbar on a page that is
     otherwise comfortable, and a row that scrolls is a row nobody scans. `.clip` on the
     site name is what keeps it narrow; the pills and the time never shrink. */
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
}
.queue-list li:last-child { border-bottom: none; }
.queue-list .nowrap { white-space: nowrap; }
@media (max-width: 900px) { .jobs-split { grid-template-columns: 1fr; } }
/* Asset tree inside the popup: rows are whole-line click targets; folders
   show a caret, files their page count. Selected rows tint with the accent. */
.tree { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.tree-node { margin: 0; }
.tree-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
}
.tree-row:hover { background: var(--panel-2); }
.tree-row.selected { background: var(--accent-soft); }
/* Rows never widen their pane: names ellipsize instead of forcing a
   horizontal scrollbar across the whole popup. */
.tree-row .tree-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tree-row .tree-hint { margin-left: auto; flex: 0 0 auto; }
.tree-row .pill { flex: 0 0 auto; }
.tree-caret {
  flex: 0 0 auto;
  width: 18px;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 0;
  font-size: 11px;
  line-height: 1;
}
.tree-caret.open { color: var(--text); transform: rotate(90deg); }
.tree-caret-spacer { flex: 0 0 auto; width: 18px; }
.tree-hint { margin-left: auto; }
.tree-children { list-style: none; margin: 0; padding: 0 0 0 16px; }
.selection-list { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.selection-list li {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: var(--radius);
}
.selection-list li:hover { background: var(--panel-2); }
.selection-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.selection-remove {
  flex: 0 0 auto;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 0 4px;
}
.selection-remove:hover { color: var(--danger); }
/* Tag chips at the bottom of the popup: checkbox + name + scope pill. */
.tag-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px 3px 8px;
  cursor: pointer;
  font-size: 12px;
}
.tag-chip input { margin: 0; accent-color: var(--accent); }

/* Why a worker is gone (lane crash, lost heartbeat) under its name. */
.worker-error { color: var(--danger); font-size: 11px; margin-top: 2px; }

/* Badges / status tags */
.pill {
  display: inline-block;
  padding: 1px 7px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-size: 11px;
  font-weight: 600;
  background: var(--panel-2);
  color: var(--muted);
  line-height: 1.5;
}
.pill.active, .pill.success { border-color: rgba(31,122,61,.35); background: rgba(31,122,61,.08); color: var(--ok); }
.pill.inactive, .pill.stopping, .pill.offline, .pill.idle, .pill.pending { border-color: var(--border); background: var(--panel-2); color: var(--muted); }
.pill.pending_payment, .pill.partial, .pill.warn, .pill.interrupted { border-color: rgba(154,106,11,.35); background: rgba(154,106,11,.08); color: var(--warn); }
.pill.suspended, .pill.failed { border-color: rgba(192,57,43,.4); background: rgba(192,57,43,.08); color: var(--danger); }
/* Deleted is its own tone rather than reusing danger: nothing is wrong or
   hostile about a retired site, and it should not read as a billing problem. */
.pill.deleted { border-color: var(--border); background: var(--panel-2); color: var(--muted); text-decoration: line-through; }
.pill.processing { border-color: rgba(76,122,192,.4); background: var(--accent-soft); color: var(--accent); }
/* `running` and `cancelled` are real status values that templates interpolate straight
   into the class name, so both need a rule or a live job renders untinted -- identical
   to a queued one, which is the one distinction that matters on the workers page. */
.pill.running { border-color: rgba(76,122,192,.4); background: var(--accent-soft); color: var(--accent); }
.pill.cancelled { border-color: var(--border); background: var(--panel-2); color: var(--muted); text-decoration: line-through; }
.pill.paused { border-color: rgba(154,106,11,.35); background: rgba(154,106,11,.08); color: var(--warn); }

/* Job *kind* chips, by kind. Every kind chip was `.pill processing`, so the seven kinds
   were indistinguishable at a glance -- the report this answers.

   Deliberately a **different vocabulary from the status palette above**, and monospace so
   the two are separable by shape as well as hue. A kind chip and a status chip sit on the
   same row (see the queue list), so borrowing green for "index" would leave a reader
   guessing whether a green chip means `active` or `index`. These hues are chosen to sit
   apart from the ok/warn/danger/accent set rather than to extend it.

   A new `KIND_*` needs a rule here or it renders neutral. `tests_worker_kinds.py` fails
   until it has one, so the gap is loud rather than invisible. */
.pill[class*="kind-"] { font-family: var(--mono); font-weight: 500; }
.pill.kind-index     { border-color: rgba(23,107,74,.4);  background: rgba(23,107,74,.10);  color: #17a06a; }
.pill.kind-gc        { border-color: rgba(122,92,17,.4);  background: rgba(122,92,17,.10);  color: #b08900; }
.pill.kind-stats     { border-color: rgba(88,86,214,.4);  background: rgba(88,86,214,.10);  color: #7a78e8; }
.pill.kind-rescan    { border-color: rgba(16,124,130,.4);  background: rgba(16,124,130,.10);  color: #17a3aa; }
.pill.kind-import    { border-color: rgba(168,72,20,.4);  background: rgba(168,72,20,.10);  color: #d0762f; }
.pill.kind-reconvert { border-color: rgba(140,74,160,.4); background: rgba(140,74,160,.10); color: #ab63c4; }
.pill.kind-cert      { border-color: rgba(20,90,150,.4);  background: rgba(20,90,150,.10);  color: #2f8fd0; }

/* Skipped pipeline runs: deliberate, not an error (amber). */
.pill.skipped { border-color: rgba(154,106,11,.35); background: rgba(154,106,11,.08); color: var(--warn); }
/* Status pills inside table cells: uniform width + centered label so the
   right edges line up regardless of the status word length. */
td .pill { min-width: 58px; box-sizing: border-box; text-align: center; }
/* The same floor for pills inside a leaderboard *card* grid, where the column is a
   fractional track rather than a `td`. Without it "interrupted" is visibly wider than
   "done" and "failed", so the Status column looks ragged rather than like a column. */
.lb-jobs .pill, .lb-jobs-site .pill { min-width: 74px; box-sizing: border-box; text-align: center; }

/* Storage columns: the used-size number is right-aligned in a fixed-width
   slot so values line up vertically across rows despite unit changes. */
.val { display: inline-block; min-width: 7ch; text-align: right; }

/* Leaderboard lists: a spaced header line (.lb-cols) whose columns line up
   with every card row through a shared grid (--lb-grid, set per list on the
   .lb-table wrapper). Rows are flat cards (.lb-head) or dropdowns
   (> summary) whose expansion reveals a .lb-detail body. Lists that only
   need ranked rows (agents, outcomes) stay outside .lb-table. */
.lb-table > .lb-cols,
.lb-table .lb-row > summary,
.lb-table .lb-row > .lb-head {
  display: grid;
  grid-template-columns: var(--lb-grid, 1fr);
  gap: 12px;
  align-items: center;
}
.lb-table > .lb-cols {
  /* 13px = the row cards' 1px border + 12px inner padding, so the header's
     content box matches the rows' exactly and every column line lines up. */
  padding: 2px 13px 6px;
  color: var(--muted);
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* Every cell clips instead of wrapping or widening its column: the header and
   each row are separate grids, so any content-driven width there would push
   that row's later columns sideways and break the shared alignment. */
.lb-table > .lb-cols > *,
.lb-table .lb-row > summary > *,
.lb-table .lb-row > .lb-head > * {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lb-table > .lb-cols > :last-child { text-align: right; }
/* Sortable header labels read as headers, not links. `.th-sort` is named as well as
   positional: it is carried by 5 templates and until now no rule mentioned it, so it
   read as a class doing a job nothing did. Both selectors are kept -- the positional
   one covers headers that carry no class. */
.lb-table > .lb-cols a, .lb-table > .lb-cols a.th-sort { color: inherit; }
.lb-table > .lb-cols a:hover, .lb-table > .lb-cols a.th-sort:hover { color: var(--accent); text-decoration: none; }
th a.th-sort { color: inherit; text-decoration: none; }
th a.th-sort:hover { color: var(--accent); }
.lb-table .clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-table .asset-path { max-width: 100%; }
.lb-row .asset-cell { display: flex; align-items: center; gap: 8px; }
.lb-row .asset-cell .asset-path { flex: 0 1 auto; min-width: 0; }
/* Action cells stretch to the last track's full width and anchor their
   buttons to the row's right edge. */
.lb-row .lb-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
/* Ranked static row (agents/outcomes style): same padding as a summary. */
.lb-row > .lb-line { display: flex; align-items: center; gap: 10px; padding: 8px 12px; }
/* Dropdown expansion body: indented under the caret column. */
.lb-row > .lb-detail {
  display: grid; gap: 8px;
  padding: 10px 12px 12px 46px;
  border-top: 1px solid var(--border);
}
.lb-row .rank.caret { transition: transform .12s; }
.lb-row[open] > summary .rank.caret { transform: rotate(90deg); }
.lb-row > .lb-head { padding: 8px 12px; }
/* Column templates per list (assets · cache · API keys · tags · sites ·
   conversion runs · worker jobs · API requests · audit entries). Only
   content-independent sizing is allowed here: a fixed px caret column plus
   fr proportions. max-content/auto tracks are sized per grid container —
   and header and rows are separate containers — so any content-driven
   track would line up differently in every row. fr resolves purely from
   the container width, which is identical for the header and all rows,
   so the columns align exactly everywhere; long text truncates via the
   cell clip rule above. Narrow split cards (dashboard board, site
   detail) use the trimmed variants below. */
/* Fixed-width chrome columns (Type/Pages labels, the Actions button set)
   take px so their content never clips and the fr columns keep all slack:
   px tracks resolve identically in the header and every row, so alignment
   still holds. The asset Actions set (Re-convert · Details · Delete) needs
   ~216px; right-anchored slack is invisible. */
.lb-asset { --lb-grid: 20px 1.6fr 56px 56px 2.6fr .9fr .9fr 1.4fr 224px; }
.lb-cache { --lb-grid: 1.5fr 3fr .9fr 1fr 1.2fr 1.2fr 1.2fr; }
.lb-keys { --lb-grid: 1.3fr 1.2fr 2fr .9fr 1fr 1fr 1.2fr 1.2fr; }
.lb-tags-global { --lb-grid: 2.2fr 1.6fr .8fr 1.4fr 3fr; }
.lb-tags-client { --lb-grid: 2fr 1.4fr 1.5fr .7fr 1.2fr 2.8fr; }
.lb-sites { --lb-grid: 1.6fr 2fr 1.6fr 1.5fr 1.5fr 1fr 1fr; }
.lb-runs { --lb-grid: 20px 1.2fr 2.8fr 1.2fr 1fr 1.4fr 1.4fr 1.1fr; }
.lb-runs.compact { --lb-grid: 20px 1.4fr 3.4fr 1.3fr 1.3fr 1.4fr; }
.lb-jobs { --lb-grid: 20px 2.2fr 1.8fr 2fr 1.6fr 3fr 2.8fr 2fr; }
.lb-jobs-site { --lb-grid: 20px 1.6fr 2fr 2.4fr 2fr 1.4fr; }
.lb-api { --lb-grid: 20px 1fr .8fr 3fr .9fr 1fr 1.4fr 1fr; }
.lb-audit { --lb-grid: 20px 1.4fr 1.2fr 1.2fr 2.2fr 1.4fr 1.2fr; }

/* Pager */
.pager { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pager-links a { text-decoration: none; padding: 1px 5px; border-radius: var(--radius); }
.pager-links a:hover { background: var(--panel-2); color: var(--accent); }
.pager-current { color: var(--accent); }

/* Misc */
.muted { color: var(--muted); }
.small { font-size: 12px; }
.mono { font-family: var(--mono); overflow-wrap: anywhere; }

/* Storage usage severity (sites table): amber / orange / red at 75/85/95%.
   Declared after .muted so the severity wins on "muted small quota-*" spans. */
.quota-warn { color: var(--warn); }
.quota-alert { color: #e0885c; }
.quota-danger { color: var(--danger); font-weight: 600; }
.mt { margin-top: 12px; }
.mb { margin-bottom: 12px; }
.flex { display: flex; gap: 8px; align-items: center; }
/* `.wrap` on a flex row was in 16 templates and meant nothing, because `.flex` never
   set flex-wrap. Those rows are chip bars whose whole point is reflowing when the
   viewport narrows; without this they stay one long line. */
.flex.wrap { flex-wrap: wrap; }
.flex.between { justify-content: space-between; }
.inline-form { display: inline-flex; align-items: center; vertical-align: middle; }

/* Filter bars: search wider than selects, identical proportions everywhere */
.filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
/* Two controls that belong together travel together. The Statistics row mixes a set of
   window buttons with a scope select and an Export button; wrapping put Export alone on its
   own line, which reads as a detached control rather than as "export what you just picked".
   `flex: none` so the pair wraps as a unit instead of the select growing to fill the row. */
.control-group { display: inline-flex; align-items: center; gap: 6px; flex: none; }
.filters input[type="text"] { width: 18rem; }
.filters select { width: 10rem; }

/* Inline setting forms: labelled fields in a row, controls bottom-aligned */
.fieldbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.fieldbar label.field { display: flex; flex-direction: column; gap: 4px; margin: 0; }
.fieldbar label.field > span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.fieldbar label.field input[type="text"],
.fieldbar label.field input[type="number"],
.fieldbar label.field select { width: 9rem; }
.fieldbar .checkbox-row { padding-bottom: 5px; }
/* Boolean toggles inside a fieldbar: checkbox beside its caption, not above. */
.fieldbar label.field.checkbox-row { flex-direction: row; align-items: center; }
.fieldbar label.field.checkbox-row > span { margin: 0; }
.error, .toast.error { color: var(--danger); }
.alert { padding: 8px 12px; border: 1px solid var(--border); border-left: 3px solid var(--border-strong); background: var(--panel); margin-bottom: 12px; }
.alert.error { color: var(--danger); border-color: rgba(224,104,95,.4); border-left-color: var(--danger); background: rgba(224,104,95,.08); }
/* warn and success were in the templates long before they were in here: 11 `warn` and
   6 `success`/`ok` banners rendered as neutral grey boxes, so an amber warning and a
   green confirmation looked identical to plain information. `.ok` is the same idea as
   `.success` spelled differently in three templates; both selectors are kept so the
   rename can happen separately from the styling that was missing. */
.alert.warn { color: var(--warn); border-color: rgba(224,176,92,.4); border-left-color: var(--warn); background: rgba(224,176,92,.08); }
.alert.success, .alert.ok { color: var(--ok); border-color: rgba(126,198,153,.4); border-left-color: var(--ok); background: rgba(126,198,153,.08); }

/* Asset preview */
.code { font-family: var(--mono); font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; margin: 0; }
/* Job "More" rundown inside a table cell */
.job-detail { max-height: 320px; overflow: auto; min-width: 24rem; text-align: left; }
.scroll { max-height: 70vh; overflow: auto; }
.markdown-body.preview { line-height: 1.55; }
.markdown-body.preview h1, .markdown-body.preview h2, .markdown-body.preview h3 { margin-top: 1.1em; }
.markdown-body.preview h1, .markdown-body.preview h2 { border-bottom: 1px solid var(--border); padding-bottom: .25em; }
.markdown-body.preview code { font-family: var(--mono); font-size: 12px; background: var(--panel-2); padding: 1px 4px; border-radius: 3px; }
.markdown-body.preview pre { background: var(--panel-2); padding: 10px 12px; border-radius: var(--radius); overflow: auto; border: 1px solid var(--border); }
.markdown-body.preview pre code { background: none; padding: 0; }
.markdown-body.preview blockquote { border-left: 3px solid var(--border-strong); margin-left: 0; padding-left: 12px; color: var(--muted); }
.markdown-body.preview table { border-collapse: collapse; width: 100%; }
.markdown-body.preview th, .markdown-body.preview td { border: 1px solid var(--border); padding: 5px 8px; text-align: left; }
.markdown-body.preview img { max-width: 100%; }

/* Toasts */
.toasts { position: fixed; top: 56px; right: 16px; z-index: 100; display: grid; gap: 6px; }
.toast {
  /* Flex so a long message wraps while the close button stays in the top-right corner,
     rather than riding down the middle of a wrapped paragraph. */
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 8px 12px;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  box-shadow: 0 4px 14px rgba(0,0,0,.35);
  max-width: min(360px, calc(100vw - 32px));
  font-size: 13px;
}
.toast-body { flex: 1; min-width: 0; }
.toast-close {
  flex: none;
  margin: -2px -4px 0 0;
  padding: 0 4px;
  border: none;
  background: none;
  color: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: .55;
  border-radius: 3px;
}
.toast-close:hover { opacity: 1; }
/* A visible focus ring, because a control reachable only by pointer is not dismissable by
   everyone — and this one is the only way to get a message off the screen. */
.toast-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }
/* Leaving is animated rather than instant: the toast is fixed to the corner, so an
   instantaneous removal reads as the page flickering rather than as the message going. */
.toast.leaving { opacity: 0; transform: translateX(8px); transition: opacity .18s, transform .18s; }

.toast.success { border-left-color: var(--ok); color: var(--ok); }
.toast.error { border-left-color: var(--danger); color: var(--danger); }
.toast.warning { border-left-color: var(--warn); color: var(--warn); }
.toast.info { border-left-color: var(--accent); color: var(--accent); }

/* Pager / lists */
.detail-grid { display: grid; grid-template-columns: 1fr 1.4fr; gap: 14px; }
/* Dashboard: the runs board needs the wide side; the leaderboard is fine narrow. */
.detail-grid.flip { grid-template-columns: 1.4fr 1fr; }
/* Two side-by-side panels get cramped before the phone breakpoint; stack early. */
@media (max-width: 1100px) { .detail-grid { grid-template-columns: 1fr; } }
.empty { color: var(--muted); padding: 20px; text-align: center; border: 1px dashed var(--border-strong); border-radius: var(--radius); }
/* Empty states rendered as a table cell keep their full dashed outline:
   the last-row rule strips td border-bottoms, which cut the box open. */
tr:last-child td.empty { border-bottom: 1px dashed var(--border-strong); }
.section-title { margin: 18px 0 8px; font-size: 14px; font-weight: 600; }

/* User menu */
.user { margin-left: auto; position: relative; }
.menu summary {
  list-style: none;
  cursor: pointer;
  padding: 4px 10px;
  border-radius: var(--radius);
  color: var(--text);
  font-size: 13px;
  user-select: none;
}
.menu summary::-webkit-details-marker { display: none; }
.menu summary::after { content: "\25be"; margin-left: 6px; color: var(--muted); font-size: 11px; }
.menu[open] summary { background: var(--panel-2); color: var(--accent); }
.menu-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  min-width: 140px;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0,0,0,.4);
  padding: 4px;
  z-index: 60;
}
.menu-item {
  display: block;
  padding: 6px 10px;
  border-radius: var(--radius);
  color: var(--text);
  font-size: 13px;
}
.menu-item:hover { background: var(--panel-2); color: var(--accent); text-decoration: none; }
/* A form wrapping a menu item must not add its own padding on top of the
   button's, or logout renders larger than its neighbours. */
form.menu-item { padding: 0; }
button.menu-item {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
}

/* Theme toggle (user menu): sun/moon icon reflects the active theme. */
#theme-toggle .theme-icon {
  width: 14px;
  height: 14px;
  vertical-align: -2px;
  margin-right: 6px;
}
html[data-theme='dark'] #theme-toggle .icon-sun { display: none; }
html[data-theme='light'] #theme-toggle .icon-moon { display: none; }

/* Login */
.login-wrap { display: flex; justify-content: center; padding-top: 72px; }
.login-card { width: 360px; max-width: calc(100vw - 32px); text-align: center; }
.login-card form { text-align: left; margin-top: 12px; }
.login-card h1 { margin: 0 0 4px; font-size: 16px; font-weight: 600; }

/* Statistics chart (uPlot, client-rendered) */
:root {
  --chart-1: #4da3ff;
  --chart-2: #34d399;
}
html[data-theme='light'] {
  --chart-1: #2563eb;
  --chart-2: #059669;
}
.chart { width: 100%; min-height: 240px; overflow: hidden; }
.chart .u-legend { font-size: 11px; color: var(--muted); }
.chart .u-legend .u-marker { width: 10px; height: 2px; }

/* Access leaderboard / asset history rows */
.leaderboard { display: flex; flex-direction: column; gap: 6px; }
.vendor-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex: none; }
.lb-row {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
}
/* Commit hash chip (asset history): square-ish like other containers. */
.chip {
  display: inline-block;
  padding: 2px 7px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel-2);
  font-size: 12px;
  font-weight: 600;
}
.lb-row > summary {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px; cursor: pointer; list-style: none;
}
.lb-row > summary::-webkit-details-marker { display: none; }
.lb-row > summary:hover { background: var(--accent-soft); }
.lb-row .rank {
  min-width: 22px; color: var(--muted); font-size: 12px; font-weight: 600;
  flex: none;
}
.lb-row .lb-hits { margin-left: auto; font-weight: 600; }
.lb-row > table { margin: 0; border-top: 1px solid var(--border); }
.lb-row > table thead th { font-size: 11px; }
.lb-row > .empty { padding: 8px 12px; }

/* Asset history: side-by-side diff */
.diff-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.diff-table th { font-size: 11px; text-align: left; }
.diff-table td {
  padding: 1px 8px; vertical-align: top;
  font-family: var(--mono); white-space: pre-wrap; word-break: break-word;
}
.diff-table .num { color: var(--muted); text-align: right; user-select: none; width: 1%; white-space: nowrap; }
.diff-table .diff-del { background: rgba(192, 57, 43, 0.14); }
.diff-table .diff-add { background: rgba(31, 122, 61, 0.14); }
.diff-table .diff-chg { background: rgba(154, 106, 11, 0.12); }
.diff-table .diff-gap td {
  text-align: center; color: var(--muted); padding: 2px;
  font-family: var(--sans, inherit); border-top: 1px dashed var(--border);
  border-bottom: 1px dashed var(--border);
}

/* Explicit light theme (account menu toggle). Default is dark regardless of
   the OS preference; the choice persists in localStorage via ui.js. */
:root[data-theme='light'] {
  --bg: #f4f5f6;
  --panel: #ffffff;
  --panel-2: #eceef1;
  /* Hairlines need enough contrast against white cards to stay visible. */
  --border: #ced3d8;
  --border-strong: #aeb5bc;
  --text: #24292f;
  --muted: #6b7280;
  --accent: #3b6cb3;
  --accent-soft: rgba(59, 108, 179, 0.10);
  --ok: #1f7a3d;
  --warn: #9a6a0b;
  --danger: #c0392b;
  color-scheme: light;
}
:root[data-theme='light'] .toast { box-shadow: 0 4px 12px rgba(0,0,0,.08); }
:root[data-theme='light'] .menu-pop { box-shadow: 0 8px 24px rgba(0,0,0,.12); }
:root[data-theme='light'] .btn.primary { color: #fff; }
:root[data-theme='light'] .btn.primary:hover { color: #fff; }

/* Phones: compact nav chips instead of a tall stacked menu, tighter cells */
@media (max-width: 700px) {
  body { font-size: 12.5px; }
  .topbar { gap: 10px; padding: 0 10px; }
  .container { padding: 12px 10px 40px; }
  th, td { padding: 5px 6px; }
  .nav-links { flex-direction: row; flex-wrap: wrap; gap: 4px; }
  .nav-links a { padding: 3px 8px; }
  .nav-group > summary { padding: 4px 10px; }
  .card { padding: 10px 10px; }
  .section-title { margin: 14px 0 6px; }
}