/* mobot-rcs operator console.
 *
 * The vocabulary is deliberately the one `mobot-sandbox`'s admin console uses,
 * because an operator moves between them: a left icon rail, a page head of
 * kicker + display title + one line of subtitle, summary strips, filter pills,
 * cards built from uppercase micro-labels over their values, and outlined
 * action rows. Warm off-white ground, white cards, one dark-green accent.
 *
 * Layout rules that keep it usable rather than merely tidy:
 *  - a card is NOT a link. Buttons live inside cards, and a card-wide anchor
 *    swallows every one of their clicks (it did; see `renderCard`).
 *  - facts are a GRID, never a run-on line. `.facts` gives each value its own
 *    column so the eye can scan down one label.
 *  - the detail page is two columns: what the robot is doing on the left,
 *    what you can do about it pinned on the right.
 */

:root {
  --bg: #f4f2ee;
  --panel: #ffffff;
  --panel-sunk: #faf9f7;
  --text: #1b1f1d;
  --muted: #6f7671;
  --border: #e3e0da;
  --border-strong: #d3cfc7;

  --accent: #1f5140;
  --accent-soft: #e8f0ec;
  --ok: #2f6f4f;
  --ok-bg: #e8f2ec;
  --warn: #8a5a12;
  --warn-bg: #fbf1de;
  --bad: #9c3328;
  --bad-bg: #fbeae7;
  --offline: #6f7671;
  --offline-bg: #eceae6;
  /* The test orders' own colour. Not a status: nothing else on the page is
     purple, so the box that sends a whole fake delivery reads as that and
     not as a warning or a success. */
  --test: #5d3f94;
  --test-bg: #f2edf9;
  /* Return to origin's own colour, for the same reason the test orders have
     one: neither a warning nor a success, so neither --warn nor --ok would
     say what it is. Blue reads as "navigation" rather than as either. */
  --origin: #2a5da8;
  --origin-bg: #eaf1fb;
  /* There were four `--mode-*` colours here, one per WCS mode, shared by
     the environment badge and the settings page's mode word. The mode is
     retired (a site's `integrations:` array is the whole truth), and both
     now name the instances themselves, in the badge's neutral grey. */

  --radius: 14px;
  --radius-sm: 9px;
  --rail: 56px;
  --display: ui-serif, Georgia, "Times New Roman", serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { box-sizing: border-box; }

/* Every view below sets an explicit `display`, and a bare `display: flex` on a
   rule beats the `hidden` ATTRIBUTE -- which is how the sign-in card ended up
   painted on top of a fully rendered fleet page. `hidden` is how this app
   switches views, so it wins here, once, for everything. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; }
button { font: inherit; }

/* -- sign-in ----------------------------------------------------------------- */

#login-view { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
#login-card {
  width: 100%; max-width: 380px; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius); padding: 30px 28px 26px;
}
.brand { font-family: var(--display); font-size: 22px; letter-spacing: -.01em; }
.brand span { color: var(--muted); }
.kicker {
  margin: 14px 0 6px; font-size: 10px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted);
}
#login-card h1 { font-family: var(--display); font-size: 26px; font-weight: 500; margin-bottom: 18px; }
#login-card label { display: block; font-size: 12px; color: var(--muted); margin: 14px 0 5px; }
#login-card input {
  width: 100%; padding: 9px 11px; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); background: var(--panel-sunk); font-size: 14px;
}
#login-card input:focus { outline: 2px solid var(--accent); outline-offset: -1px; background: var(--panel); }
#login-card button[type="submit"] {
  width: 100%; margin-top: 22px; padding: 11px; border: none; border-radius: var(--radius-sm);
  background: var(--accent); color: #fff; font-weight: 600; cursor: pointer;
}
#login-card button[type="submit"]:hover { opacity: .92; }
#login-error { color: var(--bad); font-size: 13px; margin-top: 12px; min-height: 16px; }

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

#app-view { display: flex; min-height: 100vh; }
#app-view[hidden] { display: none; }

#rail {
  position: fixed; top: 0; bottom: 0; left: 0; width: var(--rail);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 14px 0; background: var(--accent); color: #fff; z-index: 5;
}
/* `.rail-brand` -- an `m` in a disc at the top of the rail -- is gone. It was
   `aria-hidden`, linked nowhere and did nothing: a logo mark on a tool with
   one deployment per host, where nobody is wondering whose console this is.
   The rail now starts with the first thing you can press. */
.rail-nav { display: flex; flex-direction: column; gap: 4px; flex: 1; margin-top: 2px; }
.rail-link {
  position: relative; width: 44px; height: 44px; border-radius: 11px;
  display: grid; place-items: center; color: rgba(255,255,255,.85);
  text-decoration: none;
}
/* SVG, NOT CHARACTERS. The rail used ▦ ◷ ⚙ -- U+25A6, U+25F7 and U+2699,
   three different Unicode blocks with three different design metrics. At one
   `font-size` they rendered at three different optical sizes, and the gear was
   half again the height of the grid; nothing in CSS fixes that, because the
   size difference is inside the font.

   Drawn on one 24 viewBox at one stroke width, they are the same size by
   construction. 18px inside the same 44px target the character glyphs had;
   the button size has not moved through any of this. */
.rail-icon {
  width: 18px; height: 18px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.rail-link:hover { background: rgba(255,255,255,.12); color: #fff; }
.rail-link.is-active { background: rgba(255,255,255,.2); color: #fff; }
.rail-label {
  position: absolute; left: 50px; white-space: nowrap; background: var(--text); color: #fff;
  font-size: 11px; padding: 4px 8px; border-radius: 6px; opacity: 0; pointer-events: none;
  transition: opacity .12s;
}
.rail-link:hover .rail-label, .rail-link:focus-visible .rail-label { opacity: 1; }
.rail-foot { display: flex; flex-direction: column; align-items: center; gap: 10px; }
#conn-badge { font-size: 15px; line-height: 1; color: rgba(255,255,255,.35); }
#conn-badge.live { color: #8fd6a9; }
#logout {
  width: 32px; height: 32px; border-radius: 50%; border: 1px solid rgba(255,255,255,.3);
  background: none; color: #fff; font-size: 10px; font-weight: 700; cursor: pointer;
  text-transform: uppercase; letter-spacing: .04em;
}
#logout:hover { background: rgba(255,255,255,.14); }

/* -- language toggle ---------------------------------------------------- */
/*
 * Bottom of the rail, with the other two things that are about the OPERATOR
 * rather than about the fleet: the connection dot and sign-out. Nothing above
 * it in the rail is a setting, and this is not a section to navigate to.
 *
 * The flag shown is the language currently IN USE, not the one a click would
 * switch to. Both conventions exist and neither is self-evident, so the
 * `title` says outright what the click does -- a lone flag with no label is
 * exactly the control an operator is most likely to read backwards.
 *
 * Drawn as inline SVG like every other rail icon rather than as an emoji
 * flag. Emoji flags are absent entirely on most Windows builds, where they
 * render as two letter-boxes ("GB", "CN") -- legible by accident in this
 * pair, but not a thing to rely on for the one control that changes what
 * every other word on the page says.
 */
#lang-toggle {
  width: 32px; height: 32px; padding: 0; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .3);
  background: none; cursor: pointer;
  display: grid; place-items: center;
}
#lang-toggle:hover { background: rgba(255, 255, 255, .14); }
#lang-toggle:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* The flag is a picture of a thing, so unlike `.rail-icon` it is filled
   rather than stroked with `currentColor`. The hairline keeps a light flag
   from dissolving into the rail's dark green. */
#lang-toggle .flag {
  width: 19px; height: 19px; border-radius: 50%; display: block;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .45);
}
#lang-toggle .flag[hidden] { display: none; }

/* `min-width: 0` is the whole of why this page is fluid.
 *
 * A flex item defaults to `min-width: auto`, which means "never shrink below
 * your content's minimum". The action toolbar declares `min-width: max-content`
 * so it can scroll its row of selects horizontally -- and with `auto` here
 * that minimum propagated all the way up: #main grew to 841px inside a 586px
 * viewport, the whole page gained a horizontal scrollbar, and the detail
 * header stretched to match, pushing its pills off-screen.
 *
 * With `0`, #main shrinks to the viewport and the toolbar scrolls on its own,
 * which is what its `overflow-x: auto` was always for. */
#main {
  flex: 1; min-width: 0;
  margin-left: var(--rail); padding: 18px 20px 40px; max-width: 1500px;
}

/* -- page head --------------------------------------------------------------- */

.page-head { margin-bottom: 12px; }
.page-head h1 { font-family: var(--display); font-size: 27px; font-weight: 500; letter-spacing: -.015em; }
.page-sub { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
.back-link { display: inline-block; margin-bottom: 10px; font-size: 12px; color: var(--muted); text-decoration: none; }
.back-link:hover { color: var(--accent); }
#detail-heading { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; width: 100%; }
#detail-heading .robot-id { font-family: var(--display); font-size: 30px; font-weight: 500; }
.detail-title-block { display: flex; align-items: flex-start; gap: 14px; }
.robot-identities { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; }
.robot-identity { color: var(--muted); font: 11px/1.2 var(--mono); white-space: nowrap; }
.detail-identities {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 3px; padding-top: 2px;
}
.heading-availability {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 5px; margin-left: auto;
  /* WRAPS TO ITS OWN LINE WHEN IT DOES NOT FIT, at whatever width that is.
   *
   * `flex: 1 1 22rem` is the whole mechanism: 22rem is the basis, so the
   * moment the title and identity lines leave less than that, the flex
   * container breaks the line and this takes the next one whole. Above that
   * it shares the line and `margin-left: auto` still pins it right.
   *
   * A basis rather than a media query because the width it stops fitting at
   * is not a property of the VIEWPORT -- it moves with the robot id's length,
   * the identity strings, and the language (English runs about half again as
   * wide as the Chinese here). Every breakpoint guessed for it was wrong at
   * some real window. */
  flex: 1 1 22rem;
}
/* WRAPS. This row is two status pills, an AVAILABILITY label and a switch,
 * and it had `flex-wrap` at its default of `nowrap` -- so on a narrow window
 * the pills took what they needed and the switch ran off the right edge. The
 * block above it wrapping to its own line did not help, because the overflow
 * is INSIDE this row, not between it and the title. */
.heading-availability-row {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; row-gap: 6px;
}
.heading-availability-row .switch-field {
  margin: 0 0 0 5px; padding: 0 0 0 13px;
  border-top: 0; border-left: 1px solid var(--border);
}
/* First on its line means nothing to divide from. */
.heading-availability-row .switch-field:first-child {
  margin-left: 0; padding-left: 0; border-left: 0;
}
.offerability-reason {
  max-width: 54ch; color: var(--muted); font-size: 11px; line-height: 1.35;
  text-align: right;
}

/* THE HEADER STOPS BEING TWO COLUMNS BEFORE IT RUNS OUT OF ROOM.
 *
 * `margin-left: auto` pushes the availability block to the right edge, which
 * is right on a wide screen and wrong the moment the title beside it stops
 * leaving room: the block keeps its place, the flex line refuses to break,
 * and "not taking new jobs" folds into a four-line column pressed against the
 * edge with the blocked-from-jobs line running out beside it. Nothing
 * overflows by measurement -- `scrollWidth` matches to the pixel -- which is
 * why this needs a breakpoint rather than a `min-width` fix. It LOOKS broken
 * while measuring clean.
 *
 * 900px, not the 720px the rest of the page uses: the two columns here are a
 * 30px robot id plus three mono identity lines against a switch and a
 * sentence, and they run out of room long before the card grids do. A
 * breakpoint belongs to the thing it fixes.
 */
@media (max-width: 900px) {
  .heading-availability {
    margin-left: 0;                 /* stop clinging to the right edge */
    align-items: flex-start;        /* and read from the left, like the title */
  }
  .heading-availability-row .switch-field {
    /* The rule above puts this at the start of its own line, so the divider
       that separated it from the pills now floats with nothing to its left. */
    margin-left: 0; padding-left: 0; border-left: 0;
  }
  .offerability-reason { text-align: left; max-width: none; }
}

/* -- the page banner --------------------------------------------------------- */
/*
 * What an operator must read before acting on this robot, one line each,
 * full width under the page head: why it is held out of new jobs, and what
 * the last action answered (index.html `.page-banners`, app.js
 * `buildBanners`).
 *
 * THE TONE IS THE WRITER'S, never read off the words: `setNote` is told
 * pending / ok / warn / bad by the code that knows. Colour carries the tone
 * three ways at once -- the tint, the left rule and the mark -- and the text
 * stays ink on every one, because the sentence is the part that has to be
 * read and a red or amber sentence is harder to.
 */
.page-banners { display: flex; flex-direction: column; gap: 6px; margin: 0 0 14px; }
.page-banner {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 8px 7px 11px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); border-left: 4px solid var(--muted);
  background: var(--panel); color: var(--text);
  font-size: 13px; line-height: 1.4;
}
.page-banner-icon {
  flex: none; box-sizing: border-box; width: 18px; height: 18px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--muted); color: #fff; font-size: 11px; font-weight: 700; line-height: 1;
}
.page-banner-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.page-banner-close {
  flex: none; width: 26px; height: 26px; padding: 0; margin: -3px 0;
  border: 0; border-radius: 6px; background: transparent; color: var(--muted);
  font-size: 18px; line-height: 1; cursor: pointer;
}
.page-banner-close:hover { background: rgba(0, 0, 0, .06); color: var(--text); }
.page-banner-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.page-banner.is-warn { background: var(--warn-bg); border-color: transparent; border-left-color: var(--warn); }
.page-banner.is-warn .page-banner-icon { background: var(--warn); }
.page-banner.is-warn .page-banner-icon::before { content: "!"; }
.page-banner.is-bad { background: var(--bad-bg); border-color: transparent; border-left-color: var(--bad); }
.page-banner.is-bad .page-banner-icon { background: var(--bad); }
.page-banner.is-bad .page-banner-icon::before { content: "!"; }
.page-banner.is-ok { background: var(--ok-bg); border-color: transparent; border-left-color: var(--ok); }
.page-banner.is-ok .page-banner-icon { background: var(--ok); }
.page-banner.is-ok .page-banner-icon::before { content: "\2713"; }
/* A new order (app.js `newOrderBanners`): news rather than a verdict, so the
   accent, not a status colour. It goes by itself after 30 s. */
.page-banner.is-new { background: var(--accent-soft); border-color: transparent; border-left-color: var(--accent); }
.page-banner.is-new .page-banner-icon { background: var(--accent); }
.page-banner.is-new .page-banner-icon::before { content: "+"; }
/* In flight: a ring that turns, which says "still going" where a static mark
   would read as an answer. A style change, not a DOM one -- nothing is
   replaced to animate it. */
.page-banner.is-pending { border-left-color: var(--accent); }
.page-banner.is-pending .page-banner-icon {
  background: transparent; border: 2px solid var(--accent-soft); border-top-color: var(--accent);
  animation: page-banner-spin .9s linear infinite;
}
@keyframes page-banner-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .page-banner.is-pending .page-banner-icon { animation: none; border-color: var(--accent); }
}

/* -- new-order toasts ---------------------------------------------------- */
/*
 * `.order-banners` (index.html `#fleet-order-banners`, the home page only,
 * app.js `newOrderBanners`) used to inherit `.page-banners`' flow layout, which
 * is right for the robot page's OWN banner -- it is meant to push the toolbar
 * down -- but wrong here: a line that appears or expires 30 s later shifted
 * every control below it by its own height, and a press mid-shift landed on
 * whatever slid under the pointer (`~/.claude/rules/40-code.md` "Frontend
 * that refreshes"). Taken out of flow entirely: fixed top-right, below the
 * env badge, so appearing or expiring moves nothing else on the page.
 *
 * `pointer-events: none` on the stack and `auto` on each line: the stack's
 * own box (and the gap between two lines, or the space below the last one)
 * must never intercept a click meant for whatever sits under it, while a
 * line itself stays hoverable.
 */
.order-banners {
  position: fixed; top: 62px; right: 14px; z-index: 55;
  width: 320px; max-width: calc(100vw - 28px);
  margin: 0; pointer-events: none;
}
.order-banners .page-banner { pointer-events: auto; box-shadow: 0 2px 10px rgba(0, 0, 0, .16); }

/* -- summary strip and filter pills ------------------------------------------ */

/* `.summary-strip` is gone from both pages that had one. It printed the same
   four counts the filter pills print, immediately above them, and only the
   pills were clickable -- so the strip was a read-only echo of the row under
   it. Deleted rather than restyled: no amount of styling makes a duplicate
   worth its vertical space. */

.filter-pills {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px;
  justify-content: center;
}
.filter-pill {
  border: 1px solid var(--border-strong); background: var(--panel); color: var(--muted);
  border-radius: 999px; padding: 5px 13px; font-size: 12px; cursor: pointer;
}
.filter-pill:hover { border-color: var(--accent); color: var(--accent); }
.filter-pill.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
.filter-pills:empty { display: none; }

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

.card-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
}
.card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 17px 14px;
  display: flex; flex-direction: column;
  /* A grid item's min-width is its content's by default, so one long pill
     row widened the card past its column instead of wrapping inside it. */
  min-width: 0;
}
.card.is-offline { background: var(--panel-sunk); }
/* WRAPS. The name plus three pills -- online, the verdict ("not available
   for jobs", or the longer "available · low priority (under takeover)") and
   "fake order" -- is wider than a 330 px card, and without wrapping the last
   pill hung outside the card's edge. */
.card-head {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px 8px; margin-bottom: 2px;
}
.card-title {
  font-family: var(--display); font-size: 19px; font-weight: 500; color: var(--text);
  text-decoration: none; margin-right: auto;
}
.card-title:hover { color: var(--accent); text-decoration: underline; }
.card-identities { margin: 2px 0 10px; }

/* -- status chips ------------------------------------------------------------ */

.pill {
  font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
}
.pill.online { background: var(--ok-bg); color: var(--ok); }
.pill.offline { background: var(--offline-bg); color: var(--offline); }
.pill.telemetry-warn { background: var(--bad-bg); color: var(--bad); }
.pill.available { background: var(--accent-soft); color: var(--accent); }
.pill.unavailable { background: var(--warn-bg); color: var(--warn); }
/* Under takeover and nothing else: held out of offers for now, not broken.
   The pool's own green, dashed -- neither the amber of "not available" nor
   the solid fill of "available" (app.js `availabilityPill`). */
.pill.temporary { background: transparent; color: var(--accent); border: 1px dashed var(--accent); }
.pill.outcome-good { background: var(--ok-bg); color: var(--ok); }
.pill.outcome-bad { background: var(--bad-bg); color: var(--bad); }
.pill.outcome-live { background: var(--accent-soft); color: var(--accent); }
/* Dashed rather than filled: a fake order is a fact about the job, not a fault. */
.pill.fake { background: transparent; color: var(--warn); border: 1px dashed var(--warn); }

.dot { font-size: 9px; line-height: 1; }

/* -- fact grid --------------------------------------------------------------- */
/* The single most important rule in this file. Facts used to render as one
   run-on line of `label: value  label: value  …`, which is why the page read
   as a dump. Each fact is a cell: micro-label above, value below, columns
   aligned down the card. */

.facts {
  display: grid; gap: 11px 16px; margin: 13px 0 0;
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
}
.facts.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fact { min-width: 0; }
.fact-label {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); display: block; margin-bottom: 2px;
}
/* `display: block` is not cosmetic: `overflow`/`text-overflow` have NO effect
   on an inline box, so a long task number printed straight over the next
   column instead of ellipsing. */
.fact-value {
  display: block; font-size: 13px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A task number is ~20 monospace characters, and a protocol-2 step is a plan
   task id of similar length. Neither fits a 1fr column in a three-across
   grid, and `auto-fit` above adds COLUMNS as the card widens instead of
   growing them, so a wider screen does not help. They get two. */
.fact.wide { grid-column: span 2; }
.fact-value.mono { font-family: var(--mono); font-size: 12px; }
.fact-value.dim { color: var(--muted); }

/* -- blockers ---------------------------------------------------------------- */

.blockers {
  margin-top: 13px; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--warn-bg); font-size: 12px; color: var(--warn);
}
.blockers.fault { background: var(--bad-bg); color: var(--bad); }
.blockers-title {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  display: block; margin-bottom: 3px; opacity: .85;
}
.blocker-list { margin: 0; padding-left: 16px; }
.blocker-list li { margin: 1px 0; }
.blocker-list li.is-cleared { opacity: .45; text-decoration: line-through; }

.fault-detail { margin-top: 10px; font-size: 12px; color: var(--bad); }

/* The fleet card's two notices (app.js `cardNotices`). The battery one is a
 * banner -- the page banner's red, full card width -- because it is the one
 * that asks an operator to go and do something. The reasons are one quiet
 * line: the list they used to be doubled the card's height. */
.card-banner {
  margin-top: 10px; padding: 6px 9px; border-radius: 6px;
  border-left: 3px solid var(--bad); background: var(--bad-bg); color: var(--bad);
  font-size: 12px; font-weight: 600; line-height: 1.35;
}
.card-reasons { margin-top: 8px; font-size: 12px; line-height: 1.35; color: var(--warn); }
/* Red when a person has to act -- a takeover to hand back, a battery to
   charge (app.js `needsAction`); amber for holds that pass on their own. */
.card-reasons.is-bad { color: var(--bad); font-weight: 600; }

/* -- actions ----------------------------------------------------------------- */

.card-actions { margin-top: auto; padding-top: 13px; }
.actions { display: flex; flex-direction: column; gap: 14px; margin-top: 12px; }
.order-actions {
  flex-direction: row; justify-content: flex-end; align-items: center; gap: 8px;
}
.action-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.action-row-label {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); width: 100%; margin-bottom: 3px;
}

/* Raw actuation is fenced off, and the fence is a SAFETY property rather than
   decoration (AGENTS.md §3): `open cabin` sends `EXEC_ACTION OPEN_LID`, no
   plan gate completes, no milestone fires, JD hears nothing and the task stays
   open forever. An operator reaching for it when they meant `Remote-collect`
   leaves the delivery permanently unfinished, so it must not look like a
   sibling of the control that completes one. */
.action-row.raw {
  border: 1px dashed var(--warn); background: var(--warn-bg);
  border-radius: var(--radius-sm); padding: 9px 11px; margin-top: 2px;
}
.action-row.raw .action-row-label { color: var(--warn); }
.action-row.raw button { background: var(--panel); }

/* The test orders: fenced like the raw lids, because each press sends a
   whole fake delivery to the robot on screen, and in a colour of their own so
   the two fences are never mistaken for one. One button per test route of
   the robot's site, each two lines: the route's name, then the bay and the
   destination it sends it between. */
.action-row.test-orders {
  border: 1px dashed var(--test); background: var(--test-bg);
  border-radius: var(--radius-sm); padding: 9px 11px; margin-top: 2px;
}
.action-row.test-orders button {
  display: inline-flex; flex-direction: column; justify-content: center;
  align-items: center; text-align: center; gap: 1px; background: var(--panel); border-color: var(--test); color: var(--test);
}
.action-row.test-orders button:hover { border-color: var(--test); color: var(--test); opacity: .9; }
.test-order-label { font-weight: 600; }
.test-order-route { font-size: 10px; line-height: 1.2; color: var(--muted); }
/* A site with no route: one muted line in the fence, where the buttons go. */
.test-orders-empty { font-size: 12px; color: var(--muted); }

/* Return to origin: its own box between the raw lids and the test orders,
   the same fence shape as both -- a dashed border in its own colour, so the
   dedicated shortcut for "go home" is never mistaken for the raw lid escape
   hatch (AGENTS.md §3) or for a button that sends a whole fake delivery. */
.action-row.origin {
  border: 1px dashed var(--origin); background: var(--origin-bg);
  border-radius: var(--radius-sm); padding: 9px 11px; margin-top: 2px;
}
.action-row.origin button {
  background: var(--panel); border-color: var(--origin); color: var(--origin);
}
.action-row.origin button:hover { border-color: var(--origin); color: var(--origin); opacity: .9; }
/* ONE BUTTON ON THE ROBOT PAGE. The Orders card's actions, the toolbar, the
   Taskplan card's head and the step's force buttons were three styles: the
   Taskplan head's set only a height, so it fell through to the browser's grey
   default, and the force buttons were a smaller one of their own. The fenced
   rows (raw lids, return to origin, test orders) keep their fence colours --
   those are a safety property (AGENTS.md §3) -- on this same shape.
   `.set-row button` (the Settings page's Save/Change password/Switch
   buttons) joins the same rule for the same reason: `actionButton()` gives
   those a bare `primary` class with nothing in the page's own CSS to match
   it, so they fell through to the browser default while every other button
   in the console looked like this one. */
.actions button, .action-row button,
.taskplan-card-head button, .task-step-actions button, .set-row button {
  border: 1px solid var(--border-strong); background: var(--panel); color: var(--text);
  border-radius: var(--radius-sm); padding: 6px 12px; font-size: 12.5px; cursor: pointer;
}
.actions button:hover, .action-row button:hover,
.taskplan-card-head button:hover, .task-step-actions button:hover, .set-row button:hover {
  border-color: var(--accent); color: var(--accent);
}
.actions button:disabled, .action-row button:disabled,
.taskplan-card-head button:disabled, .task-step-actions button:disabled, .set-row button:disabled {
  opacity: .45; cursor: not-allowed; border-color: var(--border); color: var(--muted);
}
.actions button.primary, .action-row button.primary,
.taskplan-card-head button.primary, .task-step-actions button.primary, .set-row button.primary {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.actions button.primary:hover, .action-row button.primary:hover,
.taskplan-card-head button.primary:hover, .task-step-actions button.primary:hover,
.set-row button.primary:hover {
  color: #fff; opacity: .92;
}
.actions button.danger:hover, .action-row button.danger:hover { border-color: var(--bad); color: var(--bad); }

.order-dialog {
  width: min(520px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--panel); color: var(--text);
  box-shadow: 0 22px 70px rgba(16, 24, 40, .26);
}
.order-dialog::backdrop { background: rgba(16, 24, 40, .46); }
.order-dialog-body { padding: 22px; position: relative; }
.order-dialog-close {
  position: absolute; top: 10px; right: 12px; width: 28px; height: 28px;
  border: 0; background: none; color: var(--muted); font-size: 20px;
  line-height: 1; cursor: pointer; border-radius: 6px;
}
.order-dialog-close:hover { color: var(--text); background: var(--border); }
.order-dialog-title { margin: 0; font-size: 18px; }
/* The start-from dialog names the order, its state and the row before the
   warning, so the reader agrees to something specific. */
.order-dialog-facts { margin-top: 14px; }
.order-dialog-message {
  margin: 12px 0 22px; color: var(--muted); font-size: 13px; line-height: 1.5;
  overflow-wrap: anywhere;
}
/* The force-complete dialog's relocalize checkbox: same inline label shape
   as `.mapping-auto-open-label`, spaced off the message above it. */
.order-dialog-checkbox-label {
  display: inline-flex; align-items: center; gap: 6px; margin: 0 0 18px;
  font-size: 13px; color: var(--text);
}
.order-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
.order-dialog-actions button {
  border: 1px solid var(--border-strong); background: var(--panel); color: var(--text);
  border-radius: var(--radius-sm); padding: 7px 13px; font-size: 12.5px; cursor: pointer;
}
.order-dialog-actions button:hover { border-color: var(--accent); color: var(--accent); }
.order-dialog-actions button.danger:hover { border-color: var(--bad); color: var(--bad); }
/* -- the availability switch -------------------------------------------------- */
/* Label, state and action in ONE element. A verb-only button ("Make
   unavailable") makes the reader infer the state from the verb, and that
   inference collides with the verdict pill next to it. */

.switch-field {
  display: flex; align-items: center; gap: 12px;
  margin-top: auto; padding-top: 13px; border-top: 1px solid var(--border);
}
/* In the actions rail it is one control among several rows, not the card's
   footer, so it keeps the spacing and drops the rule. */
.actions .switch-field { margin-top: 0; padding-top: 0; border-top: none; }
.switch-label {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); flex: none;
}
.switch {
  display: inline-flex; align-items: center; gap: 9px;
  background: none; border: none; padding: 4px 2px; margin: 0;
  font: inherit; font-size: 12.5px; color: var(--muted); cursor: pointer;
}
.switch:hover:not(:disabled) { color: var(--text); }
.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.switch:disabled { cursor: not-allowed; opacity: .55; }

/* The track is drawn, not an <input>: this performs an action on press rather
   than holding a value for a form, and `aria-pressed` is what announces it. */
.switch-track {
  position: relative; width: 34px; height: 19px; flex: none;
  background: var(--border-strong); border-radius: 999px;
  transition: background .14s;
}
.switch-track::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--panel); box-shadow: 0 1px 2px rgba(0,0,0,.22);
  transition: transform .14s;
}
.switch.is-on .switch-track { background: var(--accent); }
.switch.is-on .switch-track::after { transform: translateX(15px); }
.switch.is-on .switch-state { color: var(--accent); font-weight: 600; }

.action-note { font-size: 11px; color: var(--muted); margin-top: 7px; min-height: 14px; }
.action-note:empty { min-height: 0; margin-top: 0; }

/* -- panels and robot detail dashboard --------------------------------------- */

.detail-dashboard { display: flex; flex-direction: column; gap: 16px; }
.detail-content { min-width: 0; }
/* The telemetry rail's column. Empty while the drawer is shut on a phone,
   and then it takes no grid row, so it adds no gap under the cards. */
.detail-rail { min-width: 0; }
.detail-rail:empty { display: none; }
.robot-detail-grid {
  display: grid; gap: 14px;
  grid-template-columns: minmax(0, 75fr) minmax(0, 35fr);
  align-items: start;
}
.detail-left-stack,
.detail-robot-cards { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.detail-plan { min-width: 0; }
/* A FLOOR UNDER THE TASKPLAN CARD while a plan is on screen, so it holds its
   size as the robot moves from step to step instead of shrinking and
   growing -- and dragging every card below it -- with each one. The card's
   own height, measured in a browser at 1024, 1440 and 1920px wide:

     "select a step" hint 189 · a VOICE clip 242 · a step with its force
     buttons 280 · wait_collect, with the verification code 351 ·
     take_lift, with the lift crossing 442 (532 at 1024)

   352 holds every step of a delivery. The lift crossing, the one block tall
   enough that keeping room for it would have left a third of the card empty,
   is its own card below this one now. Only with a plan (`has-plan`, app.js
   `planCard`): an idle robot's card is one line and has nothing to hold. */
.detail-plan.has-plan { min-height: 352px; }
.taskplan-card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 10px;
}
/* Pause / Resume and Clear plan, together at the right of the head. */
.taskplan-card-actions { display: flex; align-items: center; gap: 6px; }

.panel {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 18px;
}
.panel h2 {
  font-size: 10px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
}
.panel-hint { margin: 5px 0 0; font-size: 12px; color: var(--muted); }
.hollow { font-size: 13px; color: var(--muted); margin: 12px 0 0; }

/* -- orders in progress (the registered-robots page) ------------------------- */
/*
 * Which robot takes which order, one row per open order (index.html
 * `#open-orders`, app.js `openOrderRows`). A grid of rows rather than a
 * table so it can fold to two lines per order on a phone. Task numbers run
 * to 33 characters (`FAKE-RD…-0A1B2C3D`), so they wrap rather than push the
 * row wider than the page.
 */
.open-orders { margin-bottom: 14px; }
.open-orders .hollow { margin-top: 8px; }
.open-orders-list { display: flex; flex-direction: column; margin-top: 8px; }
.open-order {
  display: grid; align-items: center; gap: 4px 12px;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.4fr) minmax(0, .9fr) minmax(0, 1fr) auto;
  padding: 7px 0; border-top: 1px solid var(--border); font-size: 13px;
}
.open-order-head {
  border-top: 0; padding-top: 0; font-size: 10px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.open-order > span { min-width: 0; overflow-wrap: anywhere; }
.open-order-no { font-family: var(--mono); font-size: 12px; }
.open-order-link { color: var(--accent); font-weight: 600; text-decoration: none; }
.open-order-link:hover { text-decoration: underline; }
.open-order-state { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.open-order-tags { justify-self: end; }

/* -- the robot page ----------------------------------------------------------- */

.robot-status-card .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* TELEMETRY VALUES WRAP RATHER THAN ELLIPSE, all of them and not just `mono`.
   These cards are three columns inside a side rail, so a column is ~90px --
   and the values are protocol enums (`CONTROL_MODE_AUTO`,
   `CONTROL_SOURCE_REMOTE`) and session phrases that do not come close to
   fitting. Ellipsed, the card showed `CONTROL_M...` and `CONTROL_S...`, which
   are the same eight characters for every value the field can take: the
   operator could not tell them apart at all.

   `overflow-wrap: anywhere` because those enums carry no spaces, so nothing
   else gives the browser a break point. Rows end up uneven, which is the
   trade: an unaligned value that can be read beats a tidy one that cannot. */
.robot-status-card .fact-value {
  white-space: normal;
  overflow-wrap: anywhere;
}
.telemetry-warning {
  margin-top: 10px; padding: 9px 11px; border-radius: var(--radius-sm);
  background: var(--bad-bg); color: var(--bad); font-size: 12px; line-height: 1.4;
}
.inspect-card { min-width: 0; }
.inspect-controls {
  display: flex; align-items: center; justify-content: flex-end; gap: 7px;
  margin-top: -20px; margin-bottom: 8px;
}
.inspect-paused { color: var(--warn); font-size: 11px; margin-right: auto; }
.inspect-call {
  margin-top: 12px; border: 1px solid var(--border); border-left: 3px solid var(--ok);
  border-radius: var(--radius-sm); padding: 10px; min-width: 0;
}
.inspect-call.is-bad { border-left-color: var(--bad); }
.inspect-call.is-pending { border-left-color: var(--warn); }
.inspect-head {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px;
}
.inspect-sender { font-size: 11px; font-weight: 700; color: var(--accent); }
.inspect-time { justify-self: center; color: var(--muted); font: 11px var(--mono); }
.inspect-copy { justify-self: end; }
.inspect-path, .inspect-sign {
  margin-top: 6px; color: var(--muted); font: 11px var(--mono); overflow-wrap: anywhere;
}
.inspect-path { color: var(--text); font-weight: 600; }
.inspect-message {
  margin-top: 9px; border: 1px solid var(--border); border-radius: 5px;
  background: var(--panel-sunk); overflow: hidden;
}
.inspect-label {
  display: block; padding: 5px 8px; border-bottom: 1px solid var(--border);
  color: var(--muted); font-size: 10px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
}
.inspect-message pre {
  margin: 0; padding: 8px; max-height: 260px; overflow: auto;
  color: var(--text); font: 11px/1.45 var(--mono); white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.inspect-response { border-color: color-mix(in srgb, var(--accent) 25%, var(--border)); }

.actions-toolbar {
  padding: 0 0 14px; border-bottom: 1px solid var(--border);
  overflow-x: auto;
}
/* ONE COLUMN, TWO LINES: the plan pickers, then the raw lids beside the test
   orders. The lids used to sit at the end of the pickers' line, which left no
   room for a second box beside them. */
.actions-toolbar .actions {
  display: grid; grid-template-columns: max-content;
  gap: 0 8px; margin: 0; align-items: end; justify-content: center;
  min-width: max-content;
}
.actions-toolbar .action-row {
  min-height: 50px; padding: 0; align-content: flex-end; flex-wrap: nowrap;
}
/* Padded on every side, not just spaced: `.actions-toolbar` scrolls
   sideways (`overflow-x: auto`), and a scroll container clips on BOTH axes,
   so a select's focus ring (2px outline + 2px offset) was cut off above and
   below the row with no room inside it. */
.actions-toolbar .action-row.plan-builder {
  grid-column: 1; grid-row: 1; min-height: 32px; padding: 8px 6px; border: 0; gap: 8px 14px;
}
.actions-toolbar .action-line {
  grid-column: 1; grid-row: 2; display: flex; align-items: center; gap: 8px;
  margin-top: 6px; justify-self: center;
}
.actions-toolbar .action-row.raw,
.actions-toolbar .action-row.origin,
.actions-toolbar .action-row.test-orders {
  margin: 0; min-height: 32px; padding: 5px 7px;
}
.actions-toolbar .action-note,
.actions-toolbar .plan-confirm { grid-column: 1 / -1; }
.actions-toolbar .plan-confirm { margin-top: 6px; }

/* Sends the one pick the selects hold; off until something is picked. */
.plan-submit {
  border: 1px solid var(--accent); background: var(--accent); color: #fff;
  border-radius: 6px; padding: 4px 12px; font-size: 12px; cursor: pointer;
}
.plan-submit:disabled { opacity: .45; cursor: not-allowed; }
.plan-submit:not(:disabled):hover { opacity: .92; }

/* The arrow is drawn, not the platform's: the native one sits wherever the
   browser puts it and the label ran under it. `font: 11.5px/1.2 inherit` was
   an invalid shorthand (`inherit` cannot be combined), so the whole
   declaration was dropped and every select fell back to 13.3px Arial --
   wider text than the box was sized for. */
.plan-action-select {
  width: 132px; min-width: 0; height: 30px; box-sizing: border-box;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); color: var(--text);
  padding: 5px 24px 5px 8px;
  font-family: inherit; font-size: 11.5px; line-height: 1.2;
  cursor: pointer; -webkit-appearance: none; appearance: none;
  text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
  background: var(--panel) no-repeat right 8px center / 9px 6px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 9 6'%3E%3Cpath d='M1 1l3.5 3.5L8 1' fill='none' stroke='%236f7671' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.plan-action-select::-ms-expand { display: none; }

/* Undock: a pick with no list, so a toggle. Picked reads as selected -- the
   same accent the Submit it arms is drawn in. */
.plan-pick-toggle[aria-pressed="true"],
.actions-toolbar .action-row .plan-pick-toggle.is-picked {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}

/* The cabin button for the action that changes the lid: solid green fill,
   opposite of the reported state (open → Close, closed → Open). */
.actions-toolbar .action-row.raw button.lid-is,
.action-row.raw button.lid-is {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.plan-action-picker {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
}
.plan-action-prefix { font-size: 11.5px; color: var(--text); }
.plan-action-select:hover { border-color: var(--accent); }
.plan-action-select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.plan-action-select:disabled {
  opacity: .45; cursor: not-allowed; border-color: var(--border);
  color: var(--muted);
}

.plan-confirm {
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  min-height: 42px; padding: 8px 10px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--panel-sunk);
}
.plan-confirm-text {
  flex: 1 1 280px; min-width: 0; font-size: 12.5px; color: var(--text);
}

.actions-toolbar .action-row button {
  height: 30px; box-sizing: border-box; padding: 5px 14px;
  font-size: 11.5px; white-space: nowrap;
}
/* Two lines, so not the one-line height every other toolbar button has. */
/* ONE HEIGHT FOR EVERY BOX'S BUTTONS, the two-line test order's, so the
   cabin pair, the origin shortcut and the test orders all sit level on the
   line. Every button centres its text both ways; the cabin and origin
   buttons are flex too, so a taller button still centres its one line rather
   than pinning it to the top. */
.actions-toolbar .action-row.raw,
.actions-toolbar .action-row.origin,
.actions-toolbar .action-row.test-orders { align-items: center; }
.actions-toolbar .action-row.raw button,
.actions-toolbar .action-row.origin button,
.actions-toolbar .action-row.test-orders button {
  height: 36px; padding: 3px 14px;
  display: inline-flex; flex-direction: column; justify-content: center;
  align-items: center; text-align: center;
}

.plan-detail { min-width: 0; }
.pane-title {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 4px;
}

/* -- the timeline ------------------------------------------------------------- */
/* ASCENDING: first row at the top, so reading down is reading forward in
   time. Two lists share these rules on the Timeline card -- the order's JD
   calls and the robot's own steps -- so a done row, the row in flight and a
   row not yet reached look the same in both. */

.timeline { list-style: none; margin: 10px 0 0; padding: 0; }
.timeline-step {
  position: relative; display: grid;
  grid-template-columns: 14px minmax(0, 1fr); gap: 0 8px;
  padding: 5px 0 5px 0; font-size: 12px;
}
/* The connecting rule, drawn between marks rather than around them.
   THE NUMBERS ARE DERIVED, not eyeballed, because being 1px out reads as a
   broken line rather than as a near miss. With `box-sizing: border-box` the
   mark is 9x9 including its border, and `.timeline-step` has `padding: 5px 0`:

     mark top    = 5 (step padding) + 5 (mark margin-top) = 10px
     mark bottom = 10 + 9                                 = 19px
     mark centre = 1 (mark margin-left) + 9/2             = 5.5px

   So the rule starts at the mark's BOTTOM (19px) and runs to 10px into the
   next row, where that row's mark begins -- `bottom: -10px`. It was
   `top: 17px; bottom: -1px`, which began 2px inside this dot and stopped 9px
   short of the next one; and `left: 6px` sat a pixel right of centre. */
.timeline-step:not(:last-child)::before {
  content: ""; position: absolute; left: 5px; top: 19px; bottom: -10px;
  width: 1px; background: var(--border);
}
.timeline-mark {
  width: 9px; height: 9px; border-radius: 50%; margin: 5px 0 0 1px;
  background: var(--panel); border: 2px solid var(--border-strong);
}
.timeline-name {
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.timeline-type, .timeline-at {
  grid-column: 2; font-size: 10px; color: var(--muted); opacity: .75;
}

.timeline-step.is-new .timeline-name { opacity: .55; }
.timeline-step.is-new .timeline-mark { border-style: dashed; }

.timeline-step.is-completed .timeline-mark {
  background: var(--ok); border-color: var(--ok);
}
.timeline-step.is-completed .timeline-name { color: var(--text); }

/* The highlight is a pill 4px wider than the row on its left, made by pulling
   the box left and pushing the content back. The MARK moves back with the
   padding; the connector does not -- `::before` is absolutely positioned
   against this box, so the negative margin carried the rule 4px left of the
   dots it is supposed to join. Compensated below. */
.timeline-step.is-in-progress {
  background: var(--accent-soft); border-radius: 7px;
  padding-left: 4px; margin-left: -4px;
  /* Room on the right too: the row's "Recover from here" sat flush against
     the highlight's edge. */
  padding-right: 8px;
}
/* `::before` is positioned against this row's padding box, whose left edge the
   negative margin moved to L-4 -- so the rule needs 5+4=9px to land on the
   same x as the dot at L+5.5. The rule arriving FROM ABOVE is drawn by the
   previous row, which is not shifted, so it is already aligned and needs
   nothing. */
.timeline-step.is-in-progress:not(:last-child)::before { left: 9px; }
.timeline-step.is-in-progress .timeline-mark {
  background: var(--accent); border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(31,81,64,.16);
}
.timeline-step.is-in-progress .timeline-name { color: var(--accent); font-weight: 700; }
.timeline-step.is-in-progress .timeline-type { color: var(--accent); opacity: .9; }

/* Force start / complete sit on the selected task card, not the timeline --
   at its right edge, where the Orders card's own actions sit, so every
   action on the page lines up down one side. */
.task-step-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}
/* The step's title at the left and its force buttons at the right, on the
   step's first line: the actions for this step sit at the top of it. */
.task-detail-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 8px 12px;
}
.task-detail-head .pane-title { margin: 0; }


.timeline-step.is-skipped .timeline-mark {
  background: var(--warn-bg); border-color: var(--warn);
}
.timeline-step.is-skipped .timeline-name,
.timeline-step.is-skipped .timeline-type { color: var(--warn); }

.timeline-step.is-failed .timeline-mark {
  background: var(--bad); border-color: var(--bad);
}
.timeline-step.is-failed .timeline-name,
.timeline-step.is-failed .timeline-type { color: var(--bad); }

/* ABANDONED -- RCS took the plan off the robot before it ended. Deliberately
   not the same mark as a failure: nothing went wrong with the robot, the plan
   was superseded. Hollow and muted, the way an unfinished thing should read. */
.timeline-step.is-abandoned .timeline-mark {
  background: var(--panel); border-color: var(--muted); border-style: dashed;
}
.timeline-step.is-abandoned .timeline-name { opacity: .7; }
.timeline-step.is-cancelled .timeline-mark {
  background: var(--offline); border-color: var(--offline);
}
.timeline-step.is-cancelled .timeline-name,
.timeline-step.is-cancelled .timeline-type { color: var(--offline); }

/* -- the Timeline card: the order | the robot --------------------------------- */
/* Full width under the toolbar, two equal columns. Equal because neither is
   the lesser account: after a restart the robot column is the one that says
   what is wrong ("Robot reports no plan") and the order column is the one that
   fixes it. One column on a phone (the 720px block at the end of this file). */
.timeline-columns {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px; align-items: start; margin-top: 8px;
}
.timeline-column { min-width: 0; }
.timeline-column + .timeline-column { border-left: 1px solid var(--border); padding-left: 22px; }
/* Direction, state and age share the row's second line, in that order. */
.timeline-dir { font-family: var(--mono); margin-right: 6px; }
.timeline-state { font-family: var(--mono); }
.order-step .timeline-at { margin-left: 6px; }
.order-step .timeline-at::before { content: "· "; }
/* A row not reached yet is greyed as a whole, not just its name: its call has
   not happened, and nothing on it is a fact yet. */
.timeline-step.is-ahead { opacity: .55; }
.timeline-step.is-ahead .timeline-mark { border-style: dashed; }
/* The button sits at the RIGHT of its row, level with the row's two lines,
   so it reads as an action on that call rather than a line of the timeline. */
.order-step { grid-template-columns: 14px minmax(0, 1fr) auto; }
.order-step .timeline-name { grid-column: 2; grid-row: 1; }
.order-step .timeline-type { grid-column: 2; grid-row: 2; }
/* The page's primary button (`.actions button.primary` and its peers), placed
   at the right of its row. It had a smaller style of its own. */
.order-step .timeline-start {
  grid-column: 3; grid-row: 1 / span 2; align-self: center; justify-self: end;
  border: 1px solid var(--accent); background: var(--accent); color: #fff;
  border-radius: var(--radius-sm); padding: 6px 12px; font-size: 12.5px;
  cursor: pointer;
}
.order-step .timeline-start:hover { color: #fff; opacity: .92; }
.order-step .timeline-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.order-step .timeline-why {
  grid-column: 2 / -1; margin-top: 4px; font-size: 11px; line-height: 1.4;
  color: var(--muted); overflow-wrap: anywhere;
}
/* The plan the robot holds, above its steps -- and the order, above its
   calls: a HEADING, so no mark, no connector and no highlight. It was a
   `.timeline-step` with its state's class, so a running order drew a second
   "current" pill directly above the current row, both reading "accepted".
   Only an exit colours it, on the state word. */
.timeline-head { margin-top: 10px; display: flex; flex-direction: column; gap: 1px; }
.timeline-head .timeline-name { color: var(--text); font-weight: 700; }
.timeline-head .timeline-type { opacity: 1; }
.timeline-head.is-completed .timeline-type { color: var(--ok); }
.timeline-head.is-failed .timeline-type { color: var(--bad); }
.timeline-head.is-cancelled .timeline-type { color: var(--offline); }

/* The 70% pane carries the task's OWN actions, inline rather than as a card:
   they are part of this task's detail, not a separate thing on the page. The
   robot's controls are their own card in the right column. */
.plan-detail .actions { margin-top: 18px; border-top: 1px solid var(--border); padding-top: 14px; }

/* -- task line and code ------------------------------------------------------ */

.task-line { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin-top: 12px; }
.task-no { font-family: var(--mono); font-size: 12.5px; }
.no-task { color: var(--muted); font-size: 13px; margin-top: 12px; }
.pickup-code-block { margin-top: 11px; }
.pickup-code-label {
  display: block; margin-bottom: 4px; color: var(--muted);
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
}
.pickup-code {
  font-family: var(--mono); font-size: 19px; letter-spacing: .16em; color: var(--text);
  background: var(--panel-sunk); border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm); padding: 7px 12px; display: inline-block;
}

/* -- the taskplan trace ------------------------------------------------------ */

.trace { list-style: none; margin: 13px 0 0; padding: 0; }
.trace li {
  display: flex; align-items: baseline; gap: 10px; font-size: 12px;
  padding: 5px 0 5px 14px; border-left: 2px solid var(--border);
}
.trace li.now { border-left-color: var(--accent); }
.trace-step { font-family: var(--mono); color: var(--text); }
.trace li.now .trace-step { font-weight: 700; }
.trace-at { color: var(--muted); margin-left: auto; }
.trace-tag {
  font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--accent); background: var(--accent-soft); border-radius: 999px; padding: 2px 7px;
}

/* -- job list (history) ------------------------------------------------------ */

.jobs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.job {
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 13px 16px;
}
.job-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.job-when { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.job-no { font-family: var(--mono); font-size: 13px; margin-right: auto; color: var(--text); text-decoration: none; }
.job-no:hover { color: var(--accent); text-decoration: underline; }
.job-robot { font-size: 11px; color: var(--muted); }

.trail { list-style: none; margin: 13px 0 0; padding: 0; }
.trail li {
  display: flex; align-items: baseline; gap: 12px; font-size: 12px;
  padding: 4px 0 4px 14px; border-left: 2px solid var(--border);
}
.trail-edge { font-family: var(--mono); color: var(--text); flex: none; }
.trail-cause { color: var(--muted); flex: 1 1 auto; min-width: 0; }
.trail-at { color: var(--muted); flex: none; font-family: var(--mono); }

/* -- misc -------------------------------------------------------------------- */

.load-more { margin-top: 14px; }
.spread { display: flex; align-items: center; gap: 10px; }
.spread .grow { margin-left: auto; }

.reported-plan {
  margin-bottom: 16px;
}

/* A reported task opens for the fields the row cannot hold -- error_reason,
   remarks, the timestamps. */
.timeline-step.is-selectable { cursor: pointer; }
.timeline-step.is-selectable:hover { background: rgba(0, 0, 0, 0.03); }
.timeline-step.is-selected { background: rgba(0, 0, 0, 0.05); }
.task-detail { margin: 12px 0 4px; }
/* A rule between the plan's own facts (current task, step, stop index) and
   each task's detail under them, so the two read as two sections. */
.plan-detail .task-detail {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border);
}
.facts-note { color: #8a5a00; }

/* SETTINGS — what this deployment is wired to.
 *
 * Built from the vocabulary already on the fleet and detail pages rather than
 * a set of its own: `.card`'s panel and radius, `.pill`'s shape and colour
 * pairs, `.action-row button`'s outline. A settings page that looked like a
 * different product would read as a different product's settings.
 *
 * One column, capped: these are short statements about the deployment, and a
 * card stretched across a wide screen puts its label and its value too far
 * apart to read as one fact. */
#settings-body {
  display: flex; flex-direction: column; gap: 14px;
  max-width: 1080px;
}
/* The two tabs (sites.js), above the password card and laid out as it is:
   one capped column of cards. The tab bar reads as the page's own navigation,
   so it borrows the rail's active state and the filter pills' accent. */
#settings-tabs { max-width: 1080px; margin-bottom: 14px; }
.settings-tabs {
  display: flex; gap: 2px; margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.settings-tab {
  border: 0; border-bottom: 2px solid transparent; background: none;
  color: var(--muted); font: inherit; font-weight: 500;
  padding: 8px 16px; margin-bottom: -1px; cursor: pointer;
}
.settings-tab:hover { color: var(--accent); }
.settings-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
.settings-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* THE ADD / EDIT DIALOG: every `<dialog class="cfg-dialog">` of Settings --
   New site, Add / Edit robot, Add pickup, Add destination, Add station --
   has this one shape: a head (the title), a body, and a foot with Cancel and
   the primary Save. It is opened with showModal() (sites.js `syncDialog`), so
   the page behind it is inert and the browser draws the backdrop. */
.cfg-dialog {
  width: min(720px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
  padding: 0; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); color: var(--text); overflow: auto;
  box-shadow: 0 22px 70px rgba(16, 24, 40, .26);
}
.cfg-dialog::backdrop { background: rgba(16, 24, 40, .46); }
.cfg-dialog-head { padding: 18px 22px 0; }
.cfg-dialog-title {
  margin: 0; font-family: var(--display); font-size: 17px; font-weight: 500;
  overflow-wrap: anywhere;
}
.cfg-dialog-sub { margin: 4px 0 0; color: var(--muted); font-size: 12.5px; }
.cfg-dialog-body { padding: 4px 22px 6px; }
/* The foot is a `.set-row` in the markup, so its two buttons are styled by
   `.set-row button` like every other button of Settings (Cancel, then the
   primary Save); only its place in the dialog is set here. The longer
   selector is for `.set-row`'s own margin, which is declared further down. */
.cfg-dialog .cfg-dialog-foot {
  justify-content: flex-end; gap: 8px; margin-top: 0; padding: 14px 22px 18px;
}
/* The fields are a grid of ~200px columns, not `.set-row`'s flex: the
   `.set-address-label` they reuse is the old one-field WCS address row's,
   whose `flex: 1 1 520px` put every field on a line of its own at full
   width. A cell that is not a single field -- a multi-select, the radios, a
   tick box -- takes a row of its own (`.cfg-dialog-wide`). */
.cfg-dialog-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px 14px; align-items: end; margin-top: 13px;
}
.cfg-dialog-wide { grid-column: 1 / -1; min-width: 0; }
.cfg-dialog .set-address-input, .cfg-dialog .mapping-input,
.cfg-dialog .mapping-select, .cfg-dialog .map-select {
  min-width: 0; width: 100%; max-width: none; box-sizing: border-box;
}
/* The label is small capitals; the choices inside its select are not. */
.cfg-dialog .set-address-label select { text-transform: none; letter-spacing: normal; }
/* The radios of the new-site form and its two file pickers: one wrapping row. */
.cfg-dialog-choices { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 22px; }
/* A multi-select with its label above and its hints below, across the row. */
.cfg-dialog-multi { display: flex; flex-direction: column; align-items: stretch; gap: 5px; }
.cfg-dialog-multi > .set-mode {
  color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .04em;
}
.cfg-dialog-multi .set-hollow { margin: 0; }
.cfg-dialog .multi-select { flex: none; width: 100%; min-width: 0; max-width: none; }
/* `.set-row .ms-chip-remove` below keeps a chip's x from being a full-size
   button; these multi-selects are not in a `.set-row`. */
.cfg-dialog .ms-chip-remove, .cfg-dialog .ms-chip-remove:hover {
  border: 0; background: transparent; color: inherit; border-radius: 999px;
  padding: 0 6px; font-size: 15px; line-height: 1.3; cursor: pointer;
}
.cfg-dialog .ms-chip-remove:hover { background: var(--border); }
/* A limit box no wider than its number. */
.endpoint-limit { width: 9ch; }
.endpoint-table td { vertical-align: middle; }
.sites-pane { display: flex; flex-direction: column; gap: 14px; }
.site-page-head { margin-top: 0; }
/* Delete site sits at the far end of the head, away from Back; its dialog's
   primary button is filled red, the one button on the tab that destroys. */
.site-page-head .site-delete { margin-left: auto; }
.site-page-head .site-delete:hover { border-color: var(--bad); color: var(--bad); }
.site-delete-what { margin: 0; line-height: 1.5; }
/* Under `.set-row button` (the foot's row), which outranks a bare class. */
.set-row button.site-delete-confirm { background: var(--bad); border-color: var(--bad); color: #fff; }
.set-row button.site-delete-confirm:disabled { opacity: 0.5; }
.site-choice { display: inline-flex; align-items: center; gap: 5px; }
/* A robot's row: Edit, then Remove. Remove's first press arms it (sites.js
   `removing`), and an armed button reads as the danger it is about to do. */
.site-robot-actions { white-space: nowrap; }
.site-robot-actions .set-action + .set-action { margin-left: 6px; }
.site-robot-remove.is-armed { border-color: var(--bad); color: var(--bad); background: var(--bad-bg); }
/* A test route's row, the same way; and, under a route the door would
   refuse, the server's reason across the whole row. */
.site-route-actions { white-space: nowrap; }
.site-route-actions .set-action + .set-action { margin-left: 6px; }
.site-route-remove.is-armed { border-color: var(--bad); color: var(--bad); background: var(--bad-bg); }
.site-route-problem-row td { padding-top: 0; }
.site-route-problem { display: block; margin: 0; }
/* THE MULTI-SELECT (sites.js `multiSelect()`): chips and a filter box in one
   bordered field, and the unchosen options in a list hung under it that
   scrolls past `max-height` -- a floor or POI list can run to hundreds. */
.multi-select { position: relative; flex: 1 1 320px; min-width: 240px; max-width: 560px; }
.ms-box {
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
  border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--panel-sunk); padding: 4px 6px; cursor: text;
}
.ms-box:focus-within { border-color: var(--accent); outline: 1px solid var(--accent); }
.ms-chip {
  display: inline-flex; align-items: center; gap: 2px; max-width: 100%;
  background: var(--accent-soft); color: var(--accent); border-radius: 999px;
  padding: 2px 3px 2px 10px; font-size: 12px; line-height: 1.4;
}
.ms-chip-label { overflow-wrap: anywhere; }
/* `.set-row button` styles every button of a settings row as a full-size
   action; a chip's x is not one, hence the longer selectors (the hover rule
   of `.set-row button` is more specific than a bare class). */
.set-row .ms-chip-remove, .set-row .ms-chip-remove:hover {
  border: 0; background: transparent; color: inherit; border-radius: 999px;
  padding: 0 6px; font-size: 15px; line-height: 1.3; cursor: pointer;
}
.set-row .ms-chip-remove:hover { background: var(--border); }
.ms-input {
  flex: 1 1 120px; min-width: 100px; border: 0; outline: none;
  background: transparent; color: var(--text); padding: 3px 2px;
  font: 12px var(--mono, ui-monospace, monospace);
}
.ms-list {
  position: absolute; z-index: 30; left: 0; right: 0; top: 100%;
  margin: 4px 0 0; padding: 4px; list-style: none;
  max-height: 240px; overflow-y: auto;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
}
.ms-option { padding: 6px 10px; border-radius: 6px; font-size: 12.5px; cursor: pointer; }
.ms-option.is-active, .ms-option:hover { background: var(--accent-soft); color: var(--accent); }
.ms-note { padding: 6px 10px; color: var(--muted); font-size: 12px; }
.site-setting + .site-setting { border-top: 1px solid var(--border); margin-top: 11px; }
.staged-stations .set-integration { display: flex; align-items: center; gap: 10px; }
.set-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 17px 15px;
}
.set-card-title {
  font-family: var(--display); font-size: 17px; font-weight: 500;
  color: var(--text); margin: 0;
}
.set-card-sub { color: var(--muted); font-size: 12.5px; margin: 4px 0 0; }
.set-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 13px;
}
.set-value {
  font-family: var(--mono, ui-monospace, monospace); font-size: 12px;
  color: var(--muted); overflow-wrap: anywhere;
}
.set-address-row { align-items: flex-end; }
.set-address-label {
  display: flex; flex: 1 1 520px; flex-direction: column; gap: 5px;
  color: var(--muted); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em;
}
.set-address-input {
  width: 100%; min-width: 260px; box-sizing: border-box;
  border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--panel-sunk); color: var(--text);
  padding: 7px 9px; font: 12px var(--mono, ui-monospace, monospace);
}
.set-address-input:focus {
  border-color: var(--accent); outline: 1px solid var(--accent);
}
/* A SWITCH, NOT TWO BUTTONS.
 *
 * It was two -- "Use real WCS" and "Use mock" -- with the current one filled.
 * A filled button reads as the thing you press, so the control said the
 * opposite of what it meant, and neither label told you where you already
 * were. A switch has a POSITION, which is a state rather than an offer, and
 * the word beside it names that state.
 *
 * The colours are the console's own: green for the ordinary case, amber for
 * the one you should notice. Amber rather than red because a mock rig is a
 * legitimate thing to be, just never a thing to be by accident. */
.set-toggle {
  position: relative; flex: none;
  width: 46px; height: 26px; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--panel-sunk);
  padding: 0; cursor: pointer; transition: background .14s, border-color .14s;
}
.set-toggle-knob {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; transition: transform .14s;
  box-shadow: 0 1px 2px rgba(0,0,0,.28);
}
.set-toggle.is-real { background: var(--ok); border-color: var(--ok); }
.set-toggle.is-mock { background: var(--warn); border-color: var(--warn); }
/* Mock is the RIGHT-hand position, so "pushed over to the unusual side" is
   the thing you see without reading anything. */
.set-toggle.is-mock .set-toggle-knob { transform: translateX(20px); }
.set-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.set-toggle:disabled { opacity: .45; cursor: not-allowed; }

/* The read-out. Legible at a glance from across a desk, which is the whole
   job: whether the rig in front of you is talking to a real vendor. */
.set-mode {
  font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
}
/* `.set-mode-real` / `-mock` / `-hybrid` coloured the WCS card's mode word
   until the mode was retired; the card lists each site's instances now
   (`.set-site-line`). */
.set-integrations { list-style: none; margin: 4px 0; padding: 0; }
.set-site-line { padding: 2px 0; }
.set-integration { padding: 2px 0; font-variant-numeric: tabular-nums; }
.set-integration-bad { color: var(--warn); }

.set-link {
  display: inline-block; margin-top: 13px; color: var(--accent);
  font-size: 13px; font-weight: 500;
}
.set-hollow { color: var(--muted); font-size: 12.5px; margin: 9px 0 0; }
.set-note { font-size: 12.5px; margin: 11px 0 0; color: var(--ok); }
.set-note-bad { color: var(--bad); }

/* A long refusal reason must not stretch the row. */
.map-select { padding: 4px 7px; font: inherit; max-width: 30ch; }
.map-upload-field {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 4px; min-width: 0;
}
.map-upload-label {
  color: var(--muted); font-size: 10.5px; font-weight: 700;
  line-height: 1; letter-spacing: .05em; text-transform: uppercase;
}
.map-poi-upload { max-width: 230px; color: var(--muted); font-size: 12px; }
.map-poi-upload::file-selector-button {
  border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--panel); color: var(--text); padding: 5px 9px;
  font: inherit; cursor: pointer; margin-right: 8px;
}
.map-upload-name { max-width: 22ch; }
.set-action {
  border: 1px solid var(--border-strong); border-radius: 5px;
  background: var(--panel); color: var(--text); padding: 5px 9px;
  font: inherit; cursor: pointer;
}
.set-action:hover { border-color: var(--accent); color: var(--accent); }
.set-action:disabled { opacity: .45; cursor: not-allowed; }

/* THE TWO STATIONS TABLES of a site's Maps section (Pickups, Destinations).
   A cell holds an input, so each table is as wide as its fields need and
   `.stat-table-wrap` scrolls it sideways on a narrow screen. A floor's rows
   are one `<tbody>` headed by a row naming the floor. */
.mapping-table { min-width: 640px; }
.mapping-table td { vertical-align: middle; }
.mapping-table .mapping-input,
.mapping-table .mapping-select { width: 100%; }
.mapping-table td.mapping-auto-open { text-align: center; }
.mapping-floor-row td {
  color: var(--muted); font-size: 11px; font-weight: 600;
  padding-top: 10px; border-bottom: 0;
}
.mapping-auto-open-label { display: inline-flex; align-items: center; gap: 6px; }
/* A sub-section's title: the Maps section's Pickups, Destinations and
   Import, and the add-a-station and robot forms' own. */
.mapping-section {
  color: var(--accent); font-size: 12px;
  font-weight: 600; border-bottom: 1px solid var(--border);
  padding: 6px 0 3px; margin: 10px 0 8px;
}
.site-maps-card .mapping-section { margin-top: 18px; }
.mapping-input,
.mapping-select {
  width: 100%; min-width: 0; border: 1px solid var(--border-strong);
  border-radius: 5px; background: var(--panel); color: var(--text);
  padding: 6px 7px; font: 12px/1.35 var(--mono, ui-monospace, monospace);
}
.mapping-input:focus,
.mapping-select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The "add a mapping" row sits in an ordinary flex `.set-row`, not the CSS
 * grid above -- `.mapping-input`/`.mapping-select`'s `width: 100%` is meant
 * for a grid cell, and inside a wrapping flex row it would force every
 * field onto its own line instead. Restoring their natural width here keeps
 * them inline, sized to their own content, the way every other control in a
 * `.set-row` already is. */
.mapping-add-row .mapping-input,
.mapping-add-row .mapping-select { width: auto; min-width: 130px; }

/* -- which deployment this is, and which of its sites -------------------- */
/*
 * Top right, fixed, above everything. It is not decoration: an operator with
 * two tabs open needs to know which one is production before pressing
 * anything, and the two facts are kept VISIBLY separate -- `prd` with a site
 * listing the mock is a real thing and one merged word would hide it.
 *
 * TWO CENTRED LINES on one button. The first, `DEV · <site name>`, carries the
 * colour: PRD is red because acting on it is irreversible, STG amber, dev and
 * test green. The second names the integration instances serving the picked
 * site (`jd-mock · jd-wcs`), in neutral grey. None of it is legible
 * to a colourblind reader on hue alone, so every one spells the word out.
 *
 * It is a control now (it opens the menu of the environment's sites), so it
 * takes pointer events; it used to pass them through to whatever was beneath.
 */
#env-badge {
  position: fixed; top: 10px; right: 14px; z-index: 60;
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .06em;
}
.env-badge-button {
  display: flex; flex-direction: column; align-items: stretch; gap: 1px;
  padding: 0; border: 0; border-radius: 6px; overflow: hidden;
  font: inherit; letter-spacing: inherit; text-align: center; cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .28);
}
.env-badge-line { display: block; padding: 4px 10px; line-height: 1.35; color: #fff; }
/* The environment is upper-cased by `site-picker.js`; the site's own name is not. */
.env-badge-line-site { background: var(--muted); }
/* Instance ids as they are written -- lower case, the file names in
   `integrations/` -- not shouted like the environment beside them. */
.env-badge-line-integrations { background: #3b3b3b; text-transform: none; }
.env-badge-caret { margin-left: 6px; opacity: .8; }
/* prd: irreversible. stg: a real WCS, not a real building. dev/test: local. */
#env-badge.env-prd .env-badge-line-site { background: #b4232b; }
#env-badge.env-stg .env-badge-line-site { background: #b06a00; }
#env-badge.env-dev .env-badge-line-site,
#env-badge.env-test .env-badge-line-site { background: #2f6f4f; }
.env-badge-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The menu of the environment's sites, hung under the button. */
.env-badge-menu {
  position: absolute; top: 100%; right: 0; min-width: 100%; margin: 4px 0 0; padding: 4px;
  list-style: none; background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
  font-weight: 500; text-transform: none; letter-spacing: 0;
}
.env-badge-option {
  display: flex; flex-direction: column; gap: 1px; width: 100%; padding: 6px 10px;
  border: 0; border-radius: 6px; background: transparent; color: inherit;
  font: inherit; text-align: left; white-space: nowrap; cursor: pointer;
}
.env-badge-option:hover, .env-badge-option:focus-visible { background: var(--panel-sunk); outline: none; }
.env-badge-option.is-current { background: var(--accent-soft); color: var(--accent); }
.env-badge-option-name { font-weight: 600; }
/* `#env-badge.mode-mock` / `-real` / `-hybrid` coloured the second span per
   WCS mode (yellow / green / blue) until the mode was retired. */

/* The toggle is phone-only; above the breakpoint the column is just shown. */
#telemetry-toggle {
  display: none; width: 100%; margin-bottom: 12px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: 8px; background: var(--panel);
  color: var(--text); font: inherit; font-size: 12px; font-weight: 600;
  cursor: pointer;
}
#telemetry-toggle:hover { background: var(--bg); }

/* -- phones -------------------------------------------------------------- */
/*
 * ONE COLUMN, everywhere. The fleet grid and the robot page both auto-fill
 * columns off a minimum width, which on a 375px screen leaves one column
 * anyway -- but the robot grid's 75fr/35fr split does not, and a 35fr column
 * of telemetry on a phone is a strip of clipped numbers.
 *
 * The rail keeps its width rather than becoming a bottom bar: it is four
 * targets, it is where every page is reached from, and a horizontal bar would
 * cost the vertical space this whole block is trying to win back.
 */
@media (max-width: 720px) {
  #env-badge { top: 6px; right: 8px; font-size: 9px; letter-spacing: .06em; }
  .env-badge-line { padding: 3px 6px; }

  /* Full width between a 16px gutter on each side, rather than the fixed
     320px box: at phone width that box either clips or, if it did not,
     would be the one thing on the page wider than the viewport. */
  .order-banners { top: 50px; left: 16px; right: 16px; width: auto; max-width: none; }

  #telemetry-toggle { display: block; }

  /* Fleet: one card per row. */
  .card-grid { grid-template-columns: minmax(0, 1fr); }
  /* Orders in progress: order and robot on one line, state, destination and
     the fake tag under them; the column heads would label nothing. */
  .open-order { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .open-order-head { display: none; }
  .open-order-tags { justify-self: start; }

  /* Robot: order and taskplan stacked, telemetry only when asked for. */
  .robot-detail-grid { grid-template-columns: minmax(0, 1fr); }
  /* The Timeline card: the order above the robot. */
  .timeline-columns { grid-template-columns: minmax(0, 1fr); }
  .timeline-column + .timeline-column {
    border-left: 0; padding-left: 0; border-top: 1px solid var(--border); padding-top: 12px;
  }


  #main { padding-left: 12px; padding-right: 12px; }
  .page-head h1 { font-size: 22px; }
}

/* The pickup-timeout box. A number input rather than a toggle: this is the one
   setting on the page with a magnitude rather than two states. */
.set-input {
  width: 6.5em; padding: 6px 8px; border: 1px solid var(--border);
  border-radius: 8px; background: var(--panel); color: var(--text);
  font: inherit; font-variant-numeric: tabular-nums;
}
.set-input:disabled { opacity: .55; cursor: not-allowed; }

/* The operating-hours bounds. Same box as `.map-select`; a distinct class
   because the value here is a clock hour, not a map bundle name. */
.set-hour-select { padding: 4px 7px; font: inherit; }
.set-hour-select:disabled { opacity: .55; cursor: not-allowed; }

/* A site setting's label: a fixed width, so the boxes of the rows line up.
   (Named for the test-order destinations it first lined up, which are test
   routes now, on a card of their own.) */
.set-test-order-label { min-width: 9em; font-weight: 600; }

/* The pickup countdown. Tabular figures so the digits do not jitter as the
   seconds tick -- a number that shifts sideways every second reads as a
   glitch rather than as a clock. */
.fact-value.countdown { font-variant-numeric: tabular-nums; }
.fact-value.countdown.is-urgent { color: var(--warn); font-weight: 600; }
.fact-value.countdown.is-expired { color: var(--bad); font-weight: 600; }

/* -- stats page (#/stats) ------------------------------------------------ */
/*
 * One column of cards (spec §6): a chart page reads top to bottom, never
 * scanned the way the fleet grid is, so there is no `.card-grid` here.
 * Every control below is bound with Alpine `x-model` (`stats.js`,
 * `statsView()`); this file only supplies the look, never the refresh -- a
 * 60s tick or a filter change calls `chart.setData()` in place, per the
 * "Frontend that refreshes" rule: binds, never rebuilds.
 */

.stat-filters {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px 16px;
  padding: 12px 14px; margin-bottom: 14px;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.stat-field {
  display: flex; flex-direction: column; gap: 4px; min-width: 0;
  flex: 1 1 130px;
}
/* The robot multi-select needs real room for its chips, so it takes its own
   full row instead of squeezing in beside the narrow single-value fields. */
.stat-field-wide { flex: 1 1 100%; }
.stat-field-label {
  font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted);
}
.stat-field select, .stat-field input[type="date"] {
  width: 100%; min-width: 0; box-sizing: border-box; height: 30px;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  background: var(--panel); color: var(--text); padding: 5px 7px; font: 12px inherit;
}
.stat-field select:hover, .stat-field input[type="date"]:hover { border-color: var(--accent); }
.stat-field select:focus-visible, .stat-field input[type="date"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.stat-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.stat-chip {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--border-strong); border-radius: 999px;
  padding: 4px 11px 4px 8px; font-size: 12px; color: var(--text); cursor: pointer;
}
.stat-chip input { margin: 0; }
/* `:has()` gives the checked chip its look with no extra Alpine binding; the
   `is-active` class (bound alongside it in the markup) is the fallback for a
   browser old enough not to support it -- belt and braces, not two
   competing mechanisms. */
.stat-chip:has(input:checked), .stat-chip.is-active {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
.stat-chips-empty { margin: 0; }
/* The range shortcuts: one row across the top of the filter bar, styled as
   the robot chips are so the bar has one control language, not two. */
.stat-presets { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 100%; }
.stat-preset {
  border: 1px solid var(--border-strong); border-radius: 999px; background: none;
  padding: 4px 12px; font: inherit; font-size: 12px; color: var(--text); cursor: pointer;
}
.stat-preset:hover { border-color: var(--accent); }
.stat-preset.is-active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* The operating-hours switch sits at the end of the shortcut row: it scopes
   the same thing the shortcuts do -- which time is on the page. */
.stat-switch {
  display: inline-flex; align-items: center; gap: 6px; margin-left: auto;
  font-size: 12px; color: var(--text); cursor: pointer; white-space: nowrap;
}
.stat-switch input { margin: 0; accent-color: var(--accent); }
.stat-outside { margin: 0 0 12px; font-size: 12px; color: var(--muted); }
.stat-download {
  display: inline-flex; align-items: center; height: 30px; margin-left: auto;
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  padding: 0 14px; font-size: 12px; font-weight: 600; color: var(--accent);
  text-decoration: none; white-space: nowrap;
}
.stat-download:hover { background: var(--accent-soft); }
/* Two side by side at the right: the report, then the data. */
button.stat-download { background: var(--panel); cursor: pointer; font-family: inherit; }
button.stat-download:disabled { opacity: .5; cursor: default; }
.stat-download + .stat-download-data { margin-left: 8px; }

.stat-error { color: var(--bad); font-size: 12px; margin: 0 0 12px; }

/* One chart per row on a phone, two on a narrow screen, four on a wide one.
   The phone stays at one: two canvases side by side at 375px would be about
   150px each, too small to read a time axis. `minmax(0, 1fr)` rather than
   `1fr` so a chart's canvas cannot force its column wider than its share. */
.stat-cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
@media (min-width: 721px) {
  .stat-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1440px) {
  .stat-cards { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.stat-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 17px 15px; min-width: 0;
}
/* THE CHARTS OF A ROW START LEVEL. Each chart card is a subgrid of the card
   grid, three rows deep -- head, note, chart -- so the tallest head and the
   tallest note in a row set those rows for every card in it, and a card with
   no note (or a one-line total) leaves the space rather than pulling its
   chart up. Spacing is the children's own margins, not the grid's gap. */
.stat-cards > .stat-card {
  display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0;
}
.stat-cards > .stat-card > .stat-card-head { grid-row: 1; }
.stat-cards > .stat-card > .stat-card-note { grid-row: 2; align-self: start; }
.stat-cards > .stat-card > .stat-chart { grid-row: 3; }
.stat-card-head {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px;
}
.stat-card-title {
  font-family: var(--display); font-size: 17px; font-weight: 500; color: var(--text); margin: 0;
}
.stat-card-note { margin: 4px 0 0; font-size: 12px; color: var(--muted); }
.stat-total { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.stat-total-label {
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted);
}
.stat-total-value { font-family: var(--display); font-size: 20px; font-weight: 600; color: var(--accent); }
/* The breakdown under a total -- "P0 8 · P1 4" -- in the page's muted note
   size, so the one number above it stays the thing the eye lands on. */
.stat-total-sub {
  display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 2px 12px;
  margin-top: 3px; font-size: 11px; color: var(--muted);
}
.stat-break { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
/* Five figures on two even lines, three and two, both ending at the right
   edge: the second line sits under the first line's last two columns. */
.stat-total-sub.is-two-lines {
  display: grid; grid-template-columns: repeat(3, max-content); justify-content: end;
  justify-items: end;
}
.stat-total-sub.is-two-lines > .stat-break:nth-of-type(4) { grid-column: 2; }
.stat-total-sub.is-two-lines > .stat-break:nth-of-type(5) { grid-column: 3; }
.stat-break-dot { width: 8px; height: 8px; border-radius: 2px; flex: none; }
/* The takeovers' duration, one muted line under their P0/P1 pairs. */
.stat-total-note {
  margin-top: 2px; font-size: 11px; color: var(--muted);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.stat-break-value {
  color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums;
}
/* The takeover table: outside the chart grid, so the page's full width, and
   scrolled sideways inside its card on a narrow screen rather than the page. */
.stat-table-card { margin-top: 14px; }
.stat-table-wrap { margin-top: 12px; overflow-x: auto; }
.stat-table {
  width: 100%; border-collapse: collapse; font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
.stat-table th {
  text-align: left; white-space: nowrap; padding: 6px 10px 6px 0;
  font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); border-bottom: 1px solid var(--border);
}
.stat-table td {
  padding: 7px 10px 7px 0; white-space: nowrap; color: var(--text);
  border-bottom: 1px solid var(--border); vertical-align: top;
}
.stat-table tbody tr:last-child td { border-bottom: 0; }
.stat-table td.mono { font-family: var(--mono); font-size: 12px; }
.stat-table a { color: var(--accent); text-decoration: none; }
.stat-table a:hover { text-decoration: underline; }
/* P0 / P1 in the chart's own colours. */
.stat-type { font-weight: 600; }
.stat-type.is-p0 { color: var(--bad); }
.stat-type.is-p1 { color: var(--warn); }
/* A takeover with no hand-back: still under way reads as live; one whose end
   was never recorded reads as missing data, not as a time. */
/* A wait's reason can be a sentence: cut it, the tooltip has it whole. */
.stat-table td.stat-target { max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
.stat-table td.is-open-live { font-family: inherit; color: var(--bad); font-weight: 600; }
.stat-table td.is-open-unknown { font-family: inherit; color: var(--muted); font-style: italic; }

/* No explicit width or height: `stats.js` reads `clientWidth` off this div
   to size uPlot, and resizes it (canvas only, never a rebuild) on window
   resize and whenever the section stops being `hidden`. */
.stat-chart { width: 100%; min-width: 0; margin-top: 10px; overflow-x: auto; }
/* uPlot's own font stack is close enough to the console's; only the scale is
   off -- its default 14px legend text is a full size heavier than
   everything else on this page. */
.stat-chart .u-legend { font-size: 11px; }
.stat-chart .u-legend th, .stat-chart .u-legend td { padding: 2px 6px; }

@media (max-width: 720px) {
  .stat-filters { padding: 10px 12px; gap: 10px 12px; }
  .stat-field { flex-basis: 45%; }
  /* After the rule above, which would otherwise shrink the robot list to
     half a row and leave a blank column beside it. */
  .stat-field-wide { flex-basis: 100%; }
  .stat-download { margin-left: 0; flex-basis: 100%; justify-content: center; }
  .stat-download + .stat-download-data { margin-left: 0; }
}
