/* The manager panel's only stylesheet.
 *
 * Plain CSS, served by Flask's static handler. It is NOT built, NOT bundled and
 * NOT purged -- deliberately. "The asset pipeline is broken" is a first-class
 * reason to open this page, so the page must not depend on the asset pipeline.
 * There is no CDN either: it has to render on a box with no internet.
 *
 * The palette is the desktop tool's COL dict, one custom property per entry, so
 * the two look like the same product while both exist. Dark-only, like the
 * desktop app -- this is a control panel on a server console, not a page that
 * follows a reader's theme.
 */

:root {
  --bg:        #0d0f14;
  --panel:     #161922;
  --panel-hi:  #1d212c;
  --border:    #2a2f3b;
  --text:      #e7e9ee;
  --text-dim:  #8b93a3;
  --text-mute: #5b6373;
  --accent:    #3b82f6;
  --accent-hi: #2563eb;
  --success:   #22c55e;
  --danger:    #ef4444;
  --warn:      #f59e0b;
  --term-bg:   #0a0c10;
  --term-fg:   #00ffaa;

  --mono: Consolas, "Cascadia Mono", ui-monospace, monospace;
  --ui: "Segoe UI", system-ui, -apple-system, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 var(--ui);
  /* The desktop window is 1100px wide; this keeps the same reading measure on a
     monitor and lets everything below collapse to one column on a phone. */
  padding: 18px;
}

a { color: var(--accent); }

.wrap { max-width: 1180px; margin: 0 auto; }

/* ── Header ─────────────────────────────────────────────────────────── */

.top {
  display: flex; flex-wrap: wrap; gap: 12px;
  align-items: flex-start; justify-content: space-between;
  margin-bottom: 14px;
}
.top h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: .2px; }
.top .sub { color: var(--text-mute); font-size: 12px; margin-top: 2px; }
.top nav { display: flex; gap: 8px; flex-wrap: wrap; }

/* ── The read-only banner ───────────────────────────────────────────────
 * Loud on purpose. A panel that silently does nothing when you press a button
 * is indistinguishable from a broken one, so the reason is stated once at the
 * top and again on every disabled control. */
.banner {
  border: 1px solid var(--warn);
  border-left-width: 4px;
  background: rgba(245, 158, 11, .08);
  border-radius: 8px;
  padding: 12px 14px;
  margin-bottom: 16px;
  font-size: 13.5px;
}
.banner strong { color: var(--warn); }

/* ── Cards ──────────────────────────────────────────────────────────── */

.cards { display: grid; gap: 14px; }
@media (min-width: 900px) {
  .cards.two { grid-template-columns: 1fr 1fr; }
}

.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}
.card > header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--panel-hi);
}
.card > header h2 { margin: 0; font-size: 14px; font-weight: 600; }
.card .body { padding: 14px; }

/* ── Status dot + pills ─────────────────────────────────────────────── */

.dot { font-size: 12px; color: var(--text-mute); }
.dot.on { color: var(--success); }

.pill {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 600; letter-spacing: .3px;
  background: var(--accent); color: #fff; white-space: nowrap;
}
.pill.bad  { background: var(--danger); }
.pill.warn { background: var(--warn); color: #16202e; }
.pill.mute { background: var(--border); color: var(--text-dim); }

/* ── The four-value health grid ─────────────────────────────────────────
 * Desktop shows four across; auto-fit collapses it to two and then one
 * without a second layout being written. That is M2.2's brief: build the
 * desktop layout and let it collapse. */
.grid4 {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.cap {
  font-size: 10px; letter-spacing: .8px; text-transform: uppercase;
  color: var(--text-mute); margin-bottom: 3px;
}
.val { font-size: 13.5px; word-break: break-word; }
.val.bad { color: var(--danger); }

/* ── Buttons ────────────────────────────────────────────────────────── */

.btns { display: flex; flex-wrap: wrap; gap: 7px; }
.btn {
  font: 13px var(--ui);
  padding: 7px 13px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--panel-hi);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
.btn:hover:not(:disabled) { border-color: var(--accent); }
.btn.accent  { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.accent:hover { background: var(--accent-hi); }
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: .42;
  cursor: not-allowed;
}

/* ── Form fields ────────────────────────────────────────────────────── */

.field {
  display: block;
  width: 100%;
  max-width: 420px;
  margin-top: 4px;
  padding: 9px 11px;
  font: 14px var(--ui);
  color: var(--text);
  background: var(--term-bg);
  border: 1px solid var(--border);
  border-radius: 7px;
}
.field:focus {
  outline: none;
  border-color: var(--accent);
}
label.cap { display: block; }

/* ── Monospace panes: activity log, backup list, dirty files ────────── */

.pane {
  background: var(--term-bg);
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 9px 11px;
  font: 12px/1.55 var(--mono);
  /* Wide content scrolls inside its own box rather than making the page
     scroll sideways -- a log line can be arbitrarily long. */
  overflow-x: auto;
  white-space: pre;
  max-height: 340px;
  overflow-y: auto;
  margin: 0;
}
.pane.log { color: var(--term-fg); }
.pane.wrap-lines { white-space: pre-wrap; word-break: break-word; }

/* Opens scrolled to the BOTTOM without JavaScript.
 *
 * A flex container in `column-reverse` treats the bottom as the start, so the
 * default scroll position lands there. The child's own content order is
 * untouched — which is the point: this is used for the manager log, where the
 * newest entry should be visible on load but the lines must stay chronological
 * or every traceback prints backwards. The activity log has no such constraint
 * (one line per entry) and is genuinely reversed instead. */
.tail-anchored {
  display: flex;
  flex-direction: column-reverse;
  overflow-y: auto;
}
.tail-anchored > .pane {
  max-height: none;
  overflow-y: visible;
}

table.rows { width: 100%; border-collapse: collapse; font: 12px var(--mono); }
table.rows td { padding: 3px 10px 3px 0; white-space: nowrap; }
table.rows td.size { text-align: right; color: var(--text-dim); }
table.rows td.when { color: var(--text-mute); }

.muted { color: var(--text-mute); }
.dim   { color: var(--text-dim); }
.small { font-size: 12px; }
.mono  { font-family: var(--mono); }
.bad   { color: var(--danger); }
.good  { color: var(--success); }
.warnt { color: var(--warn); }

.note { font-size: 12px; color: var(--text-dim); margin-top: 9px; }

/* Two listeners on one port -- the failure that took a day to find. It gets
   its own treatment because "running: yes" is exactly how it hid. */
.alarm {
  border: 1px solid var(--danger);
  border-left-width: 4px;
  background: rgba(239, 68, 68, .1);
  border-radius: 7px;
  padding: 10px 12px;
  margin-bottom: 12px;
  font-size: 13px;
}

/* ── Utilities ───────────────────────────────────────────────────────────
 *
 * 🚨 These exist so the templates can carry NO `style="…"` attributes at all.
 * The Content-Security-Policy sets `style-src 'self'` with no `'unsafe-inline'`,
 * and a browser blocks an inline style attribute under that policy — silently,
 * as far as the page is concerned: no error the user sees, just a layout that
 * quietly falls apart. Adding `'unsafe-inline'` to keep 43 one-off attributes
 * would have made the policy decorative, since `'unsafe-inline'` is most of what
 * a style-based injection wants.
 *
 * So: if you need a one-off style, add a class here. Do not put it in the markup.
 * `tests/test_manager_security_headers.py` fails the build on a `style=` attribute
 * in any template. */

.mt-0  { margin-top: 0; }
.mt-1  { margin-top: 2px; }
.mt-2  { margin-top: 6px; }
.mt-3  { margin-top: 8px; }
.mt-4  { margin-top: 10px; }
.mt-5  { margin-top: 12px; }
.mt-6  { margin-top: 14px; }
.mb-4  { margin-bottom: 14px; }
.mb-5  { margin-bottom: 16px; }
.my-6  { margin: 14px 0; }

.w-full { width: 100%; }
.w-auto { width: auto; }

.inline-form { display: inline; }

/* A centred single-purpose card: sign in, setup, 429, console-only. */
.sheet      { max-width: 460px; margin: 6vh auto 0; }
.sheet.wide { max-width: 560px; }
.sheet.mid  { max-width: 520px; }

/* A diff is the one thing on this panel with genuinely unbounded line length, and
   the page BODY must never scroll sideways — on a phone that makes every other
   control drift off the edge. So the wide thing scrolls inside its own box.
   `max-height` as well, because a 1,200-line diff would otherwise push the rest of
   the page (the commit list, the remotes) below several screens of patch. */
.scroll-x {
  overflow-x: auto;
  overflow-y: auto;
  max-height: 60vh;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  background: var(--bg);
}
.scroll-x pre { margin: 0; white-space: pre; }

/* Table cells that should wrap rather than scroll — user agents, audit details. */
.wrap-cell     { white-space: normal; max-width: 320px; }
.wrap-cell.lg  { max-width: 420px; }
.pane.grow     { white-space: normal; max-height: none; }
.pane.tall     { max-height: 70vh; }
.pane.normal   { white-space: normal; }

/* Codes meant to be read aloud or typed back. */
.code-entry  { max-width: 200px; font-size: 20px; letter-spacing: 4px; }
.code-shown  { font-size: 15px; letter-spacing: 1px; }
.key-shown   { font-size: 14px; letter-spacing: 2px; }
.spaced      { letter-spacing: 3px; }

.row-tight   { display: flex; flex-wrap: wrap; gap: 22px; align-items: flex-start; }
.filter-bar  {
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: flex-end; margin: 12px 0;
}
.row-mid     { margin-top: 12px; align-items: center; }
.col-grow    { flex: 1; min-width: 260px; }

/* One choice in a list of choices — a test target, a build mode. Rendered as a
 * block rather than a row so the description sits under the control instead of
 * being squeezed beside it: these descriptions are the point (a five-hour run, a
 * production build that can strip working code), not fine print. */
.row-block {
  display: block; margin: 8px 0 14px;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
}
.row-block p { margin: 0 0 8px; }

/* The QR needs a white quiet zone whatever the surrounding theme is. */
.qr { background: #fff; padding: 8px; border-radius: 8px; }

footer.foot {
  margin-top: 20px; padding-top: 12px;
  border-top: 1px solid var(--border);
  color: var(--text-mute); font-size: 12px;
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between;
}

/* ── M11.2 — one setting on the .env viewer ──────────────────────────────
 *
 * A card per setting rather than a table row: the description is the point (what
 * an unset SCHEMA_SYNC costs, why PG_POOL_MAX pairs with WAITRESS_THREADS), and a
 * table squeezes it into a column nobody reads. Same reasoning as .row-block.
 *
 * ⚠️ No `style="…"` anywhere in the template — style-src is 'self' with no
 * 'unsafe-inline', so a browser silently DROPS an inline style and the symptom is
 * a layout quietly falling apart with no error. Everything visual lives here. */
.setting {
  display: block;
  padding: 10px 12px;
  margin: 8px 0;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: 7px;
}
/* 🚨 The app will not boot without these, so they do not look like the others. */
.setting--missing { border-left-color: var(--danger); }

.setting__head {
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: baseline; justify-content: space-between;
}
.setting__tags { display: flex; flex-wrap: wrap; gap: 5px; }


.setting__foot { margin-top: 6px; }

/* A readonly control must not read as an editable one that is simply not working.
 * Dimmed and default-cursor, so the page looks deliberate rather than broken. */
.setting .field[readonly],
.setting .field:disabled,
.setting input[type="checkbox"]:disabled {
  opacity: .72;
  cursor: default;
}

/* Hidden by the live filter in manager.js. Server-side filtering removes rows
 * outright; this only exists for the client-side enhancement. */
.setting[hidden] { display: none; }

/* M11.4 — a setting whose value is wrong, and one merely worth a look.
 * The colours carry the same distinction the code does: a problem is refused, a
 * warning is the operator's call. Flattening them into one "invalid" style is how a
 * legitimate-but-unwise pool size starts reading as a broken server. */
.setting--warning { border-left-color: var(--warn); }
.setting__problem { color: var(--danger); margin-top: 6px; }
.setting__warning { color: var(--warn); margin-top: 6px; }

/* ═══ M8.4 — the phone ══════════════════════════════════════════════════════
 *
 * This panel's whole premise is being reachable when you are NOT at the machine,
 * so how it behaves on a phone is closer to the point than "polish" suggests.
 *
 * Measured 2026-08-07 with Playwright at 390x844 (iPhone 14, touch), across 17
 * signed-in pages, BEFORE any of the rules below:
 *
 *     tap targets under 44px .................... 359
 *     tap targets under 24px (WCAG 2.2 AA 2.5.8)  188
 *     adjacent pairs closer than 8px ............ 194
 *     pages scrolling sideways ................... 1  (/settings, +47px)
 *     wide tables CLIPPED rather than scrolled ... /backup (+480px), /git
 *
 * 🚨 **The clipping is the one that would never have been guessed.** `.card` sets
 * `overflow: hidden` for its rounded corners, so a table wider than the card is not
 * scrolled off the side of the page — it is CUT OFF, with no way to reach the rest.
 * The page reports zero horizontal overflow while a backup's destination path is
 * simply absent. A sideways-scrolling page is a nuisance; silently missing content
 * is a panel telling you something untrue. */

/* Wide tables scroll inside their own box instead of being clipped by the card.
 *
 * ⚠️ Narrow widths only, on purpose. `display: block` turns the table into a block
 * containing an anonymous table box, which drops the column stretching that
 * `width: 100%` gives all 32 of these tables on a desktop. That trade is right on a
 * phone and wrong on a monitor, so it is scoped rather than applied to both. */
@media (max-width: 700px) {
  table.rows {
    display: block;
    overflow-x: auto;
    max-width: 100%;
  }
}

/* ── Touch targets ────────────────────────────────────────────────────────────
 *
 * ⚠️ Keyed on `pointer: coarse`, NOT on width. The primary pointer is what decides
 * how big a target needs to be: a narrow browser window on a desktop is still being
 * driven by a mouse and does not want 44px controls, while a tablet in landscape is
 * wide and does. A width breakpoint gets both of those backwards.
 *
 * 44px is the Apple/Google figure. WCAG 2.2 AA (2.5.8) asks only for 24px, which
 * every rule here clears with room to spare. */
@media (pointer: coarse) {
  .btn {
    min-height: 44px;
    padding: 11px 16px;
    font-size: 14px;
    /* inline-flex rather than inline-block: `min-height` on an inline-block leaves
       the label sitting at the top of a 44px box, which looks like a rendering
       fault rather than a bigger button. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* 194 adjacent pairs sat closer than 8px, which is about the distance a thumb
     cannot resolve. This is the whole of "destructive controls that cannot be hit
     by accident" that is a matter of geometry; the rest is the confirmation. */
  .btns { gap: 12px; }
  .top nav { gap: 10px; }
  .filter-bar { gap: 12px; }

  .field,
  select,
  textarea,
  /* ⚠️ `input:not([type])` is NOT redundant, and leaving it out is why two fields
     on /schedule stayed 21px through the first pass. An `<input>` with no `type`
     attribute *behaves* as text but is **not matched by `[type="text"]`** — the
     selector tests the attribute, not the resolved default. `schedule.html` writes
     `<input id="at" name="at" class="mono">`, which is perfectly valid HTML and
     invisible to every rule below. */
  input:not([type]),
  input[type="text"], input[type="password"], input[type="number"],
  input[type="time"], input[type="search"], input[type="email"] {
    min-height: 44px;
  }

  /* Measured at 13px — half the WCAG minimum, and the control that decides whether
     backups carry the encryption keys is one of them. 26px rather than 24: 24 is the
     floor the guideline sets, and a control sitting exactly on a floor fails it the
     moment a browser rounds differently. */
  input[type="checkbox"], input[type="radio"] {
    width: 26px;
    height: 26px;
  }

  /* Rows are a tap target too — every one of these tables has links in it. */
  table.rows td { padding: 8px 10px 8px 0; }

  /* A link in a table cell is an action, not prose: `change it` beside the port on
     /settings measured 14px. Vertical padding only, so the column widths — which
     these monospace tables depend on — do not move. */
  table.rows td a:not(.btn) {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* 📌 NOT touched: links inside a sentence ("edit the schedule", measured at
     16px). WCAG 2.5.8 exempts inline links precisely because inflating them breaks
     the line they sit in, and this panel's prose is where its reasoning lives. */
}

/* One-handed reading: on a phone the 18px body padding is 10% of the screen. */
@media (max-width: 600px) {
  body { padding: 12px; }
  .top { gap: 8px; }
  .top h1 { font-size: 19px; }
  .card .body { padding: 12px; }
}

/* ── The confirmation dialog (manager.js) ─────────────────────────────────────
 *
 * 🚨 `data-confirm` was DEAD MARKUP until 2026-08-07 — four destructive controls
 * carried the attribute and nothing anywhere implemented it, so *Remove remote*,
 * *Remove destination* and *Forget token* fired on a single tap with no prompt at
 * all. Found while doing M8.4; it is the literal case of "destructive controls that
 * can be hit by accident on a phone".
 *
 * ⚠️ **A guard, not a gate.** With scripting off the form still submits exactly as
 * it does today — so this can only ever be an improvement, never a new way for the
 * panel to stop working. The genuinely irreversible operations (discard a file,
 * stop the app, restore a .env) keep their SERVER-RENDERED confirmation pages,
 * which is M3.7's decision and does not depend on a browser feature.
 *
 * Not `window.confirm`: after the second one Chrome and Firefox offer "prevent this
 * page from creating additional dialogs", and once that is ticked every later call
 * returns instantly as Cancel — the control silently stops working. That is the
 * same reasoning the app's own ban on native dialogs rests on. */
dialog.confirm {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0;
  max-width: 440px;
  width: calc(100% - 28px);
}
dialog.confirm::backdrop { background: rgba(0, 0, 0, .6); }
dialog.confirm .body { padding: 16px; }
dialog.confirm p { margin: 0 0 16px; font-size: 14px; line-height: 1.5; }
/* The two answers are far apart on purpose: they are adjacent, they are a tap
   width from each other, and one of them is the destructive one. */
dialog.confirm .btns { justify-content: flex-end; gap: 14px; }
