:root {
  --bg: #f4f4f1;
  --fg: #1a1a1a;
  --card: #fff;
  --line: #d9d9d4;
  --acc: #0b6e4f;
  --err: #9b1c1c;
}
html[data-theme="dark"] {
  /* 2x darker: #121212 was a mid grey that looked washed out beside the named themes
     (midnight #04060c, void #05030e). Halved per channel. */
  --bg: #090909;
  --fg: #eee;
  --card: #0e0e0e;
  --line: #191919;
  --acc: #3dd68c;
  --err: #ff8a8a;
}
/* ============================================================================
   SYSTEM — now a COMPLETE theme, in both branches.
   It used to be a stub: no block in themes.css at all, so it fell back to bare
   `:root` for the light side and to a six-variable override for the dark side.
   Everything the named themes define (--fg2 --dim --mute --panel --glass*
   --font-head --tt-head --fw-head --ls-body --lh-body) was therefore MISSING, and
   every derived style silently read its fallback instead of a designed value —
   which is exactly why "it says system but it doesn't look like system".
   The dark branch was worse: it never redefined --glass*, so a dark OS got
   near-black panels wearing the LIGHT glass (rgba(0,0,0,.04)) — glass that does nothing.
   Both branches now carry the full variable set, so this is a real identity rather
   than an accident, and the two branches cannot drift.
   ============================================================================ */
html[data-theme="system"] {
  /* LIGHT — blue-tinted, so it reads as a sibling of the blue identity rather than
     a different product. Accent #1d4ed8. */
  --bg: #eef1f6; --panel: #f7f9fc; --card: #ffffff;
  --fg: #2a2f38; --fg2: #0c1017; --dim: #5f6874; --mute: #939ca8;
  --line: #ccd5e0; --acc: #1d4ed8;
  --red: #dc2626; --amber: #b45309; --blue: #1d4ed8; --err: #b42318;
  --glass: rgba(255,255,255,.64); --glass-strong: rgba(247,249,252,.88);
  --glass-line: #c8d2e0; --glass-shadow: 0 10px 30px rgba(20,35,70,.14);
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-head: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --tt-head: none; --ls-head: 0; --fw-head: 700;
  --ls-body: 0; --lh-body: 1.55;
}
@media (prefers-color-scheme: dark) {
  html[data-theme="system"] {
    /* DARK — identical to MIDNIGHT. Using the real midnight values (not a
       hand-copied approximation) is what keeps the two from drifting: this is the
       same palette, spelled once here so the dark branch is complete and not a subset. */
    --bg: #04060c; --panel: #0a0f1c; --card: #0b1322;
    --fg: #c3d0e8; --fg2: #eef3ff; --dim: #5f7396; --mute: #42546f;
    --line: #16203a; --acc: #4d8dff;
    --red: #ff5c6c; --amber: #ffb454; --blue: #5ec8ff; --err: #ff5c6c;
    --glass: rgba(10,16,30,.60); --glass-strong: rgba(8,13,25,.78);
    --glass-line: #1a2540; --glass-shadow: 0 10px 36px rgba(0,0,0,.6);
    --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-head: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --tt-head: none; --ls-head: 0; --fw-head: 700;
    --ls-body: -.005em; --lh-body: 1.5;
  }
  /* the dark branch also needs the panel/sidebar chrome the midnight theme paints,
     or system-on-dark shows flat panels where midnight shows glowing glass */
  html[data-theme="system"] .top, html[data-theme="system"] .side {
    background: var(--glass);
    backdrop-filter: saturate(1.4) blur(16px);
    -webkit-backdrop-filter: saturate(1.4) blur(16px);
    border-color: var(--glass-line);
    box-shadow: 0 0 30px rgba(77,141,255,.16) inset, 0 10px 36px rgba(0,0,0,.55);
  }
}
* { box-sizing: border-box; }
body { margin: 0; font: 16px/1.4 system-ui, sans-serif; background: var(--bg); color: var(--fg); }
.top { display: flex; align-items: center; gap: .75rem; padding: .6rem 1rem;
  border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.brand { flex: 0 0 auto; }
/* Logo as a sidebar list item: previously it lived in the header inside a wrapper with a
   fixed width and overflow:hidden, which clipped it. As a nav row it inherits the same
   padding/height rhythm as every other item. */
.side-logo {
  display: flex; align-items: center; gap: .4rem;
  padding: .45rem .5rem;
  margin: 0 0 .5rem;           /* .side owns the horizontal inset */
  overflow: visible;           /* the parent's overflow:hidden is what clipped it */
  width: auto;                 /* and a fixed parent width squeezed it */
  border-bottom: 0;            /* no rule under the logo: whitespace separates instead */
  padding-bottom: 15px;        /* the spacer that replaces the divider */
  flex: 0 0 auto;
}
.side-logo .brand-logo { height: 30px; width: auto; max-width: 100%; }
.top-links { flex: 1 1 auto; min-width: 0; display: flex; gap: .6rem; flex-wrap: wrap; }
.top-links a { color: var(--acc); }
/* The control cluster must never hard-clip against the viewport edge where the
   main-content scrollbar sits, and must wrap instead of overflowing when the
   window narrows. Right padding clears a 12px scrollbar plus breathing room. */
.ctl { flex: 0 1 auto; display: inline-flex; align-items: center; gap: 6px;
  margin-left: auto; flex-wrap: wrap; justify-content: flex-end; row-gap: 6px;
  max-width: 100%; }
/* nav chips then the control cluster, all pinned right. The links group also takes
   margin-left:auto so the pair sits flush right even when there are no chips. */
.top-links { flex: 0 1 auto; margin-left: auto; justify-content: flex-end; }
.top .nav-toggle { flex: 0 0 auto; }
.top { padding-right: max(1rem, calc(1rem + 12px)); }
.brand-logo { height: 36px; width: auto; display: block; }
/* Reader font scale — set by the A-/A+ controls, persisted in a cookie so it follows
   the operator across every screen and every session. */
/* Reader scale. 1..7 = 12/13/14/16/18/20/23px. The old 1..5 kept their exact pixel
   values by shifting up two steps, so adding "Smaller" and "Tiny" at the bottom changes
   nothing for anyone sitting on Normal or above. */
html[data-fs="1"] body { font-size: 12px; }
html[data-fs="2"] body { font-size: 13px; }
html[data-fs="3"] body { font-size: 14px; }
html[data-fs="4"] body { font-size: 16px; }
html[data-fs="5"] body { font-size: 18px; }
html[data-fs="6"] body { font-size: 20px; }
html[data-fs="7"] body { font-size: 23px; }
.fs-ctl { display: inline-flex; gap: 2px; margin-left: auto; }
.fs-btn { background: transparent; border: 1px solid var(--line); color: var(--fg);
  border-radius: 8px; min-width: 30px; height: 30px; font-size: 13px; line-height: 1;
  cursor: pointer; padding: 0 6px; }
.fs-btn:hover { border-color: var(--acc); color: var(--acc); }
.theme-btn { margin-left: 6px; }
.brand-name { font-weight: 700; letter-spacing: 0; }
.top-links a { margin-right: .6rem; color: var(--acc); }
.theme { margin-left: auto; }
.nav-toggle { display: none; }
/* The sidebar is a FULL-HEIGHT column and the header lives INSIDE .main, beside it.
   That is what puts the sidebar at the true top of the page: while the header was a
   sibling above .wrap, the sidebar could only ever start below it. */
.wrap { display: flex; min-height: 100vh; min-width: 0; align-items: stretch; }
.side { width: 200px; flex: 0 0 200px; border-right: 1px solid var(--line);
  padding: .75rem;            /* restored: per-row margins double-indented the items */
  overflow-y: auto;
  /* Full-height sidebar: the old `max-height: calc(100vh - 60px)` stopped it short of the
     viewport bottom, which is the gap that was visible above the page bottom. */
  max-height: none;
  height: 100vh;
  position: sticky; top: 0;      /* its own height; the column scrolls independently */
  align-self: flex-start;        /* never stretch beyond the viewport */
  display: flex; flex-direction: column;
  flex-shrink: 0; }
.nav-item { display: flex; gap: .4rem; padding: .45rem .5rem; color: var(--fg); text-decoration: none; }
.nav-item.active, .nav-item:hover { background: var(--card); }

/* ---- sidebar items inherit the status page's .led button feel -----------------
   Copied from https://status.ebotservers.com/tasks/ (the real .led rule), not
   eyeballed from a screenshot. Kept: hard corners, uppercase tracking, the light
   text-shadow, and the press-down. Added for a nav list: a hover step, since .led's
   own transition list is filter/opacity/transform and nav items need a hover state. */
.side .nav-item { position: relative; border: 1px solid transparent; border-radius: 0;
  font-size: 12.5px; font-weight: 700; letter-spacing: 0; color: var(--fg);
  text-shadow: 0 0 8px rgba(157,255,176,.18);
  transition: filter .18s ease, opacity .18s ease, transform .12s ease,
              background .18s ease, border-color .18s ease, color .18s ease; }
/* hover keeps ONLY the text-colour lift; the surface (bg/border/filter) belongs to the
   sliding pill, so nothing here may paint a row-shaped box. */
.side .nav-item:hover { color: var(--fg2, var(--fg)); }
.side .nav-item:active { transform: translateY(1px); }
/* SELECTED ROW: no surface of its own.
   The sliding pill now parks on the active row when nothing is hovered, so the selected item
   and the hovered item are literally the same element. Leaving the accent border/background on
   the row painted a second edge inside the pill — the same doubling artefact. The row keeps
   only a text brightening, which the pill cannot express. */
.side .nav-item.active { border-color: transparent; background: none;
  color: var(--fg2, var(--fg)); }
/* filter on :hover removed — it tinted the whole row box (text included), which read as
   the ROW lighting up rather than the travelling pill. The pill owns the hover surface. */
.side .nav-item:active { transform: translateY(1px); }
.side .nav-item:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; }
/* the LED dot used on the status page, applied to the active nav item */


.main { flex: 1 1 0; min-width: 0; overflow-x: hidden;
  /* header stacks above the content now that it lives here */
  display: flex; flex-direction: column;
  padding: 0;                 /* the header and the body each own their padding */
  min-height: 100vh; }
.main > .top { flex: 0 0 auto; }
/* the page body keeps the old 1rem inset without double-padding the header */
/* ONE inner frame for every module page. The shell (.main) carries no side padding,
   so the header and the body each own theirs and never double-pad. Modules inherit
   this automatically — a new module needs no CSS of its own.
   Joshua: "add like 20px padding so the inner page doesnt stick to the sides" —
   horizontal inset raised from 1.25rem (20px) to 20px explicitly, and the top/bottom
   rhythm kept. 20px is the number he asked for; writing it in px (not rem) keeps it from
   drifting when the reader text-scale changes, which is a different control. */
/* FULL HEIGHT. `.main` is a flex COLUMN and `.modbody` is its last child, so without
   flex:1 the body only grows to its content and the surface stops part-way down a short
   page — the background/glass then shows a hard edge under the last card.
   flex:1 1 auto makes it absorb the remaining column height; min-height:100% is the
   belt-and-braces for the non-flex fallback path. box-sizing keeps the padding inside. */
.modbody {
  padding: 20px 20px 1.75rem;
  min-width: 0;
  flex: 1 1 auto;
  min-height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}
/* the content inside must NOT stretch, or a short page's last child grows to fill the
   viewport and every card under it gets a huge gap. */
.modbody > * { flex: 0 0 auto; }
.modbody > .livedata { margin-top: .5rem; }
.modbody > h1:first-child, .modbody > h2:first-child { margin-top: 0; }
/* standalone cards (login, public cards) inset their own content instead */
.card-in { padding: 0; min-width: 0; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 1rem;
  min-width: 0; overflow-x: auto; }
.login { max-width: 320px; margin: 2rem auto; display: grid; gap: .7rem; background: var(--card); padding: 1.2rem; border-radius: 10px; border: 1px solid var(--line); }
.login input { width: 100%; padding: .4rem; }
.err { color: var(--err); }
.flag { font-family: ui-monospace, monospace; }
.chat-fab { position: fixed; right: 1.5rem; bottom: 1.5rem; background: var(--acc); color: #fff;
  border: 0; border-radius: 30px; padding: .55rem 1.1rem; font-weight: 600; cursor: pointer;
  box-shadow: 0 8px 20px rgba(0,0,0,.2); z-index: 9998; }
.chat-fab:hover { transform: translateY(-2px); }

/* Chat widget — launcher + panel, styles modelled on the EbotServers site widget */
.chat-pop { position: fixed; right: 1.5rem; bottom: 5.5rem; width: min(380px, 92vw);
  height: min(540px, 72vh); background: var(--card); border: 1px solid var(--line);
  border-radius: 16px; box-shadow: 0 12px 32px rgba(0,0,0,.18); display: flex;
  flex-direction: column; overflow: hidden; z-index: 9999; }
.chat-pop[hidden] { display: none; }
.chat-header { background: var(--acc); color: #fff; padding: .8rem 1rem; display: flex;
  align-items: center; justify-content: space-between; }
.chat-id { display: flex; align-items: center; gap: .7rem; }
.avatar { width: 38px; height: 38px; border-radius: 50%; background: #fff; color: var(--acc);
  display: flex; align-items: center; justify-content: center; font-weight: 700; }
.chat-name { font-weight: 600; font-size: 15px; line-height: 1.2; }
.chat-role { font-size: 12px; opacity: .85; }
.chat-x { background: transparent; border: 0; color: #fff; font-size: 18px; cursor: pointer;
  line-height: 1; padding: 2px 6px; }
.chat-messages { flex: 1; padding: 14px; overflow-y: auto; display: flex;
  flex-direction: column; gap: 10px; background: color-mix(in srgb, var(--card) 88%, var(--bg)); }
.message { max-width: 86%; padding: 10px 14px; border-radius: 12px; font-size: 14px;
  line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
.message.bot { background: var(--card); border: 1px solid var(--line);
  border-bottom-left-radius: 2px; align-self: flex-start; }
.message.user { background: var(--acc); color: #fff; border-bottom-right-radius: 2px;
  align-self: flex-end; }
.message.pending { opacity: .55; font-style: italic; }
.chat-footer { padding: 10px; border-top: 1px solid var(--line); display: flex; gap: 8px;
  background: var(--card); }
.chat-input { flex: 1; border: 1px solid var(--line); border-radius: 8px; padding: 9px 12px;
  font-size: 14px; background: var(--bg); color: var(--fg); outline: none; }
.chat-input:focus { border-color: var(--acc); }
.send-btn { background: var(--acc); color: #fff; border: 0; border-radius: 8px;
  padding: 9px 16px; font-weight: 600; cursor: pointer; }
.send-btn:hover { filter: brightness(1.08); }
.hint { font-size: .8rem; opacity: .7; }
.logout { float: right; }
@media (max-width: 720px) {
  .nav-toggle { display: inline-block; }
  .side { display: none; position: absolute; background: var(--bg); height: 100%; z-index: 5; }
  .side.open { display: block; }
  .wrap.admin { position: relative; }
}

/* Matches the header controls: same box, same glass, same accent on hover. It was a
   hardcoded dark-teal pill with an 8px radius, so it read as a foreign element on every
   theme (and clashed badly on the light ones). */
.bigbtn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 34px; padding: 0 14px; box-sizing: border-box;
  font-size: 13px; font-weight: 600; letter-spacing: 0; text-decoration: none;
  color: var(--fg); background: var(--glass, transparent);
  border: 1px solid var(--glass-line, var(--line)); border-radius: 0;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  white-space: nowrap; margin: 10px 0;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.bigbtn:hover { border-color: var(--acc); color: var(--acc); background: var(--glass-strong, var(--card)); }
.bigbtn:active { transform: translateY(1px); }

/* Refresh control: same family as the header buttons. These forms had no CSS rule at
   all, so they painted as raw browser defaults and ignored every theme. */
.refresh { display: inline-flex; align-items: center; gap: 8px; margin: 6px 10px 10px 0; }
.refresh button, .refresh input[type="submit"] {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 13px; box-sizing: border-box;
  font-size: 12.5px; font-weight: 600; letter-spacing: 0;
  color: var(--fg); background: var(--glass, transparent);
  border: 1px solid var(--glass-line, var(--line)); border-radius: 0;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  cursor: pointer; white-space: nowrap;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.refresh button:hover, .refresh input[type="submit"]:hover {
  border-color: var(--acc); color: var(--acc); background: var(--glass-strong, var(--card));
}
.refresh button:active, .refresh input[type="submit"]:active { transform: translateY(1px); }
.refresh button:disabled { opacity: .55; cursor: default; }

.nav-item.nav-ext { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 10px; }

/* theme toggle: single icon button cycling system -> light -> dark */
.theme-btn{background:transparent;border:1px solid var(--line);color:var(--fg);
border-radius:8px;width:34px;height:30px;font-size:15px;line-height:1;cursor:pointer;
padding:0}
.theme-btn:hover{border-color:var(--acc);color:var(--acc)}

.card .bigbtn{margin-top:8px}
.card .b{font-size:12px}

/* ---- shared fragment primitives -------------------------------------------
   Generators emit these classes; the harness must style them or live data
   renders as unstyled HTML. Keep this block as the contract for fragments. */
.card .b,table.b{border-collapse:collapse;width:100%;font-size:13px;margin:6px 0}
table.b th{text-align:left;padding:5px 8px;border-bottom:2px solid var(--line);color:#8b98a5;font-weight:600}
table.b td{padding:4px 8px;border-bottom:1px solid var(--line);vertical-align:top}
.b .id,.id{font-family:ui-monospace,monospace;color:#2f6fb0}
.b .pc,.pc{color:#6b7783;text-align:right}
.b .mind,.ow{color:#6b7783}
/* Column widths: RUNNING and COMPLETE were clipping because the state cell auto-sized to
   the longest word and the pill's letter-spacing pushed past it. Give the state, percent
   and owner columns explicit room so no state label is cut and the owner name fits. */
/* id column is a fixed 39px: the ids are 2-3 digits, so a fixed width stops the
   column resizing as ids grow and keeps the state/% columns aligned down the table. */
/* These widths were written for the BOARD table's own columns (2-3 digit ids, STATE, %, OWNER)
   and were being applied to EVERY module — which is what crushed "action", "gate", "node" and
   "model id" down to a few characters. Scoped to the board fragment, where they are correct. */
table.b.board-t th:nth-child(1), table.b.board-t td:nth-child(1) { width: 42px; min-width: 42px; max-width: 42px; }
table.b.board-t th:nth-child(2), table.b.board-t td:nth-child(2) { min-width: 92px; white-space: nowrap; }
table.b.board-t th:nth-child(3), table.b.board-t td:nth-child(3) { min-width: 62px; }
table.b.board-t th:nth-child(4), table.b.board-t td:nth-child(4) { min-width: 88px; }
/* Every other table: a first text column must be able to hold its header word. */
.livedata table.b th, .livedata table.b td { padding: 5px 9px; }
.livedata table.b th { white-space: nowrap; }
.livedata table.b td:first-child { min-width: 4.5em; }
.livedata table.b td:last-child, .livedata table.b th:last-child { min-width: 5.5em; }
/* Never break a state label across lines — a wrapped RUNNING is as unreadable as a
   clipped one. */
.livedata table.b .pill { white-space: nowrap; }
.b .ts,.ts{font-family:ui-monospace,monospace;color:#6b7783;white-space:nowrap}

.pill{display:inline-block;padding:1px 7px;border-radius:9px;font-size:11px;font-weight:700;
line-height:1.5;border:1px solid transparent}
.pill.ok{background:#123d18;color:#6f6;border-color:#1d5a27}
.pill.blk{background:#3d1212;color:#f77;border-color:#5a1a1a}
.pill.run{background:#3d3412;color:#fd7;border-color:#5a4d18}
.pill.que{background:#12303d;color:#7cf;border-color:#1a4a5e}
.pill.empty{background:#2a2a2a;color:#aaa}

.meta{font-size:11px;color:#77808a;font-weight:400}
.err{color:#c0392b}
.empty{color:#c0392b;font-weight:600}
.need{color:#b7791f;font-size:12px;margin-top:2px}
.blk-b{color:#c0392b;font-size:12px;margin-top:2px}
.det{color:#6b7783;font-size:11px;margin-top:4px}

/* Fragment grids: give long text room. 260px was too narrow for spec strings and log
   lines — auto-fit columns collapsed into unreadable 4-up boxes. Cards now get a real
   minimum and the column count falls away on narrow screens. */
.grid, .grid-wrap { display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  gap: 14px; margin: 10px 0; }
.grid > div, .grid-wrap > div { min-width: 0; }
ul.kv{list-style:none;margin:6px 0;padding:0;font-size:12px}
ul.kv li{display:flex;gap:8px;padding:2px 0;border-bottom:1px solid var(--line)}
ul.kv li b{min-width:110px;color:#8b98a5;font-weight:600}
pre{background:#0e1116;border:1px solid var(--line);border-radius:6px;padding:8px 10px;
font-size:11px;line-height:1.4;overflow-x:auto;color:#c9d3dd;max-height:320px}

/* card headings inside a live fragment must be smaller than the card title */
.card h2{font-size:13px;margin:14px 0 4px;letter-spacing: 0;margin:8px 0 4px;color:#8b98a5}

/* phone: one card per row, sidebar slides over, tables scroll inside the card */
@media (max-width: 720px) {
  .cards { grid-template-columns: 1fr; }
  .main { padding: .6rem; min-width: 0; }
  .top { padding: .5rem .6rem; gap: .5rem; flex-wrap: wrap; }
  .card { padding: .6rem; }
  table.b, .card .b { font-size: 11px; }
  table.b th, table.b td { padding: 3px 5px; }
  .chat-pop { right: .5rem; left: .5rem; width: auto; bottom: 5rem; height: 68vh; }
  .chat-fab { right: .75rem; bottom: .75rem; }
}

/* ---- overflow containment: wide data must scroll INSIDE its card ---------
   The table box must stay a TABLE. `display: block` here was the root cause of the column
   breakage: it abandons the table layout algorithm, so column widths stop being shared
   between rows, each cell shrinks toward its content, and any text that does not fit wraps
   character-by-character (headers rendering as "acti/on"). It also let the table stop
   claiming the full content width, which is why tables only used the left third of the page.
   Scroll the SECTION sideways instead and leave the table alone. */
.card, .livedata { overflow-x: auto; max-width: 100%; }
.card pre, .livedata pre { max-width: 100%; overflow-x: auto; }
.card h2, .card h3 { overflow-wrap: anywhere; }
.livedata, .livedata > * { max-width: 100%; }

/* Long values (specs like "32c/98304MB/2000GB", log lines, hostnames) must never be
   clipped. Wrap them instead of hiding the tail, and let tables scroll sideways.
   NOTE: `word-break: break-word` / `overflow-wrap: anywhere` were here and are the second
   half of the stacking bug — they allow a break at ANY character inside a word. Use
   `overflow-wrap: break-word` instead: it only breaks a word that genuinely cannot fit, so a
   normal word in a normal column stays intact. */
.livedata, .livedata * { overflow-wrap: break-word; }
.livedata table.b { display: table; table-layout: auto; width: 100%; }
.livedata table.b th, .livedata table.b td { overflow: visible; white-space: normal; }
/* Every cell keeps its own word together; only an over-long token (an IP, a spec string)
   is allowed to break. */
.livedata table.b th, .livedata table.b td { word-break: normal; }
.livedata table.b th:first-child, .livedata table.b td:first-child { min-width: 3.5em; }
.livedata ul.kv li { flex-wrap: wrap; }
.livedata ul.kv li b { min-width: 96px; flex: 0 0 auto; }
.livedata pre { white-space: pre-wrap; }
/* On a phone: one column, no grid at all */
@media (max-width: 720px) {
  .grid, .grid-wrap { grid-template-columns: 1fr; }
}

/* ---- ROW LAYOUT ------------------------------------------------------------
   For modules whose fragment is a sequence of bare sections (h2 / table / pre),
   not a set of card divs. Each section is full-width, separated by a rule, and the
   fragment never gets split into cells. Grid is the wrong tool whenever the markup
   has no one-element-per-card wrapper. */
.rows { display: flex; flex-direction: column; gap: 18px; }
.rows > * { max-width: 100%; }
/* A TABLE inside a card grid must span the whole row. `.grid-wrap` is a 2-column grid meant
   for CARD fragments; a table placed in it inherits a single track and is stuck at ~half the
   content width (that is the "tables only use the left third/half" finding). */
.grid-wrap > table, .grid > table { grid-column: 1 / -1; width: 100%; }
.grid-wrap > h2, .grid-wrap > h3, .grid-wrap > p, .grid-wrap > pre,
.grid > h2, .grid > h3, .grid > p, .grid > pre { grid-column: 1 / -1; }
.rows > h2 { margin: 0; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
.rows > h3 { margin: 0; }
.rows > table.b { width: 100%; }
.rows > pre { margin: 0; }
.rows > ul.kv { margin: 0; }

/* ---- "new version available" toast ---------------------------------------- */
.ver-toast { position: fixed; right: 16px; bottom: 16px; z-index: 90;
  display: flex; align-items: center; gap: 10px; max-width: min(92vw, 420px);
  padding: 10px 12px; border: 1px solid var(--acc); border-radius: 6px;
  background: var(--bg); color: var(--fg); font-size: 13px;
  box-shadow: 0 6px 22px rgba(0,0,0,.35); }
.ver-toast[hidden] { display: none; }
.ver-toast .vt-text { flex: 1 1 auto; min-width: 0; }
.ver-toast .vt-btn { font: inherit; font-size: 12px; padding: 3px 9px; cursor: pointer;
  background: transparent; color: var(--acc); border: 1px solid var(--acc);
  border-radius: 4px; }
.ver-toast .vt-btn:hover { background: var(--acc); color: var(--bg); }
.ver-toast .vt-x { border-color: var(--line); color: var(--fg); }
@media (prefers-reduced-motion: no-preference) {
  .ver-toast { animation: vt-in .22s ease-out; }
  @keyframes vt-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
}

/* ---- sidebar footer: collapse + identity/logout, pinned to the bottom --------- */
.side { display: flex; flex-direction: column; }
.side .nav-item { flex: 0 0 auto; }
.side-foot { margin-top: auto; padding-top: 10px; border: 0;
  display: flex; flex-direction: column; gap: 8px; }
.side-collapse { display: flex; align-items: center; gap: 8px; width: 100%;
  font: inherit; font-size: 12px; padding: 7px 9px; cursor: pointer;
  background: transparent; color: var(--fg); border: 1px solid var(--line);
  border-radius: 6px; }
.side-collapse:hover { background: var(--card); border-color: var(--acc); color: var(--acc); }
.side-collapse .ico { font-size: 15px; line-height: 1; }

/* collapsed: icons only, footer keeps the expand control visible */
.side.collapsed { width: 54px; flex: 0 0 54px; padding: .6rem .4rem; }
.side.collapsed .nav-item { justify-content: center; padding: .45rem .2rem; }
.side.collapsed .lbl { display: none !important; }
.side.collapsed .nav-group { font-size: 0; padding: 6px 0; overflow: hidden; }
.side.collapsed .nav-group .meta { display: none; }
.side.collapsed .sc-label { display: none; }
.side.collapsed .side-collapse { justify-content: center; padding: 7px 0; }
.side.collapsed .side-collapse .ico { transform: rotate(180deg); }
.side.collapsed .logout button { width: 100%; font-size: 11px; padding: 6px 0; }

/* ---- nav group heading (Agents / Modules) ----------------------------------- */
.nav-group { font-size: 11px; letter-spacing: 0; text-transform: uppercase;
  color: var(--dim, var(--fg)); opacity: .75; padding: 10px 6px 4px; }

/* ---- logout: a real button, not a floated afterthought ---------------------- */
.logout { float: none; margin: 0; }
.logout button { width: 100%; font: inherit; font-size: 12px; padding: 7px 9px;
  cursor: pointer; background: transparent; color: #ff8a80; border: 1px solid #5a1a1a;
  border-radius: 6px; text-align: center; }
.logout button:hover { background: #2a0f0f; border-color: #ff5a4a; color: #ffb0a6; }

.side .nav-item .ico { position: relative; display: inline-block; min-width: 1.1em; }

/* ---- header logout: same button geometry as the theme control ---------------- */
.top .logout { float: none; margin: 0; display: inline-flex; }
.top .logout-btn { width: 34px; height: 30px; padding: 0; font-size: 15px;
  line-height: 1; display: inline-flex; align-items: center; justify-content: center;
  color: #ff8a80; border-color: #5a1a1a; }
.top .logout-btn:hover { border-color: #ff5a4a; color: #ffb0a6; background: #2a0f0f; }
.top .logout-btn:active { transform: translateY(1px); }
/* the sidebar footer keeps column layout for the collapse control only */
/* superseded: the footer owns no divider (see the stripped block below) */

/* the theme control is now a word, not a 34px glyph */
.theme-btn { min-width: 44px; padding: 0 8px; font-size: 12px; font-weight: 600; }
#theme-btn { min-width: 62px; }

/* ---- collapsed rail: icons only, and NOTHING can poke outside ---------------
   Earlier version hid "every child except .ico", but the nav label was a bare text
   node — display:none cannot hide a text node, so labels stayed and the 54px rail
   overflowed. Labels are now real <span class="lbl"> elements. These rules are the
   belt-and-braces: clip the rail, force the row to centre, never wrap or scroll. */
.side.collapsed { width: 56px; flex: 0 0 56px; padding: .5rem .3rem;
  overflow-x: hidden; overflow-y: auto; }
.side.collapsed .nav-item { justify-content: center; padding: .5rem 0;
  white-space: nowrap; overflow: hidden; }
.side.collapsed .lbl, .side.collapsed .sc-label, .side.collapsed .nav-group { display: none; }
.side.collapsed .ico { font-size: 15px; min-width: auto; }
/* collapsed footer keeps NO divider: it is a plain menu item */
.side.collapsed .side-collapse { justify-content: center; padding: 7px 0; }

/* ---- chat: a header control now, not a floating bubble ---------------------
   The FAB ate bottom-right screen space and floated over content. The open control
   moved into the header; the panel is anchored to that corner so it reads as a
   dropdown from the button that opened it. */
.top .chat-btn { min-width: 34px; padding: 0 7px; }
.chat-fab { display: none; }
.chat-pop { position: fixed; right: .75rem; top: 46px; bottom: auto;
  width: min(400px, 94vw); height: min(560px, 78vh); }
@media (max-width: 720px) { .chat-pop { right: .5rem; left: .5rem; width: auto; top: 48px; height: 72vh; } }

/* ---- selected nav item: indent the text past the LED dot --------------------
   The dot is absolutely positioned inside .ico, so on the active row it sat under the
   first character. Reserve the gutter on the active item only — otherwise the whole
   list shifts when the selection moves. */
.side .nav-item { padding-left: .5rem; }
.side .nav-item .ico { position: absolute; left: .4rem; top: 50%; transform: translateY(-50%); }
.side .nav-item.active { padding-left: .8rem; }

/* ---- TASK OPS as a header control (it left the sidebar) --------------------- */
.top-ext { display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 30px; text-decoration: none; font-size: 15px; line-height: 1;
  background: transparent; border: 1px solid var(--line); color: var(--fg);
  border-radius: 8px; }
.top-ext:hover { border-color: var(--acc); color: var(--acc); }

/* ---- pre-paint collapse, so the sidebar does not snap after load ----------- */
html[data-side="collapsed"] .side { width: 56px; flex: 0 0 56px; padding: .5rem .3rem;
  overflow-x: hidden; }
html[data-side="collapsed"] .side .lbl,
html[data-side="collapsed"] .side .nav-group { display: none; }
html[data-side="collapsed"] .side .nav-item { justify-content: center; padding-left: 0; }
html[data-side="collapsed"] .side .nav-item .ico { position: static; transform: none; }

/* ---- collapsible Agents group --------------------------------------------- */
.nav-disclose { display: flex; align-items: center; gap: 5px; width: 100%;
  background: transparent; border: 0; cursor: pointer; text-align: left;
  font: inherit; font-size: 11px; letter-spacing: 0; text-transform: uppercase;
  /* Match the sidebar icons exactly. They resolve to --dim through the theme rules
     (`html[data-theme$="break"] .nav-item .ico`), so this row uses --dim too and the
     whole rail reads as one family. The earlier problem was not the hue but OPACITY:
     at .75 the row washed out on light palettes. Full opacity, same colour as the icons. */
  color: var(--dim, var(--fg)); opacity: 1; padding: 10px 6px 4px; }
.nav-disclose:hover { color: var(--acc); opacity: 1; }
/* the caret must follow the row, not inherit a washed-out tint of its own */
.nav-disclose .nd-caret { color: inherit; }
.nav-disclose .meta { color: inherit; opacity: .75; }
.nav-disclose .nd-caret { transition: transform .15s ease; display: inline-block; }
.nav-disclose.closed .nd-caret { transform: rotate(-90deg); }
.nav-kids[hidden] { display: none; }
html[data-agents="collapsed"] .nav-kids { display: none; }

/* ---- the chat glyph has to read in a DARK palette -------------------------
   An emoji inherits the OS font colour and stays dark against dark backgrounds.
   Masking it through currentColor makes it follow the theme like every other icon. */
.top .chat-btn .ico { font-size: 15px; line-height: 1;
  filter: drop-shadow(0 0 1px rgba(255,255,255,.35)); }
html[data-theme="dark"] .top .chat-btn .ico,
html[data-theme="crt"] .top .chat-btn .ico,
html[data-theme="amber"] .top .chat-btn .ico,
html[data-theme="nord"] .top .chat-btn .ico,
html[data-theme="gruvbox"] .top .chat-btn .ico,
html[data-theme="tokyo"] .top .chat-btn .ico,
html[data-theme="solar-dark"] .top .chat-btn .ico { filter: brightness(1.9) saturate(1.1); }

/* ---- text style stacks (independent of the color theme, so they compose) ---- */
html[data-tf="2"] body { font-family: Optima, Candara, "Segoe UI", system-ui, sans-serif; }
html[data-tf="3"] body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; letter-spacing: 0; }
html[data-tf="4"] body { font-family: Georgia, "Times New Roman", serif; line-height: 1.5; }
html[data-tf="5"] body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
html[data-tf="6"] body { font-family: Rockwell, "Courier New", Georgia, serif; letter-spacing: 0; }
/* headings get a little more character than body text in the serif/display stacks */
html[data-tf="4"] h1, html[data-tf="4"] h2 { font-family: Georgia, "Iowan Old Style", serif; }
html[data-tf="6"] h1, html[data-tf="6"] h2 { font-family: Rockwell, "Courier New", serif; }
/* the whole nav + tables follow the stack too, not just prose */


/* ============================================================================
   Sidebar = the status page's glass panel. Copied rule-for-rule from
   https://status.ebotservers.com/tasks/ header block, retargeted at .side.
   ============================================================================ */
.side {
  border-right: 1px solid var(--line);
  background: rgba(7, 20, 12, .82);
  box-shadow: 0 0 24px rgba(57, 255, 20, .08) inset;
  backdrop-filter: saturate(1.15) blur(2px);
}
html[data-theme="light"] .side,
html[data-theme="solar-light"] .side {
  background: rgba(255, 255, 255, .78);
  box-shadow: 0 0 24px rgba(0, 0, 0, .05) inset;
}
html[data-theme="nord"] .side   { background: rgba(46, 52, 64, .82);
  box-shadow: 0 0 24px rgba(136, 192, 208, .10) inset; }
html[data-theme="gruvbox"] .side { background: rgba(40, 40, 40, .82);
  box-shadow: 0 0 24px rgba(250, 189, 47, .10) inset; }
html[data-theme="tokyo"] .side  { background: rgba(26, 27, 38, .84);
  box-shadow: 0 0 24px rgba(122, 162, 247, .12) inset; }
html[data-theme="solar-dark"] .side { background: rgba(0, 43, 54, .84);
  box-shadow: 0 0 24px rgba(42, 161, 152, .12) inset; }
html[data-theme="amber"] .side  { background: rgba(10, 6, 2, .84);
  box-shadow: 0 0 24px rgba(255, 176, 0, .10) inset; }
html[data-theme="crt"] .side    { border-radius: 0; }

/* ---- every nav item gets its OWN accent, so the list is scannable ---------- */
/* NOTE: the left indicator bar was removed on purpose. `.side .nav-item.active` used to
   set border-left-color, which drew a coloured stripe on the active row; the active state
   is now expressed by the row background alone. */
.side .nav-item { border-left: 0; }
.side .nav-item .ico { color: var(--ni, var(--fg)); }
.side .nav-item:hover {
  border-left-color: var(--ni, var(--acc));
  background: color-mix(in srgb, var(--ni, var(--acc)) 14%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--ni, var(--acc)) 34%, transparent),
              inset 0 0 10px color-mix(in srgb, var(--ni, var(--acc)) 12%, transparent);
  color: var(--ni, var(--acc));
  text-shadow: 0 0 10px color-mix(in srgb, var(--ni, var(--acc)) 55%, transparent);
}
.side .nav-item:active { transform: translateY(1px); }
.side .nav-item.active {
  border-left-color: var(--ni, var(--acc));
  background: color-mix(in srgb, var(--ni, var(--acc)) 10%, transparent);
  color: var(--ni, var(--acc));
}
/* ONE COLOUR PER MODULE, CYCLING THE PALETTE so no two neighbours match.
   These six hues are the SIDEBAR ICON COLOURS and they are deliberately NOT overridden per
   theme. themes.css sets `html[data-theme=...] .nav-item .ico { color: var(--dim) }` for
   eight themes and the `break` family, which flattened every icon to the same grey — the
   coloured set only survived on the unstyled `system` theme, which is why it looked like a
   system-only feature.

   The fix is specificity, not a per-theme copy: `0,2,0` (two classes) beats themes.css's
   `0,1,1` (attribute selector + one class), so the hue wins in EVERY theme including new
   ones, with one definition instead of nine. A theme that WANTS flat icons can still opt out
   by writing a longer selector. */
.side .nav-item .ico,
.side .nav-item:hover .ico,
.side .nav-item.active .ico { color: var(--ni, var(--acc)); }

.side .nav-item:nth-child(6n+1) { --ni: var(--acc); }
.side .nav-item:nth-child(6n+2) { --ni: var(--blue); }
.side .nav-item:nth-child(6n+3) { --ni: var(--amber); }
.side .nav-item:nth-child(6n+4) { --ni: #b48ead; }
.side .nav-item:nth-child(6n+5) { --ni: var(--red); }
.side .nav-item:nth-child(6n+6) { --ni: #8fd694; }
/* agents keep their own hue family so the group reads as one block */
.side .nav-agent:nth-child(odd) { --ni: var(--blue); }
.side .nav-agent:nth-child(even) { --ni: #b48ead; }
/* and the same hue carries the row's own text, so icon and label agree */
.side .nav-item:hover, .side .nav-item.active { color: var(--ni, var(--acc)); }

/* ---- FontAwesome: no fixed width on the controls, theme button first ------- */
.theme-btn { min-width: 0; width: auto; padding: 0 9px; }
#theme-btn { min-width: 0; }
.top .chat-btn { min-width: 0; }
.top-ext { width: auto; min-width: 34px; padding: 0 9px; }
.fa-icon { font-size: 14px; line-height: 1; }

/* ============================================================================
   GLASSMORPHISM — applied to the CHROME only (header, sidebar, cards, chat,
   toast). Deliberately NOT to data tables: blur behind a dense table destroys
   legibility and forces a repaint on every scroll. Chromium/Safari blur properly,
   and the rgba() fill keeps it looking right in Firefox where blur is weaker.
   ============================================================================ */
:root {
  /* --panel is the OPAQUE surface value; --glass is the tint layered on top of it.
     A tint alone is just transparency, which is how text behind a panel bled through. */
  --panel: #ffffff;
  --glass: rgba(0, 0, 0, .04);
  --glass-strong: rgba(0, 0, 0, .07);
  --glass-line: rgba(0, 0, 0, .12);
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, .18);
}
html[data-theme="light"] {
  --glass: rgba(255, 255, 255, .58);
  --glass-strong: rgba(255, 255, 255, .72);
  --glass-line: rgba(0, 0, 0, .10);
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, .10);
}
html[data-theme="solar-light"] {
  --glass: rgba(253, 246, 227, .62);
  --glass-strong: rgba(253, 246, 227, .78);
  --glass-line: rgba(88, 110, 117, .18);
  --glass-shadow: 0 8px 32px rgba(88, 110, 117, .12);
}

/* the app itself gets a soft gradient bed so the glass has something to sit over */
body {
  background: radial-gradient(1200px 600px at 12% -10%, color-mix(in srgb, var(--acc) 10%, transparent), transparent 60%),
              radial-gradient(900px 500px at 100% 0%, color-mix(in srgb, var(--blue, var(--acc)) 8%, transparent), transparent 55%),
              var(--bg);
  background-attachment: fixed;
}

.top {
  background: var(--glass);
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow);
  position: sticky; top: 0; z-index: 40;
}
.side {
  /* NO !important here, deliberately. An earlier pass pinned this with
     `background: var(--glass) !important` (an 4% black TINT) to add glass. Because it was
     !important it beat every later, more specific rule — including the theme's own
     sidebar background — so the sidebar stayed see-through and text behind it bled into
     the nav labels. The tint was never the surface; it layers ON an opaque base, which
     typography.css supplies at (0,2,1). */
  background: var(--panel, var(--card));
  border-right: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow), 0 0 24px rgba(57, 255, 20, .06) inset;
}
/* Card surfaces: BORDERLESS, and the blur stays.
   The operator wants no border and no boxed background on these surfaces — they read as
   floating glass panes over the page, separated by the blur and shadow alone. The
   background is intentionally transparent; the opaque base lives on the surfaces that
   actually need contrast (.side, .top). */
.card, .livedata > div, .login, .chat-pop, .ver-toast {
  background: transparent;
  backdrop-filter: saturate(1.3) blur(12px);
  -webkit-backdrop-filter: saturate(1.3) blur(12px);
  border: 0;
  border-radius: 12px;
  box-shadow: var(--glass-shadow);
}
.card:hover { background: transparent; box-shadow: var(--glass-shadow), 0 6px 26px rgba(0,0,0,.3); }
.chat-header { background: var(--glass-strong); backdrop-filter: blur(8px); }

/* nav items sit on the glass: hover adds a frosted chip, not a flat fill */
.side .nav-item:hover {
  background: var(--glass-strong);
  box-shadow: 0 4px 18px rgba(0, 0, 0, .22),
              0 0 14px color-mix(in srgb, var(--ni, var(--acc)) 30%, transparent),
              inset 0 1px 0 rgba(255, 255, 255, .06);
  border-radius: 8px;
}
.side .nav-item.active { border-radius: 8px; }

/* tables stay OPAQUE — no blur, no translucency, full contrast for reading */
.card table.b, .livedata table.b, table.b {
  background: var(--bg);
  backdrop-filter: none;
}
/* fallback: a browser without backdrop-filter gets a solid surface, never a wash */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .top, .side, .card, .login, .chat-pop, .ver-toast { background: var(--panel, var(--card)); }
}

/* ============================================================================
   SQUARE CORNERS — the operator wants hard edges everywhere. Declared last with
   !important-free specificity where possible, and a blunt reset where a radius was
   set inline by a fragment or a third-party sheet.
   ============================================================================ */
button, input, select, textarea, .card, .login, .chat-pop, .ver-toast, .confirm-pop,
.side .nav-item, .side .nav-item:hover, .side .nav-item.active, .side-collapse,
.theme-btn, .top-ext, .fs-btn, .tf-btn, .bigbtn, .send-btn, .login-btn, .logout button,
.pill, .chat-header, .chat-id .avatar, .chat-input, pre, code, .grid > div, .livedata > div {
  border-radius: 0 !important;
}

/* ---- sidebar: real gutter between icon and label --------------------------- */
.side .nav-item { display: flex; align-items: center; gap: 10px; padding: .5rem .75rem; }
.side .nav-item .ico {
  position: static; transform: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; min-width: 18px; margin: 0;
}
.side .nav-item .lbl { flex: 1 1 auto; min-width: 0; padding-left: 2px; }
/* collapsed rail keeps the icon centred and hides the label entirely */
.side.collapsed .nav-item { gap: 0; padding: .5rem 0; }
.side.collapsed .nav-item .lbl { display: none !important; }

/* ============================================================================
   SIDEBAR COLLAPSE — the control lives pinned at the BOTTOM of the rail, and
   collapsing reduces every item to its icon.
   ============================================================================ */
.side { display: flex; flex-direction: column; }
.side-foot {
  /* No rule, no surface, no glass: the collapse control is a plain menu item pinned to
     the bottom. This block used to paint a border + glass panel, which is why removing it
     lower down had no effect — five .side-foot rules existed and this one won. */
  position: sticky; bottom: 0; margin-top: auto;
  padding: 10px 0 2px;
  border: 0; background: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none;
}
.side-collapse { width: 100%; justify-content: flex-start; }

/* collapsed: icons only. Every hideable label is an element, so this actually applies. */
.side.collapsed { width: 56px; flex: 0 0 56px; padding: .5rem .35rem; overflow-x: hidden; }
.side.collapsed .nav-item { gap: 0; padding: .5rem 0; justify-content: center; }
.side.collapsed .lbl { display: none !important; }
.side.collapsed .nav-group,
.side.collapsed .nav-disclose .nd-caret,
.side.collapsed .nav-disclose .meta { display: none !important; }
/* Collapsed rail: every text label must go, including the collapse control's own. The
   label used .sc-label and nothing in the collapsed rules matched it, so "collapse" stayed
   rendered in a 54px rail and overflowed — invisible on dark, obvious on light. */
.side.collapsed .sc-label,
.side.collapsed .side-collapse .lbl,
.side.collapsed .side-foot .lbl,
.side.collapsed .btn-lbl { display: none !important; }

/* ---- collapsed: the logo becomes an icon-sized box ----
   Parent pinned to 48px with overflow hidden and the logo sized to it. The previous
   `max-width: 100%` on the image let it shrink unpredictably inside a 56px rail, so the
   mark sat off-centre and clipped unevenly. */
/* Collapsed logo: the box stays narrow and CLIPS the wordmark, leaving the mark/plug
   visible. `object-fit: contain` shrank the whole logo to fit, which showed the full
   artwork tiny instead of cropping to the icon. So: fixed height, natural width, and
   the parent's overflow does the cropping. */
/* Collapsed rail: SAME scale and SAME x as the expanded logo, only the wordmark is
   clipped away. Measured before/after in a real browser:

     expanded   img h=30px  width auto 143.9x30  left=20   (entire logo, scaled to 30px)
     collapsed  img h=30px  width auto 143.9x30  left=20   (same, clipped to the mark)

   The bug this replaces: the collapsed rule used `object-fit: none`, which draws the image
   at its NATURAL 590x123 size and lets the box crop it — so a 38x30 window showed the mark
   roughly 4x too large. `object-fit: none` was meant to "avoid scaling down" but the expanded
   view was already scaled to height 30, so the two states disagreed by that factor, which is
   exactly the "it gets bigger and looks wrong" report. The element itself is sized
   `width:auto; height:30px`, so its content fills the box with no distortion and no
   object-fit trickery is needed.

   Left inset: the expanded logo sits at x=20 (side padding 12 + wrapper padding 8). The
   collapsed rail's own padding is ~5px, so the wrapper carries 15px to land the mark on the
   same x. Without it the logo jumps left and the whole rail looks misaligned. */
html[data-side="collapsed"] .side .side-logo,
.side.collapsed .side-logo {
  width: 100%; max-width: 100%; overflow: hidden;
  margin: 0; padding: .35rem 0 15px 15px;
  justify-content: flex-start;      /* anchor left so the mark is what survives the crop */
  border-bottom: 0;
}
html[data-side="collapsed"] .side .side-logo .brand-logo,
.side.collapsed .side-logo .brand-logo {
  max-width: none;                  /* the 100% cap is what squeezed it */
  width: auto;                      /* natural aspect, same as the expanded rule */
  height: 30px;                     /* IDENTICAL height to expanded — same scale */
  object-fit: fill;                 /* element matches aspect, so nothing to letterbox */
  flex: 0 0 auto;
  padding-left: 0;
}

/* ---- logo nudge + collapsed slot width (operator instruction) ---------------
   The logo anchor is a fixed 40px slot while the rail is collapsed, and the mark inside
   shifts -7px so the glyph sits centred in that slot instead of hugging the crop edge.
   Expanded, the same mark shifts +1px to line up with the nav text's left edge.
   Both are written here, after the collapsed block above, so source order wins cleanly and
   there is exactly ONE nudge per state. */
.side-logo .brand-logo { position: relative; left: 1px; }

html[data-side="collapsed"] .side .side-logo,
.side.collapsed .side-logo {
  width: 40px;
  max-width: 40px;
  padding-left: 0;
}

html[data-side="collapsed"] .side .side-logo .brand-logo,
.side.collapsed .side-logo .brand-logo { left: 8px; }

/* ---- collapsed: each seat row shows its robot emoji, with a hover tooltip ---- */
/* Keyed on .side.collapsed ALONE. Adding html[data-side="collapsed"] made these rules
   depend on a second, separately-written attribute; when only the class was present the
   label hid but the emoji never appeared, leaving an empty row. */
/* collapsed: hide LABELS and counts, but KEEP the row's own icon */
html[data-side="collapsed"] .side .nav-item .lbl,
html[data-side="collapsed"] .side .nav-item .seat-live,
.side.collapsed .nav-item .lbl,
.side.collapsed .nav-item .seat-live { display: none !important; }
/* Scope the robot to AGENT rows only. An unscoped `.nav-item::after` replaced the icon
   on EVERY row, so module links (Board, Operations, Fleet, Catalog...) collapsed into
   robots too. Agent rows carry data-seat; module rows do not. */
/* Robot emoji removed from the agent list on purpose: with the brand fonts in place
   the emoji clashed with the type and read as noise. Agent rows show the real FontAwesome
   seat glyph like every other row; the counts stay in data-tip for the collapsed tooltip. */
.side .nav-item[data-seat]::after { content: none; }
/* every row centres while collapsed (agent or not); only agents get the emoji */
html[data-side="collapsed"] .side .nav-item,
.side.collapsed .nav-item {
  justify-content: center; position: relative;
}
/* module rows keep their real FontAwesome glyph while collapsed */
html[data-side="collapsed"] .side .nav-item:not([data-seat]) .ico,
.side.collapsed .nav-item:not([data-seat]) .ico { display: inline-flex !important; }
/* Tooltip in the collapsed rail is a REAL ELEMENT now (see nav_tip.js), not a ::before.
   A pseudo-element cannot be transitioned — it is created and destroyed per hover, so the
   text snapped and fast movement between rows flickered. #nav-tip is one container that is
   moved and re-labelled, exactly like the hover pill. */
#nav-tip {
  position: fixed; left: 0; top: 0;
  display: none;
  align-items: center;
  padding: 0 10px;
  background: var(--card, var(--bg));
  color: var(--fg);
  border: 1px solid var(--acc);
  font-size: 11.5px; font-weight: 600; line-height: 1; white-space: nowrap;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .42);
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  /* ONE duration and ONE curve for BOTH axes, so the container morphs to the next row
     instead of arriving in one direction and settling in the other. */
  transition: transform .28s cubic-bezier(.22, 1, .36, 1),
              opacity .12s linear;
}
#nav-tip.on { display: flex; opacity: 1; }
.side.collapsed .side-collapse { justify-content: center; }
.side.collapsed .side-foot { padding: 6px 0; }
.side.collapsed .nav-disclose { justify-content: center; padding: 8px 0; }
.side.collapsed .side-collapse { justify-content: center; padding: 7px 0; }
.side.collapsed .side-collapse .ico { transform: rotate(180deg); }
html[data-side="collapsed"] .side { width: 56px; flex: 0 0 56px; }

/* ---- header controls: icon + text ----------------------------------------- */
.theme-btn { display: inline-flex; align-items: center; gap: 6px; padding: 0 10px;
  height: 30px; font-size: 12px; font-weight: 600; letter-spacing: 0;
  white-space: nowrap; }
.top .logout-btn { padding: 0 10px; }
.btn-lbl { display: inline-block; line-height: 1; }
/* the theme + style labels are dynamic, so give them a stable minimum width
   instead of letting the header jump when the word changes length */
#theme-lbl { min-width: 52px; text-align: left; }
#tf-lbl, #fs-lbl { min-width: 38px; text-align: left; }
/* on a phone the labels are the first thing to go, not the controls */
@media (max-width: 860px) { .btn-lbl { display: none; } }
@media (max-width: 1100px) { #tf-lbl, #fs-lbl { display: none; } }

/* ============================================================================
   HEADER CONTROLS — FIXED WIDTHS.
   Each control is a fixed-size box: the label changes text (Theme -> Terminal,
   Size -> Normal/XXL) but the box never moves. flex-basis is pinned as well as
   width, because a flex item ignores width for its base size.
   ============================================================================ */
.top .ctl { gap: 6px; }
/* NO fixed widths. The operator reported the fixed --w windows clipping the labels.
   A control is now sized by its content: the icon and label sit at their natural width,
   padding does the rest, and the row wraps when space runs out. The only thing still
   pinned is a sane minimum so an icon-only state does not collapse. */
.top .ctl .theme-btn,
.top .ctl .top-ext,
.top .ctl .logout,
.top .ctl .logout-btn {
  flex: 0 0 auto;
  width: auto;
  min-width: 0;
  max-width: none;
  box-sizing: border-box;
  justify-content: flex-start;
  overflow: visible;
}

/* the label keeps its natural width (no ellipsis, no truncation) */
.top .ctl .btn-lbl {
  flex: 0 0 auto; min-width: 0; overflow: visible;
  white-space: nowrap;
}
.top .ctl .fa-icon { flex: 0 0 auto; }

/* narrow screens: drop the text, keep a square icon target */
@media (max-width: 900px) {
  .top .ctl .btn-lbl { display: none; }
  .top .ctl .theme-btn, .top .ctl .top-ext, .top .ctl .logout-btn {
    justify-content: center; padding: 0 9px;
  }
}

/* ---- header nav chips: Public / Task Ops / Admin ---------------------------
   They were unstyled anchors. Now: same height and square geometry as the control
   buttons, but visually lighter, because they are navigation rather than controls. */
.top-links { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.top-links a, .nav-chip {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 10px; box-sizing: border-box;
  font-size: 12px; font-weight: 600; letter-spacing: 0; text-decoration: none;
  color: var(--fg); background: var(--glass, transparent);
  border: 1px solid var(--glass-line, var(--line));
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border-radius: 0; white-space: nowrap;
  transition: border-color .18s ease, color .18s ease, background .18s ease;
}
.top-links a:hover, .nav-chip:hover {
  border-color: var(--acc); color: var(--acc);
  background: var(--glass-strong, var(--card));
}
.top-links a:active, .nav-chip:active { transform: translateY(1px); }
.top-links a .fa-icon { font-size: 12px; }
/* the current surface gets the accent border, matching .nav-item.active */
.nav-chip.is-current { border-color: var(--acc); color: var(--acc); }
@media (max-width: 900px) {
  .top-links a .btn-lbl, .top-links a .lbl { display: none; }
  .top-links a { padding: 0 8px; }
}

/* ============================================================================
   COLLAPSE = A MENU ITEM.
   Same geometry and hover behaviour as .nav-item: no box, no border-radius, colour
   from the same --ni accent system. It only differs by being pinned to the bottom.
   ============================================================================ */
/* .side-foot is a plain container now: no divider, no surface, no glass. It was still
   carrying a backdrop-filter and background from the earlier glass pass. */
.side-foot { border: 0; background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none; padding: 6px 0 2px; }
.side-collapse {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: .5rem .75rem; margin: 0; box-sizing: border-box;
  background: transparent; border: none; border-left: 2px solid transparent;
  color: var(--fg); font: inherit; font-size: 12.5px; font-weight: 700;
  letter-spacing: 0; text-align: left; cursor: pointer;
  --ni: var(--dim, var(--fg));
  transition: background .18s ease, color .18s ease, box-shadow .18s ease;
}
.side-collapse .ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; min-width: 18px; color: var(--ni); font-size: 14px;
}
.side-collapse:hover {
  border-left-color: var(--ni);
  background: color-mix(in srgb, var(--ni) 12%, transparent);
  color: var(--ni);
  box-shadow: 0 0 12px color-mix(in srgb, var(--ni) 26%, transparent);
}
.side-collapse:active { transform: translateY(1px); }
.side-collapse .sc-label { flex: 1 1 auto; }
/* collapsed rail: icon only, still a menu item, still at the bottom */
.side.collapsed .side-collapse { justify-content: center; gap: 0; padding: .5rem 0;
  border-left-color: transparent; }
.side.collapsed .side-collapse .sc-label { display: none !important; }

/* .side-sep removed: the collapse control no longer wants a rule above it. */
.side-sep { display: none; }



/* ============================================================================
   TYPE BINDING — one place where the per-theme type variables reach every surface.
   Themes declare the variables (see themes.css); the font button overrides the whole
   thing via data-tf in typography.css, which is loaded last.
   ============================================================================ */
body, .main, .livedata, .card {
  font-family: var(--font, system-ui, sans-serif);
  letter-spacing: 0;
  line-height: var(--lh-body, 1.45);
}
h1, h2, h3, h4 {
  font-family: var(--font-head, var(--font, inherit));
  text-transform: var(--tt-head, none);
  letter-spacing: 0;
  font-weight: var(--fw-head, 700);
}
.side, .side *, nav.top-links, .nav-chip, .nav-item, .nav-disclose,
.theme-btn, .top-ext, .btn-lbl, .logout button, .side-collapse,
.pill, .meta, .det, .id, th, td, label, input, select, textarea,
.chat-pop, .chat-pop *, .ver-toast, .confirm-pop, .login, .login * {
  font-family: var(--font-ui, var(--font, inherit));
}
button, .bigbtn, .send-btn { letter-spacing: 0; }
code, pre, .kv b, .flag { font-family: var(--font, monospace); }

/* ============================================================================
   BRAND — DO NOT LET A REGEX CLEANUP EAT THIS BLOCK AGAIN.
   The anchor is a fixed 38px window with overflow hidden (operator's spec).
   Geometry: the logo is 590x123px. At height 36px it renders ~173px wide, so a
   38px window shows the LEFT EDGE of the mark only. That is intentional; the full
   mark is available via .brand--fit on the anchor.
   ============================================================================ */
.brand {
  flex: 0 0 38px; width: 38px; min-width: 38px; max-width: 38px;
  height: 38px; overflow: hidden;
  display: flex; align-items: center; justify-content: flex-start;
  text-decoration: none;
}
.brand .brand-logo {
  height: 36px; width: auto; max-width: none;
  object-fit: contain; object-position: left center;
  display: block; flex: 0 0 auto;
}
/* whole-mark variant: scale the logo into the box instead of cropping it */
.brand--fit { width: auto; min-width: 38px; max-width: none; }
.brand--fit .brand-logo { width: 38px; height: auto; }

/* ============================================================================
   THEME-MATCHED SCROLLBARS.
   Built from each palette's own --acc / --line / --glass variables, so a new theme is
   covered automatically. Two syntaxes needed: the modern standard properties, and the
   legacy ::-webkit-* set which is what Chromium/Edge/Safari actually honour today.
   ============================================================================ */
* { scrollbar-width: thin; scrollbar-color: var(--acc) transparent; }
.side, .main, pre, .card table.b, .livedata table.b, .chat-messages, #chat-log {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--acc) 55%, transparent) transparent;
}

/* WebKit / Blink */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--acc) 50%, transparent);
  border-radius: 0;                       /* square, matching the rest of the UI */
}
::-webkit-scrollbar-thumb:hover { background: var(--acc); }
::-webkit-scrollbar-corner { background: transparent; }

/* the sidebar is the element the operator scrolls most: give it a visible rail */
.side::-webkit-scrollbar { width: 8px; }
.side::-webkit-scrollbar-track {
  background: color-mix(in srgb, var(--line) 60%, transparent);
}
.side::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--acc) 65%, transparent);
}
.side::-webkit-scrollbar-thumb:hover {
  background: var(--acc);
  box-shadow: 0 0 10px var(--acc);
}
/* Log/pre panes are same-family but dimmer so they read as secondary */
pre::-webkit-scrollbar-thumb, .chat-messages::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--dim, var(--acc)) 55%, transparent);
}

/* the logout stays semantically destructive, but tinted from the theme's red */
.top .logout-btn {
  background: color-mix(in srgb, var(--red) 10%, transparent);
  border-color: color-mix(in srgb, var(--red) 42%, var(--line));
  color: color-mix(in srgb, var(--red) 80%, var(--fg));
}
.top .logout-btn:hover {
  background: color-mix(in srgb, var(--red) 20%, transparent);
  border-color: var(--red);
  box-shadow: 0 0 18px color-mix(in srgb, var(--red) 50%, transparent);
}

/* ---- nested worker subgroup under Blue -------------------------------------- */
.nav-subdisclose { display: flex; align-items: center; gap: 5px; width: 100%;
  background: transparent; border: 0; border-left: 2px solid transparent; cursor: pointer;
  font: inherit; font-size: 11px; letter-spacing: 0; text-transform: uppercase;
  color: var(--dim, var(--fg)); opacity: 1; padding: 6px 6px 4px 18px; text-align: left; }
.nav-subdisclose:hover { color: var(--acc); opacity: 1; }
.nav-subdisclose .nd-caret { transition: transform .15s ease; display: inline-block; }
.nav-subdisclose.closed .nd-caret { transform: rotate(-90deg); }
.nav-subkids[hidden] { display: none; }
.side .nav-item.nav-worker { padding-left: 1.6rem; }
.side .nav-item.nav-worker .lbl { font-size: 11.5px; }
.side.collapsed .nav-subdisclose,
.side.collapsed .nav-subkids { display: none; }

/* ============================================================================
   HEADER CONTROL CONSISTENCY.
   The Exit button rendered a different font size from its neighbours: its label
   inherited the .logout button's own font-size while the other controls set 12px on
   .theme-btn. Pin ONE size and weight for every control and its label, so nothing can
   drift when a palette changes the base size.
   ============================================================================ */
.top .ctl .theme-btn,
.top .ctl .top-ext,
.top .ctl .nav-chip,
.top .ctl .logout,
.top .ctl .logout-btn,
.top .ctl .logout-btn .btn-lbl,
.top .ctl .theme-btn .btn-lbl,
.top .ctl .top-ext .btn-lbl {
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
  font-family: var(--font-ui, var(--font, inherit));
}
.top .ctl .logout-btn { padding: 0 10px; }
.top .ctl .theme-btn, .top .ctl .top-ext, .top .ctl .logout-btn { height: 30px; }
/* the icon glyph itself stays one size, independent of the label */
.top .ctl .fa-icon { font-size: 13px; }
.top-links .nav-chip, .top-links .nav-chip .fa-icon { font-size: 12px; }

/* ============================================================================
   LOGIN PAGE — first time this has actually been styled. The earlier two attempts
   patched an anchor that no longer matched, so they silently did nothing.
   ============================================================================ */
.login-wrap { display: flex; align-items: center; justify-content: center;
  min-height: calc(100vh - 120px); padding: 2rem 1rem; }
.login {
  width: min(420px, 94vw); margin: 0; padding: 26px 24px 22px;
  display: grid; gap: 14px;
  /* borderless floating glass, matching the card surfaces */
  background: transparent;
  border: 0;
  border-radius: 0; box-shadow: var(--glass-shadow, 0 10px 36px rgba(0,0,0,.5));
  backdrop-filter: saturate(1.35) blur(14px);
  -webkit-backdrop-filter: saturate(1.35) blur(14px);
  position: relative;
}
/* accent hairline across the top, theme-coloured */
.login::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, transparent,
              color-mix(in srgb, var(--acc) 80%, transparent), transparent); }
.login-brand { display: flex; justify-content: center; padding: 4px 0 2px; }
.login-brand .brand-logo { height: 34px; width: auto; max-width: none; }
.login h1 { margin: 0; font-size: 20px; text-align: center;
  font-family: var(--font-head, var(--font)); letter-spacing: 0;
  text-transform: var(--tt-head, none); }
.login-sub { margin: -6px 0 4px; text-align: center; font-size: 11.5px;
  color: var(--dim, var(--fg)); letter-spacing: 0; text-transform: uppercase; }
.login .field { display: grid; gap: 5px; }
.login .field-lbl { font-size: 10.5px; letter-spacing: 0; text-transform: uppercase;
  color: var(--dim, var(--fg)); }
.login input {
  width: 100%; box-sizing: border-box; padding: 10px 12px;
  background: color-mix(in srgb, var(--bg) 75%, transparent);
  border: 1px solid var(--glass-line, var(--line)); border-radius: 0;
  color: var(--fg); font-size: 14px; font-family: var(--font-ui, var(--font));
  outline: none; transition: border-color .18s ease, box-shadow .18s ease;
}
.login input:focus {
  border-color: var(--acc);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--acc) 45%, transparent),
              0 0 16px color-mix(in srgb, var(--acc) 32%, transparent);
}
.login input::placeholder { color: color-mix(in srgb, var(--dim, var(--fg)) 80%, transparent); }
.login-btn {
  margin-top: 4px; padding: 11px 14px; width: 100%; cursor: pointer;
  font-size: 13px; font-weight: 700; letter-spacing: 0; text-transform: uppercase;
  background: color-mix(in srgb, var(--acc) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--acc) 55%, var(--line));
  color: var(--fg2, var(--fg)); border-radius: 0;
  transition: background .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.login-btn:hover {
  background: color-mix(in srgb, var(--acc) 26%, transparent);
  border-color: var(--acc);
  box-shadow: 0 0 20px color-mix(in srgb, var(--acc) 50%, transparent);
}
.login-btn:active { transform: translateY(1px); }
.login .err { margin: 0; padding: 8px 10px; font-size: 12px;
  border-left: 3px solid var(--red); color: color-mix(in srgb, var(--red) 85%, var(--fg));
  background: color-mix(in srgb, var(--red) 10%, transparent); }
.login-note { margin: 2px 0 0; text-align: center; font-size: 10.5px;
  color: var(--dim, var(--fg)); }
/* a logged-out page has no controls: centre the brand and keep the header slim */
.top:has(.brand) { justify-content: flex-start; }


/* ============================================================================
   HOVER DROPDOWN for the cycling header controls.
   Each control is wrapped in .ctl-wrap (position: relative) so the panel anchors to its
   own button instead of the whole header, and marks the active entry so "where am I /
   what else is there" needs no clicking.
   ============================================================================ */
.ctl-wrap { position: relative; display: inline-flex; }
.ctl-menu {
  position: absolute; top: calc(100% + 6px); right: 0; min-width: 268px;
  max-width: min(370px, 92vw); padding: 6px;
  display: grid; gap: 1px; z-index: 60;
  /* GLASS, BUT OPAQUE UNDERNEATH.
     A translucent panel let the page text behind it seep through and the entries became
     unreadable. So the blur stays (the glass read) but an OPAQUE base sits under the tint —
     two layers: a solid var(--bg), then the glass tint composited over it at ~92%. Result:
     still frosted glass, nothing bleeding through. */
  background-color: var(--bg);
  background-image: linear-gradient(
      color-mix(in srgb, var(--glass, var(--card)) 92%, transparent),
      color-mix(in srgb, var(--glass, var(--card)) 92%, transparent));
  border: 1px solid var(--glass-line, var(--line)); border-radius: 0;
  box-shadow: 0 18px 50px rgba(0,0,0,.7), 0 0 0 1px rgba(255,255,255,.04);
  backdrop-filter: saturate(1.7) blur(22px);
  -webkit-backdrop-filter: saturate(1.7) blur(22px);
  isolation: isolate;
}
/* Firefox (and any engine without color-mix) gets a fully solid panel — readable beats pretty. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .ctl-menu { background-color: var(--card); background-image: none; }
}
/* ---- GLASS SURFACES: frosted look on an OPAQUE base ------------------------
   The operator reported page text seeping through the panels. A translucent backdrop
   only reads as glass when there is something behind it to blur; over an admin table it
   just makes text unreadable. So every glass surface composites a SOLID var(--bg) base
   under the tint, keeping the blur for the light/depth while guaranteeing contrast. */
.ctl-menu, .chat-pop, .side, .card, .login, .toast, .nav-item.active {
  background-color: var(--bg);
  background-image: linear-gradient(
      color-mix(in srgb, var(--glass, var(--card)) 92%, transparent),
      color-mix(in srgb, var(--glass, var(--card)) 92%, transparent));
}
.side, .card, .login, .nav-item.active {
  background-image: linear-gradient(
      color-mix(in srgb, var(--glass, var(--card)) 88%, transparent),
      color-mix(in srgb, var(--glass, var(--card)) 88%, transparent));
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .ctl-menu, .chat-pop, .side, .card, .login, .toast, .nav-item.active {
    background-color: var(--card); background-image: none;
  }
}
.ctl-menu[hidden] { display: none; }
.ctl-menu::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--acc), transparent); opacity: .75; }
.ctl-menu-head {
  padding: 7px 9px 6px; font-size: 10px; letter-spacing: 0; text-transform: uppercase;
  color: var(--dim, var(--fg)); border-bottom: 1px solid var(--line);
}
.ctl-item {
  display: grid; grid-template-columns: 14px 1fr; grid-template-rows: auto auto;
  column-gap: 8px; align-items: center; width: 100%; text-align: left; cursor: pointer;
  padding: 7px 9px; background: none; border: 0; border-left: 2px solid transparent;
  color: var(--fg); font: inherit; border-radius: 0;
}
.ctl-item:hover, .ctl-item:focus-visible {
  background: color-mix(in srgb, var(--acc) 14%, transparent);
  border-left-color: var(--acc); outline: none;
}
.ctl-item.on {
  background: color-mix(in srgb, var(--acc) 20%, transparent);
  border-left-color: var(--acc);
}
.ctl-tick { grid-row: 1 / span 2; font-size: 10px; color: var(--acc); opacity: 0;
  text-align: center; line-height: 1; }
.ctl-item.on .ctl-tick { opacity: 1; }
.ctl-name { font-size: 12.5px; font-weight: 600; letter-spacing: 0; }
.ctl-item.on .ctl-name { color: var(--acc); }
.ctl-desc { font-size: 10.5px; color: var(--dim, var(--fg)); opacity: .85; line-height: 1.25; }
.ctl-menu-foot {
  padding: 6px 9px 7px; margin-top: 2px; border-top: 1px solid var(--line);
  font-size: 9.5px; letter-spacing: 0; color: var(--dim, var(--fg)); opacity: .8;
}


/* ============================================================================
   SEAT PAGE TABS (Tasks / Sub-Agents).
   JS-off must still show exactly one pane: the panes are hidden by the attribute
   selector below unless :target names them, so a no-JS visitor gets the Tasks pane and
   a hash link still works. With JS the .on class drives it instead.
   ============================================================================ */
.tabs { display: flex; flex-wrap: wrap; gap: 0; margin: 0 0 14px;
  border-bottom: 1px solid var(--line); }
.tab {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 15px; margin: 0; cursor: pointer; text-decoration: none;
  color: var(--dim, var(--fg)); font: inherit; font-size: 12.5px; font-weight: 600;
  letter-spacing: 0; text-transform: uppercase; border: 0; background: none;
  border-bottom: 2px solid transparent; position: relative;
}
.tab:hover { color: var(--fg); background: color-mix(in srgb, var(--acc) 8%, transparent); }
.tab.on {
  color: var(--acc); border-bottom-color: var(--acc);
  background: color-mix(in srgb, var(--acc) 10%, transparent);
}
/* The count is PLAIN TEXT in the tab's own font — no chip, no accent colour, and it
   does not change weight when the tab is active. As a coloured pill it out-shouted the
   label it belonged to and made the two tabs look like different components. */
.tab .tab-n {
  display: inline; padding: 0; margin: 0;
  font: inherit; letter-spacing: 0;
  color: inherit; background: none; border: 0;
}
.tab.on .tab-n { color: inherit; background: none; }
/* one pane at a time */
.tabpane[hidden] { display: none; }

/* ---- Live seat indicators in the sidebar ---------------------------------- */
/* Offline seats are dimmed and sorted last (see nav_html). Dimming the ROW, not
   just the icon, so the state reads at a glance from across the room. */
.nav-item.nav-offline .lbl,
.nav-item.nav-offline .ico { opacity: .45; }
.nav-item.nav-offline:hover .lbl,
.nav-item.nav-offline:hover .ico { opacity: .75; }

/* Live-polled count slot. Fixed min-width so the numbers changing every second
   do not reflow the label and make the whole nav twitch.

   Both badges — sub-agents (4) and tasks (3) — are the SAME fact-shaped thing: a
   count in parentheses. They rendered at different sizes and the task count was
   accented cyan, which read as a status light rather than a number. So: one size
   for both, one neutral grey. Sizes are absolute (13px) rather than `.78em`,
   because `.78em` resolved against two different parents — `.side .nav-item` is
   pinned to 12.5px while `.nav-group` is 11px, so the same class produced two
   different sizes depending on which row it sat in. */
.nav-item .seat-live {
  margin-left: auto;
  font-size: 13px;
  font-size: max(13px, .78em);   /* 13px standing, but still honours a large text scale */
  font-variant-numeric: tabular-nums;
  opacity: .75;
  white-space: nowrap;
  color: var(--dim, #8b8f98);    /* neutral grey for BOTH badges */
}
/* The badge owns its colour and the INNER spans must not inherit the row's.
   The row rules that beat this were `.nav-item.active { color: var(--fg2) }` (0,3,0) and the
   per-theme `.nav-item:hover { color: var(--acc) }` — on several themes --fg2 is near-white, so
   the counts on the SELECTED agent row rendered white. Raising specificity here, and setting
   the colour on the spans directly rather than via `inherit`, closes both paths. */
.nav-item .seat-live,
.nav-item .seat-live .seat-bots,
.nav-item .seat-live .seat-tasks,
.nav-item .seat-live .seat-run,
.side .nav-item .seat-live,
.side .nav-item .seat-live .seat-bots,
.side .nav-item .seat-live .seat-tasks,
.side .nav-item .seat-live .seat-run { color: var(--dim, #8b8f98); }
/* `.seat-run` was accented cyan + bold. It is a task count like any other, so it
   keeps the weight for at-a-glance scanning but drops the colour. */
.nav-item .seat-live .seat-run,
.side .nav-item .seat-live .seat-run { font-weight: 700; color: var(--dim, #8b8f98); }

/* Stale = the snapshot stopped advancing. Amber, not red: the panel is alive,
   its data is old. Red would read as "the seat is down", which is a different fact. */
.nav-item.seat-stale .seat-live,
.nav-item.seat-stale .seat-live .seat-bots,
.nav-item.seat-stale .seat-live .seat-tasks,
.nav-item.seat-stale .seat-live .seat-run { color: #ffb020; }
#agents-toggle.seat-stale .meta { color: #ffb020; }

/* Sub-agent badge: (N) for the count of agents reporting to that seat. Same size and
   colour as the task count now — it used to be a smaller, dimmer sibling. */
.nav-item .seat-bots,
.nav-item .seat-tasks {
  font-size: 13px;
  font-size: max(13px, .78em);
  opacity: 1;
  margin-left: .35em;
}

/* Live percentage flash — visible enough to catch the eye, not a strobe. */
td.pc.pct-tick { background: rgba(0, 255, 157, .14); transition: background .5s ease; }

/* ============================================================================
   BG LAYER — user-chosen body background image.
   The image sits on a fixed pseudo-element and only ITS alpha moves, so the
   opacity slider never fades the text. `background-size: cover` fills any
   viewport without tiling. Sits under .main/.side (z-index 0) and above the
   theme's own body paint.
   ============================================================================ */
html[data-bg="on"] body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;              /* must never eat clicks on the panel */
  background-image: var(--ops-bg-img);
  background-size: cover;            /* required: fill, never tile */
  background-position: center center;
  background-repeat: no-repeat;
  background-attachment: fixed;
  opacity: var(--ops-bg-op, .18);
  transition: opacity .18s ease;
}
/* Keep the content above the image layer. Any theme that paints the body gets the
   image behind it, not over it.
   NOTE: .top must NOT be downgraded here. The header is `position: sticky; z-index: 40`
   and its dropdowns are children of it, so re-declaring z-index:1 on .top dropped every
   menu BELOW the page content. Only .main/.side need the lift; the header already sits
   above the image layer by virtue of its own z-index. */
/* Keep the content above the image layer. Any theme that paints the body gets the
   image behind it, not over it.
   NOTE: .top must NOT be downgraded here. The header is `position: sticky; z-index: 40`
   and its dropdowns are children of it, so re-declaring z-index:1 on .top dropped every
   menu BELOW the page content.

   AND .side MUST NOT get `position: relative` — that was the bug behind "the sidebar
   doesn't reach the bottom". `.side` is `position: sticky; top: 0` (a full-height column
   that stays put while the page scrolls). Re-declaring its position here, at equal
   specificity but LATER in the file, silently replaced sticky with relative — so as soon
   as a background image was enabled the sidebar scrolled away with the page and only ever
   covered the first viewport. Measured: bg off -> position sticky; bg ON -> relative.

   The z-index is all that is actually needed to lift it above the image layer, and a
   sticky element already establishes a stacking context, so z-index applies without
   touching `position`. */
html[data-bg="on"] .main { position: relative; z-index: 1; }
html[data-bg="on"] .side { z-index: 1; }

/* Dropdown panels: a fixed, high stacking context so no page content, sticky header or
   background layer can ever paint over them. 9999 clears .ver-toast (90), the sticky
   header (40) and the mobile .side drawer (5). */
.ctl-menu { z-index: 9999; }
.ctl-wrap { position: relative; z-index: 60; }

/* ---- BG control menu ---- */
.bg-menu { min-width: 340px; max-width: 380px; }
.bg-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  padding: 8px;
  max-height: 300px;
  overflow-y: auto;
}
.bg-tile {
  position: relative;
  height: 62px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background-size: cover;            /* thumbnails match the real fill behaviour */
  background-position: center;
  cursor: pointer;
  padding: 0;
  overflow: hidden;
}
.bg-tile:hover { border-color: var(--acc); }
.bg-tile.on { border-color: var(--acc); box-shadow: 0 0 0 2px color-mix(in srgb, var(--acc) 45%, transparent); }
.bg-tile.bg-none {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); color: var(--dim); font-size: 11px;
}
.bg-tile-n {
  position: absolute; left: 0; right: 0; bottom: 0;
  font-size: 9px; line-height: 1.5; padding: 2px 3px;
  background: rgba(0,0,0,.62); color: #fff;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bg-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 8px; border-top: 1px solid var(--line);
}
.bg-op-label {
  display: flex; align-items: center; gap: 8px; width: 100%;
  font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0;
}
.bg-op { flex: 1; accent-color: var(--acc); cursor: pointer; }
.bg-op-val {
  font-variant-numeric: tabular-nums; font-size: 11px;
  color: var(--fg); min-width: 34px; text-align: right;
}

/* Theme row swatch in the theme dropdown. */
.ctl-swatch {
  width: 10px; height: 10px; border-radius: 50%;
  display: inline-block; margin-right: 6px; flex: 0 0 auto;
  border: 1px solid rgba(0,0,0,.25);
}


/* ============================================================================
   SIDEBAR HOVER PILL — the fluid highlight that slides between nav rows.
   Ported from the pricing toggle on ebotservers.com. Values taken from that
   element: transform .4s cubic-bezier(0,1,.5,1), width .3s, and a glass surface.

   THE .side POSITION IS DECLARED HERE, ONCE, AND NOWHERE ELSE. It has been broken twice
   by a later same-specificity rule re-declaring `position` on .side:
     1. `position: relative` here replaced the `position: sticky` above it.
     2. `html[data-bg="on"] .side { position: relative }` did it again, so the sidebar
        came unstuck the moment a background image was enabled.
   Both produced the same report: "the sidebar doesn't reach the bottom".
   Rule: if you need a z-index or a stacking context on .side, set ONLY the z-index. Never
   re-declare `position` on .side from anywhere else in this file.
   `sticky` also establishes the containing block the absolutely-positioned pill needs, so
   the pill works without `relative`.
   ============================================================================ */
.side { position: sticky; top: 0; }
.nav-hover-pill {
  position: absolute;
  left: 0; top: 0;
  width: 0; height: 0;
  opacity: 0;
  pointer-events: none;          /* must never intercept the row's own clicks */
  z-index: 0;
  /* ONE border, drawn inside the box. A border plus an inset highlight plus a backdrop
     blur reads as three separate edges while the box is in motion, which is exactly the
     "two borders moving" report. Keep a single hairline and let the fill do the talking. */
  box-sizing: border-box;
  background: color-mix(in srgb, var(--acc) 13%, var(--glass, transparent));
  border: 1px solid color-mix(in srgb, var(--acc) 30%, transparent);
  border-radius: 3px;
  /* THE DOUBLE-EDGE FIX: transform, width and height must share ONE duration and ONE
     easing curve. They were .4s cubic-bezier(0,1,.5,1) for transform but .3s ease-out for
     size, so mid-flight the box was still resizing while it was already sliding — the
     leading and trailing edges moved at different speeds and read as two elements. */
  transition: transform .3s cubic-bezier(.22, 1, .36, 1),
              width .3s cubic-bezier(.22, 1, .36, 1),
              height .3s cubic-bezier(.22, 1, .36, 1),
              opacity .15s linear;
  will-change: transform, width, height;
}
.nav-hover-pill.on { opacity: 1; }

/* Rows must sit ABOVE the pill, and their own hover background is suppressed so there is
   only ever one highlight on screen — two would show as a doubled/blinking surface. */
.side .nav-item, .side .nav-disclose, .side .side-collapse,
.side .side-foot, .side .side-logo {
  position: relative;
  z-index: 1;
}
/* HOVER IS THE PILL'S, NOT THE ROW'S.
   Every per-row hover treatment is neutralised — background, border, filter and shadow —
   because the row is an invisible slot that the sliding pill highlights on its behalf.
   Anything left on the row itself showed up as a SECOND surface travelling with (or
   lagging) the pill: the "two borders moving" artefact. The border now belongs to the
   pill only (see .nav-hover-pill above), so there is exactly one edge on screen and it
   is the thing that animates. Specificity is raised to (0,4,0) and the rule sits last so
   it also beats the per-theme hover rules in themes.css and glass-buttons.css. */
.side .nav-item:hover,
.side .nav-disclose:hover,
.side .side-collapse:hover,
html body .side .nav-item:hover,
html body .side .nav-disclose:hover,
html body .side .side-collapse:hover {
  background: none !important;
  border-color: transparent !important;
  box-shadow: none !important;
  filter: none !important;
  outline: none !important;
}
/* the pill keeps its own border — that is the single edge the operator asked to keep */
.nav-hover-pill {
  border-width: 1px;
}
.side.collapsed .nav-hover-pill { border-radius: 0; }
/* keep the active row's own marking visible; it is state, not hover */
.side .nav-item.active { background: none; }
.side .nav-item.active::before { opacity: 0; }
/* The focus ring is the THIRD moving edge: on an active row the browser keeps it painted
   underneath the flowing pill, so the operator sees a rectangle that lags the pill. Keyboard
   users still need it, so it moves to :focus-visible - which does NOT fire on mouse hover. */
.side .nav-item:focus:not(:focus-visible) { outline: none; }

/* Collapsed rail: the pill still works, it just has fewer, square-er targets. */
.side.collapsed .nav-hover-pill { border-radius: 0; }

/* ---------- HEADERS: no letter-spacing (operator instruction) --------------
   Headings read as headers by SIZE and WEIGHT, not by tracked-out letters.
   The small-caps micro-labels (.nav-group, .field-lbl, group titles) keep their
   tracking — that IS their design. This block is last in the cascade so it is
   the single source of truth for heading spacing. */
h1, h2, h3, h4, h5, h6, .card h2, .ap-card h2,
.modbody > h1, .modbody > h2, .modbody > h3 {
  letter-spacing: 0;
}

/* ============================================================================
   HEADER CONTROL GEOMETRY — ONE RULE FOR EVERY CONTROL.
   Operator directive, applied as a single block at the END of the cascade so it is the
   last word on these selectors.

   What it covers: every button in the header cluster (theme, text style, text size, BG,
   Orbs, Chat, Exit) and the label spans inside them. The label spans are listed explicitly
   because they inherit font metrics from their parent but were previously being scaled
   independently, which made two buttons in the same row render at different heights.

   border-radius carries !important DELIBERATELY: glass-buttons.css re-declares the radius
   per theme (a 0-radius "hard corner" identity), and without the flag that per-theme rule
   wins back its own value on .theme-btn and the row stops matching.

   NOTE on the selector list: .top-ext and .top .ctl .nav-chip are kept in the rule even
   though nothing currently renders them — the header nav chips (Public / Task Ops / Admin)
   are built in app.py but never interpolated into the markup, so those classes are dead.
   Leaving them here costs nothing and means a chip that comes back is already styled.
   ============================================================================ */
.top .ctl .theme-btn, .top .ctl .top-ext, .top .ctl .nav-chip, .top .ctl .logout,
.top .ctl .logout-btn,
.top .ctl .logout-btn .btn-lbl, .top .ctl .theme-btn .btn-lbl,
.top .ctl .top-ext .btn-lbl, .top .ctl .nav-chip .btn-lbl {
  font-size: 12px;
  font-weight: 600;
  border-radius: 1px !important;
}

/* ---- Agents dropdown (top bar) ------------------------------------------------
   Same panel as the Theme / Style / Size menus, but its rows are LINKS and carry a live
   count. Wider than the control menus because a seat name plus two counts must not wrap. */
.agents-menu { min-width: 262px; }
.agents-menu .ctl-item { text-decoration: none; }
.agents-menu .agents-ico { color: var(--acc); opacity: .85; }
.agents-menu .agents-row .ctl-name { font-weight: 600; }
.agents-live {
  margin-left: auto; font-size: 10.5px; font-weight: 700; white-space: nowrap;
  color: var(--dim, var(--fg)); letter-spacing: .02em;
}
.agents-menu .ctl-desc { flex: 1 1 auto; min-width: 0; }
.agents-menu .ctl-item { gap: 8px; }
.ctl-count {
  display: inline-block; margin-left: 5px; padding: 0 5px; border-radius: 8px;
  font-size: 9.5px; font-weight: 700; line-height: 15px;
  background: color-mix(in srgb, currentColor 16%, transparent);
}
.seat-stale { opacity: .6; }
