/* QuIC Intelligence Factory — hand-written component styles.
 *
 * Replaces the Tailwind Play CDN: the palette lives in CSS variables (light
 * values on :root, flipped under prefers-color-scheme: dark, with .light/.dark
 * class hooks to force a theme), and every UI piece is a semantic component
 * class used by index.html and app.py's fragments — no inline utility classes.
 */

/* ── Design tokens ─────────────────────────────────────────────────────── */
:root {
  --quic-50: #eef2ff;
  --quic-100: #e0e7ff;
  --quic-300: #a5b4fc;
  --quic-500: #6366f1;
  --quic-600: #4f46e5;
  --quic-700: #4338ca;

  --bg: #f8fafc;
  --surface: #ffffff;
  /* A quiet fill for a pill or an inset block. It was never defined and every use fell
     back to a hardcoded light grey, so in dark mode a light panel took the theme's light
     text — the invite token came out invisible. */
  --muted-bg: #f1f5f9;
  --border: #e2e8f0;
  --border-soft: #f1f5f9;
  --text: #1e293b;
  --text-strong: #0f172a;
  --text-label: #334155;
  --text-muted: #64748b;
  --text-faint: #94a3b8;

  --field-bg: #ffffff;
  --field-border: #cbd5e1;

  --accent: var(--quic-600);
  --accent-hover: var(--quic-700);
  --accent-ring: rgba(99, 102, 241, 0.3);
  --accent-text: var(--quic-600);
  --accent-soft: var(--quic-50);
  --accent-link: var(--quic-500);
  --accent-outline: var(--quic-300);

  --ok-border: #a7f3d0;
  --ok-bg: #ecfdf5;
  --ok-text: #065f46;
  --ok-accent: #047857;

  --warn-border: #fcd34d;
  --warn-bg: #fffbeb;
  --warn-text: #92400e;
  --warn-btn: #d97706;
  --warn-btn-hover: #b45309;

  --info-border: #a5b4fc;
  --info-bg: #eef2ff;
  --info-text: #3730a3;

  --danger: #ef4444;
  --danger-border: #fca5a5;

  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #020617;
    --surface: #0f172a;
    --muted-bg: #1e293b;
    --border: #1e293b;
    --border-soft: #1e293b;
    --text: #e2e8f0;
    --text-strong: #ffffff;
    --text-label: #cbd5e1;
    --text-muted: #94a3b8;
    --text-faint: #64748b;

    --field-bg: #020617;
    --field-border: #334155;

    --accent-text: var(--quic-300);
    --accent-soft: rgba(67, 56, 202, 0.2);
    --accent-outline: rgba(67, 56, 202, 0.4);

    --ok-border: #064e3b;
    --ok-bg: rgba(2, 44, 34, 0.3);
    --ok-text: #6ee7b7;
    --ok-accent: #34d399;

    --warn-border: #92400e;
    --warn-bg: rgba(69, 26, 3, 0.4);
    --warn-text: #fde68a;

    --info-border: rgba(67, 56, 202, 0.4);
    --info-bg: rgba(67, 56, 202, 0.1);
    --info-text: #c7d2fe;

    --danger: #f87171;
  }
}

/* Explicit override hooks (a future theme toggle stamps .light/.dark on <html>). */
:root.light {
  color-scheme: light;
  --bg: #f8fafc; --surface: #ffffff; --muted-bg: #f1f5f9;
  --border: #e2e8f0; --border-soft: #f1f5f9;
  --text: #1e293b; --text-strong: #0f172a; --text-label: #334155;
  --text-muted: #64748b; --text-faint: #94a3b8;
  --field-bg: #ffffff; --field-border: #cbd5e1;
  --accent-text: var(--quic-600); --accent-soft: var(--quic-50); --accent-outline: var(--quic-300);
  --ok-border: #a7f3d0; --ok-bg: #ecfdf5; --ok-text: #065f46; --ok-accent: #047857;
  --warn-border: #fcd34d; --warn-bg: #fffbeb; --warn-text: #92400e;
  --info-border: #a5b4fc; --info-bg: #eef2ff; --info-text: #3730a3; --danger: #ef4444;
}
:root.dark {
  color-scheme: dark;
  --bg: #020617; --surface: #0f172a; --muted-bg: #1e293b;
  --border: #1e293b; --border-soft: #1e293b;
  --text: #e2e8f0; --text-strong: #ffffff; --text-label: #cbd5e1;
  --text-muted: #94a3b8; --text-faint: #64748b;
  --field-bg: #020617; --field-border: #334155;
  --accent-text: var(--quic-300); --accent-soft: rgba(67, 56, 202, 0.2); --accent-outline: rgba(67, 56, 202, 0.4);
  --ok-border: #064e3b; --ok-bg: rgba(2, 44, 34, 0.3); --ok-text: #6ee7b7; --ok-accent: #34d399;
  --warn-border: #92400e; --warn-bg: rgba(69, 26, 3, 0.4); --warn-text: #fde68a;
  --info-border: rgba(67, 56, 202, 0.4); --info-bg: rgba(67, 56, 202, 0.1); --info-text: #c7d2fe; --danger: #f87171;
}

/* ── Base ──────────────────────────────────────────────────────────────── */
* { box-sizing: border-box; }

html { height: 100%; }

body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
}

a { color: inherit; }

.hidden { display: none !important; }

.empty-note { padding: 1rem; font-size: 0.875rem; color: var(--text-faint); }

/* ── Layout ────────────────────────────────────────────────────────────── */
.page,
.browse-main {
  /* Width tracks the window (a percentage) so a big screen fills instead of
     leaving a fixed column of void; capped only so an ultrawide monitor does not
     stretch a form to absurd widths. Small screens keep near-full width. */
  width: 90%;
  max-width: 1600px;
  margin: 0 auto;
  padding: 1.5rem 0;
}
@media (min-width: 640px) { .page { padding: 2rem 0; } }

.site-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.brand { display: flex; align-items: center; gap: 0.75rem; }
.logout-form { flex-shrink: 0; margin: 0; }
.logout-btn { white-space: nowrap; font-weight: 600; }
.brand__logo { height: 3rem; width: auto; flex-shrink: 0; }
/* The back link leading the brand row, matching the ghost link the other pages put in
   their brand bar (← Schedules on a schedule run). Never shrinks, so a long title cannot
   squeeze it away.
   Which of the two shows depends on what the page is displaying, keyed off the same
   #result signal that hides the form below: from the form, back means Home; from a run or
   its result, back means the form — jumping home from a finished newsletter would throw
   away the configuration you were working in. Where :has() is unsupported both links
   show, which is untidy but still correct. */
.brand__back { flex-shrink: 0; white-space: nowrap; }
body:has(#result:not(:empty)) .brand__back--home { display: none; }
body:not(:has(#result:not(:empty))) .brand__back--form { display: none; }
.brand__title {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-strong);
}
.brand__subtitle { margin: 0; font-size: 0.875rem; color: var(--text-muted); }

.stack { display: flex; flex-direction: column; gap: 1rem; }
.stack--tight { gap: 0.75rem; }

.footer {
  margin-top: 1.5rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--text-faint);
}
.footer strong { font-weight: 500; color: var(--text-muted); }

/* ── Card ──────────────────────────────────────────────────────────────── */
.card {
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 1rem;
  padding: 1.25rem;
  box-shadow: var(--shadow-sm);
}
@media (min-width: 640px) { .card { padding: 1.5rem; } }

.card__header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}
.card__title {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-strong);
}
.card__hint { margin: 0.25rem 0 0; font-size: 0.75rem; color: var(--text-muted); }
.card__actions { display: flex; gap: 0.25rem; font-size: 0.75rem; font-weight: 500; }

/* ── Fields ────────────────────────────────────────────────────────────── */
/* min-width: 0 on the grid and its control: a grid item defaults to min-width: auto,
   which refuses to shrink below its content's intrinsic width — so one unshrinkable
   child (a text input's default size, a long source title) widened the column past the
   screen and the whole form overflowed to the right on a phone. */
.field { display: grid; gap: 0.375rem; min-width: 0; }
.field__control { min-width: 0; }
@media (min-width: 640px) {
  .field { grid-template-columns: 11rem 1fr; align-items: center; gap: 0.5rem 1.25rem; }
  .field--top { align-items: start; }
  .field__control { grid-column: auto; }
}
.field__label { font-size: 0.875rem; font-weight: 500; color: var(--text-label); }
.field__optional { font-weight: 400; color: var(--text-muted); }
.window { display: flex; flex-direction: column; gap: 0.5rem; }
.window__mode { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; }
.window__mode .input--narrow { width: 4.5rem; }
.window__range { display: flex; flex-wrap: wrap; gap: 0.75rem; padding-left: 1.4rem; }
.window__date { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem;
  color: var(--text-muted); }
.field__label--top { padding-top: 0.5rem; }
.field__hint { margin: 0.25rem 0 0; font-size: 0.75rem; color: var(--text-muted); }

.input {
  width: 100%;
  border: 1px solid var(--field-border);
  background: var(--field-bg);
  color: var(--text);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  font-family: inherit;
  box-shadow: var(--shadow-sm);
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.input::placeholder { color: var(--text-faint); }
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-ring); }
.input--narrow { }
@media (min-width: 640px) { .input--narrow { width: 8rem; } }
.input--mono { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }
textarea.input { resize: vertical; }

/* ── Buttons ───────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border: 1px solid transparent;
  border-radius: 0.5rem;
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
  outline: none;
  text-decoration: none;
}
.btn:focus-visible { box-shadow: 0 0 0 2px var(--accent-ring); }

.btn-primary {
  background: var(--accent);
  color: #ffffff;
  padding: 0.625rem 1.5rem;
  font-size: 0.875rem;
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover { background: var(--accent-hover); }
/* A control shown but not usable yet: it stays put and explains itself in its title
   rather than vanishing, so nobody wonders whether the page has the feature at all. */
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-ghost {
  background: transparent;
  color: var(--text-muted);
  padding: 0.25rem 0.5rem;
  border-radius: 0.375rem;
  font-size: 0.75rem;
  font-weight: 500;
}
.btn-ghost:hover { background: var(--border-soft); }
.btn-ghost--accent { color: var(--accent-text); }
.btn-ghost--accent:hover { background: var(--accent-soft); }

.btn-outline {
  background: transparent;
  border-color: var(--accent-outline);
  color: var(--accent-text);
  padding: 0.375rem 0.625rem;
  font-size: 0.75rem;
  font-weight: 500;
  flex-shrink: 0;
}
.btn-outline:hover { background: var(--accent-soft); }

.btn-warn {
  background: var(--warn-btn);
  color: #ffffff;
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
}
.btn-warn:hover { background: var(--warn-btn-hover); }

.btn-neutral {
  background: transparent;
  border-color: var(--field-border);
  color: var(--text-label);
  padding: 0.375rem 0.75rem;
  font-size: 0.75rem;
  font-weight: 500;
}
.btn-neutral:hover { background: var(--border-soft); }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--field-border);
  background: transparent;
  color: var(--text-muted);
  border-radius: 0.5rem;
  padding: 0 0.75rem;
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.icon-btn:hover { border-color: var(--danger-border); color: var(--danger); }

/* ── Checkboxes & checklist ────────────────────────────────────────────── */
.checkbox {
  height: 1rem;
  width: 1rem;
  flex-shrink: 0;
  accent-color: var(--accent);
}

.checklist {
  max-height: 14rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
}
.checklist__item {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  color: var(--text-label);
  cursor: pointer;
}
.checklist__item + .checklist__item { border-top: 1px solid var(--border-soft); }
.checklist__item:hover { background: var(--border-soft); }

/* ── Sources ───────────────────────────────────────────────────────────── */
.source-list {
  border: 1px solid var(--border);
  border-radius: 0.5rem;
}
.source-row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  min-width: 0;
}
.source-row__head, .source-row > * { min-width: 0; }
.sources-list { max-width: 100%; }
.source-row + .source-row { border-top: 1px solid var(--border-soft); }
@media (min-width: 640px) {
  .source-row { flex-direction: row; align-items: center; }
}
.source-row__head { display: flex; align-items: center; gap: 0.625rem; }
@media (min-width: 640px) { .source-row__head { width: 13rem; flex-shrink: 0; } }
.source-name { font-size: 0.875rem; font-weight: 500; color: var(--text-label); }
.source-link { font-size: 0.75rem; color: var(--accent-link); text-decoration: none; }
.source-link:hover { text-decoration: underline; }
.source-kw {
  width: 100%;
  border: 1px solid var(--field-border);
  background: var(--field-bg);
  color: var(--text);
  border-radius: 0.375rem;
  padding: 0.375rem 0.625rem;
  font-size: 0.875rem;
  font-family: inherit;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.source-kw::placeholder { color: var(--text-faint); }
.source-kw:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-ring); }

.add-source { border-top: 1px solid var(--border); padding-top: 1rem; }
.add-source__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.new-source-rows { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.new-source-row { display: grid; gap: 0.5rem; }
@media (min-width: 640px) {
  .new-source-row { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .new-source-row .input--url { grid-column: span 3 / span 3; }
  .new-source-row .input--kw { grid-column: span 2 / span 2; }
}

/* ── Run area ──────────────────────────────────────────────────────────── */
.run-bar { display: flex; align-items: center; justify-content: flex-end; }
/* The run view sits above the config; while it has content the config form is
   hidden (pure CSS — no swaps, no routing), so a run is a focused view. */
.result:not(:empty) { margin-bottom: 1.5rem; }
#result:not(:empty) ~ #config-area { display: none; }

/* Run view: a summary of the chosen config above the live status panel. */
.run-summary { margin-bottom: 0.75rem; }
.run-summary__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  flex-wrap: wrap;
}
.run-summary__back { flex-shrink: 0; }
.run-summary__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 1rem;
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--text-muted);
}
.run-summary__facts strong { color: var(--text-label); font-weight: 600; }
.run-config { margin-top: 0.75rem; }

.panel {
  border: 1px solid;
  border-radius: 0.75rem;
  padding: 1rem;
  font-size: 0.875rem;
}
.panel__title { font-weight: 600; }
.panel__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.panel__actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
.panel p { margin: 0.25rem 0 0; }

.panel--info {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  border-color: var(--info-border);
  background: var(--info-bg);
  color: var(--info-text);
}
/* Trailing controls in an info panel: the stop button on a run, the unlock prompt on a
   locked queue. Pushed to the far end, and never squeezed by the prose beside them. */
.panel__stop, .panel__end { margin-left: auto; flex-shrink: 0; }
/* Wide enough to type in once the prompt replaces the button, and wrapping under the
   prose rather than crushing it on a narrow screen. */
.panel__end .unlock-form { flex-wrap: wrap; }
.panel--ok { border-color: var(--ok-border); background: var(--ok-bg); color: var(--ok-text); }
.panel--warn { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn-text); }
.panel--error { border-color: var(--danger-border); color: var(--danger); }

/* A prominent warning: thicker border, more room, a larger heading. */
.panel--big { padding: 1.25rem; border-width: 2px; }
.panel--big .panel__title { font-size: 1.05rem; margin-bottom: 0.25rem; }

.panel__links { display: flex; align-items: center; gap: 0.5rem 0.75rem; flex-wrap: wrap; }

/* Send-by-email row on the ready panel. */
.send-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.75rem; }
.send-row__input { flex: 1 1 18rem; }
.send-status { font-size: 0.875rem; }
.send-msg--ok { color: var(--ok-accent); font-weight: 500; }
.send-msg--err { color: var(--danger); font-weight: 500; }
.panel__summary { margin-top: 0.5rem; font-size: 0.875rem; }
.panel__summary strong { font-weight: 600; }

/* "No change since last run" note, and the build-time line. */
.panel__note {
  margin-top: 0.5rem;
  padding: 0.4rem 0.6rem;
  border-radius: 0.4rem;
  background: color-mix(in srgb, currentColor 8%, transparent);
  font-size: 0.85rem;
}
.panel__timing { margin-top: 0.4rem; font-size: 0.8125rem; opacity: 0.85; }
.panel__timing strong { font-weight: 600; }

/* Live pipeline-phase checklist on the running panel. The info panel is a flex row
   by default; a live run stacks the note over the checklist. */
.panel--running { flex-direction: column; align-items: stretch; }
.phases {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0.6rem 0 0;
  padding: 0;
  font-size: 0.8125rem;
}
.phase { display: flex; align-items: center; gap: 0.35rem; opacity: 0.5; }
.phase::before { content: "○"; }
.phase.is-done { opacity: 0.85; }
.phase.is-done::before { content: "✓"; color: var(--ok-accent, currentColor); }
.phase.is-active { opacity: 1; font-weight: 600; }
.phase.is-active::before { content: "▸"; }
.phase.is-pending { opacity: 0.45; }
.phase__detail { font-weight: 400; color: var(--text-muted); }
/* The model a stage is on: mono, because it is an identifier ("ollama:qwen3") to be read
   exactly rather than prose. Muted-on-muted made it nearly invisible, which defeated the
   point of showing it — it carries the accent colour and a border of its own now, so the
   eye finds it without it shouting over the stage name. */
.phase__model { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem; font-weight: 500; color: var(--accent-text, #3730a3);
  background: var(--accent-soft); border: 1px solid var(--accent-ring, transparent);
  padding: 0.05rem 0.4rem; border-radius: 0.3rem; }
/* The ranking model when it is not the one that extracted: the difference is the whole
   point of showing two, so it is marked rather than left to a careful read. */
.phase__model--other { color: var(--warn-text, #9a6700); background: #fdecc8;
  border-color: #e0b44a; }
/* The gate's model pickers sit beside the two decision buttons. Each label stacks over
   its select rather than sitting beside it: side by side, the label ate the width and the
   select truncated its own options ("Same as the s…"), which is the one part that must
   stay readable. */
.gate-model { display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 0.2rem; font-size: 0.8rem; color: var(--text-muted); }
.gate-model select { min-width: 13rem; }

/* The rebuild control under a past newsletter's failures. It can land in a narrow column,
   so the row wraps and every field keeps a floor width instead of being squeezed until
   its options are unreadable. */
.rebuild { margin-top: 0.9rem; padding-top: 0.9rem; border-top: 1px solid var(--border-soft); }
.rebuild__title { margin: 0 0 0.6rem; font-size: 0.9rem; color: var(--text-label); }
.rebuild__row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.75rem; }
.rebuild__field { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.8rem;
  color: var(--text-muted); flex: 1 1 13rem; min-width: 13rem; }
.rebuild__field select { width: 100%; }
/* The per-model split under the finding count: subordinate to the total it breaks down. */
.summary-by-model { display: inline-block; padding-left: 0.6rem; font-size: 0.78rem;
  color: var(--text-muted); }
.summary-by-model code { font-size: 0.74rem; }

/* While a run is in flight, leaving is via Stop, not "Configure another run" or
   the browser Back (blocked in the popstate handler) — so hide the button until
   the run reaches a terminal panel. */
.run-active .run-summary__back { display: none; }

/* ── Spinner ───────────────────────────────────────────────────────────── */
.spinner { height: 1rem; width: 1rem; flex-shrink: 0; animation: spin 1s linear infinite; }
.spinner__track { opacity: 0.25; }
.spinner__head { opacity: 0.75; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Summary tables ────────────────────────────────────────────────────── */
.details {
  margin-top: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 0.75rem;
}
.details__summary {
  cursor: pointer;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text-label);
}
.details__note { margin: 0.5rem 0 0; font-size: 0.75rem; color: var(--text-muted); }
.table-wrap { margin-top: 0.5rem; overflow-x: auto; }
.table-wrap--flush { margin-top: 0.75rem; }

.summary-table { width: 100%; font-size: 0.75rem; border-collapse: collapse; }
.summary-table th {
  text-align: left;
  font-weight: 500;
  color: var(--text-muted);
  padding: 0.25rem 0.75rem 0.25rem 0;
}
.summary-table td {
  padding: 0.25rem 0.75rem 0.25rem 0;
  color: var(--text-label);
}
.summary-table tbody tr { border-top: 1px solid var(--border-soft); }
.summary-table tr.is-total { border-top: 1px solid var(--border); font-weight: 500; }
.summary-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.summary-table .name { font-weight: 500; color: var(--text-label); }
.summary-table .muted { color: var(--text-muted); }

/* Login / registration pages */
.auth-body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.auth-card {
  width: 100%;
  max-width: 22rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.75rem;
}
.auth-brand { display: flex; align-items: center; justify-content: center; gap: 0.6rem; }
.auth-logo { height: 1.85rem; }
.landing-choices { display: flex; flex-direction: column; gap: 0.75rem; }
.landing-choice { justify-content: flex-start; padding: 0.9rem 1.1rem; font-size: 1rem; }
/* When a run started, pushed to the right of its row: it is what tells two runs of the
   same kind for the same day apart, so it needs a fixed place the eye can scan down. */
.job-started { margin-left: auto; padding-left: 1rem; font-size: 0.78rem;
  color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.landing-logout { text-align: center; }
.refresh-widget { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 0.5rem; font-size: 0.8rem; }
.btn-sm { padding: 0.3rem 0.6rem; font-size: 0.85rem; }
/* The bar wraps rather than pushing its last child off the screen: with a back link, a
   title and an Unlock control, a narrow screen had nowhere to put the third and it left
   the viewport entirely — unreachable, since the page itself does not scroll sideways. */
.brand-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 1rem;
  padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); }
/* Sizing lives on .page (one rule, both containers): .browse-main used to carry a
   fixed 1100px of its own, so the schedules and browse pages shrank with the window
   but never grew with it — a wide screen left a narrow column between two margins
   while the table inside it wrapped. Only the padding differs. */
.browse-main { padding: 1.5rem; }
/* Wraps for the same reason the brand bar does: the pager, the count and the rows-per-
   page control together are wider than a phone, and without a wrap the row simply
   overflowed — the count clipped on the left, the select cut off on the right. */
.browse-pager { display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 0.5rem 1rem; margin-top: 1rem; }
.browse-date { white-space: nowrap; min-width: 11rem; }
.browse-fold > summary { cursor: pointer; color: var(--accent-text); }
.browse-fold[open] > summary { margin-bottom: 0.25rem; }
.auth-title { margin: 0; font-size: 1.2rem; font-weight: 600; color: var(--text-label); }
.auth-heading {
  margin: 0;
  text-align: center;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text-muted);
}
.auth-form { display: flex; flex-direction: column; gap: 0.75rem; }
.auth-submit { margin-top: 0.25rem; }
.auth-switch { margin: 0; text-align: center; font-size: 0.8rem; color: var(--text-muted); }
.auth-switch a { color: var(--accent-text); }
.auth-footer { margin: 0; text-align: center; font-size: 0.72rem; color: var(--text-muted); }

/* Schedule cadence picker + state pills */
.wd-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.wd { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.85rem; }
.pill { display: inline-block; padding: 0.1rem 0.55rem; border-radius: 999px; font-size: 0.72rem;
  font-weight: 600; }
.pill--on { background: var(--ok-bg, #e6f4ea); color: var(--ok-text, #1e7e34); }
.pill--off { background: var(--muted-bg, #ececec); color: var(--text-muted); }
.pill--run { background: #fdecc8; color: #9a6700; }
.pill--warn { background: #fde2e1; color: #a4262c; }
/* A pill that is a link: keep the pill's own colour rather than the page's link colour,
   and say so on hover, since a pill does not otherwise read as pressable. */
a.pill--link { text-decoration: none; cursor: pointer; }
a.pill--link:hover, a.pill--link:focus-visible { text-decoration: underline; filter: brightness(0.95); }

/* Schedule rows. The last two cells hold a variable number of children — up to three
   badges, up to five buttons — so both are wrapping flex rows: the buttons keep an even
   gap and align to the right edge whether the row shows three of them or five, and the
   badges stack in a column instead of trailing off the cell. Cadence and Model never
   break mid-phrase ("Daily at 08:00 UTC", "Ollama → Claude (CLI)"); the name column is
   the one given leave to wrap, since it is the only free-text cell. */
.row-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: flex-end;
  align-items: center; }

/* A row's occasional actions (edit, toggle, delete) behind a labelled "More". It opens
   **in the flow** — the row grows by a line — rather than as an absolutely positioned
   dropdown: .table-wrap has overflow-x for the sideways scroll, which is a clipping
   context, and the floating panel was drawn half over the next row. */
.row-menu { display: inline-block; }
.row-menu > summary { list-style: none; cursor: pointer; user-select: none; }
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu[open] > summary { background: var(--accent-soft); }
.row-menu__items { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem;
  padding: 0.4rem; border: 1px solid var(--border); border-radius: 0.5rem;
  background: var(--muted-bg, transparent); }

/* Below the breakpoint a table stops being a table: each row becomes a card with its
   fields labelled from data-label, and the actions get a full line of their own. The
   alternative was the sideways scroll .table-wrap gives, which on a phone pushes the
   buttons — the reason the row exists — off the screen until you drag it. */
@media (max-width: 639px) {
  /* A 1.5rem title took two lines and shoved the Unlock control off the edge; smaller,
     and given the whole line, it leaves the controls a line of their own. */
  .brand__title { font-size: 1.15rem; flex: 1 1 100%; order: -1; }
  .brand-bar { padding: 0.75rem 1rem; }
  .brand-bar__end { margin-left: 0; }

  /* The summary's three breakdowns sit side by side on a desktop. On a phone the meta
     column held its 10rem and left the tables a strip too narrow to read — a paper's
     title came out one word per line — so they stack instead. */
  .summary-cols { display: block; }
  .summary-cols > * { margin-bottom: 1rem; }
  .summary-cols > .summary-meta { flex: none; width: auto; }

  .stack-rows thead { display: none; }
  .stack-rows, .stack-rows tbody, .stack-rows tr, .stack-rows td { display: block;
    width: 100%; }
  .stack-rows tr { border: 1px solid var(--border); border-radius: 0.5rem;
    padding: 0.75rem; margin-bottom: 0.75rem; }
  .stack-rows td { padding: 0.15rem 0; border: 0; }
  /* sched__nowrap keeps "Daily at 08:00 UTC" and an identity pair on one line in a
     table row, where there is room. In a card there is not, and forbidding the wrap
     made the text run out past the card's edge instead — so the cards let it wrap,
     and a single long identity breaks rather than overflowing. */
  .stack-rows .sched__nowrap { white-space: normal; }
  .stack-rows td { overflow-wrap: anywhere; }
  .stack-rows td[data-label]::before { content: attr(data-label) ": ";
    color: var(--text-muted); font-size: 0.78rem; }
  .stack-rows td.num { text-align: left; }
  .stack-rows .row-actions { justify-content: flex-start; margin-top: 0.5rem; }
  /* On a card the menu takes the full width when open, so its buttons line up under
     the row rather than squeezing beside it. */
  .stack-rows .row-menu[open] { flex-basis: 100%; }
  .stack-rows .row-menu__items .btn { flex: 1 1 6rem; }

  /* The pager's three parts each take a line rather than fighting for one, and the
     rows-per-page select stops being nowrap-wide. */
  .browse-pager { flex-direction: column; align-items: stretch; text-align: center; }
  .browse-size { margin-left: 0; white-space: normal; }
  .browse-size select { max-width: 100%; }
  /* .browse-date reserves 11rem for a timestamp, which a card has no room to spare. */
  .stack-rows .browse-date { min-width: 0; white-space: normal; }
  /* These two already wrap to their own line here, so the widths they reserve for
     sitting side by side only risk overflowing a small phone. */
  .active-list, .job-view { flex-basis: 100%; min-width: 0; }
  .sched__name { min-width: 0; }
  /* Every reserved input width goes: on a phone a control takes the line it is given
     rather than insisting on a size the line does not have. */
  .input--sm, .gate-model select, .rebuild__field { min-width: 0; }
  .input, select { max-width: 100%; }

  /* The page header put a back link, a logo, a title and a subtitle on one row. On a
     phone the title had a column a few characters wide and set itself one word per
     line. Stacked, each part gets the width it needs. */
  .site-header { flex-direction: column; align-items: stretch; gap: 0.5rem; }
  .brand { flex-wrap: wrap; gap: 0.5rem; min-width: 0; }
  .brand__logo { height: 2.25rem; }
  .brand__title { flex: 1 1 100%; }
  .stack-rows .row-badges { flex-direction: row; flex-wrap: wrap; }
  /* Nothing to scroll sideways any more, and a card must not be clipped by it. */
  .table-wrap:has(.stack-rows) { overflow-x: visible; }
}
.row-badges { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem; }
.sched__nowrap { white-space: nowrap; }
.sched__name { min-width: 8rem; }

/* Administration: the inline working-groups editor in a user row, and the invite token
   shown once after minting (selectable, monospace — it is copied by hand). */
.inline-form { display: flex; gap: 0.35rem; align-items: center; }
/* Four fields and a button in one cell: they wrap rather than squeezing each other to
   nothing, and each keeps a floor width so a name stays typeable. The working-group box
   asks for the most room, since a list of them is the longest thing typed here. */
.inline-form--wrap { flex-wrap: wrap; align-items: stretch; }
.inline-form--wrap .input { flex: 1 1 7rem; min-width: 6rem; }
.inline-form--wrap .input[name="workgroups"] { flex: 2 1 12rem; }
.inline-form--wrap .btn { align-self: stretch; }
.input--sm { padding: 0.2rem 0.4rem; font-size: 0.8rem; min-width: 10rem; }
/* Browse-page admin controls: the unlock pushed to the right of the brand bar, and the
   select-all / delete-selected row above the table. */
.brand-bar__end { margin-left: auto; display: flex; align-items: center; gap: 0.4rem; }
.unlock-form { display: flex; gap: 0.35rem; align-items: center; margin: 0; }
.bulk-bar { display: flex; align-items: center; gap: 0.9rem; margin: 0 0 0.6rem; }
/* Rows-per-page sits at the right of the footer, opposite the pager. */
.browse-size { margin-left: auto; white-space: nowrap; }
.nl-pick, .nl-all { cursor: pointer; }
/* A message on a signed-out card (a reset link that no longer works, mismatched
   passwords). Not a .panel: the card is narrow and a full panel crowds it. */
.auth-error { margin: 0 0 0.75rem; padding: 0.6rem 0.75rem; border-radius: 0.5rem;
  border: 1px solid var(--warn-border); background: var(--warn-bg);
  color: var(--warn-text); font-size: 0.9rem; }

/* One submitted source awaiting a decision. The address is the thing being judged, so it
   leads and is set in mono — it is a URL to read exactly, not prose. */
.request { margin-top: 0.75rem; }
.request__url { margin: 0 0 0.25rem; font-family: ui-monospace, SFMono-Regular, Menlo,
  monospace; font-size: 0.9rem; word-break: break-all; }
.request__decide { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  margin-top: 0.6rem; }
.request__decide .input { flex: 1 1 16rem; min-width: 0; }
/* A password box and its Show control on one line. min-width releases the input's auto
   floor so the button cannot push it off a narrow screen. */
.reveal-row { display: flex; align-items: stretch; gap: 0.35rem; min-width: 0; }
.reveal-row .input { flex: 1 1 auto; min-width: 0; }
.reveal { flex: 0 0 auto; align-self: stretch; }

/* The reset queue: a name, how long they have waited, and the button, on one line each
   until a narrow screen wraps them. min-width releases the flex item's auto floor, which
   is what pushed these off the side of a phone. */
.request__lines { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.request__line { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
  min-width: 0; padding: 0.35rem 0; border-top: 1px solid var(--border); }
.request__line:first-child { border-top: none; }
.request__line > .muted { flex: 1 1 auto; min-width: 0; }

/* The invite is shown once and never stored, so it has to be readable and easy to take
   away with you: real theme colours (it used to take a hardcoded light fill under the
   dark theme's light text and came out invisible), a copy button, and the two lines
   saying what to do with it. */
.invite { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.75rem;
  padding: 0.75rem; border: 1px solid var(--accent-outline, var(--border));
  border-radius: 0.5rem; background: var(--accent-soft); }
.invite__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.invite-token { flex: 1 1 18rem; min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem;
  color: var(--text-strong); background: var(--surface);
  border: 1px solid var(--border); padding: 0.4rem 0.55rem; border-radius: 0.3rem;
  user-select: all; word-break: break-all; }
.sources-list { max-height: 18rem; overflow-y: auto; border: 1px solid var(--border, #ddd);
  border-radius: 0.4rem; padding: 0.5rem; }
.field.is-disabled { opacity: 0.4; }
table.sortable th { cursor: pointer; user-select: none; }
table.sortable th:hover { text-decoration: underline; }
.sort-ind { font-size: 0.8em; opacity: 0.75; }

/* Active jobs: the list and the selected run's live panel, side by side (stacked when narrow). */
.active-layout { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
.active-list { flex: 1 1 16rem; min-width: 14rem; }
.job-view { flex: 2 1 22rem; min-width: 18rem; }

/* Newsletter summary: the three breakdown tables side by side (stacked when narrow). */
.summary-cols { display: flex; gap: 1.25rem; flex-wrap: wrap; align-items: flex-start; margin-top: 0.75rem; }
.summary-cols > details { flex: 1 1 20rem; min-width: 0; }
.summary-cols .table-wrap { overflow-x: auto; }

/* Newsletter summary: the counts block as a fixed left column beside the breakdown tables. */
.summary-cols > .summary-meta { flex: 0 0 10rem; line-height: 1.7; }
/* Highlight the browse row whose summary is currently shown, so it is clear which one. */
.summary-table tbody tr.is-shown > td { background: var(--accent-soft); }
.summary-table tbody tr.is-shown > td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
