/* SoloInc.
 *
 * Phone-first, because that is where the Human reads everything: he drives this
 * over ssh from a handheld and reads briefs on a phone in another timezone. So
 * the base layout is one column and the wide layouts are the enhancement,
 * rather than the other way round.
 *
 * No web fonts, no CDN, no images. The page is served under
 * `default-src 'none'` and everything it needs is in this file, which is also
 * why the "icons" are text glyphs.
 */

/* == tokens == (the one place colour, space and motion are defined; the
 * standby page keeps a parity copy of the same names — see standby.py.
 * Derived tone surfaces use color-mix against the CURRENT scheme's hues, so
 * the light block below only overrides the base palette.) */
:root {
  color-scheme: dark light;
  /* base palette — dark, the default */
  --bg: #0b0d10;
  --panel: #12161b;
  --panel-2: #171c23;
  --line: #232a33;
  --ink: #e6edf3;
  --dim: #93a1b1;
  --faint: #6b7887;
  --accent: #4aa3ff;
  --ok: #3fb950;
  --warn: #d29922;
  --bad: #f85149;
  --info: #a371f7;
  --on-accent: #04121f;
  --on-tone: #ffffff;
  /* derived tone surfaces — replaces every sprinkled hex */
  --ok-fill:     color-mix(in srgb, var(--ok)     11%, var(--panel));
  --ok-line:     color-mix(in srgb, var(--ok)     36%, var(--line));
  --warn-fill:   color-mix(in srgb, var(--warn)   11%, var(--panel));
  --warn-line:   color-mix(in srgb, var(--warn)   36%, var(--line));
  --bad-fill:    color-mix(in srgb, var(--bad)    11%, var(--panel));
  --bad-line:    color-mix(in srgb, var(--bad)    36%, var(--line));
  --info-fill:   color-mix(in srgb, var(--info)   11%, var(--panel));
  --info-line:   color-mix(in srgb, var(--info)   36%, var(--line));
  --accent-fill: color-mix(in srgb, var(--accent) 11%, var(--panel));
  --accent-line: color-mix(in srgb, var(--accent) 36%, var(--line));
  /* space — the existing 2/4/7/10/14 rhythm, named */
  --s1: 2px; --s2: 4px; --s3: 7px; --s4: 10px; --s5: 14px; --s6: 20px;
  /* radius */
  --r1: 6px; --r2: 8px; --r3: 10px; --r-pill: 999px;
  /* type */
  --fs-body: 15px;
  --fs-stat: clamp(21px, 4.5vw, 27px);
  /* motion — one vocabulary: travelling dash = carrying now, pulse = unwell */
  --t-fast: 120ms; --t-med: 200ms;
  --ease: cubic-bezier(.2, .7, .3, 1);
  --flow-period: 1.1s;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
/* Auto (no [data-theme]) follows the device. [data-theme="light"/"dark"] is
   the header toggle overriding that — guard the media query so a forced dark
   choice isn't clobbered by a light OS, and repeat the light values on the
   attribute so a forced light choice works on a dark OS too. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #f5f6f8;
    --panel: #ffffff;
    --panel-2: #eef1f5;
    --line: #d9dee5;
    --ink: #16181d;
    --dim: #4c5560;
    --faint: #77808c;
    --accent: #1f6fd6;
    --ok: #1a7f47;
    --warn: #8a5a00;
    --bad: #b42323;
    --info: #6d47c7;
    --on-accent: #ffffff;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f5f6f8;
  --panel: #ffffff;
  --panel-2: #eef1f5;
  --line: #d9dee5;
  --ink: #16181d;
  --dim: #4c5560;
  --faint: #77808c;
  --accent: #1f6fd6;
  --ok: #1a7f47;
  --warn: #8a5a00;
  --bad: #b42323;
  --info: #6d47c7;
  --on-accent: #ffffff;
}
:root[data-theme="dark"] { color-scheme: dark; }

/* == LCH == Where the browser can do oklch (Safari 15.4+, Chrome 111+,
   Firefox 113+) the palette is re-expressed perceptually and the accent is
   DERIVED from the current cell's hue -- `--org-hue`, set by orgs.js from the
   cell's accent -- so surfaces, lines, fills and states agree across cells and
   every screen carries a faint tint of which cell it is in. Lightness and
   chroma are fixed here; only the hue travels. The hex palette above is the
   fallback and stays the source of truth for the standby page's parity copy. */
@supports (color: oklch(50% 0.1 200)) {
  :root {
    --org-hue: 250;
    --accent:    oklch(72% 0.16 var(--org-hue));
    --on-accent: oklch(18% 0.04 var(--org-hue));
    --bg:      oklch(16% 0.012 var(--org-hue));
    --panel:   oklch(21% 0.014 var(--org-hue));
    --panel-2: oklch(24% 0.015 var(--org-hue));
    --line:    oklch(30% 0.016 var(--org-hue));
    --ok:   oklch(70% 0.17 148);
    --warn: oklch(74% 0.15 80);
    --bad:  oklch(66% 0.20 27);
    --info: oklch(70% 0.16 300);
    --ok-fill:     color-mix(in oklch, var(--ok)     12%, var(--panel));
    --ok-line:     color-mix(in oklch, var(--ok)     38%, var(--line));
    --warn-fill:   color-mix(in oklch, var(--warn)   12%, var(--panel));
    --warn-line:   color-mix(in oklch, var(--warn)   38%, var(--line));
    --bad-fill:    color-mix(in oklch, var(--bad)    12%, var(--panel));
    --bad-line:    color-mix(in oklch, var(--bad)    38%, var(--line));
    --info-fill:   color-mix(in oklch, var(--info)   12%, var(--panel));
    --info-line:   color-mix(in oklch, var(--info)   38%, var(--line));
    --accent-fill: color-mix(in oklch, var(--accent) 12%, var(--panel));
    --accent-line: color-mix(in oklch, var(--accent) 38%, var(--line));
  }
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
      --accent:    oklch(52% 0.17 var(--org-hue));
      --on-accent: oklch(99% 0.002 var(--org-hue));
      --bg:      oklch(97% 0.004 var(--org-hue));
      --panel:   oklch(99.5% 0.002 var(--org-hue));
      --panel-2: oklch(95% 0.006 var(--org-hue));
      --line:    oklch(89% 0.008 var(--org-hue));
      --ok:   oklch(50% 0.15 148);
      --warn: oklch(50% 0.12 80);
      --bad:  oklch(50% 0.19 27);
      --info: oklch(50% 0.17 300);
    }
  }
  :root[data-theme="light"] {
    --accent:    oklch(52% 0.17 var(--org-hue));
    --on-accent: oklch(99% 0.002 var(--org-hue));
    --bg:      oklch(97% 0.004 var(--org-hue));
    --panel:   oklch(99.5% 0.002 var(--org-hue));
    --panel-2: oklch(95% 0.006 var(--org-hue));
    --line:    oklch(89% 0.008 var(--org-hue));
    --ok:   oklch(50% 0.15 148);
    --warn: oklch(50% 0.12 80);
    --bad:  oklch(50% 0.19 27);
    --info: oklch(50% 0.17 300);
  }
}
/* == /tokens == */

* { box-sizing: border-box; }

/* The scrollbar's width is reserved whether or not there is one to show.
   The flow boards size themselves to the height that is left in the window, so
   a board growing is exactly what tips a short page into having a scrollbar —
   which takes width away, re-wraps the lanes, changes the height again, and
   the page shivers sideways as well as down. Reserving the gutter takes that
   loop out of existence rather than damping it. Costs nothing where the
   platform draws overlay scrollbars, which is where the Human reads this. */
html { scrollbar-gutter: stable; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg); color: var(--ink);
  font-family: var(--sans);
  font-size: 15px; line-height: 1.45;
  -webkit-text-size-adjust: 100%;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code, .mono { font-family: var(--mono); font-size: 0.86em; }

/* ---------------------------------------------------------------- shell */

/* header.top and the crumb bar under it stick together as one unit -- see
   .shell-top below. Sticking them independently with a hardcoded pixel offset
   between them (the approach before) breaks the moment the header wraps to a
   second line, which it does on a narrow phone once the live indicator and
   the header buttons no longer fit beside the brand. The tab row that used to
   sit here was retired on 2026-09-02: the map is the home, and the screens
   it does not carry open from the Screens menu (.screens). */
header.top {
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  border-bottom: 1px solid var(--line);
  padding: 8px 12px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.brand { font-weight: 700; letter-spacing: -0.2px; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.brand span { color: var(--faint); font-weight: 400; }
/* The mark (brand/README.md): the Human's square is ink; it turns accent
   when the Desk holds a decision, the same meaning blue has everywhere here. */
.brand .brand-mark { width: 18px; height: 18px; flex: none; color: var(--ink); }
.brand .brand-mark .brand-human { fill: currentColor; transition: fill var(--t-med) var(--ease); }
.brand.desk-waiting .brand-mark .brand-human { fill: var(--accent); }
/* Archivo's Solo-500 / Inc-800 at -3% tracking, in the system face: the
   console ships no web font by its own CSP rule, so the wordmark here is
   the one place it is not Archivo (brand/README.md says so on purpose). */
.brand .brand-word { color: var(--ink); font-weight: 500; letter-spacing: -0.03em; }
.brand .brand-word b { font-weight: 800; color: var(--accent); }
/* The brand is the way home: a button that looks like the wordmark. */
button.brand { background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit; font-weight: 700; }
button.brand:hover span { color: var(--dim); }
.grow { flex: 1 1 auto; }

.live {
  font-size: 11px; color: var(--faint);
  display: inline-flex; align-items: center; gap: 5px;
}
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); }
.dot.on { background: var(--ok); }
.dot.off { background: var(--bad); }

.shell-top { position: sticky; top: 0; z-index: 40; }

/* The Screens menu: a popover hung off the header's right edge. Two columns
   so eleven screens fit under a thumb without scrolling. */
.screens {
  position: absolute; right: 8px; top: 100%; z-index: 41;
  display: grid; grid-template-columns: repeat(2, minmax(128px, 1fr)); gap: 2px;
  padding: 6px; margin-top: 4px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
  max-width: min(440px, calc(100vw - 16px));
}
.screens[hidden] { display: none !important; }
.screens button {
  background: none; border: 0; color: var(--dim); text-align: left;
  padding: 8px 12px; border-radius: 6px; cursor: pointer;
  font: inherit; font-size: 13px; white-space: nowrap;
}
.screens button:hover { background: var(--panel-2); color: var(--ink); }
.screens button[aria-current="page"] { background: var(--accent); color: var(--on-accent); font-weight: 600; }
#btn-screens[aria-expanded="true"] { background: var(--panel-2); color: var(--ink); }

/* Off the map: where you are, and the way back. Hidden on the map itself. */
.screen-crumb {
  display: flex; align-items: center; gap: 10px; padding: 6px 12px;
  border-bottom: 1px solid var(--line); background: var(--panel); font-size: 13px;
}
.screen-crumb[hidden] { display: none !important; }

/* What this console is serving, when that is not main. Deliberately the full
   width of the header and in the warning colour: the whole point is that it
   cannot be mistaken for part of a screen, because every screen below it is
   the thing being called into question. */
.serving-warn {
  padding: 7px 12px; font-size: 12.5px; line-height: 1.45;
  background: var(--warn-fill); border-bottom: 1px solid var(--warn-line);
}
.serving-warn b { font-weight: 620; }
.serving-warn code {
  font-size: 12px; padding: 0 4px; border-radius: 3px;
  background: color-mix(in srgb, var(--warn) 16%, transparent);
}
.serving-warn[hidden] { display: none !important; }

.pip {
  display: inline-block; min-width: 16px; padding: 0 4px; margin-left: 5px;
  border-radius: 8px; background: var(--bad); color: var(--on-tone);
  font-size: 10px; font-weight: 700; text-align: center;
}

main { padding: 12px; max-width: 1500px; margin: 0 auto; }
section[hidden] { display: none !important; }

/* ---------------------------------------------------------------- panels */

.grid { display: grid; gap: 10px; grid-template-columns: 1fr; }
@media (min-width: 720px)  { .grid.two   { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1040px) { .grid.three { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1040px) { .grid.four  { grid-template-columns: repeat(4, 1fr); } }

.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 10px; padding: 12px; min-width: 0;
}
.panel h2 {
  margin: 0 0 9px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint); font-weight: 700;
}
.panel h3 { margin: 14px 0 6px; font-size: 13px; color: var(--dim); }

.stat { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; }
.stat.sm { font-size: 19px; }
.sub { color: var(--faint); font-size: 12px; }
.note {
  color: var(--dim); font-size: 12px; background: var(--panel-2);
  border-left: 2px solid var(--line); padding: 7px 9px;
  border-radius: 0 6px 6px 0; margin: 8px 0 0;
}
.note.warn { border-left-color: var(--warn); }
.note.bad  { border-left-color: var(--bad); }
.note.ok   { border-left-color: var(--ok); }

/* ---------------------------------------------------------------- bits */

.tag {
  display: inline-block; padding: 1px 7px; border-radius: 10px;
  font-size: 11px; font-weight: 600; border: 1px solid var(--line);
  color: var(--dim); background: var(--panel-2); white-space: nowrap;
}
.tag.ok   { color: var(--ok);   border-color: var(--ok-line); background: var(--ok-fill); }
.tag.warn { color: var(--warn); border-color: var(--warn-line); background: var(--warn-fill); }
.tag.bad  { color: var(--bad);  border-color: var(--bad-line); background: var(--bad-fill); }
.tag.info { color: var(--info); border-color: var(--info-line); background: var(--info-fill); }
.tag.dim  { color: var(--faint); }

/* A tag that is a control rather than a label. Without this a filter chip and
   a caption look identical, which is the bug: the GitHub tab's chips read as
   filters, so they now behave as filters and say so by hovering and lighting. */
.tag.click { cursor: pointer; user-select: none; }
.tag.click:hover  { border-color: var(--dim); color: var(--ink); }
.tag.click:focus-visible { outline: 2px solid var(--info); outline-offset: 1px; }
.tag.click.on { color: var(--ink); border-color: var(--info); background: var(--info-fill); }

.bar { height: 7px; border-radius: 4px; background: var(--panel-2); overflow: hidden; margin: 6px 0 3px; }
.bar > i { display: block; height: 100%; width: var(--pct, 0%); background: var(--ok); }
.bar.warn > i { background: var(--warn); }
.bar.bad  > i { background: var(--bad); }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; }
tbody tr:hover { background: var(--panel-2); }
td.num, th.num { text-align: right; font-family: var(--mono); }

/* Sortable column headers. The arrow is always present -- a marker that only
   appears on the active column gives the reader nothing to click towards. */
th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable:hover, th.sortable:focus-visible { color: var(--accent); }
th.sortable .arrow { margin-left: 5px; font-size: 9px; opacity: 0.35; }
th.sortable.sorted { color: var(--accent); }
th.sortable.sorted .arrow { opacity: 1; }
th.sortable.num { text-align: right; }
.scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

button, .btn {
  font: inherit; font-size: 13px; cursor: pointer;
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line); border-radius: 7px; padding: 5px 11px;
}
button:hover { border-color: var(--accent); }
button:disabled { opacity: 0.45; cursor: not-allowed; }
button.primary, .btn.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); font-weight: 600; }
button.danger  { background: var(--bad-fill); color: var(--bad); border-color: var(--bad-line); }
button.tiny { padding: 2px 7px; font-size: 11px; }

/* One refresh control per screen -- see injectScreenBar() in app.js. Sits
   flush right above whatever the screen renders, so it is always in the same
   place no matter which of the 14 tabs (or 4 settings sub-views) is open. */
.screen-bar { display: flex; justify-content: flex-end; margin: 0 0 8px; }
.screen-refresh.spin { animation: console-spin 0.6s linear infinite; }
@keyframes console-spin { to { transform: rotate(360deg); } }

/* An <a> wearing .btn: the OAuth sign-in has to be a real link so the browser
   opens it in a new tab, but it should read as the primary action it is. */
a.btn { display: inline-block; text-decoration: none; }
a.btn:hover { border-color: var(--accent); }
summary { cursor: pointer; }
.row { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }

input, select, textarea {
  font: inherit; font-size: 13px; background: var(--bg); color: var(--ink);
  border: 1px solid var(--line); border-radius: 7px; padding: 6px 9px; width: 100%;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
textarea { font-family: var(--mono); font-size: 12px; line-height: 1.5; resize: vertical; min-height: 120px; }
/* iOS Safari zooms the whole page on focus of any text input under 16px --
   below the threshold every ordinary form on the console (gate, rule, hw,
   sched-cron, editor sheets) was sitting at 13px. #code-body is exempted
   deliberately (see its own comment): a monospace editor that reflows on
   zoom is worse than one that stays small. */
@media (max-width: 719px) {
  input, select, textarea:not(#code-body) { font-size: 16px; }
}
label.field { display: block; margin: 9px 0; }
/* `hidden` has to actually hide these two. Both set `display` themselves, and
   a rule with a class in it beats the user agent's `[hidden] { display: none }`
   -- which is how the step-up sheet, asked to drop its code field for a plain
   yes/no confirmation, would otherwise keep showing it. */
.row[hidden], label.field[hidden] { display: none !important; }
label.field > span { display: block; font-size: 12px; color: var(--faint); margin-bottom: 3px; }

/* The model picker sits in a table cell on Workloads, Schedule and Org, where the
   full-width select above would stretch the column to the widest option and
   push the rest of the row off screen. It also has to stay legible as a
   CONTROL rather than reading like the static tag it replaced, so it keeps a
   visible border and gains a pointer cursor. */
select.modelpick {
  width: auto; max-width: 100%; padding: 2px 6px; font-size: 12px;
  font-family: var(--mono); cursor: pointer;
}
select.modelpick:disabled { opacity: 0.55; cursor: progress; }

/* ---------------------------------------------------------------- variables */

.var {
  border-bottom: 1px solid var(--line); padding: 9px 2px;
  display: grid; gap: 6px; grid-template-columns: 1fr;
}
@media (min-width: 900px) { .var { grid-template-columns: 1fr 1.1fr; align-items: start; } }
.var .k { font-family: var(--mono); font-size: 12px; word-break: break-all; }
.var .meta { font-size: 11px; color: var(--faint); margin-top: 2px; }
.var .val { display: flex; gap: 6px; align-items: flex-start; }
.var .val > :first-child { flex: 1 1 auto; min-width: 0; }
.var.locked .k { color: var(--faint); }
.var.isnew { background: var(--accent-fill); }

/* ---------------------------------------------------------------- overlays */

.sheet {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,0.72);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 6vh 12px 12px; overflow-y: auto;
}
.sheet[hidden] { display: none !important; }
/* Step-up must win every stacking fight. Every mutation routes through it
   (index.html's own comment on #stepup says so), and several call sites open
   it from INSIDE an already-open sheet (e.g. openWorkloadForm's Save button
   calls mutateWorkload -> stepUp() without closing #roster first — unlike
   laneFormSave, which closes its sheet first for exactly this reason). Both
   #stepup and #roster share the plain .sheet z-index (90), so DOM order
   decided the winner: #roster is later in index.html and painted over
   #stepup, hiding "Confirm this change" and the Telegram-send button behind
   a form that then sat there unresponsive, mid-await, with no visible way
   out short of a reload. One rule here fixes it at the single choke point
   every mutation passes through, rather than auditing every call site that
   opens a sheet without closing the one already open. */
#stepup { z-index: 95; }
.sheet > .box {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 16px; width: 100%; max-width: 700px;
}
.sheet > .box.wide { max-width: 1000px; }
.sheet h2 { margin-top: 0; }
.sheet-grab { display: none; }
/* On the handheld the sheet becomes a bottom sheet: the thumb lives at the
   bottom of the screen, and a centered box means reaching. Transform-only
   entrance, per the motion budget. */
@media (max-width: 719px) {
  .sheet { align-items: flex-end; padding: 10vh 0 0; }
  .sheet > .box {
    max-width: none; border-radius: var(--r4, 12px) var(--r4, 12px) 0 0;
    border-bottom: 0; max-height: 90dvh; overflow-y: auto;
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
    animation: sheet-up var(--t-med) var(--ease) 1;
  }
  .sheet-grab {
    display: block; width: 36px; height: 4px; border-radius: var(--r-pill);
    background: var(--line); margin: 0 auto 10px;
  }
}
@keyframes sheet-up { from { transform: translateY(24px); opacity: 0.6; }
                      to { transform: none; opacity: 1; } }

/* The code editor.
 *
 * Full-height on purpose. Editing a 600-line Python file through a small box is
 * what makes people give up and reach for ssh, which defeats the point of
 * having the editor here at all. */
.sheet.code { padding: 0; align-items: stretch; }
.sheet.code > .box.full {
  max-width: none; width: 100%; border-radius: 0; border: 0;
  display: flex; flex-direction: column; padding: 8px 10px;
  height: 100dvh;
}
.codebar { flex: 0 0 auto; padding: 4px 0; gap: 8px; }
#code-body {
  flex: 1 1 auto; min-height: 0; resize: none;
  font-family: var(--mono); font-size: 13px; line-height: 1.5;
  /* 16px on the phone would be kinder to the eye, but a monospace editor that
   * reflows on zoom is worse than a small one. wrap="off" plus horizontal
   * scroll keeps column numbers meaningful, which is what a syntax error's
   * "line 41" needs to point at. */
  tab-size: 4; white-space: pre; overflow: auto;
}
#code-body.bad { outline: 2px solid var(--bad); outline-offset: -1px; }
#code-body.good { outline: 2px solid var(--ok); outline-offset: -1px; }

.filerow {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 4px; border-bottom: 1px solid var(--line); cursor: pointer;
}
.filerow:hover { background: var(--panel-2); }
.filerow .p { font-family: var(--mono); font-size: 12px; word-break: break-all; }
.filerow .s { color: var(--faint); font-size: 11px; white-space: nowrap; }

.palette input { font-size: 16px; padding: 10px; }
.palette ul { list-style: none; margin: 10px 0 0; padding: 0; max-height: 55vh; overflow-y: auto; }
.palette li { padding: 7px 9px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.palette li:hover, .palette li.on { background: var(--panel-2); }
.palette li .k { font-family: var(--mono); font-size: 12px; }
.palette li .g { color: var(--faint); font-size: 11px; }

#toasts { position: fixed; right: 10px; bottom: 10px; z-index: 100; display: grid; gap: 7px; max-width: min(440px, 92vw); }
.toast {
  background: var(--panel-2); border: 1px solid var(--line); border-left-width: 3px;
  border-radius: 8px; padding: 9px 11px; font-size: 13px;
}
.toast.ok  { border-left-color: var(--ok); }
.toast.bad { border-left-color: var(--bad); }
/* A warning toast is not a failure: the write succeeded and something
   about the result still needs saying -- a lane changed while running,
   for instance, which keeps its old settings until it is restarted. */
.toast.warn { border-left-color: var(--warn); }
.toast .x { float: right; cursor: pointer; color: var(--faint); }

/* ---------------------------------------------------------------- login */

.gate { max-width: 430px; margin: 12vh auto; padding: 0 14px; }
.gate .panel { padding: 20px; }
/* The wordmark rule, unchanged from the header: Solo in ink at 500, Inc in
   the accent at 800. orgs.js replaces this heading with the cell's label
   once there is one, and a label is a name rather than the wordmark, so it
   correctly arrives as plain text. */
.gate h1 { font-size: 20px; margin: 0 0 4px; text-align: center;
           letter-spacing: -0.03em; font-weight: 500; }
.gate h1 b { font-weight: 800; color: var(--accent); }
.gate > .panel > .sub { text-align: center; }
/* The stacked lockup: mark over name, with the brand's clear space (the width
   of the Human's square, 28% of the mark's height) under it. The Human lights
   accent here for the same reason it does in the header — but only once there
   is a session to have a Desk, so on the gate it stays ink. */
.gate-mark { display: flex; justify-content: center; margin: 2px 0 12px; color: var(--ink); }
.gate-mark .brand-mark { width: 44px; height: 44px; flex: none; }
.gate-mark .brand-human { fill: currentColor; }
/* The first run wears the same mark beside its heading. */
.onboard-head .brand-mark { width: 22px; height: 22px; flex: none; color: var(--ink); }
.onboard-head .brand-human { fill: currentColor; }

/* The first run (PR 12): a rail of steps beside the step's body, the
   Portfolio preview under it. The same panel look as the gate. */
.onboard { max-width: 1100px; margin: 4vh auto; padding: 0 14px; }
.onboard-wrap { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 18px; }
.onboard-kicker { margin: 0 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--faint); font-weight: 700; }
.onboard-head h1 { font-size: 20px; margin: 0 8px 0 0; }
.onboard-head h1 b { font-weight: 800; color: var(--accent); }
.onboard-grid { display: grid; grid-template-columns: 220px 1fr; gap: 16px; margin-top: 12px; }
@media (max-width: 720px) { .onboard-grid { grid-template-columns: 1fr; } }
.onboard-rail { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.onboard-step { padding: 6px 8px; border-radius: 8px; border: 1px solid transparent; cursor: pointer; }
.onboard-step:hover { border-color: var(--line); }
.onboard-step.on { border-color: var(--accent); background: var(--bg); }
.onboard-step.done b::after { content: ' ✓'; color: var(--ok); }
.onboard-step.skipped { opacity: .6; }
.onboard-step.locked { opacity: .5; cursor: not-allowed; }
.onboard-step .sub { display: block; font-size: 11px; }
.onboard-body h2 { margin: 0 0 6px; font-size: 17px; }
.onboard-nav { margin-top: 10px; }
.onboard-factor { margin-top: 12px; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
.onboard-factor h3 { margin: 0 0 4px; font-size: 14px; }
.onboard-factor pre.mono { font-size: 15px; letter-spacing: .08em; user-select: all; }

/* The QR code and the key it duplicates. The frame is white in both themes
   on purpose -- a scanner wants dark modules on a light ground, and the
   quiet zone around the symbol is part of the code, not padding. */
.qr-frame { display: inline-block; background: #fff; border: 1px solid var(--line);
            border-radius: 8px; padding: 8px; line-height: 0; }
.qr { display: block; image-rendering: pixelated; }
.enroll-seed { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
.enroll-seed > div { flex: 1 1 220px; }
.enroll-seed pre.mono { font-size: 15px; letter-spacing: .08em; user-select: all; }
.onboard-preview { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.onboard-contract { max-height: 40vh; overflow: auto; white-space: pre-wrap; font-size: 12px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px; }
.onboard-contract.rendered { white-space: normal; font-size: 13px; }
.onboard-contract.rendered h1 { font-size: 16px; margin: 0 0 8px; }
.onboard-contract.rendered h2 { font-size: 14px; margin: 12px 0 4px; }

/* The pipeline wizard wears the first run's clothes -- every `onboard-*`
   class above is its own -- but it opens OVER the map instead of instead of
   it: the first run happens before there is a map to cover, and this one is
   started from a box on one. So the only thing added here is the covering,
   and the look inside it is unchanged. */
.wizard-over { position: fixed; inset: 0; z-index: 60; overflow: auto; background: var(--bg);
               margin: 0; padding: 4vh 14px; }
.wizard-over > .onboard-wrap { max-width: 1100px; margin: 0 auto; }

pre.doc {
  white-space: pre-wrap; word-break: break-word; font-family: var(--mono);
  font-size: 12px; line-height: 1.55; background: var(--bg);
  border: 1px solid var(--line); border-radius: 8px; padding: 11px;
  max-height: 62vh; overflow-y: auto; margin: 0;
}

/* Rendered markdown view for docs (openReaderDoc / mdToHtml) -- same box
   treatment as pre.doc, prose typography instead of monospace. */
.doc-rendered {
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  padding: 14px 18px; max-height: 62vh; overflow-y: auto;
  font-family: var(--sans); font-size: 13.5px; line-height: 1.6; color: var(--ink);
}
.doc-rendered > :first-child { margin-top: 0; }
.doc-rendered h1, .doc-rendered h2, .doc-rendered h3,
.doc-rendered h4, .doc-rendered h5, .doc-rendered h6 {
  margin: 1.3em 0 0.5em; line-height: 1.3; font-weight: 600; color: var(--ink);
}
.doc-rendered h1 { font-size: 1.5em; border-bottom: 1px solid var(--line); padding-bottom: 0.3em; }
.doc-rendered h2 { font-size: 1.25em; border-bottom: 1px solid var(--line); padding-bottom: 0.25em; }
.doc-rendered h3 { font-size: 1.1em; }
.doc-rendered h4, .doc-rendered h5, .doc-rendered h6 { font-size: 1em; }
.doc-rendered p { margin: 0.7em 0; }
.doc-rendered strong { font-weight: 700; }
.doc-rendered em { color: var(--dim); }
.doc-rendered code {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 4px;
  padding: 0.1em 0.35em; font-family: var(--mono); font-size: 0.88em;
}
.doc-rendered pre.md-code {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px;
  padding: 10px; overflow-x: auto; margin: 0.8em 0;
}
.doc-rendered pre.md-code code { background: none; border: none; padding: 0; }
.doc-rendered blockquote {
  margin: 0.8em 0; padding: 0.2em 0 0.2em 12px;
  border-left: 3px solid var(--accent-line); color: var(--dim);
}
.doc-rendered blockquote > :first-child { margin-top: 0; }
.doc-rendered blockquote > :last-child { margin-bottom: 0; }
.doc-rendered ul, .doc-rendered ol { margin: 0.6em 0; padding-left: 1.6em; }
.doc-rendered li { margin: 0.25em 0; }
.doc-rendered hr { border: none; border-top: 1px solid var(--line); margin: 1.4em 0; }
.doc-rendered a { color: var(--accent); }
.doc-rendered .md-table-wrap { overflow-x: auto; margin: 0.8em 0; }
.doc-rendered table { border-collapse: collapse; width: 100%; font-size: 0.92em; }
.doc-rendered th, .doc-rendered td {
  border: 1px solid var(--line); padding: 6px 9px; text-align: left; vertical-align: top;
}
.doc-rendered th { background: var(--panel-2); font-weight: 600; }
/* The Floor's per-card tail: same box, sized to sit inside a grid card
   rather than a full-page reader. */
pre.doc.floor-tail { max-height: 160px; font-size: 11px; margin-top: 6px; }
.floor-card .row { align-items: center; }
.muted { color: var(--faint); }
.right { text-align: right; }
.nowrap { white-space: nowrap; }
.spinner { color: var(--faint); font-size: 13px; padding: 14px 2px; }

/* ---------------------------------------------------------------- accounts */

/* The API switch borrows the warning colour when it is ON, because on is the
   state that can cost money and off is the safe resting position. Every other
   stat on the console reads the opposite way, so it is stated here rather than
   left for someone to infer from a colour. */
.stat.warn { color: var(--warn); }
/* The schedule editor's preview. The whole reason the editor exists is that a
   cron is unreadable, so the answer it produces gets more visual weight than
   the field that produced it. */
.preview { margin: 10px 0; padding: 10px 12px; border-radius: 8px;
           background: var(--panel-2, rgba(255,255,255,.04));
           border: 1px solid var(--line, rgba(255,255,255,.10)); }
.preview .fire { font-variant-numeric: tabular-nums; display: block;
                 padding: 1px 0; }
.preview .head { font-size: 12px; text-transform: uppercase;
                 letter-spacing: .06em; opacity: .65; margin-bottom: 4px; }
.row.wrap { flex-wrap: wrap; gap: 8px; }
.row.wrap .field { min-width: 130px; flex: 1 1 130px; }

/* Drift, inline in a table row. `.sub` alone renders it as another grey note,
   which is the wrong reading: launchd holding a different calendar from the
   manifest is a discrepancy, not a footnote. */
.sub.warn { color: var(--warn); }
.sub.bad  { color: var(--bad); }

/* The account a seat resolves to. Not grey, because it is the answer to
   "whose allowance does this spend" rather than a caption on the label above
   it -- and on the machine sign-in it is the only line on the card that names
   an account at all. */
.sub.who { color: var(--ink); }

/* ------------------------------------------------------------- hardware
 *
 * The flow diagram. Three things drove the shape of this:
 *
 * 1. IT IS READ ON A PHONE, in a hotel, usually because something is wrong.
 *    So the diagram scrolls horizontally inside its own box rather than
 *    reflowing -- a power chain that wraps onto three lines is no longer a
 *    picture of a power chain -- and every node is legible at the width of a
 *    handheld without pinching.
 *
 * 2. MOTION CARRIES ONE MEANING AND ONLY ONE: this link is carrying right now.
 *    A dead link is static. That is the whole vocabulary, which is why the
 *    cold standby is drawn still: it is not carrying, and a standby animated
 *    like a live path would be the most misleading thing on the page.
 *
 * 3. SOMEBODY WILL READ THIS WITH REDUCED MOTION ON. The animation is a
 *    progressive enhancement over a diagram that is already complete and
 *    already colour-coded without it.
 */

.hw-map {
  position: relative;
  overflow-x: auto; overflow-y: hidden;
  background: var(--panel);
  border: 1px solid var(--line); border-radius: 10px;
  padding: 6px;
}
.hw-map svg { display: block; min-width: 760px; }

.hw-edge      { fill: none; stroke-width: 1.6; opacity: 0.5; }
.hw-edge-flow { fill: none; stroke-width: 2.2; stroke-linecap: round; opacity: 0.95; }
.hw-edge-hit  { fill: none; stroke: transparent; stroke-width: 16; cursor: pointer; }

/* The flow itself. The dash pattern travels along the path, so direction is
   read from the movement rather than from an arrowhead nobody can see at
   phone size. Arrowheads are drawn too, for the static case. */
@keyframes hw-flow { to { stroke-dashoffset: -28; } }
.hw-edge-flow.moving {
  stroke-dasharray: 5 23;
  animation: hw-flow 1.1s linear infinite;
}
.hw-edge-flow.moving.slow { animation-duration: 2.2s; }
.hw-edge-flow.still { stroke-dasharray: 3 6; opacity: 0.4; }

.hw-node-box { stroke-width: 1.4; rx: 8; }
.hw-node-label { font: 600 12px var(--sans); fill: var(--ink); }
.hw-node-note  { font: 10px var(--mono); fill: var(--dim); }
.hw-node-foot  { font: 9.5px var(--mono); fill: var(--faint); }
.hw-edge-label { font: 9px var(--sans); fill: var(--faint); }
.hw-node-g { cursor: pointer; }
.hw-node-g:hover .hw-node-box { stroke-width: 2.4; }
.hw-node-g:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
.hw-node-g.selected .hw-node-box { stroke-width: 2.6; }

/* A slow pulse on anything that is not well. The eye finds it before it has
   read a single label, which is the point on a page opened in an emergency. */
@keyframes hw-pulse { 50% { opacity: 0.35; } }
.hw-alarm { animation: hw-pulse 1.6s ease-in-out infinite; }

.hw-legend {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  margin-top: 8px; font-size: 11px; color: var(--faint);
}
.hw-legend i {
  display: inline-block; width: 18px; height: 0; vertical-align: middle;
  border-top: 2px solid currentColor; margin-right: 5px;
}


.hw-card { border: 1px solid var(--line); border-radius: 10px;
           background: var(--panel); padding: 10px 12px; }
.hw-card.on { border-color: var(--info); }
.hw-card h3 { margin: 0 0 2px; font-size: 14px; display: flex;
              align-items: center; gap: 7px; }
.hw-card .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.hw-metrics { display: grid; grid-template-columns: auto 1fr; gap: 2px 12px;
              margin: 8px 0 0; font-size: 12px; }
.hw-metrics dt { color: var(--faint); }
.hw-metrics dd { margin: 0; font-family: var(--mono); font-size: 11px;
                 text-align: right; overflow-wrap: anywhere; }
/* The rows behind a tap. Left-aligned and proportional rather than right-
   aligned monospace: a metric value is a token compared down a column, a
   detail value is a sentence or an object key, and a ragged right-aligned
   paragraph is unreadable on a phone. */
.hw-detail { border-top: 1px solid var(--line); margin-top: 8px;
             padding-top: 8px; gap: 4px 12px; }
.hw-detail dd { text-align: left; font-family: inherit; font-size: 12px;
                line-height: 1.45; }
.hw-more { margin-top: 8px; }
.hw-head-tap { cursor: pointer; }
.hw-head-tap:hover span:not(.tag) { color: var(--fg); }

.hw-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.hw-actions button.danger { border-color: var(--bad-line); color: var(--bad); }
.hw-actions button.danger:hover:not(:disabled) { background: var(--bad-fill); }
.hw-actions button:disabled { opacity: 0.45; cursor: not-allowed; }

/* A panel a card raises inside itself -- the sign-in, or a paste box for a
   credential. Set off with a rule and an indent rather than a box, so it reads
   as this card's own work rather than as a second card that has appeared
   inside the first one. */
.hw-panel { margin-top: 10px; padding-top: 10px; padding-left: 10px;
            border-top: 1px solid var(--line); border-left: 2px solid var(--info); }
.hw-panel input { width: 100%; }

/* Recommendations. A left border in the severity colour rather than a filled
   card: the panel can hold a dozen of these on a phone, and a dozen coloured
   blocks is a page nobody can find the urgent one in. The bar carries the
   severity, the verdict chip carries the action, and the text stays readable. */
.hw-rec { border-left: 3px solid var(--line); border-radius: 0 8px 8px 0;
          background: var(--panel); padding: 8px 12px; margin-top: 8px; }
.hw-rec.bad  { border-left-color: var(--bad); }
.hw-rec.warn { border-left-color: var(--warn); }
.hw-rec.opt  { border-left-color: var(--info); }
/* Confirmed-good and declined rows are deliberately quiet. They are reference,
   not work, and they must not compete with a red row above them. */
.hw-rec.keep { border-left-color: var(--faint); opacity: 0.82; }
.hw-rec-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
               font-size: 13px; }
.hw-rec-head .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.hw-rec p { margin: 5px 0 0; }

/* Inbound hardware. The countdown is the one number on this panel still worth
   deciding about -- the money is already spent -- so it is the one that gets
   the size. Everything else is ordinary body text. */
.hw-countdown { display: flex; align-items: center; gap: 12px; margin-top: 8px; }
.hw-days { font: 700 30px/1 var(--mono); color: var(--info); }
.hw-days.warn { color: var(--warn); }
.hw-days.bad  { color: var(--bad); }
.hw-days.ok   { color: var(--ok); }

/* A phase heading, and the steps under it. A phase the machine has not reached
   is drawn at reduced contrast rather than hidden: it is genuinely coming, and
   somebody planning a trip needs to be able to read ahead. */
.hw-phase { display: flex; align-items: center; gap: 7px; font-size: 12px;
            color: var(--dim); margin: 14px 0 2px; }
.hw-phase.live { color: var(--ink); }
.hw-steps.dim { opacity: 0.6; }
.hw-step { border-left: 2px solid var(--line); border-radius: 0 8px 8px 0;
           background: var(--panel); padding: 8px 12px; margin-top: 6px; }
.hw-step.blocking { border-left-color: var(--warn); }
.hw-step.live.blocking { border-left-width: 3px; }
.hw-step p { margin: 5px 0 0; }

.hw-verdict { font: 600 9.5px var(--mono); letter-spacing: 0.06em;
              padding: 2px 5px; border-radius: 4px; flex: 0 0 auto;
              border: 1px solid var(--line); color: var(--dim); }
.hw-verdict.add     { color: var(--ok);   border-color: var(--ok-line); }
.hw-verdict.change  { color: var(--warn); border-color: var(--warn-line); }
.hw-verdict.upgrade { color: var(--info); border-color: var(--info-line); }
.hw-verdict.delete  { color: var(--bad);  border-color: var(--bad-line); }
.hw-verdict.keep    { color: var(--faint); }

/* The remedy is the line somebody copies, so it is monospace and it wraps
   rather than scrolling: a command that runs off the edge of a phone is a
   command that gets mistyped. */
.hw-remedy { font-family: var(--mono); font-size: 11.5px; color: var(--ink);
             background: var(--panel-2); border-radius: 6px;
             padding: 6px 8px; overflow-wrap: anywhere; white-space: pre-wrap; }

.hw-price { font: 600 12px var(--mono); color: var(--ok); flex: 0 0 auto; }
.hw-price.dim { color: var(--faint); font-weight: 400; }

.hw-set { display: flex; align-items: baseline; gap: 8px; padding: 7px 0;
          border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.hw-set:last-child { border-bottom: 0; }
.hw-set .k { color: var(--dim); font-size: 12px; min-width: 168px; }
.hw-set .v { font-family: var(--mono); font-size: 12px; }

/* reduced-motion: unified at end of file */

/* -- the value chain ---------------------------------------------------- */

.chain { display: flex; gap: 0; align-items: stretch; min-width: min-content; }

.chainstep {
  position: relative;
  flex: 0 0 190px;
  padding: 10px 12px 10px 14px;
  margin-right: 12px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--ok);
  border-radius: 8px;
  background: var(--panel-2);
}

/* The arrow between steps is drawn, not typed, so that a step inserted in the
   middle of the chain does not need anybody to move a character. */
.chainstep::after {
  content: '';
  position: absolute; right: -9px; top: 50%;
  width: 0; height: 0;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-left: 6px solid var(--line);
  transform: translateY(-50%);
}
.chain > .chainstep:last-child { margin-right: 0; }
.chain > .chainstep:last-child::after { display: none; }

/* A break in the chain is a dashed box, not merely a red one. The colour says
   "bad" to a reader who can see it; the dashes say "nothing is here" to one who
   cannot, and this is the single most important state on the page. */
.chainstep.broken {
  border-style: dashed;
  border-left-color: var(--bad);
  background: transparent;
}
.chainstep.broken .chainstep-name { color: var(--bad); }

/* A phase the contract reserves to the Human is not a break and must not read as
   one. Solid, and marked in the informational colour rather than the bad one:
   colouring CLAUDE.md sections 1 and 3 as a defect teaches the eye to skip the
   colour that means something is genuinely missing. */
.chainstep.owners { border-left-color: var(--info); }

.chainstep-n {
  font-family: var(--mono); font-size: 10px; color: var(--faint);
}
.chainstep-name { font-weight: 600; font-size: 14px; margin-bottom: 3px; }
.chainstep-arc { font-family: var(--mono); font-size: 10px; color: var(--faint);
                 text-transform: uppercase; letter-spacing: 0.06em;
                 margin-top: 6px; }

/* The two axes, stacked and labelled, on every card without exception. The
   panel this replaced showed one of them and read as the whole answer. */
.chainstep-axes { margin-top: 6px; display: grid; gap: 3px; }
.chainstep-axes .row { gap: 5px; align-items: baseline; flex-wrap: wrap; }
.chainstep-lbl { font-family: var(--mono); font-size: 9px; color: var(--faint);
                 text-transform: uppercase; letter-spacing: 0.06em;
                 flex: 0 0 26px; }

/* A tag that is also a button — the seat on a chain card opens that position,
   so an open seat reaches its hire form in one tap from the finding. */
.tag.linky { cursor: pointer; border-style: solid; font: inherit;
             font-size: 10px; }
.tag.linky:hover { filter: brightness(1.25); }
.tag.linky:focus-visible { outline: 2px solid var(--info); outline-offset: 1px; }

/* -- open positions ----------------------------------------------------- */

.req {
  border: 1px dashed var(--line);
  border-left: 3px solid var(--warn);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 9px;
}
.req-pri {
  font-family: var(--mono); font-size: 11px; color: var(--faint);
  min-width: 22px;
}

/* -- division cards ----------------------------------------------------- */

.divcard { border-left: 3px solid var(--divhue, var(--line)); }
.divcard h2 { color: var(--divhue, var(--dim)); }

/* ---- the product cycle ------------------------------------------------
   Four arcs of phases, read left to right, wrapping to a column on a phone.
   Grid rather than flex on the arc row so the four groups keep a shared width
   and the diagram stays readable as a sequence rather than reflowing into a
   ragged pile the moment one arc has a longer phase name than another. */
.lc-track { display: flex; align-items: stretch; gap: 8px;
            flex-wrap: wrap; margin-top: 10px; }
.lc-arc { flex: 1 1 190px; min-width: 0; border: 1px solid var(--line);
          border-radius: 10px; background: var(--panel-2); padding: 8px 9px 9px; }
.lc-arc-head { display: flex; align-items: center; gap: 6px; font-size: 11px;
               color: var(--dim); text-transform: uppercase;
               letter-spacing: 0.06em; margin-bottom: 6px; }
.lc-arc-n { display: inline-flex; align-items: center; justify-content: center;
            width: 15px; height: 15px; border-radius: 50%; flex: 0 0 auto;
            border: 1px solid var(--line); background: var(--panel);
            font-family: var(--mono); font-size: 10px; color: var(--faint); }
.lc-arrow { align-self: center; color: var(--faint); font-size: 13px; }
.lc-phases { display: grid; gap: 4px; }

/* A phase is a button and its name has to survive being one on a handheld, so
   the name wraps and the coverage line sits under it rather than beside. */
.lc-phase { display: grid; grid-template-columns: 9px 1fr auto; gap: 4px 6px;
            align-items: baseline; text-align: left; width: 100%;
            padding: 5px 8px; border-radius: 7px; font-size: 12px;
            border: 1px solid var(--line); background: var(--panel);
            color: var(--ink); cursor: pointer; }
.lc-phase:hover { border-color: var(--dim); }
.lc-phase.on { border-color: var(--info); background: var(--info-fill); }
.lc-phase:focus-visible { outline: 2px solid var(--info); outline-offset: 1px; }
.lc-phase .dot { width: 8px; height: 8px; border-radius: 50%;
                 align-self: center; flex: 0 0 auto; }
.lc-name { min-width: 0; overflow-wrap: anywhere; }
.lc-by { grid-column: 2 / -1; font-family: var(--mono); font-size: 10px;
         color: var(--dim); overflow-wrap: anywhere; }
.lc-by.faint { color: var(--faint); }

/* A phase nothing covers is drawn as an OUTLINE, not merely a red dot. The
   colour alone cannot be seen by every reader and cannot be seen at all at a
   glance on a phone in sunlight, and a hole in the cycle is the single thing
   on this panel worth noticing from across a room. */
.lc-phase.gap { border-style: dashed; border-color: var(--bad); }

/* The SECOND axis: a phase a job runs and no filled seat answers for.
 *
 * Deliberately a different mark from `.gap`, and deliberately on a different
 * edge. A gap is a dashed box — nothing is here. An unowned phase is a solid
 * box with a warning stripe down its left — something IS here, running, and
 * nobody answers for it. Those need opposite fixes (schedule a job / hire a
 * seat) and drawing them the same way is how the Lanes tab and the Roster tab
 * came to contradict each other about the liability gate. The stripe is a
 * shape, not only a colour, for the reason the dashes above are. */
.lc-phase.unowned { border-left: 3px solid var(--warn); padding-left: 6px; }
.lc-phase.gap.unowned { border-left-style: dashed; }

/* The seat, under the job line. Quieter than the coverage line because this
   panel's subject is coverage — present because "running, and nobody answers
   for it" is one sentence and the reader must be able to read all of it. */
.lc-seat { grid-column: 2 / -1; font-size: 10px; color: var(--dim);
           overflow-wrap: anywhere; }
.lc-seat.open { color: var(--warn); }

.lc-counts { margin-top: 8px; gap: 5px; flex-wrap: wrap; }

.lc-detail { margin-top: 10px; border: 1px solid var(--line);
             border-radius: 8px; background: var(--panel); padding: 9px 11px; }
.lc-asks { margin: 6px 0 4px; font-size: 13px; color: var(--ink); }

/* One lane's own place on the cycle, under its pipeline row. Deliberately
   quieter than the panel above: it is an orientation, not a diagram. */
.lc-strip-wrap { display: flex; align-items: baseline; gap: 7px;
                 margin-top: 7px; flex-wrap: wrap; }
.lc-strip-label { font-size: 10px; color: var(--faint);
                  text-transform: uppercase; letter-spacing: 0.06em; }
.lc-strip { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.lc-sep { color: var(--faint); font-size: 11px; }
.lc-chip { display: inline-flex; align-items: center; gap: 4px;
           padding: 2px 7px; font-size: 11px; border-radius: 999px;
           border: 1px solid var(--line); background: var(--panel);
           color: var(--dim); cursor: pointer; }
.lc-chip:hover { border-color: var(--dim); color: var(--ink); }
.lc-chip .dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; }

/* -- the portfolio comparison ------------------------------------------
 *
 * One CSS grid holds the whole board rather than a table per family, because
 * separate tables cannot keep their columns aligned with each other and the
 * alignment is the entire point of the layout: every row means the same thing
 * across all the columns, so the comparison is read across.
 *
 * The gutter is sticky and the columns scroll under it. the Human reads this from
 * a handheld on another continent, and a grid whose row labels scroll off the
 * left edge is unreadable at exactly the width he uses.
 */
.lb-bar { margin: 4px 0 10px; gap: 6px; flex-wrap: wrap; align-items: center; }
.lb-spacer { flex: 1 1 auto; }
button.lb-seg.on { background: var(--info-fill); color: var(--ink); border-color: var(--info); }

.lb-legend { gap: 12px; flex-wrap: wrap; margin: 2px 0 8px; }
.lb-legend-item { display: inline-flex; align-items: center; gap: 5px; }

.lb-wrap { overflow-x: auto; overscroll-behavior-x: contain;
           border: 1px solid var(--line); border-radius: 10px;
           background: var(--panel); }
.lb-grid { --lb-gutter: 172px; display: grid; min-width: min-content;
           font-size: 12px; }

/* The sticky gutter. `background` is not optional here: a transparent sticky
   cell lets the scrolling columns show through underneath it, which is worse
   than no gutter at all because the text stays readable just long enough to be
   misread. */
.lb-grid > .lb-label, .lb-grid > .lb-corner {
  position: sticky; left: 0; z-index: 2;
  background: var(--panel-2); border-right: 1px solid var(--line);
}
.lb-corner { padding: 9px 10px; border-bottom: 1px solid var(--line); }
.lb-label { padding: 8px 10px; color: var(--dim); line-height: 1.35;
            border-bottom: 1px solid var(--line); display: flex;
            align-items: center; }
.lb-label.on { color: var(--ink); box-shadow: inset 2px 0 0 var(--info); }

.lb-head { padding: 9px 10px; border-bottom: 1px solid var(--line);
           border-left: 1px solid var(--line); }
.lb-head.on { background: var(--info-fill); box-shadow: inset 0 2px 0 var(--info); }
.lb-head p { margin: 4px 0 0; }
.lb-pick { display: flex; align-items: center; gap: 6px; cursor: pointer;
           margin-bottom: 5px; }
.lb-pick input { accent-color: var(--info); }

/* A family heading spans the full width so the four groups read as sections
   rather than as an undifferentiated wall of rows. */
.lb-fam { padding: 9px 10px 7px; background: var(--bg);
          border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
          position: sticky; left: 0; }
.lb-fam b { display: block; font-size: 12px; }
.lb-fam .sub { display: block; margin-top: 2px; max-width: 78ch; }

.lb-cell { padding: 8px 10px; border-bottom: 1px solid var(--line);
           border-left: 1px solid var(--line); cursor: pointer;
           display: flex; flex-direction: column; gap: 3px; position: relative; }
.lb-cell:hover { background: var(--panel-2); }
.lb-cell:focus-visible { outline: 2px solid var(--info); outline-offset: -2px; }
.lb-cell.lb-none { cursor: default; }
.lb-cell.lb-none:hover { background: none; }
.lb-val { font-family: var(--mono); font-size: 13px; color: var(--ink); }
.lb-val.dim { color: var(--faint); }

/* The provenance mark. A dot rather than a word: it appears on every cell on the
   page, and four words repeated forty times would drown the numbers they are
   there to qualify. The word itself is in the legend and in the detail. */
/* The dot itself carries no positioning: it is used both in a cell, where it
   sits in the corner, and in the legend, where it sits inline before its label.
   Absolutely positioning it here sent the legend's four dots to the page corner,
   where they had no width and were invisible -- and the legend is the only thing
   that says what the marks on every cell mean. */
.lb-basis { width: 6px; height: 6px; border-radius: 50%; display: inline-block;
            flex: 0 0 auto; }
.lb-cell > .lb-basis { position: absolute; top: 9px; right: 8px; }
.lb-basis.ok   { background: var(--ok); }
.lb-basis.info { background: var(--info); }
.lb-basis.warn { background: var(--warn); }
.lb-basis.dim  { background: var(--faint); opacity: 0.5; }

.lb-flag { font-size: 9px; text-transform: uppercase; letter-spacing: 0.07em;
           color: var(--ok); }
.lb-cell.lb-best { background: var(--ok-fill); }

/* A credible interval as the range it is. The band is the finding and the tick
   is the point estimate inside it -- never the other way round, because a bar
   drawn to the midpoint claims a precision the sample does not carry. */
.lb-iv { position: relative; height: 5px; border-radius: 3px;
         background: var(--panel-2); margin-top: 2px; }
.lb-iv-band { position: absolute; top: 0; bottom: 0; border-radius: 3px;
              background: var(--accent-fill); }
.lb-iv-tick { position: absolute; top: -1px; width: 2px; height: 7px;
              background: var(--accent); border-radius: 1px; }

.lb-detail-wrap { border-bottom: 1px solid var(--line); background: var(--bg);
                  position: sticky; left: 0; }
.lb-detail { padding: 10px 12px; max-width: 92ch; }
.lb-detail-row { margin-top: 8px; padding-top: 8px;
                 border-top: 1px solid var(--line); }
.lb-detail-row:first-of-type { border-top: 0; }
.lb-detail-row p { margin: 4px 0 0; }

.lb-gates { display: grid; gap: 9px; margin-top: 8px;
            grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.lb-gate { border: 1px solid var(--line); border-radius: 8px;
           padding: 8px 10px; background: var(--panel-2); }
.lb-gate .bar { margin: 6px 0 4px; }
/* A repealed gate is dimmed rather than hidden: it is the count of candidates
   stopped by a rule that no longer exists, which is the whole finding. */
.lb-gate.dead { opacity: 0.62; }

.lb-trade { margin-top: 10px; }
.lb-trade-row { padding: 7px 0; border-top: 1px solid var(--line); }
.lb-trade-row .sub { display: block; margin-top: 3px; font-family: var(--mono); }

/* ------------------------------------------------------- the workload sheet
 *
 * One dialog adds a workload and changes one, and it is the only form in this
 * console with a second column. That is not decoration. Every field in the
 * left column is a cause and the right column is its effect -- what this will
 * actually run, when it will next fire, and which lines of the manifest move.
 * A cron is unreadable and a ceiling in minor units is unreadable, so the
 * answer gets at least as much of the screen as the question that produced it.
 *
 * The right column is STICKY. On a phone the grid collapses to one column and
 * the effect panel sits under the fields it explains, which is the correct
 * reading order when they cannot be side by side. */
.sheet > .box.xwide { max-width: 1120px; }

.wl { display: grid; gap: 14px; align-items: start;
      grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
@media (max-width: 900px) { .wl { grid-template-columns: 1fr; } }

.wl-live { position: sticky; top: 4px; }
@media (max-width: 900px) { .wl-live { position: static; } }

.wl-sec { border: 1px solid var(--line); border-radius: 10px;
          padding: 4px 12px 10px; margin-bottom: 10px; background: var(--panel-2); }
.wl-sec > .wl-h { display: flex; align-items: baseline; gap: 8px;
                  margin: 10px 0 2px; font-size: 12px; font-weight: 600;
                  letter-spacing: .05em; text-transform: uppercase;
                  color: var(--dim); }
.wl-sec > .wl-h .sub { text-transform: none; letter-spacing: 0; font-weight: 400; }
.wl-sec label.field:first-of-type { margin-top: 4px; }

/* A changed field is marked where the field is, not only in a list at the
   bottom. Somebody scrolling back up to check what they touched should not
   have to hold the list in their head to do it. */
.wl-sec.dirty { border-color: var(--info); }
label.field.dirty > span::after {
  content: '\2022 changed'; color: var(--info); margin-left: 6px;
  font-variant: small-caps; letter-spacing: .04em;
}
button.wl-undo { padding: 0 5px; font-size: 10px; margin-left: 6px; }

/* The schedule presets. A segmented control rather than a `<select>` because
   the whole set is six items and seeing all six is the point: "on demand" is a
   real choice here and it is invisible at the bottom of a closed dropdown. */
.wl-seg { display: flex; flex-wrap: wrap; gap: 4px; margin: 2px 0 8px; }
.wl-seg button { padding: 4px 10px; font-size: 12px; }
.wl-seg button.on { background: var(--info-fill); color: var(--ink);
                    border-color: var(--info); font-weight: 600; }

/* The sentence. Bigger than any field that feeds it, because it is the answer
   and they are the inputs -- and because `30 0 * * 2` has fooled everybody who
   has ever read one. */
.wl-when { font-size: 16px; line-height: 1.4; color: var(--ink); margin: 0 0 6px; }
.wl-when .mono { font-size: 13px; color: var(--dim); }
.wl-when.bad { color: var(--bad); }

.wl-money { font-variant-numeric: tabular-nums; }

.wl-chg { border-top: 1px solid var(--line); padding: 6px 0; font-size: 12px; }
.wl-chg:first-child { border-top: 0; }
.wl-chg .k { color: var(--dim); }
.wl-chg .from { color: var(--faint); text-decoration: line-through; }
.wl-chg .to { color: var(--ink); font-weight: 600; }

/* The manifest diff. Folded away by default: it is the proof, not the point,
   and a wall of YAML above the Save button is how a dialog stops being read. */
.wl-diff { max-height: 44vh; overflow: auto; margin: 6px 0 0; padding: 8px 10px;
           background: var(--bg); border: 1px solid var(--line);
           border-radius: 8px; font-family: var(--mono); font-size: 11.5px;
           line-height: 1.5; white-space: pre; }
.wl-diff div { padding: 0 3px; border-radius: 3px; }
.wl-diff .add { background: var(--ok-fill); color: var(--ok); }
.wl-diff .del { background: var(--bad-fill); color: var(--bad); }
.wl-diff .gap { color: var(--faint); text-align: center; padding: 2px 0; }

.wl-count { float: right; color: var(--faint); font-size: 11px;
            font-variant-numeric: tabular-nums; }

/* Deleted workloads, on the tab they vanished from. Dimmed, because these are
   not running -- but present, because "where did it go" needs an answer on the
   screen where it went missing rather than in an audit log somebody has to
   know to open. */
.wl-gone { opacity: 0.78; }
.wl-gone:hover { opacity: 1; }

.programs-head {
  margin: 18px 0 8px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--faint); font-weight: 700;
}

/* ------------------------------------------------------- the stage strip
 * flowStrip's DOM half of the grammar: stage boxes in a row (or a week of
 * columns), item chips as real buttons, connectors that carry the same
 * travelling-dash meaning as the SVG edges. Scrolls, never reflows.
 */
.strip {
  display: flex; align-items: stretch; gap: 0;
  overflow-x: auto; padding: 4px 2px 8px;
  overscroll-behavior-x: contain;
}
.strip.columns { gap: var(--s3); }
.stage {
  flex: 0 0 auto; min-width: 148px; max-width: 220px;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--r2); padding: var(--s3) var(--s4);
}
.strip.columns .stage { min-width: 128px; }
.stage.dashed { border-style: dashed; background: transparent; }
.stage.clicky { cursor: pointer; }
.stage.clicky:hover { border-color: var(--dim); }
.stage.tone-ok   { border-color: var(--ok-line); }
.stage.tone-warn { border-color: var(--warn-line); }
.stage.tone-bad  { border-color: var(--bad-line); }
.stage.tone-info, .stage.tone-standby { border-color: var(--info-line); }
.stage-head { display: flex; align-items: center; gap: 6px; }
.stage-label { font-size: 12px; font-weight: 700; letter-spacing: 0.03em; }
.stage-count {
  font-size: 11px; font-weight: 700; min-width: 18px; text-align: center;
  padding: 0 5px; border-radius: var(--r-pill);
  background: var(--panel); border: 1px solid var(--line);
}
.chip-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px; min-width: 0;
  font: inherit; font-size: 12px; color: var(--ink); text-align: left;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 3px 10px; cursor: pointer;
}
.chip:hover { border-color: var(--accent); }
.chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-advance { white-space: nowrap; }
@keyframes chip-in { from { opacity: 0; transform: translateY(4px); }
                     to { opacity: 1; transform: none; } }
.chip-in { animation: chip-in var(--t-med) var(--ease) 1; }
.flow-link {
  flex: 0 0 26px; height: 2px; align-self: center; position: relative;
  background: repeating-linear-gradient(90deg,
    var(--link-color, var(--faint)) 0 5px, transparent 5px 10px);
  opacity: 0.75;
}
.flow-link.carrying { animation: link-carry var(--flow-period) linear infinite; }
@keyframes link-carry { to { background-position: 10px 0; } }
/* The board — flowBoard in flow.js. The map's boxes are DOM, one flex column
   per stage layer, each column an equal share of the width, so the pipeline
   runs left to right and fits whatever screen it is on — the Human's ask,
   sized like the hardware cards rather than the fixed SVG grid. The wires
   are an SVG overlay behind the boxes, redrawn from where the boxes land.
   `.vertical` is flipped on by flowBoardWire when the columns would fall
   under 150px each: the layers stack and each layer's boxes wrap. */
.flow-board {
  position: relative; background: var(--panel);
  border: 1px solid var(--line); border-radius: 10px; padding: 14px 12px;
}
/* `pointer-events: none` because this layer is `aria-hidden` decoration and
   nothing on it is interactive. It sits under the lanes, and the lanes are
   transparent to the pointer, so before this it was what `elementFromPoint`
   returned for every blank pixel of the board -- an element inside no band and
   no box, which is a hit that resolves to nothing rather than a miss that falls
   through to what is behind. */
.flow-wires { position: absolute; inset: 0; width: 100%; height: 100%;
              overflow: visible; z-index: 0; pointer-events: none; }
.flow-lanes { position: relative; z-index: 1; display: flex; align-items: stretch;
              gap: var(--lane-gap, 44px); pointer-events: none; }
.flow-lane { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
             justify-content: center; gap: 14px; }
.flow-board.vertical .flow-lanes { flex-direction: column; gap: 34px; }
.flow-board.vertical .flow-lane { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 10px; }
.flow-board.vertical .flow-box { flex: 1 1 150px; max-width: 100%; }
/* Wrap mode — the workload chain on the map's home and at the top of
   Workloads, and the pipeline's stages. Lanes are phases, headed by their
   title, boxes stacked beneath; the lanes flow left to right and wrap like
   text. The fixed head keeps every lane's first box level, so same-line wires
   run straight.

   `--lane-basis` is set by flowBoardWire once the board can be measured: the
   exact share of the line, so the lanes reach both edges and every box on the
   board is the same width, however many lines it took. The 140px here is only
   what stands before the first measurement. A short last line stays short —
   its lanes keep the basis rather than stretching across the board. */
/* `--lane-row-gap` is set by flowBoardWire once the board can be measured: the
   lines spread down the screen so the board uses the height it has been given
   as well as the width, which is what the Human asked for ("use the full
   available screen area ... width and available height"). 44px is what stands
   before the first measurement and the floor it never goes below. */
.flow-board.wrap .flow-lanes { flex-wrap: wrap; row-gap: var(--lane-row-gap, 44px);
                               column-gap: var(--lane-gap, 18px);
                               align-content: flex-start; }
/* 200px, matching flow.js's laneMax, so the frame before the first
   measurement is already the right shape. At the old 280 the board drew one
   paint of fat boxes and then snapped narrower, which reads as a flicker. */
/* 18px between stacked boxes rather than 10. A stage that holds outcomes below
   it — the Signal pipeline's "Not reproducible" under Desk, and
   "Left alone" and "Rolled back" under Confirmed stopped — had its boxes and
   the wires threading between them sharing ten pixels, which the Human read as
   "too crowded under the owners decision box and confirmed stopped box". The
   column gap between lanes is 18, so this matches it and the stack breathes the
   same amount in both directions. */
.flow-board.wrap .flow-lane { flex: 1 1 140px; min-width: 0; max-width: 200px;
                              justify-content: flex-start; gap: 18px; }
/* Measured: the lane takes exactly its share of the line and no more, so a
   short last line does not stretch across the board. `--lane-max` is the
   board's own ceiling, so a board with only a few lanes never stretches them
   across the whole screen. */
.flow-board.wrap.measured .flow-lane { flex: 0 1 var(--lane-basis, 140px);
                                       max-width: var(--lane-max, 200px); }
/* A panel opened under a box takes a line of its own. Wrap and bands rows
   wrap already; a columns row does not, and without this the panel's full
   width would squeeze the boxes beside it into a sliver (flowPanelClass). */
.flow-board.has-panel .flow-lanes { flex-wrap: wrap; }
/* Bands mode — the seats board. One band per seat, stacked down the page:
   the seat is the `lead` box at the left of its own band and the workloads it
   pays for run beside it and wrap. Drawn this way because seats-to-workloads
   is a grouping, not a chain, and a grouping drawn as two parallel columns
   puts four boxes against fourteen and sends thirteen wires crossing between
   them. Here membership is adjacency, so the wires are stubs and the board is
   three bands instead of a fourteen-row column.
   Below 560px the lead box goes full width above its run: at handheld width a
   190px seat plus a 132px workload leaves nothing, and a seat heading its own
   list reads better than a seat squeezed beside one. */
.flow-board.bands .flow-lanes { flex-direction: column; gap: 20px; }
.flow-board.bands .flow-lane { flex: 0 0 auto; min-width: 0; justify-content: flex-start; gap: 7px; }
/* POINTER-EVENTS: AUTO, and it is load-bearing rather than tidy. `.flow-lanes`
   above is `pointer-events: none` so the wires underneath stay hoverable, and
   each thing that wants a hit opts back in (`.flow-box`, `.flow-band-empty`,
   `.flow-lane-head`). The band ROW never did -- so its blank area was
   transparent, `document.elementFromPoint` returned the decorative `.flow-wires`
   overlay, and the drag engine's `closest('[data-drop-target]')` walked up from
   an SVG that is not inside any band and found nothing.

   Which made the seats board's whole drag design a half-truth: it registers the
   band ROW as the target precisely because a target the size of what you are
   aiming at is what makes a gesture work, and then only the boxes inside it
   could actually be hit. A band packed with workloads hid this -- there was
   always a box under the finger. A band with one lead box and an empty run,
   which is exactly what Local and API look like, was dead almost everywhere. */
.flow-band-row { display: flex; align-items: flex-start; gap: 12px; min-width: 0;
                 pointer-events: auto; }
.flow-band-run { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap;
                 align-items: flex-start; align-content: flex-start; gap: 8px; }
.flow-board.bands .flow-box.lead { flex: 0 0 auto; width: 190px; max-width: 45%; }
.flow-board.bands .flow-band-run .flow-box { flex: 0 1 auto; min-width: 132px; max-width: 220px; }
.flow-band-empty { pointer-events: auto; align-self: center; font-style: italic; }
.flow-board.bands .flow-lane-head { height: auto; -webkit-line-clamp: 1; font-size: 10.5px; color: var(--dim); }
@media (max-width: 560px) {
  .flow-band-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .flow-board.bands .flow-box.lead { width: auto; max-width: none; }
}
.flow-lane-head { height: 28px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2;
                  -webkit-box-orient: vertical; font: 600 10px/1.3 var(--sans); letter-spacing: 0.02em;
                  color: var(--faint); border-bottom: 1px solid var(--line); pointer-events: auto; }
.flow-lane-head.dim { color: var(--dim); font-style: italic; }
/* A lane under no phase draws no rule — a strip of nothing with a line under
   it reads as a heading that failed to load. */
.flow-lane-head:empty { border-bottom-color: transparent; }
/* The phase carried onto the next line (flowWrapHeads): the same words, said
   quietly, so it reads as "still in this phase" and not as a second one. */
.flow-lane-head.carried { color: var(--faint); font-style: italic; opacity: 0.75; }
.flow-box {
  pointer-events: auto; position: relative; min-width: 0; cursor: pointer;
  background: var(--box-fill, var(--panel-2));
  border: 1.5px solid var(--box-color, var(--line)); border-radius: 10px;
  padding: 9px 11px 8px; box-sizing: border-box;
  transition: box-shadow var(--t-med, .15s) var(--ease, ease);
}
.flow-box:hover, .flow-box.selected { box-shadow: inset 0 0 0 1.5px var(--box-color); }
.flow-box:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }
.flow-box.dashed { border-style: dashed; background: transparent; }
/* Something waits for a decision: the same halo the SVG node had. */
.flow-box.glow { box-shadow: 0 0 0 3px color-mix(in srgb, var(--box-color) 28%, transparent); }
.flow-box.glow.selected, .flow-box.glow:hover {
  box-shadow: inset 0 0 0 1.5px var(--box-color), 0 0 0 3px color-mix(in srgb, var(--box-color) 28%, transparent);
}
/* Elastic seats draw as a stack: two offset ghosts behind the box. */
.flow-box.stack::before, .flow-box.stack::after {
  content: ''; position: absolute; inset: 0; border-radius: 10px; z-index: -1;
  border: 1.5px solid var(--box-color); background: var(--box-fill, var(--panel-2)); opacity: 0.35;
}
.flow-box.stack::before { transform: translate(3px, 3px); }
.flow-box.stack::after  { transform: translate(6px, 6px); }
.flow-box-head { display: flex; align-items: center; gap: 7px; min-width: 0; }
.flow-box .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.flow-box-label { font: 600 13px/1.25 var(--sans); color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.flow-box-badge { margin-left: auto; flex: 0 0 auto; font: 700 8.5px var(--sans); letter-spacing: 0.08em;
                  text-transform: uppercase; color: var(--faint); }
.flow-box-badge:empty { display: none; }
.flow-box-badge.tone-ok { color: var(--ok); }
/* The map's status line above the chain: the tank, the machine, the way
   into each pipeline. */
.map-status { align-items: center; gap: 8px; margin: 0 0 10px; }
.map-status .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%;
                   margin-right: 5px; vertical-align: middle; }
.flow-box-body { display: flex; align-items: flex-end; gap: 8px; margin-top: 4px; min-height: 14px; }
.flow-box-note { flex: 1 1 auto; min-width: 0; font: 10.5px/1.35 var(--mono); color: var(--dim);
                 overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.flow-box-count { flex: 0 0 auto; font: 700 22px/1 var(--mono); color: var(--ink); }
.flow-box-count.zero { color: var(--faint); font-weight: 500; }
.flow-box-foot { margin-top: 4px; font: 9.5px var(--mono); color: var(--faint); }

.hw-node-badge { font-size: 8px; font-weight: 700; letter-spacing: 0.08em;
                 fill: var(--faint); text-transform: uppercase; }

/* ----------------------------------------------------------- pipeline map
 * The cell's process drawn with the hardware grammar. The count is
 * the loudest thing in a box; a box awaiting a decision glows; a particle is
 * one real transition travelling its edge. */
.hw-node-count { font: 700 17px/1 var(--mono); fill: var(--ink); }
.hw-node-count.zero { fill: var(--faint); font-weight: 500; }
.hw-node-g.glow .hw-node-box { animation: pm-glow 2.2s ease-in-out infinite; }
@keyframes pm-glow { 50% { stroke-width: 3; filter: drop-shadow(0 0 6px var(--warn)); } }
.hw-edge-hit.clicky { cursor: pointer; }
.hw-particle { fill: var(--ok); stroke: var(--bg); stroke-width: 1.5;
               filter: drop-shadow(0 0 4px var(--ok)); pointer-events: none; }
.pm-map svg { min-width: 0; }
.pm-lens { gap: 8px; align-items: center; margin-bottom: 6px; }
.pm-controls { gap: 6px; align-items: center; margin: 2px 0 8px; }
/* The pre-flight sheet: blockers first, then one row per layer. */
.pf-blocker { gap: 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--line); }
.pf-layer { gap: 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--line); }
.pf-layer.dim { color: var(--dim); }
.pf-stages { gap: 4px; margin: 6px 0; }
.pm-lens button.on { border-color: var(--accent); color: var(--accent); }
.pm-legend { gap: 8px; align-items: center; margin: 4px 0 8px; }
.pm-drawer { margin-top: 10px; padding: 10px 12px; border: 1px solid var(--line);
             border-left: 3px solid var(--accent); border-radius: 0 10px 10px 0;
             background: var(--panel-2); }
.pm-drawer-head { gap: 8px; align-items: center; }
/* The row inspector (flow.js flowInspectPlace): the map's detail panel, a
   full-width flex item seated straight after the row of boxes that holds the
   tapped one, with a caret under that box. Negative margins pull it into the
   44px row gap so it reads as part of the row rather than a row of its own;
   the wires behind it are hidden by its opaque ground, on purpose. */
.flow-inspector { flex: 1 1 100%; width: 100%; box-sizing: border-box; pointer-events: auto;
                  position: relative; z-index: 2; margin: -24px 0 -14px;
                  background: var(--panel-2); border: 1px solid var(--line); border-top: 2px solid var(--accent);
                  border-radius: 10px; padding: 10px 12px;
                  animation: flow-insp-in var(--t-med, .16s) var(--ease, ease-out); }
.flow-inspector::before { content: ''; position: absolute; top: -7px; left: var(--caret-x, 28px);
                          width: 12px; height: 12px; transform: translateX(-50%) rotate(45deg);
                          background: var(--panel-2); border-left: 2px solid var(--accent); border-top: 2px solid var(--accent); }
.map-status .flow-inspector { margin: 2px 0 6px; }
@keyframes flow-insp-in { from { opacity: 0; transform: translateY(-6px); } }
.insp-head { gap: 8px; align-items: center; }
.insp-head b { font-size: 15px; }
.insp-verbs { gap: 6px; align-items: center; margin-top: 6px; }
.insp-facets { margin: 8px 0 0; }
/* The body scrolls, not the panel, so the caret above is never clipped and
   the rows beneath the panel stay reachable however much a facet holds. */
.insp-body { margin-top: 8px; max-height: 58vh; overflow: auto; }
.insp-body .panel { margin-top: 8px; }
.insp-body .grid { margin-top: 6px; }
.insp-kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; margin: 6px 0; font-size: 13px; }
.insp-kv dt { color: var(--dim); }
.insp-kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.insp-kv dd .row { gap: 6px; align-items: center; }
.flow-box.add { border-style: dashed; background: transparent; color: var(--dim); }
.pm-item { gap: 8px; padding: 6px 0; border-top: 1px solid var(--line); align-items: center; }
/* Same drag mechanics as the seat and stage boards (flow.js's dndSource) --
   the row itself becomes the drag handle, so an item moves to another stage
   by dragging it onto that stage's box on the map above the drawer. */
.pm-item.draggable, .pm-card.draggable { cursor: grab; }
.pm-item.draggable:active, .pm-card.draggable:active { cursor: grabbing; }
.pm-item.dragging, .pm-card.dragging { opacity: 0.35; }

/* Which cell the page is about: the switcher in the header and an
   accent that follows it, so acting in the wrong cell is visible. */
.org-switcher { gap: 6px; align-items: center; margin-left: 12px; }
.org-switcher .org-dot { width: 10px; height: 10px; border-radius: 50%; }
.org-switch { max-width: 180px; }
.top { border-bottom: 2px solid var(--org-accent, var(--line)); }
.org-form { display: grid; gap: 8px; margin: 8px 0; }
.org-form .row { gap: 8px; }
.org-form input:not([type=color]) { flex: 1 1 200px; }

/* Talk to the map: the bar, the answer, the spotlight. */
.talk-bar { gap: 8px; align-items: center; margin: 0 0 8px; }
.talk-bar input { flex: 1 1 200px; }
.talk-noa { font-weight: 700; color: var(--accent); }
.talk-reply { margin: 0 0 10px; padding: 8px 12px; border-left: 3px solid var(--accent); background: var(--panel-2); border-radius: 0 10px 10px 0; }
.talk-answer { margin: 0; }
.talk-proposal { gap: 8px; align-items: center; margin-top: 6px; }
/* A run proposal: the workload on one line, and under it the instruction the
   agent will actually be given, quoted so it reads as words rather than as a
   field. Wrapped, never clipped -- the whole point is that he can read it. */
.talk-run { margin-top: 8px; }
.talk-direction { margin: 6px 0 8px; padding: 6px 10px; border-left: 2px solid var(--accent);
                  background: var(--panel); border-radius: 0 8px 8px 0; white-space: pre-wrap;
                  overflow-wrap: anywhere; font-size: 0.95em; }
/* The box under Run now: one run's instructions, typed on the chain. */
.insp-verbs-wrap { display: block; }
.insp-direction-row { margin: 6px 0 2px; }
.insp-direction { width: 100%; box-sizing: border-box; resize: vertical; font: inherit; }
.hw-node-g.talk-dim, .hw-edge-flow.talk-dim, .hw-edge.talk-dim { opacity: 0.22; }
.hw-node-g.talk-lit .hw-node-box { stroke: var(--accent); stroke-width: 2.6; filter: drop-shadow(0 0 6px var(--accent)); }

/* Compose: tiles, the interview, the review. */
.compose-tiles { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.compose-tile { text-align: left; cursor: pointer; font: inherit; color: inherit; }
.compose-tile:hover { border-color: var(--accent); }
.compose-glyph { font-size: 20px; margin-right: 8px; }
.compose textarea, .compose input { width: 100%; margin: 6px 0; }
.compose-q { margin: 8px 0; padding: 8px 10px; border-left: 3px solid var(--line); }
.compose-q .row { gap: 6px; margin-top: 4px; }
.compose-section { margin-top: 10px; }
.compose-section .row { gap: 8px; align-items: center; padding: 3px 0; }
.compose-preview svg { min-width: 0; }
.compose .note.ok { border-color: var(--ok); }
/* The draft arriving. Dimmed and dashed on purpose: what is on screen while
   the model writes is not yet a draft anyone can lock or apply, and it should
   not read like one. It resolves into the review's own boxes when the run
   lands. */
.compose-live { margin-top: 12px; padding: 10px; border: 1px dashed var(--line); border-radius: 8px; opacity: .85; }
.compose-live > .row:first-child { gap: 8px; align-items: baseline; }
.compose-live-row { gap: 8px; align-items: center; padding: 2px 0; }
.compose-preview.is-live { opacity: .9; }
.compose-preview.is-live svg { transition: opacity .2s ease-in; }

/* The map's own chrome: crumbs, the scrubber, stage cards, the item view. */
.map-crumbs { gap: 8px; align-items: center; margin-bottom: 8px; font-size: 13px; }
.map-crumbs b { font-size: 15px; }
.map-lens { margin-bottom: 8px; }

/* The layout toggle. Deliberately NOT styled like the lens row it sits under:
   a lens recolours the boxes and a layout replaces them, so the two controls
   must not read as one row of seven. Hence the lead-in word, the tighter gap
   and the segment pair drawn as a single joined control rather than as
   free-standing buttons. */
.map-layout { gap: 6px; align-items: center; margin: 0 0 8px; }
.map-layout > .sub { margin-right: 2px; opacity: 0.75; }
.map-layout button { border-radius: 0; margin: 0; }
.map-layout button:first-of-type { border-radius: 4px 0 0 4px; }
.map-layout button:last-of-type { border-radius: 0 4px 4px 0; margin-left: -1px; }
.map-layout button.on { border-color: var(--accent); color: var(--accent); z-index: 1; }
.map-layout button[disabled] { opacity: 0.4; cursor: not-allowed; }
.map-scrub { margin-top: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
             background: var(--panel-2); }
.map-scrub input[type=range] { flex: 1 1 200px; min-width: 160px; accent-color: var(--accent); }
/* ------------------------------------------------ selecting several cards
 *
 * The bar sticks to the top of whatever is scrolling it, because it acts on
 * the cards below it and the Human reads this on a phone: a bar that scrolls
 * away above 132 cards is a bar he has to scroll back to in order to press.
 * It is quiet until something is ticked -- an always-loud bar over an empty
 * selection is the wallpaper the serving banner's comment warns about. */
.sel-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;
           padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
           background: var(--panel-2); position: sticky; top: 0; z-index: 3; }
.sel-bar.on { border-color: var(--accent-line); background: var(--accent-fill); }
.sel-bar .grow { flex: 1 1 auto; }
.sel-bar .sub.bad { color: var(--bad); flex: 1 1 100%; }
.sel-box { accent-color: var(--accent); width: 18px; height: 18px; flex: 0 0 auto; cursor: pointer; }
/* A ticked card is legible as ticked without reading its checkbox -- the
   whole point of the edge is that a selection of nine across a board of a
   hundred can be checked at a glance before the code is typed. */
.pm-card.picked, .pm-item.picked { border-color: var(--accent-line); background: var(--accent-fill); }
.sel-list { margin: 6px 0; padding-left: 18px; }
.sel-list li { margin: 2px 0; }
.pm-cards { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); margin-top: 12px; }
.pm-card { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: var(--panel); }
.pm-card .row { gap: 8px; align-items: center; }
.pm-card p { margin: 6px 0; }
button.linkish { background: none; border: 0; padding: 0; color: var(--ink); font: inherit; font-weight: 600;
                 cursor: pointer; text-align: left; }
button.linkish:hover { color: var(--accent); text-decoration: underline; }
.map-fields { margin-top: 10px; }
.map-timeline { list-style: none; padding: 0; margin: 6px 0 0; border-left: 2px solid var(--line); }
.map-timeline li { padding: 4px 0 4px 12px; position: relative; font-size: 13px; }
.map-timeline li::before { content: ''; position: absolute; left: -6px; top: 10px; width: 8px; height: 8px;
                           border-radius: 50%; background: var(--accent); }
.map-body { white-space: pre-wrap; max-height: 50vh; overflow: auto; margin-top: 10px; padding: 10px;
            border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); font-size: 12px; }

/* ------------------------------------------------------------- drag & drop
 * One gesture, drawn once: a chip lifts off under the pointer as a floating
 * proxy, the ONE stage box that is its real next stage glows to invite the
 * drop, every box that is not a legal landing dims by a hair so the choice
 * reads at a glance instead of by trial. The dock (Reject/Route/Hold/Release/
 * Remove, next to the Approve a stage box already offers) descends from just
 * under the sticky header the same way, glassed rather than opaque so the
 * rail stays visible underneath it while it is open. See flow.js's
 * dndSource/dndTarget/flowDock
 * for the mechanics this file only ever gives a look to.
 */
/* PRESS AND HOLD, on anything draggable. The one thing a long press must not
   do is become a text selection or, on iOS, the callout menu -- either hijacks
   the gesture before flow.js's timer can arm it. There is deliberately no
   `touch-action` on .flow-box: it would buy the drag at the cost of scrolling
   the board at all. See flow.js dndSource for how the finger chooses instead. */
.chip.draggable, .flow-box.draggable, .pm-item.draggable, .pm-card.draggable {
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
.chip.draggable { cursor: grab; }
.chip.draggable:active { cursor: grabbing; }

/* The press has taken and the next movement will drag. Without this a finger
   held on the glass gets no acknowledgement until it moves, which reads as a
   dead screen for the whole 420ms. */
.dnd-armed {
  transform: scale(1.04);
  box-shadow: 0 0 0 2px var(--accent), 0 10px 24px -10px rgba(0, 0, 0, 0.5);
  transition: transform 120ms var(--ease), box-shadow 120ms var(--ease);
}
.chip.dragging { opacity: 0.35; }
.drag-proxy {
  position: fixed; top: 0; left: 0; z-index: 500;
  display: flex; align-items: center; gap: 6px;
  transform: translate(-50%, -50%) scale(1.08) rotate(-2deg);
  background: var(--panel); border: 1px solid var(--accent);
  border-radius: var(--r-pill); padding: 5px 12px; font-size: 12px;
  font-weight: 600; color: var(--ink); pointer-events: none;
  box-shadow: 0 14px 34px -10px rgba(0, 0, 0, 0.55), 0 0 0 1px var(--accent-line);
}
.drag-proxy.snap-back {
  transition: left 200ms var(--ease), top 200ms var(--ease), opacity 200ms var(--ease);
  opacity: 0; transform: translate(-50%, -50%) scale(0.7);
}
body.dnd-live .stage { transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease), transform 140ms var(--ease), opacity 140ms var(--ease); }
.stage.dnd-na { opacity: 0.35; }
.stage.drop-hover {
  border-color: var(--ok); box-shadow: 0 0 0 3px var(--ok-fill), 0 0 24px -6px var(--ok);
  transform: translateY(-2px) scale(1.02);
}
.stage.drop-refuse {
  border-color: var(--bad); box-shadow: 0 0 0 3px var(--bad-fill);
  animation: dnd-shake 260ms var(--ease) 1;
}
@keyframes dnd-shake {
  20% { transform: translateX(-3px); } 60% { transform: translateX(3px); } 100% { transform: translateX(0); }
}
.dnd-dock {
  /* Below the sticky header (45px) and the sticky tab bar (top: 45px, its
     own ~34px) rather than at the very top of the viewport — a dock at
     top: 0 would sit z-above the header and cover the brand and live dot,
     since it has to outrank ordinary page content to float over the rail. */
  position: fixed; left: 50%; top: 90px; z-index: 400;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  max-width: min(92vw, 640px); padding: 8px;
  background: color-mix(in srgb, var(--panel) 76%, transparent);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  backdrop-filter: blur(18px) saturate(1.5);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  box-shadow: 0 18px 44px -14px rgba(0, 0, 0, 0.55);
  opacity: 0; pointer-events: none;
  transform: translate(-50%, -14px);
  transition: opacity 160ms var(--ease), transform 160ms var(--ease);
}
body.dnd-live .dnd-dock { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
.dnd-dock-pill {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; white-space: nowrap;
  padding: 8px 14px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--panel-2); color: var(--dim);
  transition: transform 140ms var(--ease), background 140ms var(--ease),
              border-color 140ms var(--ease), color 140ms var(--ease), opacity 140ms var(--ease);
}
.dnd-dock-icon { font-size: 12px; line-height: 1; }
.dnd-dock-pill.tone-ok   { border-color: var(--ok-line); }
.dnd-dock-pill.tone-bad  { border-color: var(--bad-line); }
.dnd-dock-pill.tone-info { border-color: var(--info-line); }
.dnd-dock-pill.dnd-na { opacity: 0.3; }
.dnd-dock-pill.drop-hover {
  background: var(--ok-fill); border-color: var(--ok); color: var(--ok);
  transform: scale(1.12) translateY(-3px);
}
.dnd-dock-pill.tone-bad.drop-hover  { background: var(--bad-fill);  border-color: var(--bad);  color: var(--bad); }
.dnd-dock-pill.tone-info.drop-hover { background: var(--info-fill); border-color: var(--info); color: var(--info); }
.dnd-dock-pill.drop-refuse { animation: dnd-shake 260ms var(--ease) 1; }

/* -------------------------------------------------- reduced motion, once
 * The whole motion vocabulary lives here: travelling dashes freeze to a
 * static dasharray (the link still reads as present), pulses stop, chip
 * entrances become plain appearance. New flow-* classes join THIS block,
 * never a scattered one — two half-blocks is how the first exception ships.
 */
@media (prefers-reduced-motion: reduce) {
  .flow-inspector { animation: none; }
  .hw-edge-flow.moving, .flow-edge.moving { animation: none; stroke-dasharray: 5 5; }
  .hw-alarm, .flow-alarm { animation: none; }
  .hw-node-g.glow .hw-node-box { animation: none; stroke-width: 2.4; }
  .hw-particle { display: none; }
  .lp-spent.moving { animation: none; }
  .chip-in { animation: none; }
  .flow-link.carrying { animation: none; }
  .sheet > .box { animation: none; }
  .stage.drop-refuse, .dnd-dock-pill.drop-refuse, .flow-band-row.drop-refuse { animation: none; }
  .stage.drop-hover, .dnd-dock-pill.drop-hover { transform: none; }
  .dnd-armed { transform: none; transition: none; }
  .drag-proxy { transform: translate(-50%, -50%); }
}

/* compose phases */
/* Gallery → describe → interview → review, drawn into the sheet or into a
   host on the map. Everything is a tap target: chips, suggestion cards,
   shared tiles. No colour here that is not a token. */
.compose-head { gap: 8px; align-items: baseline; margin-bottom: 8px; }
.compose-label { display: block; font-weight: 600; margin-top: 8px; }
.compose-describe textarea { min-height: 9em; }
.compose-shared { margin-top: 12px; }
.compose-tile.is-seconded { opacity: .6; cursor: default; }
.compose-tile.is-seconded:hover { border-color: var(--line); }
.compose-ask { font-weight: 600; }
.compose-other { width: auto !important; min-width: 8em; flex: 1 1 8em; margin: 0 !important; }
.compose-q button.tiny[aria-pressed="true"] { border-color: var(--accent); }
.compose-suggestions { margin-top: 12px; }
.compose-suggest {
  display: block; width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer;
  margin: 4px 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: transparent;
}
.compose-suggest .row { gap: 6px; align-items: center; }
.compose-suggest.on { border-color: var(--accent); }
.compose-suggest:not(.on) { opacity: .55; text-decoration: line-through; }
.compose-suggest:not(.on) .tag { text-decoration: none; }
.compose-suggest:hover { border-color: var(--accent); opacity: 1; }
.compose [data-compose-status="static"] button { margin-left: 8px; }
[data-compose-live] .compose-interview textarea { min-height: 5em; }

/* boards — the seats and lanes levels, and the inline forms on a panel */
/* A rename, a lane form, a device form, a role form and a setting editor
   all live INSIDE the panel under the tapped box now, so they share one
   quiet frame; a field being typed into is never repainted from under the
   finger (inspector.js inspectHoldingInput). */
.insp-rename { gap: 8px; align-items: center; margin: 6px 0 2px; }
.insp-rename input { flex: 1 1 12em; min-width: 8em; margin: 0; }
.lane-form, .device-form, .role-form, .hw-set-edit {
  margin: 8px 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel, transparent);
}
.lane-form .field, .device-form .field, .hw-set-edit .field { margin: 6px 0; }
.device-form .row > div, .role-form .row > div { min-width: 0; }
.device-form textarea { min-height: 4em; }
.hw-set { flex-wrap: wrap; }
.hw-set > .row { flex: 1 1 100%; }
.hw-settings { margin: 4px 0; }
[data-add-facet] { margin: 0 0 8px; }
[data-lane-form-host] .lane-form { margin-top: 8px; }

/* ------------------------------------------------------------- the rail
 * Every pipeline the cell runs, one per line, fitted across the width.
 * Two rules here are load-bearing and must not be "tidied":
 *
 *   min-width: 0 on .ps-boxes and .ps-box IS the fit-to-a-line mechanism.
 *   A flex item's default min-width is auto, which refuses to shrink below
 *   its content, so without these the row overflows instead of fitting.
 *
 *   .ps-strip must never get overflow-x: auto. The Human asked for the
 *   boxes to FIT the line; a scroller hides pipelines' later stages behind
 *   a gesture and is exactly what this replaces. Too many stages to fit is
 *   handled by collapsing to arcs in JS (RAIL_MAX_BOXES), not by scrolling.
 */
.ps-rail { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
.ps-strip { display: flex; align-items: center; gap: 10px;
            background: var(--panel); border: 1px solid var(--line);
            border-radius: var(--r2); padding: 7px 10px; }
.ps-strip.open { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
.ps-head { flex: 0 0 auto; max-width: 38%; min-width: 0; display: flex; align-items: center; gap: 7px;
           background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
           font: 600 13px/1.25 var(--sans); color: var(--ink); }
.ps-head:hover .ps-name, .ps-head[aria-current="true"] .ps-name { color: var(--accent); }
.ps-head:focus-visible { outline: 2px solid var(--info); outline-offset: 3px; border-radius: var(--r1); }
.ps-head .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.ps-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ps-count { flex: 0 0 auto; font: 700 15px/1 var(--mono); }
.ps-boxes { flex: 1 1 0; min-width: 0; display: flex; flex-wrap: nowrap; align-items: stretch; gap: 2px; }
.ps-gap { flex: 0 0 10px; }
.ps-box { flex: 1 1 0; min-width: 0; height: 30px; overflow: hidden;
          display: flex; align-items: center; justify-content: center; gap: 6px;
          border: 1.5px solid var(--box-color, var(--line));
          background: var(--box-fill, var(--panel-2)); border-radius: var(--r1);
          font: 700 12px/1 var(--mono); color: var(--ink); }
.ps-box.zero { background: transparent; border-style: dashed; color: var(--faint); font-weight: 500; }
/* A stage's name has nowhere to go at this size and is the box's title
   instead; in arc mode there are three or four boxes, so it fits. */
.ps-box-label { display: none; }
.ps-rail[data-ps-rail="arc"] .ps-box { justify-content: flex-start; padding: 0 8px; }
.ps-rail[data-ps-rail="arc"] .ps-box-label { display: block; font: 600 11px/1 var(--sans);
    color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ps-stat { flex: 0 0 auto; max-width: 26%; font: 10.5px/1.35 var(--mono); color: var(--dim);
           overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.ps-stat.ps-error { color: var(--bad); max-width: none; }
.ps-rail-names .ps-strip { padding: 5px 10px; }

/* The board's slot under the rail. The min-height is what keeps the board in
   one place while the next pipeline's graph loads: renderMap clears the tab
   and rebuilds, so without a floor the page collapses upward for a round
   trip every time you switch — the exact jump the fixed slot prevents. */
.ps-slot { min-height: 320px; }

@media (max-width: 560px) {
  /* The boxes take a full-width second row rather than shrinking to noise.
     They still never wrap: at 390px a twelve-stage strip is ~30px a box. */
  .ps-strip { flex-wrap: wrap; }
  .ps-head, .ps-stat { max-width: none; }
  .ps-head { flex: 1 1 auto; }
  .ps-stat { flex: 0 0 auto; }
  .ps-boxes { flex: 1 0 100%; }
  .ps-box { height: 26px; }
  .ps-slot { min-height: 240px; }
}

/* ---- the desk, a stage's panel, and dragging a stage box (increment 3) ---- */
/* A card the Human cannot decide from is dimmed and edged, so a desk holding
   one undecidable item among four reads as one glance rather than four. */
.desk-card.desk-nomemo { border-left: 3px solid var(--bad); }
.desk-modify-fields { display: grid; grid-template-columns: 1fr; gap: 6px; margin: 6px 0 10px; }
.desk-modify-more { margin-top: 10px; }
.desk-modify-more > summary { cursor: pointer; }
.desk-modify-row { display: flex; flex-direction: column; gap: 2px; }
.desk-group { border-left: 3px solid var(--warn); }
.desk-group[data-desk-releasable="0"] { opacity: 0.85; }
.desk-group-items { margin-top: 6px; }
.desk-group-items > summary { cursor: pointer; }
.desk-group-items .pm-cards { margin-top: 8px; }
.desk-card.desk-nomemo .desk-summary,
.desk-card.desk-nomemo .desk-numbers { opacity: .55; }
.desk-card .desk-why { margin: 2px 0 6px; }
.desk-card .desk-summary { margin: 4px 0 8px; line-height: 1.4; }

/* The number strip on a desk card. A grid rather than a row so the figures
   line up down the card the way they would in a memo, and so a phone gets two
   or three columns instead of a sideways scroll. Tabular figures because
   numbers that jitter as they change are numbers nobody trusts. */
.desk-card .desk-numbers {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 6px 12px; margin: 6px 0 10px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 6px;
}
.desk-card .dn-cell { display: grid; gap: 1px; min-width: 0; }
.desk-card .dn-label { font-size: .72em; letter-spacing: .04em; text-transform: uppercase; opacity: .65; }
.desk-card .dn-value { font-variant-numeric: tabular-nums; font-size: 1.05em; font-weight: 600; }
.desk-card .dn-delta { font-variant-numeric: tabular-nums; font-size: .72em; opacity: .85; }
.desk-card .dn-delta.ok { color: var(--ok); }
.desk-card .dn-delta.bad { color: var(--warn); }
.insp-form { display: grid; gap: 8px; margin-top: 8px; }
.insp-field { display: grid; gap: 3px; }
.insp-field input, .insp-field select { max-width: 100%; }
.insp-field .faint { opacity: .7; font-size: .85em; }
.flow-box.draggable { cursor: grab; }
.flow-box.dragging { opacity: .5; cursor: grabbing; }
.flow-box.dnd-na { opacity: .45; }
.flow-box.drop-hover { outline: 2px dashed var(--accent); outline-offset: 2px; }
.flow-box.drop-refuse { outline: 2px dashed var(--bad); outline-offset: 2px; }

/* A whole BAND as a landing area (boards.js mapSeatsDnd): the seats board's
   drop target is the seat's row rather than its lead box, because the row is
   what the reader is aiming at -- "this seat and everything it pays for". The
   band has no border of its own, so it takes the invitation as a tinted fill
   and an inset ring; a box uses an outline, and an outline on something this
   wide reads as a page-level error rather than as a target. */
body.dnd-live .flow-band-row {
  transition: background 140ms var(--ease), box-shadow 140ms var(--ease), opacity 140ms var(--ease);
  border-radius: var(--r2);
}
.flow-band-row.dnd-na { opacity: .4; }
.flow-band-row.drop-hover {
  background: var(--ok-fill); box-shadow: inset 0 0 0 2px var(--ok);
}
.flow-band-row.drop-refuse {
  background: var(--bad-fill); box-shadow: inset 0 0 0 2px var(--bad);
  animation: dnd-shake 260ms var(--ease) 1;
}

/* PR 9: the pipeline level's search box and its hit list; diff lines. */
.map-search { position: relative; display: inline-block; }
.map-search input[type=search] { font: inherit; font-size: 13px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--paper, #fff); color: var(--ink); width: 8em; transition: width 120ms var(--ease, ease); }
.map-search input[type=search]:focus { width: 14em; }
.map-search-hits { position: absolute; right: 0; top: 100%; z-index: 30; min-width: 22em; max-width: 34em; max-height: 50vh; overflow: auto;
  background: var(--paper, #fff); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.12); padding: 6px 10px; text-align: left; }
.map-search-hits button.linkish { width: 100%; padding: 4px 0; border-bottom: 1px solid var(--line); }
.map-search-hits button.linkish:last-child { border-bottom: 0; }
.diff-hunk .diff-line { padding: 0 4px; }

/* The Reset screen: the phrase that has to be typed back. Set apart and made
   selectable-looking so it reads as a thing to copy, not as a heading -- the
   whole point of the gate is that it is read once before it is retyped. */
.phrase {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 15px; letter-spacing: .02em;
  margin: 8px 0; padding: 8px 10px;
  border: 1px dashed var(--line); border-radius: var(--r2, 8px);
  background: var(--bad-fill); color: var(--ink);
  user-select: all;
}
/* ------------------------------------------------------------ the demo
   Loaded only by the demo bundle (demo-live.js): a bar along the bottom
   that says what this is and sets the pace, and the embed modes the site's
   frames use, which hide the chrome and leave one board. Nothing here
   applies on a real console, where body carries neither class. */
body.demo-page main { padding-bottom: 56px; }
.demo-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 7px 12px; background: var(--panel); border-top: 1px solid var(--line); font-size: 12.5px; color: var(--dim); }
.demo-bar .demo-bar-name b { color: var(--ink); font-weight: 600; }
.demo-bar .grow { flex: 1; }
.demo-bar .demo-bar-speeds { display: inline-flex; gap: 4px; }
.demo-bar a.demo-bar-install { text-decoration: none; }
body.demo-embed header.top, body.demo-embed #screens, body.demo-embed .screen-crumb, body.demo-embed .serving-warn,
body.demo-embed [data-status-row], body.demo-embed .map-layout, body.demo-embed .map-rail, body.demo-embed .talk-bar + .sub,
body.demo-embed #tab-map > .sub:first-child { display: none !important; }
body.demo-embed main { padding: 6px 8px; }
body.demo-embed-road .talk-bar, body.demo-embed-map .talk-bar, body.demo-embed-hero .talk-bar, body.demo-embed-records .talk-bar,
body.demo-embed-work .talk-bar, body.demo-embed-governance .talk-bar, body.demo-embed-support .talk-bar { display: none !important; }
.demo-embed-tag { position: fixed; right: 8px; bottom: 8px; z-index: 40; font-size: 11.5px; color: var(--dim); text-decoration: none;
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 3px 9px; }
.demo-embed-tag:hover { color: var(--ink); border-color: var(--accent); }
