/* TRACEWEAVER: file-role=support-console-instrument-skin; req=REQ-AIR-plan-2026-08-18-003-U15
   U15 (docs/plans/2026-08-18-003-feat-improved-support-dashboards-plan.html,
   section U15, D3=yes): the dense-instrument restyle of /support, loaded
   ONLY by static/support.html (one <link>, right after styles.css so equal-
   specificity rules here win the cascade without a single !important).
   styles.css is READ-ONLY from this file and stays the single source of
   truth for /admin and the landing page -- nothing below redefines a
   :root custom property or a shared bare selector's own base rule; every
   token used below is an EXISTING var() already defined in styles.css
   (--surface/--surface-2/--surface-3/--border/--border-strong/--text/
   --text-muted/--primary/--accent-blue/--warning/--danger-text/
   --success-text/--lab-history-series-0/--lab-history-series-1), reused
   verbatim -- no new hue anywhere in this file, matching styles.css's own
   "colour is reserved for data" discipline (plan's own words for this
   unit's governing rule).

   ZERO markup changes ride with this file (support.js and support.html
   are otherwise untouched -- see support.html's own single new <link>).
   Every selector below targets a class or id support.js ALREADY emits
   (verified against the current tree; see the plan's own U1-U14 units)
   or an id support.html already ships. Where a render function composes
   its OWN layout via an inline `style="..."` attribute (support.js's own
   comment above opsKpiTileHtml: "/support ships style-src 'self' with no
   'unsafe-inline' ... an inline style attribute is silently DROPPED by
   the browser"), that inline attribute never actually applies in a real
   browser (verified empirically against this exact CSP header) -- the
   rules below restore the SAME intended layout via a class or structural
   selector instead, which the CSP allows because it comes from this
   external stylesheet. This is not a workaround bolted on top of working
   code; for the ops-talker bars, the heatmap grid/cells, and every SVG
   axis-label/gap-glyph <text>, the rules below are the ONLY styling that
   actually reaches the page today. See this unit's own findings doc for
   the full list.

   SCOPING: every rule below either (a) uses a class support.js emits
   nowhere except /support (.ops-*, .fleet-table, .dev/.devgrid, .iftile/
   .ifgrid, .chartwrap, .msr, .attached-grid, .chat-rail, #chat-dock,
   .dev-actions*, .idband, .qfilter -- none of these strings appear in
   admin.html/admin.js/index.html/landing.html, checked), or (b) prefixes
   a shared house-theme class/element (table, .card, .status-pill, .tabs,
   .panel, .readout) with #support-workspace so it can never touch
   /admin's own table/.card/.tabs/.panel even if this file were ever
   linked somewhere it shouldn't be. The single <link> in support.html is
   the primary guarantee; the prefixing below is defence in depth.
   ============================================================ */

/* ------------------------------------------------------------------
   1. Board-arranged instrument panels (Ops tab). The ops-board mockup's
   own 12-column .board (docs/plans/phase3-handovers/2026-08-18-support-
   console-screen-ops-board.html) is a grid of nine <section class=
   "panel"> boxes at full/sp7/sp6/sp5 widths -- support.html's
   #ops-content is a flat, frozen sequence of <h3>/<p class="help">/
   <div id=...> siblings with no such wrapper (the plan's own "zero
   markup changes" constraint), so a literal grid of BORDERED BOXES is
   still not reachable from CSS alone. What IS reachable, and is the gap
   this pass closes: #ops-content itself becomes the 12-column board,
   and every direct child gets an explicit grid-column -- full width by
   default (the `> *` rule directly below), narrowed only for the two
   panel PAIRS that mirror the mockup's own side-by-side panels one-for-
   one (same widths, same order): "Top talkers" (sp6) beside "Wi-Fi
   signal distribution" (sp6) -- mockup's own consecutive sp6+sp6 pair --
   and "Session trail" (sp7) beside "Router health" (sp5) -- mockup's own
   consecutive sp7+sp5 pair. Every other heading (Coverage, Household
   throughput, Remainder, Device x time, and the Staged-changes tray)
   stays full width: the mockup has no equivalent for Remainder or the
   tray's own pairing partner (its "Interfaces" panel belongs to the
   Workbench tab, not #ops-content), and splitting either one without a
   mockup-specified partner would be inventing a pairing, not reading
   one off the approved design. See the "Panel pairs" rules below this
   block for the mechanism (grid-auto-flow: dense, explicit columns,
   fully auto rows) and the 760px collapse in §13.
   #ops-kpi-tiles, .ops-heatmap, .ops-talkers etc. still get their own
   true grids below wherever the underlying markup is already a flat,
   repeatable list -- see §3/§7/§9; those are unrelated to this section
   and untouched by it.
   ------------------------------------------------------------------ */
/* minmax(0, 1fr) per column, not a bare 1fr: #ops-heatmap's own SVG rows
   are pixel-wide (they can run to thousands of px at a busy window) and
   .ops-heatmap's own overflow-x:auto is meant to contain that -- but a
   bare `1fr` implicit column has an AUTOMATIC MINIMUM SIZE of `auto`
   (its content's own min-content), which lets that width propagate
   straight up through this grid and .attached-grid's OWN narrow-
   viewport collapse (styles.css, also a bare 1fr) into a page-body-
   width overflow at 360px -- measured directly (a hidden #ops-heatmap
   dropped .attached-grid's resolved track from 574px to 385px). This
   minmax(0, ...) is what actually lets the containment happen where it
   is supposed to (inside .ops-heatmap below), not a decorative choice;
   repeat(12, minmax(0, 1fr)) keeps that same zero-minimum floor on
   every one of the 12 tracks now that there are 12 instead of 1. */
#ops-content {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-flow: row dense;
  gap: 4px 20px;
}
/* Full width by default. `dense` (not the sparse default) is required
   for the panel pairs below, not decorative: two sibling elements that
   must stack in the SAME half (e.g. the "Top talkers" <h3> then its
   #ops-top-talkers content div, both given the identical explicit
   `grid-column` range below) push the second one to the next row
   because their column is already taken -- dense packing is what lets
   the OTHER half's own first element back-fill the gap that leaves,
   producing two aligned side-by-side columns instead of a one-row-per-
   element staircase. This cannot reorder DOM/tab order (dense only ever
   moves a LATER same-column item into an EARLIER same-column gap) and
   cannot pull a later full-width item ahead of an earlier one either: a
   `1 / -1` item needs all 12 columns free simultaneously, which never
   happens until both halves of whichever pair precedes it have finished
   -- verified empirically, see this branch's own findings doc. */
#ops-content > * { grid-column: 1 / -1; }

/* Panel pair 1 -- "Top talkers" (left, sp6) beside "Wi-Fi signal
   distribution" (right, sp6): the mockup's own consecutive sp6+sp6 pair,
   same order. Neither heading carries an id or class of its own in
   support.html's frozen markup, so h3:nth-of-type stands in for one --
   the fixed 8-heading order (Coverage, Household throughput, Remainder,
   Top talkers, Wi-Fi signal distribution, Device x time, Session trail,
   Router health) is the same sequence #ops-content > h3:first-of-type
   above already depends on. "Top talkers" has no <p class="help"> of its
   own (support.html has none between its <h3> and #ops-top-talkers);
   "Wi-Fi signal distribution" does, so its selector list includes it. */
#ops-content > h3:nth-of-type(4),
#ops-top-talkers {
  grid-column: 1 / 7;
}
#ops-content > h3:nth-of-type(5),
#ops-content > h3:nth-of-type(5) + p.help,
#ops-signal-distribution {
  grid-column: 7 / 13;
}

/* Panel pair 2 -- "Session trail" (left, sp7) beside "Router health"
   (right, sp5): the mockup's own consecutive sp7+sp5 pair, same order.
   Every loading/empty/error/list/button sibling in each group is listed
   so the column assignment holds no matter which one support.js
   currently leaves un-hidden ([hidden] is display:none, so only one of
   each group's state elements is ever actually placed in the grid). */
#ops-content > h3:nth-of-type(7),
#ops-content > h3:nth-of-type(7) + p.help,
#ops-activity-loading,
#ops-activity-empty,
#ops-activity-error,
#ops-activity-trail,
#ops-activity-load-more {
  grid-column: 1 / 8;
}
#ops-content > h3:nth-of-type(8),
#ops-content > h3:nth-of-type(8) + p.help,
#ops-health-refresh,
#ops-health-loading,
#ops-health-error,
#ops-health-content {
  grid-column: 8 / 13;
}

#ops-content > h3 {
  margin: 22px 0 6px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 700;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
#ops-content > h3:first-of-type { margin-top: 0; padding-top: 0; border-top: none; }
#ops-content > .section-heading { margin: 22px 0 6px; padding-top: 14px; border-top: 1px solid var(--border); }
#ops-content > .section-heading:first-child { margin-top: 0; padding-top: 0; border-top: none; }
#ops-content > .section-heading h3 { margin: 0; padding: 0; border: 0; }
#ops-content > .help { margin-bottom: 10px; }

/* ------------------------------------------------------------------
   2. Household KPI tile strip -- support.js's own comment names this
   file as the one that gives .ops-kpi-tile a rule at all ("no CSS rule
   for any of them exists yet on purpose ... U15's CSS-only pass gives
   them one"). auto-fit keeps this readable from 360px (one column) up
   to a wide desktop board without a breakpoint of its own.
   ------------------------------------------------------------------ */
.ops-kpi-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 8px;
  margin-bottom: 6px;
}
.ops-kpi-tile {
  display: grid;
  gap: 2px;
  align-content: start;
  padding: 9px 11px 8px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface-2);
  min-height: 44px;
}
.ops-kpi-label {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ops-kpi-value {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-weight: 600;
  font-size: 1.3rem;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ops-kpi-sub { font-size: 0.7rem; color: var(--text-muted); }

/* Window selector (#ops-window-select) reuses .tabs/.tab from
   styles.css already; only tightened toward mono here. */
#support-workspace #ops-window-select.tabs { margin-bottom: 10px; }
#support-workspace #ops-window-select .tab {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
}

/* Coverage ribbon (#ops-coverage): a flex-wrap strip of .status-pill --
   the container itself carries no class in support.js's own output, so
   this rule addresses it by id. */
#ops-coverage {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 4px;
}
#support-workspace #ops-coverage .status-pill {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------------
   3. Chart furniture. .chartwrap/.axis/.gapglyph/.pt already carry a
   base rule in styles.css; every axis-tick/"now"-anchor/y-tick <text>
   support.js draws is a BARE <text style="..."> with no class (its own
   OPS_AXIS_LABEL_STYLE constant) -- that inline style is CSP-dropped
   (see file banner), so without the element-selector rule below those
   ticks render as unstyled (default black, default sans) text, unreadable
   on the dark panel. Scoped to the three chart mount ids so it can never
   pick up an unrelated <text> elsewhere on the page.
   ------------------------------------------------------------------ */
#ops-throughput-chart text:not(.gapglyph),
#ops-heatmap text:not(.gapglyph),
#ops-signal-distribution text:not(.gapglyph) {
  font: 10px ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  fill: var(--text-muted);
}
/* "now" anchor: the one chart element that IS a live fact about the
   present moment, so it earns --primary (the page's own "selected/
   current" token, never a verdict colour) rather than the muted axis
   tone every other tick uses. Line only (a stable class-based
   selector) -- the label beside it is left in the general axis-text
   tone above rather than a second, order-dependent selector. */
#ops-throughput-chart .now-anchor { stroke: var(--primary); }
#ops-throughput-caption { margin-bottom: 4px; }
#ops-heatmap-caption { margin-bottom: 6px; }

/* ------------------------------------------------------------------
   4. Remainder readout (#ops-remainder / .ops-remainder-*). The
   ".readout"/".readout-row" chrome already exists; the per-state class
   (over_measured/unknown/measured, etc.) is deliberately left WITHOUT a
   verdict colour here -- GIVEN 7/the plan's own HONESTY §9 says a
   negative remainder is "expected, not an error", and the hard
   constraint bans any red/amber/green meaning "bad" on a signal. Only
   spacing/emphasis, never a colour switch keyed to `state`.
   ------------------------------------------------------------------ */
.ops-remainder { margin-bottom: 4px; }
.ops-remainder-note { font-style: italic; }

/* ------------------------------------------------------------------
   5. Top talkers. opsTalkerBarRowHtml/opsTopTalkersHtml build their
   ENTIRE layout (row grid, track, fill width, bar colour) via inline
   `style="..."` -- CSP-dropped in full (see file banner); without the
   rules below the bar track/fill do not render as a bar at all (a
   default-sized <span> with no dimensions). Grid columns/track chrome
   are restored to the EXACT values support.js's own (now-inert) inline
   style specified; the width PERCENTAGE itself is a per-talker computed
   number that only ever existed in that dropped attribute and cannot be
   recovered from CSS alone (no data-* carries it) -- the exact download/
   upload figures remain fully readable as text in .ops-talker-value
   regardless, so no fact is lost, only the bar's own relative-length
   cue. Bar colour: --lab-history-series-0 for a real talker (the same
   token every other download series on this page already uses), muted
   --text-muted for the synthetic "other" row (support.js's own
   `ops-talker-other` class) -- state/kind colour, never a verdict.
   ------------------------------------------------------------------ */
.ops-talkers { display: grid; gap: 2px; }
.ops-talkers-scale {
  display: flex;
  justify-content: space-between;
  font-size: 0.72rem;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--text-muted);
  padding-left: 9rem;
  margin-bottom: 4px;
}
.ops-talker-row {
  display: grid;
  grid-template-columns: minmax(7.5rem, 9rem) 1fr auto;
  gap: 8px;
  align-items: center;
  padding: 3px 0;
}
.ops-talker-name { overflow-wrap: anywhere; font-size: 0.82rem; }
.ops-talker-bar-track {
  display: block;
  height: 10px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}
.ops-talker-bar {
  display: block;
  height: 100%;
  background: var(--lab-history-series-0);
}
.ops-talker-row.ops-talker-other .ops-talker-bar { background: var(--text-muted); }
.ops-talker-row.ops-talker-other .ops-talker-name { color: var(--text-muted); font-style: italic; }
.ops-talker-value {
  white-space: nowrap;
  font-size: 0.78rem;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.talker-samples { color: var(--text-muted); }

/* ------------------------------------------------------------------
   6. Wi-Fi signal distribution lanes -- .chartwrap/.pt already styled;
   only inter-lane rhythm is new here.
   ------------------------------------------------------------------ */
.ops-signal-lane { margin-bottom: 10px; }
.ops-signal-lane:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------
   7. Device x time heatmap. THE HONESTY-CRITICAL BLOCK: opsHeatmapCellHtml
   draws three structurally distinct things and every one of them must
   keep reading as distinct after this restyle (this unit's own hard
   constraint):
     - sample_count === 0            -> a <text class="gapglyph hm-gap">
       "x" glyph. No fill/rect at all -- an absence of data collection.
     - sampled, value === null       -> <rect class="hm-unmeasured">, a
       HOLLOW (stroke-only) square -- sampled, but no honest reading.
     - sampled, value a real number  -> <rect class="hm-cell">, a FILLED
       square -- a genuine reading, including a genuine 0.0.
   support.js's own comment documents an opacity FLOOR on .hm-cell so a
   measured zero still paints solid rather than fading to invisible --
   that floor is computed per-cell and written into the SAME dropped
   inline `style="fill:...;opacity:..."` attribute (see file banner), so
   the graduated per-cell intensity cannot be recovered here; what CAN
   and MUST be recovered is the STRUCTURAL three-way distinction, which
   depends only on which class/element was emitted, not on the missing
   numeric value -- .hm-cell below is given one fixed, fully-solid fill
   (a floor of 1, not 0), which is honest (every filled cell -- including
   a real zero -- reads identically solid, never fading toward the
   "nothing happened" look of an unfilled cell) even though the relative-
   magnitude cue between two filled cells is lost along with the inline
   style that carried it. The exact figure is still available on hover
   (each cell's own <title>, untouched, CSP-independent).
   ------------------------------------------------------------------ */
.ops-heatmap { overflow-x: auto; border-radius: 4px; }
.ops-heatmap-axis,
.ops-heatmap-row {
  display: grid;
  grid-template-columns: minmax(7rem, 10rem) 1fr;
  gap: 8px;
  align-items: center;
  padding: 2px 0;
}
.ops-heatmap-axis { background: var(--surface); }
.ops-heatmap-rows { max-height: 420px; overflow-y: auto; background: var(--surface); }
.ops-heatmap-name {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  padding-right: 6px;
  overflow-wrap: anywhere;
  font-size: 0.78rem;
}
.ops-heatmap-axis > span:first-child { position: sticky; left: 0; z-index: 1; background: var(--surface); }
.ops-heatmap-row-unmeasured {
  border-left: 2px dashed var(--border-strong);
  padding-left: 6px;
  margin-left: -8px;
}
.ops-heatmap-row-unmeasured .ops-heatmap-name { color: var(--text-muted); font-style: italic; }
/* The three distinct cell renderings -- see the block comment above. */
.hm-cell { fill: var(--lab-history-series-0); opacity: 1; }
.hm-unmeasured { fill: none; stroke: var(--text-muted); stroke-width: 1; }
.gapglyph.hm-gap {
  fill: var(--text-muted);
  font: 700 11px ui-monospace, Menlo, Consolas, monospace;
  text-anchor: middle;
  opacity: 0.85;
}

/* ------------------------------------------------------------------
   8. Session trail (#ops-activity-trail / .ops-activity-line) -- a
   second, narrative reader of the same activity payload the Workbench
   table already shows (U7, GIVEN). .chip/.handle already styled.
   ------------------------------------------------------------------ */
.ops-activity-trail { list-style: none; margin: 0; padding: 0; display: grid; }
.ops-activity-line {
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.82rem;
  line-height: 1.5;
}
.ops-activity-trail li:last-child { border-bottom: none; }

/* ------------------------------------------------------------------
   9. Router health "as run" reading (#ops-health-content). No class of
   its own in support.js's output (opsHealthRowHtml emits bare <div>
   rows) -- addressed by id, mirroring the existing .readout well/row
   treatment styles.css already uses elsewhere on this page.
   ------------------------------------------------------------------ */
#ops-health-content {
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  background: var(--surface-3);
  margin: 8px 0;
}
#ops-health-content > div {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.8rem;
}
#ops-health-content > div:last-child { border-bottom: none; }

/* ------------------------------------------------------------------
   10. Staged-changes tray. PENDING vs EXPIRED is a second honesty
   distinction this unit must keep readable (D1/U14's own rule: an
   expired row states plainly "nothing was applied" and drops its
   Discard control). Mirrors the EXACT visual convention styles.css's
   own .pending-action/.pending-action.expired already establishes
   elsewhere on this page (a live rail vs a drained one) rather than
   inventing a second one -- pending keeps the --warning rail at full
   strength, expired mutes to --border-strong, same as that pattern.
   ------------------------------------------------------------------ */
.ops-tray-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.ops-tray-row {
  padding: 10px 12px 10px 14px;
  border-radius: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
}
.ops-tray-row p { margin: 0 0 4px; font-size: 0.84rem; line-height: 1.5; }
.ops-tray-row p:last-of-type { margin-bottom: 0; }
.ops-tray-row-pending { border-left-color: var(--warning); }
.ops-tray-row-pending strong { color: var(--warning); }
.ops-tray-row-expired { border-left-color: var(--border-strong); opacity: 0.82; }
.ops-tray-row-expired strong { color: var(--text-muted); }
.ops-tray-row .secondary { margin-top: 6px; }

/* ==================================================================
   Fleet selection screen (#support-selection) and shared house-theme
   selectors -- prefixed with #support-workspace so this stays inert on
   /admin's own table/.card/.tabs/.panel/.status-pill/.readout even in
   the hypothetical case this file were ever linked somewhere else.
   ================================================================== */
#support-workspace .table-wrap { border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
#support-workspace .fleet-table th {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.68rem;
}
#support-workspace .fleet-table td { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
#support-workspace .fleet-table td .name { font-family: "Inter", system-ui, sans-serif; }
#support-workspace #fleet-filter-form { margin-bottom: 14px; }
#support-workspace #fleet-filter-form label { font-size: 0.72rem; }

/* Ops/Workbench tab switch + attached content frame density. */
#support-workspace #support-attached > .attached-grid > #attached-content > .tabs .tab {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.78rem;
  letter-spacing: 0.03em;
}
#support-workspace .attached-grid { gap: 16px; }

/* ------------------------------------------------------------------
   11. Device cards / interface tiles (Workbench). .dev/.devgrid/.iftile/
   .ifgrid already carry full chrome from styles.css's own S11 section
   (border/radius/background) -- this unit densifies toward the
   instrument's tighter radius and enforces tabular monospace figures
   on every numeric readout, without touching a single colour or the
   .msr-absent/.r-absent italics that carry the "absent, not zero"
   distinction (only spacing/font-variant properties are added here,
   never font-style/color on those selectors).
   ------------------------------------------------------------------ */
.dev { border-radius: 6px; }
.iftile { border-radius: 6px; }
.rates .n,
.msr .n,
.ifkv dd {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
/* .dropfig: the interface tile's collapsed-face drops chip (support.js's
   own interfaceDropsFigureHtml), deliberately styled as the OPPOSITE of
   its sibling .warnfig (styles.css) -- no var(--warning), no triangle
   glyph, reusing var(--text-muted) (already defined in styles.css; no new
   hue introduced by this file). A nonzero drop count with zero errors is
   ordinary queue behaviour, not a fault -- REQ-AIR-174/176's own "nonzero,
   never bad" discipline means this figure earns no warning treatment at
   all, only a legible, bold, still-noticeable readout. The chip's own
   text always carries the word "drops" (support.js), so the errors-vs-
   drops distinction survives even with colour removed entirely --
   colour is never the only tell. */
.dropfig { color: var(--text-muted); font-weight: 600; }
/* .readout-val is ALSO emitted by admin.js (the hardware-router-readout
   panel) -- scoped, unlike its .rates/.msr/.ifkv siblings above, which
   are support-console-exclusive (confirmed absent from admin.html/
   admin.js). */
#support-workspace .readout-val { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
/* .reading-age (CARD-age, owner report 2026-08-23): support.js's own
   deviceReadingAgeHtml -- the note beside the traffic-provenance .src
   chip stating WHEN the figure was read, not just where it came from.
   var(--text-muted) is the SAME quiet tone .spark-note already uses for
   passive metadata beside a measured value; var(--warning) (the
   .stale variant) reuses the SAME token .src.typed already carries
   elsewhere in this file for "reported, not confirmed" -- no new hue
   either way. .unknown (no reading timestamp at all) is italicised, the
   SAME "absent, not zero" tell .msr-absent already uses. */
.reading-age { font-size: .64rem; letter-spacing: .02em; color: var(--text-muted); margin-left: 4px; }
.reading-age.stale { color: var(--warning); }
.reading-age.unknown { font-style: italic; }

/* ------------------------------------------------------------------
   12. Chat rail -- placement/chrome only (S11's own rule, carried
   forward): no new chat behaviour, only a slightly tighter frame to
   match the instrument density around it.
   ------------------------------------------------------------------ */
#support-workspace .chat-rail .chat-shell { border-radius: 10px; }

/* ==================================================================
   13. Responsive -- readable at 360px. Controls stay >=44px (the page-
   wide button/input floor in styles.css already guarantees this; the
   rows below are non-interactive, so no floor applies to them). Charts
   and wide grids scroll inside their OWN container (.ops-heatmap above,
   the pre-existing .table-wrap for the fleet table) -- the page body
   itself never gains a horizontal scrollbar from anything in this file.
   ================================================================== */
/* Board collapse: below 760px the two panel pairs (§1) no longer have
   room for a readable half-width column each -- the whole board drops
   to #ops-content's original single track and every explicit pairing
   column is reset back to full width. This MUST re-declare every
   selector §1 narrowed, not just the grid-template-columns: leaving a
   child's `grid-column: 7 / 13` (etc.) in place against a grid that no
   longer HAS 12 explicit tracks would make the browser generate narrow
   implicit tracks to satisfy that line number instead, which is exactly
   the page-body-width overflow at 360px this file's own §1 comment
   already measured once for a different bare-1fr cause. Same
   specificity as §1's rules (equal id/child-combinator/pseudo-class
   weight on each selector), so this wins purely on appearing later in
   the file -- no !important needed. */
@media (max-width: 760px) {
  #ops-content { grid-template-columns: minmax(0, 1fr); }
  #ops-content > h3:nth-of-type(4),
  #ops-top-talkers,
  #ops-content > h3:nth-of-type(5),
  #ops-content > h3:nth-of-type(5) + p.help,
  #ops-signal-distribution,
  #ops-content > h3:nth-of-type(7),
  #ops-content > h3:nth-of-type(7) + p.help,
  #ops-activity-loading,
  #ops-activity-empty,
  #ops-activity-error,
  #ops-activity-trail,
  #ops-activity-load-more,
  #ops-content > h3:nth-of-type(8),
  #ops-content > h3:nth-of-type(8) + p.help,
  #ops-health-refresh,
  #ops-health-loading,
  #ops-health-error,
  #ops-health-content {
    grid-column: 1 / -1;
  }
}
@media (max-width: 480px) {
  .ops-kpi-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* MEASURED at 360-375px: the desktop 3-column row (name/bar/value)
     put .ops-talker-value's own nowrap text (restoring what support.js's
     dropped inline style specified, e.g. "37.2 Mbps down . 1.1 Mbps up .
     120 samples") in an `auto` column competing with the `1fr` bar-track
     for the same leftover space -- the auto column won it every time,
     which both pushed the row wider than the viewport (a real page-body
     horizontal scrollbar, confirmed against this exact breakpoint) AND
     starved the bar down to 0px, hiding the one colour cue this panel
     has. Re-flowed to two rows instead: name+value share a row (value
     still nowrap -- it fits on its own row's full width), the bar-track
     gets a full-width row of its own underneath, via grid-area rather
     than DOM order (no markup change). */
  .ops-talker-row {
    grid-template-columns: 1fr auto;
    grid-template-areas: "name value" "track track";
    row-gap: 4px;
    column-gap: 8px;
  }
  .ops-talker-name { grid-area: name; }
  .ops-talker-value { grid-area: value; }
  .ops-talker-bar-track { grid-area: track; }
  .ops-talkers-scale { padding-left: 0; }
  .ops-heatmap-axis,
  .ops-heatmap-row { grid-template-columns: minmax(5.5rem, 6.5rem) 1fr; gap: 6px; }
}
@media (max-width: 360px) {
  .ops-kpi-tiles { grid-template-columns: 1fr; }
}

/* ===========================================================================
   CHAT-DOCK: the owner's own defect report -- "the chat window should be
   floating" -- turns the pre-existing #chat-rail (sticky at desktop width)
   / #chat-dock (a summary bar shown only below 980px) pair into a single
   floating dock at EVERY viewport width, matching the approved mockup's
   own "Ask this view" widget (docs/plans/phase3-handovers/2026-08-18-
   support-console-screen-ops-board.html, .nl-fab/.nl-dock). support.html
   is unchanged here -- every id/class below already exists in that file;
   only its computed position/size changes.

   ID selectors (#chat-dock, #chat-rail) beat styles.css's OWN .chat-dock/
   .chat-rail class rules (and the 980px media query inside them) purely
   on specificity -- an id always outweighs a class regardless of media-
   query nesting -- so this wins at every viewport width with zero
   !important anywhere in this block (this file's own hard constraint).
   Every value below is a bare length/position; no new colour token, no
   :root, matching this file's own "dark-only, no new hue" discipline.

   #chat-dock is now the always-visible resting pill (support.js's
   updateChatDockCount still drives its own count text, unchanged).
   #chat-rail is the panel it opens -- toggled via the native `hidden`
   property (support.js's setChatDockOpen), never a class this CSS could
   independently disagree with, so "hidden" here means exactly what the
   DOM already says. No transition/animation rides with open/close: the
   swap is instant, which trivially satisfies `prefers-reduced-motion`
   (nothing to disable) rather than adding motion and then gating it. */
.attached-grid:has(#chat-rail) { grid-template-columns: minmax(0, 1fr); }

#chat-dock {
  display: flex;
  position: fixed;
  left: auto;
  right: 14px;
  top: auto;
  bottom: 14px;
  z-index: 30;
  width: auto;
  max-width: min(360px, calc(100vw - 20px));
  border-radius: 999px;
}
#chat-dock .count-pill { white-space: nowrap; }
#chat-dock-open { min-height: 44px; min-width: 44px; }

/* The panel. Anchored above the resting pill (14px pill inset + a
   MEASURED, real-render 67px pill height -- corrected here from an
   earlier ~44px estimate that undercounted it, see fix/chat-panel-grows'
   own grip-fix comment on `#chat-rail` below for the elementFromPoint
   proof that undercount actually occluded the resize grip -- + a real
   7px gap = 88px), right-aligned to match it, capped short of the full
   viewport height so it can never reach the sticky .app-bar header at
   the top (styles.css, z-index: 10; this dock is z-index: 30 but its own
   `bottom`/`max-height` keep it well clear of the header regardless) or
   grow tall enough to sit over the Ops tab's own #ops-window-select
   time-range control, which lives well above the viewport's bottom edge
   in normal scroll position.

   DEFECT G FIX (owner-measured: "the chat is unusable -- could not type
   at all"): `#chat-rail` used to just cap `max-height` with `overflow:
   hidden` and no flex column pinning the composer -- styles.css's own
   `.chat-shell` is `display: grid; grid-template-rows: auto 1fr auto;`
   with `min-height: 520px` (a shared class also used by index.html's
   own customer chat, so it is not edited here), and with no definite
   height on that grid the 1fr row does not actually constrain
   `.messages` the way it looks like it should -- the shell's natural
   content height (header + messages + the pending card, when shown +
   form + feedback) routinely exceeds this rail's `max-height` budget on
   anything shorter than ~820px tall, and `overflow: hidden` on the RAIL
   then clips whatever does not fit -- always the LAST rows, i.e. the
   composer -- off the bottom, exactly the reported defect. `#support-
   chat-panel`/`#support-chat-messages` below are a flex column, id-
   scoped exactly like `.chat-shell`/`.chat-dock` above (beats the class
   on specificity, zero `!important`): `.messages` (plus the pending
   card, when shown) is the ONLY thing that grows/shrinks and scrolls;
   the composer (`.chat-form`/`.feedback`) is `flex: none`, so it is
   ALWAYS rendered at its natural size and can never be squeezed off the
   bottom, at any viewport height down to this file's own 360px-width /
   short-desktop-height requirement. `min-height: 0` on both the panel
   and the messages list is the standard flex fix for a flex child that
   would otherwise refuse to shrink below its own content size (the
   classic reason `overflow-y: auto` silently does nothing inside a flex
   column) -- without it this fix would look identical but not work. */
/* DEFECT B FIX (owner: "it needs to be able to expand wider and higher
   or at least allow some grab handles"): native `resize: both` --
   acceptable per this unit's own brief over a hand-rolled drag handle,
   and far less to get wrong (pointer capture, touch, RTL, a11y are all
   the browser's problem, not this file's). `overflow: hidden` already
   set above still permits the native resize handle (the resize spec's
   requirement is a non-`visible` computed overflow, not `auto`
   specifically) -- and does not hide anything from the operator: the
   flex column below already gives `#support-chat-messages` its own
   internal scroll, so nothing depends on the outer overflow being
   `auto`.

   Bounds double as the safety constraints this unit's own brief names:
   `max-height: min(88vh, 820px)` deliberately leaves real headroom
   above the panel at every viewport this console runs at (the sticky
   `.app-bar` header plus a further margin) so growing taller can never
   reach the Ops tab's own `#ops-window-select` time-range control,
   which -- per this file's own long-standing comment a few lines below
   -- "lives well above the viewport's bottom edge in normal scroll
   position." `min-width`/`min-height` stop a shrink from ever making
   the composer itself unreadable. Resize is OFF below 480px (see that
   media query below): the panel is already the existing full-width
   bottom sheet there, which is what keeps this usable at the 360px
   width this unit's own brief requires -- a resize handle would have
   nothing meaningful to do on an edge-to-edge sheet and risks a
   confusing interaction with the viewport edge itself. */
/* fix/chat-panel-grows -- GRIP FIX (owner: "i don't see any grip handles
   ... nor does it grow it just this little box that is fixed size"):
   MEASURED, real render, 1280x600, document.elementFromPoint at the
   panel's own bottom-right corner (the resize spec's grip lives there):
   with `bottom: 70px` the resting pill (#chat-dock, id-selector below,
   also z-index: 30, LATER in the DOM than #chat-rail in support.html --
   equal z-index means DOM order breaks the tie, so the pill paints on
   top) measured 67px tall at `bottom: 14px`, i.e. its own top edge sits
   at 14 + 67 = 81px from the viewport bottom -- 11px INSIDE #chat-rail's
   own bottom edge at `bottom: 70px`. elementFromPoint confirmed only a
   pixel-precise sliver at the exact corner still resolved to #chat-rail;
   one row over resolved to #chat-dock and did nothing. Raising #chat-
   rail's z-index instead (so it paints over the pill) was rejected: that
   would cover the pill's OWN open/close button and staged count instead
   -- trading one occlusion for another, never actually fixing anything.
   `bottom: 88px` moves the grip itself, not the stacking order: 88 - 81
   = a real, deliberate 7px gap ABOVE the pill's own top edge, so the
   entire native resize hotspot -- not just a sliver of it -- sits clear
   of the pill at every measured viewport (re-verified after this change,
   see this unit's own findings). */
#chat-rail {
  position: fixed;
  left: auto;
  right: 14px;
  top: auto;
  bottom: 88px;
  z-index: 30;
  /* fix/chat-panel-grows -- DEFAULT SIZE (owner: "the chat box is so
     small ... does not ... scale with the text as it becomes more"):
     392x600 was sized for a short reply; detailed staff answers are the
     NORMAL case on this surface (owner, explicitly: verbose answers are
     wanted, not a defect -- the container is the problem). 460px keeps a
     real margin under `max-width: min(640px, ...)` below (still real
     room for a further manual drag) while being immediately, visibly
     wider than before at zero risk: width has no bearing on the
     composer-visibility math the `height`/`max-height` comments below
     are all about. */
  width: min(460px, calc(100vw - 20px));
  /* DEFINITE `height`, not just `max-height` (MEASURED, real render,
     1280x600 AND 360x740: with max-height alone -- an "auto, clamped"
     size -- the flex column's shrink math for the NESTED flex column one
     level down (#support-chat-panel, itself sized via `flex: 1 1 auto`
     with no definite height of its own either) landed a few px short of
     this element's own clamped box, silently clipping the last flex:none
     row -- #support-chat-feedback -- exactly defect C. A definite
     `height` gives both flex levels an unambiguous budget to shrink
     against, so nothing at the bottom can fall outside it. Harmless
     alongside `max-height` below (identical value; `max-height` still
     governs once `resize: both` starts writing an inline `height` from a
     drag, same as it always would).

     fix/chat-panel-grows: UNCHANGED at 420px at the 1280x600 regression
     viewport specifically, on purpose -- MEASURED, real render: with the
     grip fix above already consuming ~18px of this viewport's own
     headroom under the sticky `.app-bar` (76px tall), raising THIS
     formula's own 70vh term as well leaves only a few px of margin
     before the panel's top edge would reach the header -- too thin to
     trust across browsers/font metrics. The `@media (min-height: 700px)`
     block further down raises the default on taller viewports instead,
     where the same header-clearance math has real room to spare (see
     that block's own comment); auto-growth (support.js's
     growChatRailToFitContent) is what actually answers "scale with the
     text as it becomes more" at every viewport, including this one. */
  height: min(70vh, 600px);
  /* DEFECT B's own ceiling (see that fix's comment above): deliberately
     LARGER than the default `height` above, or `resize: both` would have
     no room to grow at all -- a real mistake this unit's own G/C
     follow-up caught before commit (it had briefly matched `max-height`
     to the same `min(70vh, 600px)` as `height`, silently collapsing
     defect B's whole "expand wider and higher" ceiling down to zero
     growable range). fix/chat-panel-grows: this is also the ceiling
     support.js's own growChatRailToFitContent reads live (getComputedStyle
     .maxHeight) and never exceeds -- UNCHANGED by this unit, per its own
     brief ("that definite height ... must not simply be deleted" /
     "up to the existing max"). */
  max-height: min(88vh, 820px);
  min-width: 280px;
  min-height: 320px;
  max-width: min(640px, calc(100vw - 40px));
  overflow: hidden;
  display: flex;
  flex-direction: column;
  resize: both;
}
/* fix/chat-panel-grows: a real, larger default on viewports with actual
   headroom to spare -- MEASURED, real render: at 1280x800 the sticky
   header still ends at 76px regardless of viewport height, so a taller
   viewport is pure slack. 700px is comfortably above the 1280x600
   regression viewport (so that viewport's own conservative height above
   is never touched by this block) and comfortably below typical desktop
   heights (so this actually fires for the owner's real usage, and for
   this unit's own required 1280x800 check). 74vh/700px both stay well
   under max-height's own 88vh/820px ceiling above at every height this
   query can match, so `height` can never exceed `max-height` here. */
@media (min-height: 700px) {
  #chat-rail { height: min(74vh, 700px); }
}
#chat-rail #support-chat-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  flex: 1 1 auto;
  overflow: hidden;
}
#chat-rail #support-chat-messages {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
}
/* MEASURED FOLLOW-UP (real render, 360x740, a visible pending card):
   #support-chat-messages alone as the only flex:1 item was not enough --
   header + a shown pending card + the composer form + feedback (all
   flex: none) can, together, still exceed this panel's own available
   height even with messages shrunk to nothing, and panel's own
   `overflow: hidden` then silently clipped the LAST flex: none row
   (#support-chat-feedback) off the bottom -- the same defect C symptom,
   one level deeper than #chat-rail's own height fix above reaches.
   #support-chat-pending shares the flexible/scrollable budget too now
   (flex-grow: 0 -- it never competes with messages for extra room, it
   only gives room back under real pressure; min-height: 0 +
   overflow-y: auto so it can scroll internally rather than disappear).
   #support-chat-form/#support-chat-feedback -- the actual composer --
   stay flex: none, unconditionally, the one thing this whole fix exists
   to guarantee is never clipped. */
#chat-rail #support-chat-pending {
  flex: 0 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
}
#chat-rail #support-chat-form,
#chat-rail #support-chat-feedback {
  flex: none;
}

/* The pending card's "stale" state (support.js's chatPendingState/
   renderChatPending) -- a LATER turn in the same conversation superseded
   an earlier staged suggestion, and this chat has no way to learn
   whether it was confirmed, discarded, or expired (see support.js's own
   comment on chatPendingState for why: it addresses a different pending
   store than the one the Ops tab's tray reads). Reuses the exact SAME
   drained-urgency treatment styles.css's own `.pending-action.expired`
   already establishes for the per-device pending card (same two tokens,
   --text-muted/--border-strong, already used above) -- a DIFFERENT class
   name because the wording is different (this is "no longer the active
   suggestion", never "expired", since expiry is not something this file
   can actually verify for this pending store) -- never a fabricated new
   hue. */
.pending-action.stale { background: transparent; border-left-color: var(--border-strong); }
.pending-action.stale strong { color: var(--text-muted); }
.pending-action.stale strong::before { content: "\21BB "; }

@media (max-width: 480px) {
  /* Full-width bottom sheet, per this unit's own 360px readability
     requirement -- both the pill and the open panel span edge to edge
     (10px inset, matching the mockup's own ≤480px rule) rather than
     staying a narrow right-anchored column that would crowd a 360px
     screen. MEASURED (real render, 360x740, real CSP,
     getBoundingClientRect): the pill (styles.css's own .chat-dock
     padding/min-height, unchanged here) renders ~67px tall -- an
     earlier `bottom: 66px` on the panel put its own bottom edge INSIDE
     the pill's own vertical span, a real 15px overlap. `bottom: 96px`
     (panel) + `bottom: 10px` (pill, tightened from the desktop 14px)
     leaves a real, measured ~19px gap between them at this exact
     viewport -- re-measured after this change, see this unit's own
     findings. */
  #chat-dock, #chat-rail { left: 10px; right: 10px; width: auto; max-width: none; }
  #chat-dock { bottom: 10px; }
  /* DEFECT B FIX, mobile: resize OFF -- see this unit's own comment on
     the desktop #chat-rail rule above for why (the panel is already the
     full-width sheet a resize handle would have nothing useful to do
     on). min-width/min-height from the desktop rule are harmless but
     moot once width/max-width are both overridden to auto/none above. */
  #chat-rail { bottom: 96px; height: min(70vh, 480px); max-height: min(70vh, 480px); resize: none; }
  /* DEFECT G FIX, mobile: no fixed max-height on the messages list here
     any more -- the flex column above (`#chat-rail #support-chat-
     messages { flex: 1 1 auto; min-height: 0; }`) already makes it the
     one thing that shrinks/scrolls within whatever `#chat-rail`'s own
     max-height leaves after the composer's fixed-size rows, at every
     width this media query covers, including 360px. A second, smaller
     max-height here would silently re-introduce the same defect on
     mobile specifically once messages/pending-card content grows past
     it, the exact bug this unit fixes. */
}

/* ===========================================================================
   CHAT RESIZE, KEYBOARD (unit-l) -- owner, verbatim: "the chat needs grip
   handles so we can make it bigger to read the answers, not scroll up or
   down". DEFECT B above already answers this for a mouse/touch operator
   (`#chat-rail { resize: both; }`), but the native CSS resize handle has
   NO keyboard equivalent in any browser -- there is no key or combination
   that invokes it. #support-chat-resize-toggle (support.html, inside
   .chat-header's own .row-actions group) is a plain <button>, so it needs
   no bespoke keyboard wiring of its own: native <button> semantics already
   make it focusable and Enter/Space-activatable. support.js's
   setChatRailExpanded is the click handler -- it grows #chat-rail to the
   exact same max-height ceiling `resize: both`/growChatRailToFitContent
   already respect, so a keyboard operator can reach the same "as big as
   this layout allows" state a mouse drag can, never past it.

   This block exists only because that SECOND button made the pre-existing
   single-button header row wider. styles.css's own `.chat-header` (SHARED
   with index.html's own customer chat -- not edited here, and that file
   never gains a #chat-rail ancestor to match this selector against) has
   no `flex-wrap` of its own, and two buttons competing with the eyebrow/
   heading text for one un-wrapped row is exactly the kind of squeeze this
   file's own DEFECT B/C/G block above spent three defects ruling out at
   360px width. `flex-wrap: wrap` here lets the button group drop to its
   own line instead of shrinking the heading text further -- MEASURED,
   real render, 360x740: the eyebrow/heading block and the two-button
   .row-actions group each get their own full-width row, no clipping, no
   overlap with #support-chat-clear or #support-chat-resize-toggle
   themselves. ID-scoped exactly like every other override in this file
   (#chat-rail .chat-header, beats the class on specificity, zero
   !important). */
#chat-rail .chat-header { flex-wrap: wrap; row-gap: 8px; }
