/* ============================================================
   Components

   The shared vocabulary every page is built from. Depends on the
   tokens in style.css and adds no colours of its own — if a value
   here is not a var(), that is a bug.

   Loaded after style.css so it can lean on the tokens, and kept
   separate so the legacy per-page blocks in the big templates can
   be deleted against it one page at a time.
   ============================================================ */

/* ---- page container -----------------------------------------------------
   Fluid, with no max-width. A comfortable reading measure (~70ch) is the right
   cap for prose and the wrong one here: this is a dense instrument surface read
   on anything from a 13" laptop to a wall-mounted display, and capping it left
   a third of a widescreen empty while the strips and tables that actually want
   the room stayed cramped. The padding scales instead of the content stopping. */
.page {
  width: 100%;
  padding: var(--s-5) clamp(16px, 2.2vw, 40px) var(--s-7);
}

/* The one place a measure IS wanted: running prose. */
.page .prose {
  max-width: 68ch;
}

/* ---- page header -------------------------------------------------------- */
.page-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding-bottom: var(--s-3);
  margin-bottom: var(--s-5);
  border-bottom: 1px solid var(--etch-strong);
}

.page-title {
  font-family: var(--font-ui);
  font-size: var(--t-h2);
  font-weight: 650;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0;
}

.page-meta {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ---- section rule ------------------------------------------------------- */
.rule {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-5) 0 var(--s-2);
  font-family: var(--font-data);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--etch);
}

.rule b {
  font-weight: 600;
  color: var(--ink-faint);
}

/* ---- readings ----------------------------------------------------------
   A measurement is not a state, so it is set in ink and never in a state
   colour. Mono with tabular figures so a row of them lines up. */
.readings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1px;
  background: var(--etch);
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: var(--s-5);
}

/* A tile is a label over a figure. The cells are grid items of equal height, so
   the figure is pinned to the BOTTOM of the cell: a long label that wraps to two
   lines ("Task count · transport") then costs the row nothing, because every
   number still sits on the same line as its neighbours. */
.reading {
  background: var(--sheet);
  padding: var(--s-3) var(--s-4);
  display: flex;
  flex-direction: column;
}

.reading-l {
  font-family: var(--font-data);
  font-size: 0.5938rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--ink-faint);
}

.reading-n {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.15;
  margin-top: 5px;
}

/* Inside a readings row the figure is bottom-set (see .reading), so the gap
   above it is whatever the tallest label leaves — the 5px is kept as padding so
   a one-line label still has air under it. */
.readings .reading-n {
  margin-top: auto;
  padding-top: 5px;
}

.reading-n small {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ink-dim);
  margin-left: 2px;
}

.reading-d {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-dim);
  margin-top: 3px;
}

/* Direction is stated by the glyph, so the colour is reinforcement only. */
.reading-d.is-up::before { content: "\25B2 "; color: var(--state-run); }
.reading-d.is-down::before { content: "\25BC "; color: var(--state-hold); }

/* ---- strip -------------------------------------------------------------
   One strip per machine, after the air-traffic control flight strip: the
   left edge is a lit state bar, so a stack of strips reads as a column of
   edges before a single word is read. */
/* Every flexible column is a fraction with a floor, so a strip grows into the
   window instead of the last column drifting away from the edge. The floors are
   what stop it collapsing on a narrow laptop. */
.strip {
  display: grid;
  grid-template-columns:
    6px                      /* state edge  */
    minmax(210px, 1.1fr)     /* identity    */
    minmax(260px, 2.6fr)     /* work        */
    minmax(150px, 1fr)       /* state       */
    minmax(104px, 0.7fr)     /* reading     */
    auto;                    /* actions     */
  align-items: stretch;
  background: var(--sheet);
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  margin-bottom: 5px;
}

.strip:hover { border-color: var(--etch-strong); }

.strip__edge { border-radius: var(--radius) 0 0 var(--radius); }
.strip.is-run .strip__edge { background: var(--state-run); }
/* Idle is healthy, so its edge is present but unlit. Leaving it blank made the
   strip read as missing something rather than as a machine at rest. */
.strip.is-idle .strip__edge { background: var(--etch-strong); }
.strip.is-hold .strip__edge { background: var(--state-hold); }
.strip.is-fault .strip__edge { background: var(--state-fault); }
/* No signal is a broken bar, so the absence reads as absence. */
.strip.is-none .strip__edge {
  background: repeating-linear-gradient(
    180deg, var(--state-none) 0 3px, transparent 3px 6px
  );
}

.strip__cell { padding: 9px 12px; min-width: 0; }
.strip__cell + .strip__cell { border-left: 1px solid var(--etch); }

.strip__id {
  font-family: var(--font-data);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  line-height: 1.2;
}

.strip__sub,
.strip__note,
.strip__since {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-dim);
}

.strip__sub { margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.strip__note { margin-top: 3px; }
.strip__since { margin-top: 3px; color: var(--ink-faint); }

.strip.is-fault .strip__note { color: var(--state-fault); }
.strip.is-hold .strip__note { color: var(--state-hold); }

.strip__task {
  font-family: var(--font-data);
  font-size: var(--t-small);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Step ruler. Discrete ticks because the work is genuinely discrete — an
   AutoXing task carries stepIndex/stepNum — and a smooth bar would imply a
   continuous fraction the platform does not actually know. The current step
   is outlined so it is findable without counting. */
.ruler { display: flex; gap: 2px; align-items: center; margin-top: 6px; }

.ruler i {
  display: block;
  width: 13px;
  height: 5px;
  background: var(--etch);
  border-radius: 1px;
}

.strip.is-run .ruler i.on { background: var(--state-run); }
.strip.is-hold .ruler i.on { background: var(--state-hold); }
.strip.is-fault .ruler i.on { background: var(--state-fault); }
.strip.is-idle .ruler i.on,
.strip.is-none .ruler i.on { background: var(--state-none); }
.ruler i.at { box-shadow: inset 0 0 0 1px var(--ink); }

.ruler span {
  font-family: var(--font-data);
  font-size: 0.625rem;
  color: var(--ink-faint);
  margin-left: 6px;
  letter-spacing: 0.04em;
}

.strip__state,
.strip__read { display: flex; flex-direction: column; justify-content: center; }
.strip__read { text-align: right; }

/* The unit rides small beside its number so the figure keeps the eye. */
.read-unit { font-size: 0.6875rem; font-weight: 500; color: var(--ink-dim); margin-left: 1px; }

/* Reasons attached to disabled controls are announced but not drawn. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.read-n {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
}

.read-n.is-low { color: var(--state-hold); }
.read-n.is-absent { color: var(--ink-faint); font-weight: 500; }

.read-l {
  font-family: var(--font-data);
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: 1px;
}

/* Actions stay in the DOM at all times so a strip never changes height. */
.strip__act { display: flex; gap: 6px; align-items: center; padding: 0 12px 0 0; }
.strip__act .btn-thin { visibility: hidden; }
.strip:hover .strip__act .btn-thin,
.strip:focus-within .strip__act .btn-thin { visibility: visible; }

/* Hover-to-reveal is right when the row is the subject and the actions are a
   secondary affordance — a fleet list, where you are reading status. It is
   wrong on a management list, where the actions ARE the reason for the page:
   hiding Edit and Retire until hover makes the screen look read-only, and on a
   touch device there is no hover to discover them with. */
.strip--manage .strip__act .btn-thin { visibility: visible; }

/* The attention block is tinted so it reads as one object, not N rows. */
.strip-attn .strip { background: var(--state-fault-fill); }
.strip-attn .strip.is-hold { background: var(--state-hold-fill); }

/* ---- thin control ------------------------------------------------------- */
.btn-thin {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.04em;
  padding: 4px 9px;
  min-height: 30px;
  border: 1px solid var(--etch-strong);
  background: var(--sheet);
  color: var(--ink);
  border-radius: var(--radius);
  cursor: pointer;
}

.btn-thin:hover { background: var(--slab); }
.btn-thin[disabled] { color: var(--ink-faint); cursor: not-allowed; background: var(--slab); }

/* ---- segmented filter --------------------------------------------------- */
.seg {
  display: inline-flex;
  border: 1px solid var(--etch-strong);
  border-radius: var(--radius);
  overflow: hidden;
}

.seg button {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.04em;
  padding: 5px 11px;
  min-height: 32px;
  border: 0;
  background: var(--sheet);
  color: var(--ink-dim);
  cursor: pointer;
}

.seg button + button { border-left: 1px solid var(--etch); }
.seg button[aria-pressed="true"] { background: var(--accent); color: #fff; }

/* ---- date range ---------------------------------------------------------
   Presets AND explicit dates. The presets cover the question people ask most
   ("last two weeks"), the date fields cover the one they ask when something
   went wrong on a specific day — and that second case is the whole reason an
   operations tool has a range picker at all. The API takes YYYY-MM-DD, so the
   native date input is the right control: no library, correct on tablets, and
   it speaks the wire format already. */
.daterange {
  display: flex;
  align-items: flex-end;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-5);
}

.daterange__group { display: flex; flex-direction: column; gap: 4px; }

.daterange__legend {
  font-family: var(--font-data);
  font-size: 0.5938rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.daterange__dates { display: flex; align-items: center; gap: 6px; }

.daterange input[type="date"] {
  font-family: var(--font-data);
  font-size: var(--t-small);
  padding: 5px 8px;
  min-height: 32px;
  color: var(--ink);
  background: var(--sheet);
  border: 1px solid var(--etch-strong);
  border-radius: var(--radius);
}

.daterange input[type="date"]:focus {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgba(26, 111, 212, 0.16);
}

.daterange__to { font-family: var(--font-data); font-size: var(--t-legend); color: var(--ink-faint); }

/* Pushes the applied-range readout to the far edge. */
.daterange__note {
  margin-left: auto;
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-dim);
  padding-bottom: 6px;
}

/* ---- figure ------------------------------------------------------------- */
.fig {
  margin: 0;
  background: var(--sheet);
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  padding: var(--s-3) var(--s-4) var(--s-3);
}

.fig figcaption {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: 2px;
}

.fig-t { font-family: var(--font-ui); font-size: 0.9375rem; font-weight: 600; color: var(--ink); }

.fig-u {
  font-family: var(--font-data);
  font-size: 0.5938rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.fig-note {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-dim);
  margin-bottom: var(--s-2);
}

.plot { position: relative; height: 190px; }
.plot.is-tall { height: 230px; }

/* Identity is never colour alone: a legend is always present for two or more
   series, and the swatch sits beside a written label. */
.legend { display: flex; gap: var(--s-4); flex-wrap: wrap; margin-top: var(--s-2); }

.legend span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-dim);
}

.legend i { width: 10px; height: 10px; border-radius: 2px; flex: none; }

/* Two up, explicitly.
   This was repeat(auto-fit, minmax(340px, 1fr)), which on a wide screen made
   FOUR 340px columns — so two figures filled columns 1-2 and left 3-4 empty,
   stranding both cards on the left of a half-blank row. auto-fit is right when
   the item count is unknown and every slot gets filled; a figure grid has a
   known count and wants a predictable rhythm, so the column count is fixed and
   the columns share the width. */
.fig-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
  align-items: stretch;
}

/* PLOTS LINE UP ACROSS A ROW, whatever sits above them. Only some figures carry
   a note now — one may say what its coverage was while the chart beside it says
   nothing — and with cards sized to their own content that one extra line
   dropped its canvas below its neighbour's, so two axes that measure the same
   days no longer started at the same height. The card stretches to the row and
   the plot is pushed to the bottom of it, which pins every baseline together.
   Scoped to figures that actually hold a plot: a figure of prose has nothing to
   align and should stay at its natural height. */
.fig-grid > .fig:has(.plot) { display: flex; flex-direction: column; }
.fig-grid > .fig:has(.plot) .plot { margin-top: auto; }

.fig-grid .is-wide { grid-column: 1 / -1; }

/* A lone figure should not sit at half width beside nothing. */
.fig-grid > .fig:only-child { grid-column: 1 / -1; }

/* One up below the point where two charts stop being readable side by side. */
@media (max-width: 900px) {
  .fig-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Wider cards can afford a taller plot; a squat chart in a wide box reads as a
   mistake. */
@media (min-width: 1280px) {
  .plot { height: 220px; }
  .plot.is-tall { height: 280px; }
}

/* ---- empty / loading / error -------------------------------------------
   An empty screen is an invitation to act, so it says what to do next
   rather than reporting that there is nothing. */
.state-block {
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  background: var(--sheet);
  padding: var(--s-6) var(--s-5);
  text-align: center;
}

.state-block h3 {
  font-family: var(--font-ui);
  font-size: var(--t-h3);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 var(--s-2);
}

.state-block p {
  font-size: var(--t-body);
  color: var(--ink-dim);
  margin: 0 auto var(--s-4);
  max-width: 48ch;
}

.state-block.is-error { border-color: #e8c4c1; background: var(--state-fault-fill); }

/* ---- narrow screens ----------------------------------------------------- */
@media (max-width: 860px) {
  .strip { grid-template-columns: 6px 1fr; }
  .strip__cell + .strip__cell { border-left: 0; border-top: 1px solid var(--etch); }
  .strip__read, .strip__state { text-align: left; }
  .strip__act { padding: 0 12px 10px; }
  .strip__act .btn-thin { visibility: visible; }
  .page-head { flex-direction: column; align-items: flex-start; gap: var(--s-1); }
}

/* ---- legacy date-range control ------------------------------------------
   business.html and employee.html share a hand-rolled range picker built from
   Bootstrap outline pills and a blue Apply button. Restyled here rather than
   rewritten in both templates: the markup and its JS handlers stay untouched,
   so nothing that depends on #applyDateRange or .date-preset-option changes.

   The presets become one segmented control instead of three floating pills,
   which is what they are — a single choice, not three buttons. */
.date-preset-option,
.date-range-preset {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.04em;
  padding: 5px 11px;
  min-height: 32px;
  border-radius: 0;
  border: 1px solid var(--etch-strong);
  border-right-width: 0;
  background: var(--sheet);
  color: var(--ink-dim);
  box-shadow: none;
}

.d-flex.gap-1 > .date-preset-option:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.d-flex.gap-1 > .date-preset-option:last-child {
  border-radius: 0 var(--radius) var(--radius) 0;
  border-right-width: 1px;
}

/* The gap between pills is what made them read as separate controls. */
.d-flex.gap-1:has(> .date-preset-option) { gap: 0 !important; }

.date-preset-option:hover,
.date-range-preset:hover { background: var(--slab) !important; color: var(--ink); }

.date-preset-option.active,
.date-range-preset.active,
.date-preset-option:active {
  background: var(--accent) !important;
  border-color: var(--accent);
  color: #fff !important;
}

#applyDateRange {
  font-family: var(--font-ui);
  font-size: var(--t-small);
  font-weight: 600;
  min-height: 32px;
  padding: 5px 14px;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: none;
}

#applyDateRange:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

#dashboardStartDate, #dashboardEndDate,
.date-range-container input[type="date"] {
  font-family: var(--font-data);
  font-size: var(--t-small);
  min-height: 32px;
  color: var(--ink);
  background: var(--sheet);
  border: 1px solid var(--etch-strong);
  border-radius: var(--radius);
  box-shadow: none;
}

.date-range-container {
  background: var(--sheet);
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  box-shadow: none;
}


/* ---- table --------------------------------------------------------------
   The platform's dense list. A card grid stops being scannable at about six
   robots and this fleet is already past forty, so anything longer than a short
   list is a table.

   Machine values are mono with tabular numerals, so the digits line up in a
   column and the outlier is findable by shape without reading any of them.
   Rules are hairlines and there is no zebra striping: the alignment does the
   work banding would otherwise be compensating for. */
.tbl-wrap {
  border: var(--hairline);
  border-radius: var(--radius);
  background: var(--sheet);
  overflow-x: auto;
}

.tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-small);
}

.tbl th,
.tbl td {
  padding: 9px var(--s-3);
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--etch);
}

.tbl tbody tr:last-child td { border-bottom: 0; }

.tbl thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--slab);
  border-bottom: 1px solid var(--etch-strong);
  font-family: var(--font-data);
  font-size: var(--t-legend);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Numbers right-align so magnitudes stack; words stay left. */
.tbl .num {
  text-align: right;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* "Did none" and "does not measure it" are different facts. A dash is the
   second one, and it must never be mistaken for a zero. */
.tbl .is-absent { color: var(--ink-faint); }

.tbl .t-id { font-weight: 600; color: var(--ink); }
.tbl .t-sub { color: var(--ink-dim); }

/* ---- the way into a machine ---------------------------------------------
   A name that opens something and a name that opens nothing were the same
   600-weight ink string. Every robot row's name is a link into that machine's
   own page — its readings, its daily figures and its task record — while an
   estate row read from the makers' listing has no page of ours to open, and
   nothing on either row said which was which. The only way to find out was to
   sweep a pointer across the table, and these pages are read on tablets where
   there is no pointer to sweep.

   NOT A TRAILING ACTION COLUMN. This table already scrolls sideways on a
   tablet, so a "View" column pinned to the right-hand end would be the first
   thing off the screen — the affordance has to live in the cell that is on
   screen before anybody scrolls anywhere, which is the first one.

   Ink, not accent. --accent is 3.6:1 on white and this is 13px text read down
   a whole column; colouring it blue to prove it is a link would have spent the
   contrast that makes it legible on a claim the underline and the mark already
   make. Both are drawn AT REST, not on hover. */
a.t-id {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  color: var(--ink);
  text-decoration: underline;
  /* --ink-dim, not --etch-strong: an underline drawn in the border colour is
     1.4:1 on white and measured as invisible — the row read as plain text with
     a speck after it. The underline is the affordance, so it is drawn in the
     same value as the mark it is paired with. */
  text-decoration-color: var(--ink-dim);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  border-radius: 4px;
  /* No 300ms double-tap wait on the control this whole table is read through. */
  touch-action: manipulation;
}

/* A hover state may not be a dimmer state: --accent-hover is 5.9:1 on white
   and 7:1 on the dark sheet, so the row under the pointer is the most legible
   row on the table rather than the least. */
a.t-id:hover,
a.t-id:focus-visible {
  color: var(--accent-hover);
  text-decoration-color: currentColor;
}

a.t-id:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A mark, not a word. "Stats" written down forty rows is a column of one
   repeated string, and it would have to be translated; a chevron is read as
   "goes somewhere" in any language and costs no width.
   A chevron and not the disclosure triangle on purpose — the account rows above
   these use a rotating triangle to mean "opens in place", and one glyph must not
   mean two different things in one table.
   --ink-dim, not --ink-faint: this is the row's only sign that there is anywhere
   to go, so it has to clear 3:1 as a graphical object in both themes. */
.t-id__go {
  flex: none;
  color: var(--ink-dim);
  font-family: var(--font-data);
  /* Slightly up from the 13px row text: at 1em a chevron reads as a speck of
     punctuation rather than as a direction. */
  font-size: 1.15em;
  line-height: 1;
  transition: transform 150ms ease;
}

a.t-id:hover .t-id__go,
a.t-id:focus-visible .t-id__go { color: inherit; transform: translateX(2px); }

/* Renaming a machine from the list it is read in.
   Quiet — it sits beside a name, not on it — but never hidden until hover: a
   control an operator has to discover by sweeping the mouse over a table is a
   control most of them will never find, and these pages are opened on tablets
   where there is no hover at all. */
.tbl .t-rename {
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  padding: 0 4px;
  margin-left: 4px;
  line-height: 1.4;
  font-size: 0.8em;
  color: var(--ink-dim);
  cursor: pointer;
}
.tbl .t-rename:hover,
.tbl .t-rename:focus-visible {
  color: var(--ink);
  border-color: var(--etch-strong);
}

.tbl .t-rename-edit { display: flex; align-items: center; gap: var(--s-1); }

.tbl .t-rename-edit input {
  font-family: var(--font-ui);
  font-size: inherit;
  min-width: 9rem;
  max-width: 100%;
  padding: 3px 6px;
  color: var(--ink);
  background: var(--sheet);
  border: 1px solid var(--etch-strong);
  border-radius: 6px;
}

/* The reason a name was refused, in the cell that refused it. */
.tbl .t-rename-msg { color: var(--state-fault-ink); margin-top: 2px; }

/* A serial is a machine value, so it is set in the data face: the digits line
   up between rows, which is the only way two units of the same model can be
   told apart at a glance. */
.tbl .t-sn {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.tbl tbody tr:hover { background: var(--slab); }

/* Sortable heads are buttons, so they are reachable by keyboard and announce
   their state. The arrow is drawn, not coloured, so sort direction survives
   greyscale and colour-blindness. */
.tbl th button {
  all: unset;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
}
.tbl th button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.tbl th[aria-sort] button { color: var(--ink); }
.tbl th[aria-sort="descending"] button::after { content: "\25BC"; font-size: 0.55em; }
.tbl th[aria-sort="ascending"] button::after  { content: "\25B2"; font-size: 0.55em; }
.tbl th.num button { justify-content: flex-end; width: 100%; }

/* The lamp inside a row: shape and colour, no label, because the row's own
   Status column already says the word. */
.tbl .lamp-cell { display: inline-flex; align-items: center; gap: var(--s-2); }

/* ---- groups inside one table -------------------------------------------
   When rows differ by where they come from rather than by what they are, they
   belong in ONE table under labelled groups — not in two tables. Two tables
   read as two subjects, which is how a page ends up saying "none yet" directly
   above eight of them. The group band carries the count and the one sentence
   that explains what is different about the rows beneath it, so the difference
   is stated where the rows are instead of in a footnote below the fold. */
.tbl-group th {
  padding: var(--s-3);
  white-space: normal;
  background: var(--slab);
  border-top: 1px solid var(--etch);
  border-bottom: 1px solid var(--etch-strong);
}

/* The band under the head needs no rule above it — the head already has one. */
.tbl tbody:first-of-type .tbl-group th { border-top: 0; }

.tbl-group__t {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}

.tbl-group__n {
  margin-left: var(--s-2);
  font-family: var(--font-data);
  font-size: var(--t-legend);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink-faint);
}

.tbl-group__d {
  display: block;
  margin-top: 3px;
  max-width: 72ch;
  font-family: var(--font-ui);
  font-size: var(--t-small);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-dim);
}

/* A full-width row that says something rather than measuring something: an
   empty group's invitation to act, a "still checking", a read that failed.
   Scoped to its group, so it can never be mistaken for a verdict on the whole
   table the way a page-wide empty state can. */
.tbl-msg td {
  padding: var(--s-4) var(--s-3);
  white-space: normal;
  color: var(--ink-dim);
}
.tbl-msg td b { color: var(--ink); font-weight: 600; }
.tbl-msg .btn-thin { margin-left: var(--s-2); vertical-align: baseline; }

/* ---- a folded table ----------------------------------------------------
   One row per account, opening onto its robots. The parent row is not a group
   BAND — it carries figures of its own in the same columns as its children, so
   the total and its parts are read in one alignment instead of as a rollup
   above a list. That is the whole reason the two tables became one.

   The parent is weighted and sits on the slab; the children are indented and
   plain. Depth is carried by indent and weight rather than by colour, so it
   survives greyscale and a projector, like the lamps do. */
.tbl--folded .fold-row > td {
  background: var(--slab);
  border-bottom: 1px solid var(--etch-strong);
}

.fold {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  /* 44px on the BUTTON, not on the cell around it. The cell's padding makes the
     row tall enough to look tappable while the actual target is the height of
     one line of text — which is the version that fails on a tablet, and fails
     invisibly, because it looks correct on a desktop. A whole-row target would
     be bigger still, but the child rows carry links to a machine's page and a
     row-sized control would swallow the taps meant for them. */
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
  /* No 300ms double-tap wait on the one control this table is driven by. */
  touch-action: manipulation;
}
.fold:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A triangle that turns, not a plus that becomes a minus: one glyph rotating is
   read as the same control in two states, where two glyphs are read as two
   controls. */
/* --ink-dim, not --ink-faint: this mark is the only thing on the row that says
   whether it is open, so it has to clear 3:1 as a graphical object. Faint is for
   text that may be skipped, and a state indicator may not be. */
.fold__mark {
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  color: var(--ink-dim);
  transition: transform 150ms ease;
}
.fold[aria-expanded="true"] .fold__mark { transform: rotate(90deg); }

/* Indented to the depth of the disclosure triangle, so a child row's name lines
   up with its parent's name and not with the triangle. */
.tbl--folded .fold-kid > td:first-child { padding-left: calc(var(--s-3) + 13px); }
.tbl--folded .fold-kid > td { background: var(--sheet); }

/* Both row kinds paint their own background, which at this specificity beat the
   table's own `tbody tr:hover` — so the rows carrying the links to each robot's
   page were the only rows on the platform that answered a pointer with nothing.
   Restated here rather than weakened there: the parent steps up from slab to
   etch, the child picks up the hover the flat table has always had. */
.tbl--folded .fold-row:hover > td { background: var(--etch); }
.tbl--folded .fold-kid:hover > td { background: var(--slab); }

@media (prefers-reduced-motion: reduce) {
  .fold__mark { transition: none; }
  .t-id__go { transition: none; }
}

/* ---- pager --------------------------------------------------------------
   For a list that is long enough that its end is four screens away. It sits
   UNDER the table, because it is read after the last row rather than before the
   first, and it carries the slice it is showing beside the controls: "Accounts
   26–50 of 63" is the sentence that stops a paged table from looking like a
   table that lost rows.

   Same thin controls as the rest of the platform, so a page number is visibly
   the same kind of thing as Refresh or Apply. The current page is filled with
   the accent rather than outlined, which is the one state that has to survive a
   glance across ten identical buttons. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-3);
}

.pager__count {
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.pager__count b {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

.pager__ctl { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }

/* 36px, not the thin control's 30px: this is a repeated small target that gets
   pressed on a tablet, and a row of 30px squares is the exact shape that gets
   mis-tapped. */
.pager__ctl .btn-thin { min-height: 36px; }

.pager__n {
  min-width: 36px;
  padding: 4px 6px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.pager__n.is-current {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  cursor: default;
}
.pager__n.is-current:hover { background: var(--accent); }

.pager__gap {
  padding: 0 2px;
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-faint);
}

/* A caption under a table stating what the numbers do and do not cover. */
.tbl-note {
  margin-top: var(--s-2);
  font-family: var(--font-data);
  font-size: var(--t-legend);
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}

/* Bootstrap's focus ring is a 4px translucent blue halo, which is what put a
   glowing circle around whichever preset was last tapped. Replaced with the
   platform's own focus outline — visible for keyboard users, invisible after a
   tap, and the same shape on every control. */
.date-preset-option:focus,
.date-range-preset:focus,
#applyDateRange:focus { box-shadow: none !important; outline: none; }

.date-preset-option:focus-visible,
.date-range-preset:focus-visible,
#applyDateRange:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  z-index: 1;
}

/* Bootstrap sets these on .btn-outline-primary with its own colour. The
   presets are a segmented control now, so the pill colouring has to go. */
.date-preset-option.btn-outline-primary:not(.active) {
  color: var(--ink-dim);
  border-color: var(--etch-strong);
}

/* Bootstrap draws cards, inputs, dropdowns and badges from its own radius
   variables, so bumping the platform token alone left half the page square and
   half of it rounded. Mapped here so one token governs every corner. */
:root {
  --bs-border-radius: var(--radius);
  --bs-border-radius-sm: var(--radius);
  --bs-border-radius-lg: var(--radius-lg);
  --bs-border-radius-xl: var(--radius-lg);
}

/* Pushes a control to the end of the date-range row — the download sits with
   the range it exports, not in a separate toolbar. */
.daterange__group--end { margin-left: auto; }


/* ===========================================================================
   Bootstrap contextual colour, brought into the platform's language.

   The legacy templates carry ~200 uses of bg-primary / bg-success / bg-info /
   bg-warning and btn-success / btn-info / btn-warning. On a plan-drawing
   surface that reads as noise, and worse, it spends the one thing this
   platform reserves: colour means MACHINE STATE here, and a green button
   beside a green "running" lamp teaches the operator that green means nothing
   in particular.

   Fixed here rather than across fourteen templates, because a stylesheet is
   one reversible change and fourteen hand-edits are fourteen chances to break
   a page nobody opens until Monday.

   The mapping is not "make it all grey". It follows what each class is
   actually being used FOR:

       card header      never coloured — it is a label, not a state
       action button    the one accent; an action is an action
       destructive      fault red, because that one is a warning worth having
       status badge     the state tokens, because a badge usually IS a state
       decorative icon  ink, so it stops competing with the lamps
   =========================================================================== */

/* ---- card headers: a label, never a state ---- */
.card-header.bg-primary,
.card-header.bg-success,
.card-header.bg-info,
.card-header.bg-warning,
.card-header.bg-secondary,
.card-header.bg-dark {
  background: var(--slab) !important;
  color: var(--ink) !important;
  border-bottom: 1px solid var(--etch);
}
.card-header.bg-primary .text-white,
.card-header.bg-success .text-white,
.card-header.bg-info .text-white,
.card-header.bg-warning .text-dark,
.card-header [class*="text-white"] { color: var(--ink) !important; }

/* ---- action buttons: one accent ----
   Success/info/warning on a BUTTON never meant a state — they were picked to
   tell three buttons apart. Size, order and label do that without spending
   colour. */
/* Set Bootstrap's OWN variables rather than the final background. Overriding
   `background` on .btn-success looks right until a button is disabled: the
   rule that wins then is `.btn.disabled, .btn:disabled`, which is two classes
   and outranks a single-class override, so every disabled button kept its
   Bootstrap green. Driving the variables themes every state — rest, hover,
   focus, active, disabled — through Bootstrap's own cascade. */
.btn-success, .btn-info, .btn-warning, .btn-primary {
  --bs-btn-bg: var(--accent);
  --bs-btn-border-color: var(--accent);
  --bs-btn-color: var(--accent-ink);
  --bs-btn-hover-bg: var(--accent-hover);
  --bs-btn-hover-border-color: var(--accent-hover);
  --bs-btn-hover-color: var(--accent-ink);
  --bs-btn-active-bg: var(--accent-hover);
  --bs-btn-active-border-color: var(--accent-hover);
  --bs-btn-active-color: var(--accent-ink);
  --bs-btn-disabled-bg: var(--accent);
  --bs-btn-disabled-border-color: var(--accent);
  --bs-btn-disabled-color: var(--accent-ink);
}
.btn-outline-primary, .btn-outline-success, .btn-outline-info {
  --bs-btn-color: var(--ink-dim);
  --bs-btn-border-color: var(--etch-strong);
  --bs-btn-bg: var(--sheet);
  --bs-btn-hover-bg: var(--slab);
  --bs-btn-hover-border-color: var(--ink-faint);
  --bs-btn-hover-color: var(--ink);
  --bs-btn-active-bg: var(--slab);
  --bs-btn-active-color: var(--ink);
  --bs-btn-disabled-color: var(--ink-faint);
  --bs-btn-disabled-border-color: var(--etch);
}

/* Destructive keeps its warning. This is the one button colour that is doing
   a job the label alone cannot. */
.btn-danger {
  --bs-btn-bg: var(--state-fault);
  --bs-btn-border-color: var(--state-fault);
  --bs-btn-hover-bg: var(--state-fault);
  --bs-btn-hover-border-color: var(--state-fault);
  --bs-btn-disabled-bg: var(--state-fault);
  --bs-btn-disabled-border-color: var(--state-fault);
}

/* ---- badges: usually a state, so use the state vocabulary ---- */
.badge.bg-success { background: var(--state-run) !important; color: #fff; }
.badge.bg-danger  { background: var(--state-fault) !important; color: #fff; }
.badge.bg-warning { background: var(--state-hold) !important; color: #fff; }
.badge.bg-info,
.badge.bg-primary { background: var(--slab) !important; color: var(--ink-dim); }
.badge.bg-secondary { background: var(--slab) !important; color: var(--ink-dim); }

/* ---- kind chips: a CATEGORY, not a state -------------------------------
   The rule above sends .bg-primary badges to grey because a badge is usually
   reporting a state, and a state has a reserved colour. But some labels name a
   KIND -- what a place on the map is -- and grey there is actively wrong: on a
   grid of places to send a robot to, thirty grey chips read as thirty machines
   with no signal. That is what operators reported.

   So kinds get their own two-tone vocabulary, and it is deliberately only two
   tones rather than one per type:

     "somewhere you can send it"     accent blue -- a place, and the only kind
                                     that answers the question the page asks
     "infrastructure it uses itself"  quiet outline -- a charger or a standby
                                     spot; you don't route work there, so
                                     de-emphasis is the correct reading

   No new hue per type, because the colour is never the only channel: every chip
   carries its own icon and its own word. That is also why this does not reach
   for a fourth or fifth colour -- the chart-series note in style.css records
   what happens when hues have to stay apart for a protanope, and a category
   label does not need to win that argument. */
.kind-chip {
  display: inline-flex; align-items: center; gap: .3em;
  /* --t-small (13px), not --t-legend (11px). A chip is a label somebody reads to
     decide where to send a machine, on a tablet, at arm's length -- 11px is below
     the floor for that and was smaller than the Bootstrap badge it replaced. */
  font-family: var(--font-ui); font-size: var(--t-small); font-weight: 600;
  line-height: 1.4; padding: .2em .6em; border-radius: 999px;
  border: 1px solid transparent; white-space: nowrap;
}
.kind-chip i { font-size: .95em; opacity: .85; }

/* BOTH chips carry their OWN background, and that is the accessibility contract
   rather than a style preference. These sit on cards that change colour on hover
   and again when the place is added to the plan, so a chip that inherited the
   card's background inherited its contrast too: measured against the four card
   states, a transparent chip fell to 4.1:1 on a hovered card, 3.95:1 on a
   selected one, and 2.67:1 on a hovered selected card in dark mode. Owning the
   fill makes the ratio a property of the chip -- 5.1:1 or better in every state,
   in both themes. */

/* A place. */
.kind-chip--place {
  background: var(--accent-soft); color: var(--accent-hover);
  border-color: color-mix(in srgb, var(--accent) 22%, transparent);
}
/* The robot's own furniture. Quiet on purpose -- the card colour is what changes
   around it, so this reads as "set back" and not as "this one failed to load". */
.kind-chip--infra {
  background: var(--sheet); color: var(--ink-dim);
  border-color: var(--etch-strong);
}

/* There was a --unclaimed variant here, for accounts the makers listed that we
   had no rows for, and a .t-add button to file them. Both are gone with the
   two-tier design that needed them: the Dashboard lists the makers' whole
   estate now, so there is no second class of row to mark and nothing to add. */

/* ---- decorative icons stop competing with the lamps ---- */
.card-header .text-primary,
.card-header .text-success,
.card-header .text-info,
h1 .text-primary, h2 .text-primary, h3 .text-primary,
h4 .text-primary, h5 .text-primary, h6 .text-primary { color: var(--ink-faint) !important; }

/* Text that genuinely reports a state keeps its meaning. */
.text-success { color: var(--state-run) !important; }
.text-danger  { color: var(--state-fault) !important; }
.text-warning { color: var(--state-hold) !important; }

/* ---- alerts ---- */
.alert-success { background: var(--state-run-fill); border-color: var(--state-run); color: var(--ink); }
.alert-danger  { background: var(--state-fault-fill); border-color: var(--state-fault); color: var(--ink); }
.alert-warning { background: var(--state-hold-fill); border-color: var(--state-hold); color: var(--ink); }
.alert-info    { background: var(--slab); border-color: var(--etch-strong); color: var(--ink); }


/* ---- an odd figure should not leave a hole ------------------------------
   Three figures in a two-column grid left the bottom-right empty, which reads
   as a chart that failed to load rather than as a deliberate end. The last one
   spans the row when the count is odd. */
.fig-grid > .fig:last-child:nth-child(odd) { grid-column: 1 / -1; }

/* ---- colour on the readings --------------------------------------------
   The instrument language keeps colour for machine state, and it earns that on
   the map and in the tables where a lamp has to be found at two metres. A
   readings row is not that: nothing here is a state, so a rule of colour along
   the top costs the state vocabulary nothing and gives the page a spine.

   THE RULE SAYS WHICH FLEET THE NUMBER IS ABOUT, and nothing else. Colouring by
   position said nothing — six hues for six measures is a legend with no key —
   and one accent for all of them said nothing either. The two fleets are the
   one split that runs through this row: the tiles come in transport/cleaning
   pairs, and mixing 101 transport tasks with 171 cleaning tasks is the actual
   mistake available to the reader.

   The hues are --series-1 and --series-2: the same validated pair the stacked
   chart and its legend use further down the page, so the swatch beside
   "Transport" and the rule above "Task count · transport" are one colour, not
   two systems. Both are theme-aware — style.css steps the warm one down in
   dark. The word is still in the label, so the colour is reinforcement and a
   reader who cannot separate the hues loses nothing. */
.readings .reading { position: relative; }
.readings .reading::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--reading-accent, var(--etch-strong));
}
.readings .reading.is-transport { --reading-accent: var(--series-1); }
.readings .reading.is-cleaning  { --reading-accent: var(--series-2); }

/* ONE MACHINE'S PAGE COLOURS BY MEASURE INSTEAD.
   The fleet hue answers "whose number is this", which is the question the
   estate row asks — its tiles come in transport/cleaning pairs. On a machine's
   own page there is nothing to pair: every tile would carry the same colour,
   which is decoration. So a tile takes its MEASURE's colour, and it is the same
   colour that measure's chart is drawn in further down, so the eye can go from
   a total to its shape without reading either heading.

   Same ramp, same slots as machine.html's TONE map — the two must not drift.
   Mileage is transport's and Coverage is cleaning's, so they never appear on
   one machine and can share slot 4. */
.readings .reading.is-m-tasks    { --reading-accent: var(--series-1); }
.readings .reading.is-m-runtime  { --reading-accent: var(--series-3); }
.readings .reading.is-m-distance { --reading-accent: var(--series-4); }
.readings .reading.is-m-area     { --reading-accent: var(--series-2); }
.readings .reading.is-m-coverage { --reading-accent: var(--series-4); }

/* Where one fleet's block ends and the next begins. The 1px hairline between
   every pair of tiles is the grid gap showing through, so on its own it said
   "next tile" at the seam that means "different fleet". This is the same line
   drawn twice as heavy and in the stronger etch — a division, not a gap. */
.readings .reading.is-fleet-edge {
  border-left: 2px solid var(--etch-strong);
}

/* The number itself stays ink. Colouring the digits would make two metrics
   look like two states, and a figure you have to decode is slower than one you
   can just read. */


/* ===========================================================================
   Apple-style light surface.

   The page is #F5F5F7 and cards are white ON it. Depth comes from that
   layering plus a hairline and a soft shadow — not from a hard border and not
   from a heavy drop shadow.

   =========================================================================== */

/* The lamps themselves are defined once, in style.css — every page loads
   that file, including the ones that do not extend base.html. */

/* ---- cards float on the canvas ---- */
.card,
.tbl-wrap,
.fig,
.state-block,
.dispatch-strip,
.readings {
  background: var(--sheet);
  border: 1px solid var(--etch);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}

/* The readings row is one object, so its cells are divided rather than
   individually bordered. */
.readings { gap: 1px; background: var(--etch); overflow: hidden; }
.readings .reading { background: var(--sheet); padding: var(--s-4) var(--s-5); }

.card { border-color: var(--etch); }
.card-header {
  background: transparent;
  border-bottom: 1px solid var(--etch);
  padding: var(--s-4) var(--s-5);
  font-weight: 600;
}
.card-body { padding: var(--s-5); }

/* ---- controls ---- */
.btn { border-radius: var(--radius); font-weight: 500; }
.form-control, .form-select, input[type="date"], input[type="search"] {
  border-radius: var(--radius);
  border-color: var(--etch-strong);
  background: var(--sheet);
  color: var(--ink);
}
.form-control:focus, .form-select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0,122,255,.15);
}

/* Secondary action: tinted, not outlined. */
.btn-thin, .btn-outline-secondary {
  --bs-btn-bg: var(--accent-soft);
  --bs-btn-color: var(--accent);
  --bs-btn-border-color: transparent;
  --bs-btn-hover-bg: #E3EFFF;
  --bs-btn-hover-color: var(--accent-hover);
  --bs-btn-hover-border-color: transparent;
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--radius);
}

/* ---- the rail sits on its own tint, like a sidebar ---- */
.app-rail { background: var(--rail-bg); border-right: 1px solid var(--etch); }
.app-rail-item.active { background: var(--accent-soft); color: var(--accent); }

/* ---- readings accents ----
   A slightly heavier rule on the light sheet, where a 3px line disappears. The
   hue itself is the fleet's, set once above; nothing here restates it. */
.readings .reading::before { height: 4px; }


/* ===========================================================================
   Top bar — the last dark thing on a light page.

   It was solid #1D1D1F while everything beneath it moved to the Apple light
   surface, so the page read as two designs stacked. Apple's own chrome on a
   light UI is not a dark slab: it is the surface itself, made translucent, so
   content scrolling underneath tints it and the bar reads as glass rather than
   as a lid.
   =========================================================================== */
.app-topbar {
  background: var(--glass) !important;
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--etch) !important;
  position: sticky;
  top: 0;
  /* Above the rail (1030). Nothing in the app may cover the top bar: it
     carries the identity of the system and the way out of the page. */
  z-index: 1040;
}
/* Browsers without backdrop-filter get an opaque bar rather than a
   see-through one with unreadable text over it. */
@supports not (backdrop-filter: blur(1px)) {
  .app-topbar { background: var(--sheet) !important; }
}

/* Bootstrap's navbar-dark paints its own text white; this page is light. */
.app-topbar .navbar-brand,
.app-topbar .nav-link,
.app-topbar .navbar-nav .nav-link { color: var(--ink) !important; }
.app-topbar .nav-link:hover { color: var(--accent) !important; }

/* The wordmark asset is white, drawn for the old dark bar. Knocked down to
   ink rather than shipping a second file for one element. */
.app-topbar .navbar-brand img[alt="RT Robotics"] { filter: brightness(0); }

/* The two chrome controls become quiet tinted circles, not outlined boxes. */
.app-topbar .btn-outline-light {
  --bs-btn-color: var(--ink-dim);
  --bs-btn-border-color: transparent;
  --bs-btn-bg: var(--slab);
  --bs-btn-hover-bg: var(--etch);
  --bs-btn-hover-color: var(--ink);
  --bs-btn-hover-border-color: transparent;
  border-radius: 999px;
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
}
.app-topbar .badge.bg-secondary {
  background: var(--slab) !important;
  color: var(--ink-dim);
  font-weight: 500;
}
.app-topbar .dropdown-menu {
  border: 1px solid var(--etch);
  border-radius: var(--radius);
  box-shadow: var(--shadow-raised);
}

/* Dark mode keeps a dark bar, but translucent for the same reason.
   The background itself comes from --glass, which both themes define, so the
   dark value is not repeated here. */
[data-theme="dark"] .app-topbar,
body.dark-mode .app-topbar {
  border-bottom-color: var(--etch) !important;
}
[data-theme="dark"] .app-topbar .navbar-brand,
body.dark-mode .app-topbar .navbar-brand { color: var(--ink) !important; }
[data-theme="dark"] .app-topbar .navbar-brand img[alt="RT Robotics"],
body.dark-mode .app-topbar .navbar-brand img[alt="RT Robotics"] { filter: none; }

/* ---- readings ------------------------------------------------------------
   This used to force every rule to the one accent, which was right while the
   colours meant nothing — six pastels for six measures is a legend with no key.
   Now the rule names the FLEET (see the accent block above), so overriding it
   here would throw away the only thing it says. A tile with no fleet still
   falls back to a neutral etch rule. */
.readings .reading::before { height: 3px; }


/* ===========================================================================
   Touch targets.

   Audited against the UX rules rather than guessed: every clickable element on
   the dashboard was measured in the browser, and the range presets (64x32),
   the thin buttons (55x30) and the sortable table headers (99x18) were all
   under the 44px floor. This platform's primary client is a wall-mounted
   tablet, so that floor is the rule that matters most here — a 30px control is
   hittable with a mouse and a coin toss with a glove on.

   Applied as a MINIMUM with centred content, so nothing is stretched wider
   than it needs to be; only the hit area grows.
   =========================================================================== */
.btn,
.btn-thin,
.seg button,
.daterange button,
.date-preset-option {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Table sort headers were 18px tall — the label only. The whole header cell is
   the target now. */
.tbl th button {
  min-height: 44px;
  width: 100%;
}

/* Inputs match, so a row of controls lines up. */
.daterange input[type="date"],
.dispatch-filter input[type="search"],
.form-control {
  min-height: 44px;
}

/* ...except a segmented control was 2px taller than the inputs beside it. The
   44px floor above is on `.seg button`, and the buttons have no border of their
   own — the hairline belongs to the `.seg` wrapper, which therefore measured
   46px. Two pixels is not nothing on a row whose whole job is to look like one
   row, so the floor moves to the wrapper and the buttons stretch to fill it. */
.seg { min-height: 44px; }
.seg button { min-height: 42px; align-self: stretch; }

/* 8px minimum between adjacent targets, so a thumb cannot hit two. */
.daterange { gap: var(--s-4); }
.dispatch-strip__act .btn + .btn,
.table-actions .btn + .btn { margin-left: var(--s-2); }

/* A segmented control is one object: its buttons are deliberately flush, and
   the segment boundary is the hairline. Spacing does not apply inside it. */
.seg button + button { margin-left: 0; }

/* Icon-only controls need the floor in BOTH axes. Raising only the height left
   them 32-34px wide — a tall sliver, which is no easier to hit than a small
   square. Bootstrap's .btn-group sets its own widths, hence the specificity. */
.btn-group > .btn,
.table-actions .btn,
.app-topbar .btn-outline-light,
td .btn-group .btn {
  min-width: 44px;
}

/* The top-bar controls are circles, so they take the floor as a diameter. */
.app-topbar .btn-outline-light { width: 44px; height: 44px; }

/* The account menu is a text link in a bar; give it the same vertical reach. */
.app-topbar .nav-link { min-height: 44px; display: inline-flex; align-items: center; }

/* ---- brand ---------------------------------------------------------------
   Height is fixed and width follows, so the wordmark keeps its own
   proportions. Both images previously shared `.robot-icon` — a class meant for
   the round avatar — and carried inline width/height that did not match the
   asset. */
.app-topbar .brand-mark {
  height: 22px;
  width: auto;
  filter: brightness(0);          /* white asset, dark text on a light bar */
}
[data-theme="dark"] .app-topbar .brand-mark,
body.dark-mode .app-topbar .brand-mark { filter: none; }

.app-topbar .brand-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;              /* a square crop of a photo, on purpose */
}

/* The range limit, stated where the dates are picked. Quiet by default —
   it is a fact about the tool, not a warning — and only takes the fault
   colour when an entered range actually breaks it. */
.daterange__hint {
  /* A whole row of its own. `grid-column` was wrong here — .daterange is flex,
     not grid, so that declaration did nothing and the hint sat inside a group
     where it broke the row's bottom alignment. flex-basis 100% is what
     actually wraps it onto its own line. */
  flex: 0 0 100%;
  margin-top: calc(-1 * var(--s-2));
  font-size: var(--t-small);
  color: var(--ink-faint);
}
.daterange__hint.is-error {
  color: var(--state-fault-ink);
}

/* A one-line statement of what a page is for, under its title.
   Borrowed from the Node portal, which put one under every heading — it is the
   cheapest thing on a screen and the difference between a tool someone learns
   and one they guess at. */
.page-sub {
  margin-top: 3px;
  font-size: var(--t-small);
  color: var(--ink-dim);
  max-width: 68ch;
}

/* =====================================================================
   SUPPORT PANEL
   ---------------------------------------------------------------------
   A fixed panel, NOT a Bootstrap Offcanvas. Offcanvas defaults to
   z-index 1045 over a 1040 backdrop, and the rule at line ~1326 of this
   file is that nothing may cover the top bar (1040). So: 1035 -- above
   the rail (1030), below the bar -- and it starts below the bar rather
   than under it, using the --topbar-h that base.html publishes with a
   ResizeObserver.

   No backdrop. The operator is looking at the screen showing the problem
   while they describe it; dimming the page would hide the thing they are
   talking about.

   Colour: chrome is graphite and --accent only. The single exception is
   the .lamp on each robot button, which encodes machine state -- the one
   thing colour is allowed to mean here.
   ===================================================================== */

.support-panel {
  position: fixed;
  top: var(--topbar-h, 64px);
  right: 0;
  bottom: 0;
  z-index: 1035;
  width: min(26rem, 100vw);
  display: flex;
  flex-direction: column;
  background: var(--sheet);
  border-left: var(--hairline) solid var(--etch);
  box-shadow: var(--shadow-raised);
  transform: translateX(100%);
  transition: transform var(--motion) ease-out;
}
.support-panel.is-open { transform: translateX(0); }

.support-panel__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-4) var(--s-3);
  border-bottom: var(--hairline) solid var(--etch);
}
.support-panel__title { margin: 0; font-size: var(--t-h3); }
.support-panel__sub {
  margin: 2px 0 0;
  font-size: var(--t-legend);
  font-family: var(--font-data);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.support-panel__close {
  flex: none;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.support-panel__close:hover { background: var(--slab); color: var(--ink); }

/* A <button> in the rail, among <a> items. Everything else there navigates;
   this one opens a panel, and a link that goes nowhere is the wrong element
   for it. These properties are the ones a button brings that an anchor does
   not -- the rail's own .app-rail-item supplies the rest. */
.app-rail-item--button {
  border: 0;
  background: none;
  font: inherit;
  width: 100%;
  text-align: inherit;
  cursor: pointer;
}

/* Start over. Quieter than the mail button beside it -- it is chrome, not an
   offer -- but the same 44px target, because both get tapped on a tablet. */
.support-panel__restart {
  flex: none;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.support-panel__restart:hover { background: var(--slab); color: var(--ink); }

/* The mail button.
   Was a bare 44px envelope with a screen-reader-only label -- icon-only, in a
   header, next to a close button. The owner's report was exactly what that
   predicts: the assistant said "a draft email is ready for you to send" and
   nobody could tell where to send it, or that this icon was the way.
   So it says what it is. A labelled pill in the accent colour, because it is
   an offer rather than a dismissal, and it is the one control in this panel
   that reaches a person. Still a 44px touch target via the vertical padding. */
.support-panel__mail {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 44px;
  padding: 0 0.85rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
.support-panel__mail:hover { filter: brightness(1.08); }
.support-panel__mail-label { line-height: 1; }
/* Under ~380px the title, this and the close button stop fitting. The label
   goes and the icon stays -- losing the words is better than wrapping the
   header onto two lines. */
@media (max-width: 380px) {
  .support-panel__mail-label { display: none; }
  .support-panel__mail { padding: 0; width: 44px; justify-content: center; }
}
.support-panel__mail:disabled {
  /* Visible but plainly inert, and it keeps its size so it never moves under
     a finger already reaching for it. */
  background: transparent;
  border-color: var(--etch);
  color: var(--ink-faint);
  cursor: default;
}

.support-panel__log {
  flex: 1 1 auto;
  overflow-y: auto;
  /* Contain the scroll so a flick inside the transcript does not chain to
     the page behind and trip main.js's document-level pull-to-refresh. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.support-msg {
  max-width: 92%;
  padding: 0.6rem 0.8rem;
  border-radius: var(--radius);
  font-size: var(--t-body);
  line-height: 1.45;
  white-space: normal;
}
.support-msg--assistant {
  align-self: flex-start;
  background: var(--slab);
  color: var(--ink);
}
.support-msg--user {
  align-self: flex-end;
  background: var(--accent);
  color: var(--accent-ink);
}
.support-msg--wait { color: var(--ink-dim); font-style: italic; }

.support-panel__ask {
  flex: none;
  /* Capped, and scrollable if it must be. The describe step offers four
     symptom buttons AND a text box; unconstrained, that grew tall enough to
     push the transcript out of view, so the operator lost the line naming the
     fault we had just found for them. The conversation is the point -- the
     controls give way, not the other way round. */
  max-height: 50%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-3) var(--s-4);
  border-top: var(--hairline) solid var(--etch);
  background: var(--sheet);
}
.support-ask__label {
  margin: 0 0 var(--s-2);
  font-size: var(--t-small);
  color: var(--ink-dim);
}
.support-ask__done {
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}
/* Sits under the question and tells the reader how to answer it -- one step
   quieter than the question itself, so it reads as help rather than a second
   thing being asked. */
.support-ask__hint {
  margin: calc(var(--s-2) * -1) 0 var(--s-2);
  font-size: var(--t-legend);
  color: var(--ink-faint);
}
/* The question the reader is already asking, so it carries the emphasis and
   one step more contrast than the answer behind it. */
.support-ask__hint-lead { font-weight: 600; color: var(--ink-dim); }

/* Full-width rows: the robot picker, where each row carries a name, its model,
   the full serial and a state lamp, and needs the width. */
.support-choices { display: flex; flex-direction: column; gap: var(--s-2); }

/* Short labels with nothing else on the row -- symptoms, outcomes. Wrapping
   these as chips instead of stacked bars is the difference between the
   transcript having room and not. */
.support-choices--compact { flex-direction: row; flex-wrap: wrap; }
.support-choices--compact .support-choice { width: auto; flex: 0 1 auto; }
.support-choices--compact .support-choice__label { font-weight: 400; }

.support-choice {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  /* 44px floor: this is used on tablets mounted at a workstation. */
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: var(--canvas);
  border: var(--hairline) solid var(--etch-strong);
  border-radius: var(--radius);
  cursor: pointer;
}
.support-choice:hover { border-color: var(--accent); }
.support-choice:disabled { opacity: 0.5; cursor: default; }
.support-choice__label { font-weight: 500; }
/* Name over identifier. `min-width: 0` is what lets a long serial ellipsise
   inside the row instead of pushing the state lamp off the edge. */
.support-choice__id {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}
.support-choice__meta {
  /* Mono: this is a machine identifier, read character by character against
     the serial on the robot's screen. */
  font-family: var(--font-data);
  font-size: var(--t-legend);
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Pushed to the trailing edge, so the lamps line up down the column whatever
   the names are. */
.support-choice .lamp { flex: none; margin-left: auto; }

.support-form {
  display: flex;
  gap: var(--s-2);
  align-items: stretch;   /* not flex-end: the button matched the wrapped
                             placeholder's height and sat off the baseline */
  margin-top: var(--s-2);
}
.support-input {
  flex: 1 1 auto;
  font: inherit;
  color: var(--ink);
  background: var(--canvas);
  border: var(--hairline) solid var(--etch-strong);
  border-radius: var(--radius);
  padding: 0.5rem 0.65rem;
  resize: none;
  min-height: 44px;
}
.support-input:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
.support-send {
  flex: none;
  min-height: 44px;
  padding: 0 var(--s-4);
  font: inherit;
  font-weight: 500;
  color: var(--accent-ink);
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}
.support-send:disabled { opacity: 0.5; cursor: default; }

.support-panel__foot {
  flex: none;
  padding: var(--s-2) var(--s-4) var(--s-4);
  font-size: var(--t-legend);
  color: var(--ink-faint);
  border-top: var(--hairline) solid var(--etch);
}

/* ===========================================================================
   FLEET WARNINGS -- a machine has stopped, and the tablet says so unprompted

   Included on every signed-in page by base.html, because the operator who needs
   to be told is not necessarily looking at the map when it happens.

   Fixed under the top bar rather than in the page flow. In the flow it would
   push the whole page down each time a fault opened and pull it back up when it
   cleared, which on a wall tablet means the button somebody is reaching for
   moves as their finger arrives. z-index sits BELOW the top bar (1040) and below
   the support panel (1035): the way out of the page and the way to reach a human
   both outrank a notice, and the panel is how you report the fault this banner
   is describing.

   Severity is carried by a colour AND a shape, never colour alone: these get
   read across a hall, at an angle, by somebody who may not distinguish the two
   reds -- and by a camera pointed at a wall tablet.
   =========================================================================== */
.fleet-warnings {
  /* STICKY, NOT FIXED, and the difference is load-bearing. Fixed under the top
     bar covered the page's own heading and the first row of controls whenever
     two faults were open -- a warning that hides the thing you need to press is
     worse than one that moves it. Sticky occupies real space, so nothing is ever
     covered, and still stays on screen while the page scrolls. */
  position: sticky;
  top: var(--topbar-h, 64px);
  z-index: 1034;                 /* under the top bar (1040) and support (1035) */
  margin-bottom: var(--s-3);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  /* Its own ground, and the reason is specific to being sticky: only the cards
     have a background, so while the band was stuck the page's own text scrolled
     up through the gap between the card and the "and N more" strip and the two
     overprinted each other. An opaque band reads as docked chrome instead. The
     negative inline margins let it span the padding of the <main> it sits in, so
     the ground reaches the full width rather than leaving two bright gutters. */
  background: var(--canvas);     /* the page ground, not the card colour */
  padding: var(--s-2) var(--s-3);
  margin-left: calc(var(--s-3) * -1);
  margin-right: calc(var(--s-3) * -1);
  border-bottom: var(--hairline);
  /* No max-height and no internal scroll. A bounded box clipped the second card
     mid-sentence, which reads as broken rather than as bounded -- and a warning
     the operator has to scroll INSIDE to finish reading is a warning they will
     misread. The bound is on the CONTENT instead: the script shows one card and
     states how many machines it is not showing. */
}
.fleet-warnings[hidden] { display: none; }

.fleet-warning {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: var(--s-3);
  padding: var(--s-3);
  background: var(--sheet);
  border: var(--hairline);
  border-left: 3px solid var(--state-fault);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-raised);
  max-width: 72rem;
  margin: 0 auto;
  width: 100%;
}
/* "Worth a look" is not "needs a person now". The rail changes colour and the
   glyph changes shape, so the two never rely on the same channel. */
.fleet-warning.is-attention { border-left-color: var(--state-hold); }

.fleet-warning__mark {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--state-fault-fill);
  color: var(--state-fault-ink);
  font-size: 1rem;
}
.fleet-warning.is-attention .fleet-warning__mark {
  background: var(--state-hold-fill);
  color: var(--state-hold-ink);
}

.fleet-warning__who {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
  margin-bottom: var(--s-1);
}
/* The machine's identity is a machine-produced value, so it is mono with
   tabular figures -- the same treatment the plan gives the label it matches. */
.fleet-warning__tail {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--ink);
}
.fleet-warning__name { color: var(--ink); font-weight: 600; }
.fleet-warning__state {
  font-size: var(--t-legend);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--state-fault-ink);
}
.fleet-warning.is-attention .fleet-warning__state { color: var(--state-hold-ink); }

/* WHY it fired -- "Stuck 7 min", "Repeating - 3x in 1h". Sentence case and not
   uppercase, unlike the state word beside it: the state is a label to recognise
   at a glance, this is the one thing on the row somebody has to actually read,
   and uppercase is measurably slower to read than the words it is made of. */
.fleet-warning__why {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink);
}

/* The fault code, as a token an operator can scan for -- they name these faults
   by number ("it's throwing a 1007"), so the number must be findable without
   reading the sentence. Mono and tabular because it is a machine-produced value,
   the same treatment the serial tail gets two rules up.

   BORDERED, NOT FILLED. The rail colour and the glyph shape already carry
   severity; a coloured chip would be a third channel saying the same thing, and
   worse, it would imply the number itself is the severity -- which is precisely
   the model the sweep was rewritten to abandon. 1007 is not urgent because it is
   1007; it is urgent because the machine has not moved. */
.fleet-warning__code {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-legend);
  color: var(--ink-dim);
  border: var(--hairline);
  border-radius: var(--radius);
  padding: 0 .35em;
}

.fleet-warning__what {
  color: var(--ink);
  font-size: var(--t-body);
  margin: 0;
}
/* The one thing to do about it. Reads as an instruction, not as more of the
   fault: same size as the job line, but in full ink because it is the line the
   operator is meant to act on. */
.fleet-warning__fix {
  margin: var(--s-1) 0 0;
  font-size: var(--t-small);
  color: var(--ink);
}
/* What it was doing when it stopped -- the half that decides whether you walk
   over or send another machine. */
.fleet-warning__job {
  margin: var(--s-1) 0 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}
.fleet-warning__job strong { color: var(--ink); font-weight: 600; }
.fleet-warning__meta {
  margin: var(--s-1) 0 0;
  font-size: var(--t-legend);
  /* --ink-dim, not --ink-faint: faint measures 2.21:1 on a white card, and this
     line carries how long the machine has been down and how often. */
  color: var(--ink-dim);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}

.fleet-warning__ack {
  flex: none;
  min-width: 44px;
  min-height: 44px;
  padding: 0 var(--s-3);
  font: inherit;
  font-size: var(--t-small);
  color: var(--ink-dim);
  background: transparent;
  border: var(--hairline);
  border-radius: var(--radius);
  cursor: pointer;
}
.fleet-warning__ack:hover { color: var(--ink); border-color: var(--etch-strong); }

/* "and 3 more" -- the banner shows the worst few and says how many it is not
   showing. Silently truncating would read as "that is all of them". */
.fleet-warnings__more {
  align-self: center;
  font-size: var(--t-small);
  color: var(--ink-dim);
  background: var(--sheet);
  border: var(--hairline);
  border-radius: var(--radius);
  padding: var(--s-2) var(--s-3);
  min-height: 44px;
  text-decoration: none;
}

@media (max-width: 575.98px) {
  .fleet-warning {
    grid-template-columns: auto 1fr;
    row-gap: var(--s-2);
  }
  /* The button drops to the second row and spans it, rather than squeezing the
     text into a column too narrow to read a stop name in. */
  .fleet-warning__ack { grid-column: 2; justify-self: start; }
}

/* Phones: full width, and clear of the rail that becomes a bottom bar
   (style.css turns .app-rail into a fixed bottom bar under 768px). */
@media (max-width: 767.98px) {
  .support-panel {
    width: 100vw;
    bottom: calc(5.5rem + env(safe-area-inset-bottom));
  }
}

/* Touch: no hover geometry, matching the block that must stay last in
   style.css. Nothing here animates position or scale on hover, so this
   only needs to neutralise the colour hover. */
@media (hover: none), (pointer: coarse) {
  .support-choice:hover { border-color: var(--etch-strong); }
  .support-panel__close:hover { background: transparent; }
  .support-panel__mail:hover { background: transparent; }
  .support-panel__restart:hover { background: transparent; }
  /* Same reason: on a tablet the hover state sticks after a tap, so an
     acknowledged-looking button would stay lit under the operator's finger. */
  .fleet-warning__ack:hover { color: var(--ink-dim); border-color: var(--etch); }

  /* The link into a machine is the one control a finger has to hit on these
     rows, and at desktop density its box is about 18px tall. Padding the link
     rather than the cell keeps the numbers on the row where they are while
     growing the target to roughly a 44px row. */
  a.t-id { padding: 6px 0; }

  /* Never animate geometry on a touch pointer: the nudge would stay nudged
     after the tap, on the row the operator has just left. */
  a.t-id:hover .t-id__go { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .support-panel { transition: none; }
}
