/* console.css — the operator console layered over the map: live alerts,
 * coverage, and per-site topology.
 *
 * Everything here draws from the variables applyMapMode() sets (--panel,
 * --border, --text, --dim, --accent), so the console follows the dashboard into
 * dark mode without a second palette to keep in step. The only colours declared
 * literally are the SEVERITY ones — those must mean the same thing on every
 * basemap and in both themes, or the panel is lying.
 */

:root {
  --sev-critical: #cc2200;
  --sev-warning:  #e8a800;
  --sev-info:     #0068b5;
  --sev-none:     #7a97b0;
  --sev-ok:       #008c44;

  /* ── 4Trak service state ────────────────────────────────────────────────
     THREE STATES AND A DELIBERATE ASYMMETRY. In service is the baseline and
     gets NO decoration -- same reasoning as the manage grid not tinting healthy
     rows: decorating the normal case makes it compete with the exception.
     Out of service is a stated fact and gets a solid mark. Unknown is NOT a
     third flavour of fact, it is the absence of one, so it gets the dashed
     outline -- "we could not ask" has to look different from "it is tied up",
     or the map asserts a roster it never read.

     TEAL, not red/amber/green. Service state is orthogonal to health: a vessel
     can be out of service AND its switch down, and both have to stay readable
     at once. Reusing a health hue would collapse the two questions into one
     colour. Teal and slate sit outside the severity ramp and stay separable
     under deuteranopia and protanopia, where amber-vs-green does not.
     Colour is never the only carrier -- every use pairs it with a glyph
     (map) or a glyph plus a word (manage). */
  --svc-out:      #0f7b8a;
  --svc-out-tint: #0f7b8a26;
  --svc-unknown:  #7a8a99;
}
body.dark-mode {
  /* Lifted for contrast on the navy panel, same hues. */
  --sev-critical: #ff6a4d;
  --sev-warning:  #ffc247;
  --sev-info:     #54c7ee;
  --sev-none:     #8ba6bd;
  --sev-ok:       #35d17e;

  --svc-out:      #4bd0e0;
  --svc-out-tint: #4bd0e033;
  --svc-unknown:  #93a6b6;
}

/* ── the button that opens the dock ─────────────────────────────────────── */
.mo-btn .alert-badge {
  display: inline-block; min-width: 16px; padding: 0 4px; margin-left: 6px;
  font-family: var(--mono); font-size: 10px; font-weight: 700; line-height: 15px;
  text-align: center; border-radius: 8px; vertical-align: 1px;
  background: var(--sev-none); color: #fff;
}
.mo-btn .alert-badge.critical { background: var(--sev-critical); }
.mo-btn .alert-badge.warning  { background: var(--sev-warning); color: #2b2000; }
.mo-btn .alert-badge.clear    { background: var(--sev-ok); }
/* GREY IS THE ANSWER HERE, NOT A MISSING RULE. `unknown` is the badge when the
   console could not ask Alertmanager at all, and it resolves to exactly the
   same var(--sev-none) the base rule six lines up already sets — so this line
   changes nothing on screen and exists to be READ. `.info` is already an
   unmatched class quietly inheriting that grey, so the next person to notice
   the gap would "fix" it by inventing a colour for unknown. Do not: this is
   the map's grey for a wharf nobody measured, and it has to keep saying the
   same thing here.
   The character beside it is an em dash, not a 0 — that part lives in
   console.js. This rule only makes sure it does not go out in `clear` green,
   which must be reachable ONLY from a poll that was actually answered. */
.mo-btn .alert-badge.unknown  { background: var(--sev-none); }
/* LAST KNOWN, NOT CURRENT. `stale` keeps the severity colour on purpose: a
   critical we were told about an hour ago is still a critical, and greying the
   number out would be the same lie told in the other direction. The ring is
   the caveat — the count is a memory, not a measurement.
   outline, NOT box-shadow, and both reasons will otherwise be undone:
   .mo-btn's background is a translucent color-mix over a blurred backdrop, so
   a box-shadow ring's inner gap would have to fake --panel and would not
   match; and alert-pulse below ANIMATES box-shadow, so a box-shadow ring would
   silently disappear on the one badge that matters most — an unacknowledged
   critical we can no longer confirm is still true. An outline survives it. */
.mo-btn .alert-badge.stale    { outline: 2px solid var(--sev-none); outline-offset: 2px; }
/* Unacknowledged criticals pulse. Nothing else on the map animates, so this is
   the single most attention-grabbing thing on screen — which is the point, and
   also why it stops the moment someone acknowledges. */
@media (prefers-reduced-motion: no-preference) {
  .mo-btn .alert-badge.pulse { animation: alert-pulse 1.8s ease-in-out infinite; }
}
@keyframes alert-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(204, 34, 0, .55); }
  50%      { box-shadow: 0 0 0 6px rgba(204, 34, 0, 0); }
}

/* ── the dock ───────────────────────────────────────────────────────────── */
#console-dock {
  position: absolute; top: 0; right: 0; bottom: 0; width: 380px; z-index: 1200;
  display: flex; flex-direction: column;
  background: var(--panel); border-left: 1px solid var(--border);
  box-shadow: -6px 0 22px rgba(0, 0, 0, .13);
  transform: translateX(100%); transition: transform .22s cubic-bezier(.4, 0, .2, 1);
  font-family: var(--sans);
}
#console-dock.open { transform: translateX(0); }
@media (prefers-reduced-motion: reduce) { #console-dock { transition: none; } }
@media (max-width: 700px) { #console-dock { width: 100%; } }

.cd-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.cd-title {
  font-family: var(--cond, var(--sans)); font-weight: 700; font-size: 14px;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text); flex: 1;
}
.cd-close {
  background: none; border: 0; color: var(--dim); font-size: 20px; line-height: 1;
  cursor: pointer; padding: 2px 6px; border-radius: 3px;
}
.cd-close:hover { color: var(--text); background: var(--bg); }

/* Severity tally. Doubles as a filter — clicking one narrows the list, which is
   how you go from "23 alerts" to "the 2 that will wake someone". */
.cd-tally { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.cd-pill {
  display: flex; align-items: baseline; gap: 5px; cursor: pointer;
  padding: 3px 9px; border: 1px solid var(--border); border-radius: 3px;
  background: var(--bg); font-family: var(--mono); font-size: 10px;
  letter-spacing: .07em; text-transform: uppercase; color: var(--dim);
}
.cd-pill b { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--text); }
.cd-pill.on { border-color: var(--accent); color: var(--text); }
.cd-pill[data-sev="critical"] b { color: var(--sev-critical); }
.cd-pill[data-sev="warning"]  b { color: var(--sev-warning); }
.cd-pill[data-sev="info"]     b { color: var(--sev-info); }

.cd-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; }

/* ── one alert ──────────────────────────────────────────────────────────── */
.al {
  position: relative; padding: 9px 12px 9px 15px;
  border-bottom: 1px solid var(--border); cursor: pointer;
}
.al::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--sev-none);
}
.al[data-sev="critical"]::before { background: var(--sev-critical); }
.al[data-sev="warning"]::before  { background: var(--sev-warning); }
.al[data-sev="info"]::before     { background: var(--sev-info); }
.al:hover { background: var(--bg); }
/* An acknowledged alert is still firing — it recedes, it does not disappear.
   Hiding it would let a real outage vanish because somebody clicked a button. */
.al.acked { opacity: .55; }
.al.acked::before { opacity: .5; }
@media (prefers-reduced-motion: no-preference) {
  .al.fresh { animation: al-in .5s ease-out; }
}
@keyframes al-in {
  from { background: color-mix(in srgb, var(--accent) 18%, transparent); }
  to   { background: transparent; }
}

.al-top { display: flex; align-items: baseline; gap: 8px; }
.al-name {
  font-family: var(--cond, var(--sans)); font-weight: 600; font-size: 13.5px;
  color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.al-age {
  font-family: var(--mono); font-size: 11px; color: var(--dim);
  font-variant-numeric: tabular-nums; flex-shrink: 0;
}
.al-site {
  font-family: var(--mono); font-size: 11px; color: var(--accent);
  margin-top: 1px; display: flex; align-items: center; gap: 5px;
}
.al-site .al-kind {
  font-size: 9px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--dim); border: 1px solid var(--border); border-radius: 2px; padding: 0 3px;
}
.al-sum { font-size: 12px; color: var(--dim); margin-top: 3px; line-height: 1.4; }
.al-ackby {
  font-family: var(--mono); font-size: 10px; color: var(--sev-ok); margin-top: 3px;
}

.al-acts { display: none; gap: 6px; margin-top: 7px; flex-wrap: wrap; }
.al.open .al-acts { display: flex; }
.al-btn {
  font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  padding: 3px 8px; border: 1px solid var(--border); border-radius: 3px;
  background: var(--panel); color: var(--text); cursor: pointer;
}
.al-btn:hover { border-color: var(--accent); color: var(--accent); }
/* Silence stops real emails. It gets the one destructive-looking treatment in
   the panel so nobody reaches for it by accident. */
.al-btn.danger { color: var(--sev-critical); border-color: color-mix(in srgb, var(--sev-critical) 45%, var(--border)); }
.al-btn.danger:hover { background: color-mix(in srgb, var(--sev-critical) 12%, transparent); border-color: var(--sev-critical); }
.al-btn[disabled] { opacity: .5; cursor: default; }

/* ── suppressed: firing, and deliberately not notified about ─────────────── */
/* Alertmanager NEVER DISPATCHES a silenced or inhibited alert to any receiver,
   so not one of these has ever arrived down the webhook and the panel has
   never been able to show them. They reach this list from the poll, and they
   are the answer to "why do wharf device faults never appear here".
   Styled as evidence, not as an incident: grey bar, dim text, no fresh-flash.
   Colouring one red would turn a single dead wharf router into a dozen
   alarming rows and undo the exact inhibition Alertmanager applied — the same
   reason the map draws a masked device grey rather than red. They are kept out
   of every count server-side too, so nothing here can make the badge jump.
   THIS BLOCK MUST STAY BELOW .al[data-sev="..."]::before. Those rules and this
   one have identical specificity, so source order is the only thing making a
   suppressed critical grey instead of red. Moving it up re-reds the lot. */
.al.suppressed::before   { background: var(--sev-none); }
.al.suppressed .al-name  { color: var(--dim); font-weight: 500; }
.al.suppressed .al-site  { color: var(--dim); }
/* The flash means "this is new to you", and a poll re-asserts every suppressed
   alert on every tick — left alone, the whole group would strobe twice a
   minute. Higher specificity than .al.fresh, so it wins wherever it lands. */
.al.suppressed.fresh     { animation: none; }

/* The one collapsed row that stands for all of them, at the foot of the list.
   Collapsed because the room must not have to scroll past a dozen alerts that
   are the same fault seen further down the tree; present, and counted, because
   hiding them completely is precisely how they became invisible. The 3px bar
   plus 12px padding puts the text on .al's 15px column, so it reads as the end
   of the list rather than as chrome bolted underneath it. */
/* .cd-suppressed is the WRAPPER, not the header. It holds two children —
   button.cd-sup-head and div.cd-sup-list — so the flex/cursor/padding that
   belongs to the header has to live on the header. Styled as a flex row here,
   the expanded alert list was laid out BESIDE its own heading. */
.cd-suppressed {
  border-top: 1px solid var(--border);
  border-left: 3px solid var(--sev-none);
}
/* A <button>, so the UA's own font, background and centring have to go before
   it can look like the row it is. */
.cd-sup-head {
  display: flex; align-items: baseline; gap: 6px; width: 100%;
  padding: 8px 12px; border: 0; background: none; text-align: left;
  cursor: pointer;
  font-family: var(--mono); font-size: 11px; line-height: 1.45; color: var(--dim);
}
.cd-sup-head:hover { background: var(--bg); color: var(--text); }
.cd-sup-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cd-sup-list { border-top: 1px solid var(--border); }

.cd-empty { padding: 40px 20px; text-align: center; color: var(--dim); }
.cd-empty .big {
  font-family: var(--cond, var(--sans)); font-size: 17px; font-weight: 600;
  color: var(--sev-ok); margin-bottom: 4px;
}
.cd-empty .sub { font-family: var(--mono); font-size: 11px; }
/* NOT MEASURED. The rule directly above hardcodes var(--sev-ok), which is why
   an empty alert list could not be made honest from JavaScript alone: a
   console that had never once been told anything rendered the identical
   confident green as a genuinely quiet network, under a timestamp taken from
   the browser's own clock. `nodata` is the class that lets an empty list say
   so — grey, and in the wharf popups' words ("not measured"), which is
   vocabulary the operator has already learned rather than a new one.
   GREY REPLACES A ZERO, NEVER A NON-ZERO. This only ever reaches the EMPTY
   state; an alert we were told about an hour ago still renders red, because
   hiding a real critical behind a dash is the same mistake reversed. */
.cd-empty.nodata .big { color: var(--sev-none); }

/* One line above the list saying why the list may not be the whole truth.
   Shown whenever the source is anything but live. `warn` is the degraded case
   — Alertmanager answered, so the numbers ARE current and only the push leg is
   dead — which is why it is amber and why the empty state above it is still
   allowed its green "All clear". `nodata` is everything we could not measure;
   it is spelled out rather than left to the base rule for the same reason
   .alert-badge.unknown is, so neither reads as a forgotten selector. */
.cd-banner {
  padding: 8px 12px; border-bottom: 1px solid var(--border);
  border-left: 3px solid var(--sev-none);
  font-family: var(--mono); font-size: 11px; line-height: 1.45; color: var(--dim);
}
.cd-banner.nodata { border-left-color: var(--sev-none); }
.cd-banner.warn   { border-left-color: var(--sev-warning); }

.cd-foot {
  padding: 7px 12px; border-top: 1px solid var(--border); flex-shrink: 0;
  font-family: var(--mono); font-size: 10px; color: var(--dim);
  display: flex; justify-content: space-between; gap: 8px;
}
.cd-foot a { color: var(--accent); text-decoration: none; }

/* ── topology sub-view ──────────────────────────────────────────────────── */
.topo { padding: 10px 12px; }
.topo-site {
  font-family: var(--cond, var(--sans)); font-weight: 700; font-size: 15px;
  color: var(--text); margin-bottom: 8px;
}
.topo-row { display: flex; align-items: center; gap: 7px; padding: 3px 0; font-size: 12px; }
.topo-row .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--sev-none); }
.topo-row .dot.online  { background: var(--sev-ok); }
.topo-row .dot.offline { background: var(--sev-critical); }
.topo-row .lbl { font-family: var(--mono); font-size: 11px; color: var(--text); flex: 1;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topo-row .cnt { font-family: var(--mono); font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }
.topo-row .cnt.bad { color: var(--sev-critical); font-weight: 600; }
/* Indentation IS the hierarchy — NTU, router, switch, device type. The rails
   make the parent-child relationship readable at a glance, which is the whole
   reason to draw a tree instead of a list. */
.topo-l2 { margin-left: 12px; border-left: 1px solid var(--border); padding-left: 9px; }
.topo-l3 { margin-left: 12px; border-left: 1px solid var(--border); padding-left: 9px; }

/* ── coverage legend ────────────────────────────────────────────────────── */
#coverage-card {
  /* Rides ABOVE the in-view chip (the map legend), which keeps the bottom edge.
     --legend-h is published by console.js and is 0 when the chip is empty, so on a
     map with nothing in view this card sits where it always did.
     --bottom-stack is in here too, which it was not before: the track scrubber
     grows from the bottom of the map, and at 12px flat this card slid underneath
     it whenever a track was open. */
  position: absolute; left: 12px; z-index: 1100; width: 268px;
  bottom: calc(12px + var(--bottom-stack) + var(--legend-h, 0px));
  background: var(--panel); border: 1px solid var(--border); border-radius: 4px;
  box-shadow: 0 2px 14px rgba(0, 0, 0, .16); font-family: var(--sans);
  padding: 10px 12px;
}
#coverage-card[hidden] { display: none; }
.cv-title {
  font-family: var(--cond, var(--sans)); font-weight: 700; font-size: 12px;
  letter-spacing: .05em; text-transform: uppercase; color: var(--text);
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
}
.cv-title .cv-close { margin-left: auto; background: none; border: 0; color: var(--dim);
                      cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; }
.cv-headline { font-family: var(--mono); font-size: 11px; color: var(--dim); margin-bottom: 7px; }
.cv-headline b { font-size: 15px; color: var(--text); font-variant-numeric: tabular-nums; }
.cv-ramp { height: 9px; border-radius: 2px; margin: 5px 0 3px; }
.cv-scale { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 9px; color: var(--dim); }
.cv-controls { display: flex; gap: 4px; margin: 9px 0 6px; flex-wrap: wrap; }
.cv-controls button {
  font-family: var(--mono); font-size: 10px; padding: 2px 7px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 3px; background: var(--bg); color: var(--dim);
}
.cv-controls button.on { border-color: var(--accent); color: var(--accent); background: var(--panel); }
.cv-worst { margin-top: 7px; border-top: 1px solid var(--border); padding-top: 6px; }
.cv-worst h4 {
  font-family: var(--mono); font-size: 9px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--dim); margin: 0 0 3px;
}
.cv-item {
  display: flex; gap: 7px; align-items: baseline; font-size: 11.5px;
  padding: 1px 0; cursor: pointer; color: var(--text);
}
.cv-item:hover { color: var(--accent); }
.cv-item .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cv-item .pc { font-family: var(--mono); font-size: 11px; font-variant-numeric: tabular-nums; }
.cv-item .n  { font-family: var(--mono); font-size: 9.5px; color: var(--dim); }
.cv-empty { font-family: var(--mono); font-size: 10.5px; color: var(--dim); padding: 6px 0; line-height: 1.5; }

/* ── fleet KPI strip (header) ────────────────────────────────────────────── */
/* Deliberately not a dashboard of gauges. Three numbers, each one a question
   somebody actually asks out loud, and each one clickable through to the detail
   behind it. A number nobody can act on is decoration. */
.kpi-strip { display: flex; gap: 14px; align-items: center; margin-left: 14px; flex-wrap: wrap; }
.kpi {
  display: flex; align-items: baseline; gap: 5px; cursor: default;
  font-family: var(--mono); font-size: 0.58rem; letter-spacing: 1px;
  text-transform: uppercase; color: var(--dim); white-space: nowrap;
}
.kpi.clickable { cursor: pointer; }
.kpi.clickable:hover .kpi-val { color: var(--accent); }
.kpi-val {
  font-size: 0.95rem; font-weight: 700; letter-spacing: 0;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.kpi-val.good { color: #008c44; }
.kpi-val.warn { color: #b07800; }
.kpi-val.bad  { color: #cc2200; }
body.dark-mode .kpi-val.good { color: #35d17e; }
body.dark-mode .kpi-val.warn { color: #ffc247; }
body.dark-mode .kpi-val.bad  { color: #ff6a4d; }
/* The incidents KPI when we could not ask. It carries an em dash, not a 0, and
   it must not be able to reach `good`: a green 0 INCIDENTS sitting beside an
   amber 19 SITES AFFECTED, in the same strip, is the contradiction this whole
   change exists to remove. The device KPIs next to it already vanish when
   unmeasured; this one greys instead of vanishing, because a gap reads as a
   layout glitch and the incident count is the number people come here for.
   NO body.dark-mode PARTNER, AND THAT IS NOT AN OMISSION. Its three neighbours
   above need one because they are literal hex. --dim has exactly one
   definition, on bare :root in dashboard.html, and by that page's deliberate
   design (see its "NO DARK PALETTE" note) there is no dark redefinition to
   shadow — so a dark twin here would be a second definition of a one-value
   token, which is the failure this file's own header warns about. */
.kpi-val.unknown { color: var(--dim); }
@media (max-width: 900px) { .kpi-strip { display: none; } }

/* ── timeline ────────────────────────────────────────────────────────────── */
.tl { padding: 4px 0; }
.tl-day {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--dim); padding: 8px 12px 3px; position: sticky; top: 0; background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.tl-row { display: flex; gap: 9px; padding: 6px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
.tl-time {
  font-family: var(--mono); font-size: 11px; color: var(--dim);
  font-variant-numeric: tabular-nums; flex-shrink: 0; width: 44px;
}
.tl-mark { width: 3px; border-radius: 2px; flex-shrink: 0; background: var(--sev-none); }
.tl-row[data-status="firing"] .tl-mark   { background: var(--sev-critical); }
.tl-row[data-status="resolved"] .tl-mark { background: var(--sev-ok); }
.tl-what { flex: 1; min-width: 0; }
.tl-what b { font-family: var(--cond, var(--sans)); font-weight: 600; font-size: 12.5px; color: var(--text); }
.tl-what .site { font-family: var(--mono); font-size: 10.5px; color: var(--accent); margin-left: 5px; }
.tl-what .sum { color: var(--dim); font-size: 11.5px; margin-top: 1px; }

/* ── wallboard ───────────────────────────────────────────────────────────── */
/* For the screen on the wall nobody is sitting at: no controls, no sidebar,
   numbers legible from across the room, and the current incidents cycling so a
   glance from the doorway tells you whether today is fine. Everything here is
   presentation — the data is exactly what the console already has. */
body.wallboard > header,
body.wallboard #sidebar,
body.wallboard #panel-tabs,
body.wallboard .map-overlay,
body.wallboard #inview-chip,
body.wallboard #console-dock,
body.wallboard #coverage-card,
body.wallboard .leaflet-control-container { display: none !important; }
/* The page is a CSS grid (sidebar | map). Hiding the sidebar with display:none
   removes it from the grid entirely, and auto-placement then drops the map into
   the FIRST column — which is still the narrow sidebar track, so the map
   collapsed to zero width and the wallboard showed an empty rectangle.
   Collapse the grid to a single full-bleed cell and place the map in it
   explicitly rather than relying on auto-placement. */
body.wallboard {
  grid-template-columns: 1fr !important;
  grid-template-rows: 1fr !important;
}
body.wallboard #map {
  grid-column: 1 !important;
  grid-row: 1 !important;
  width: auto !important;
}

#wallboard-bar {
  position: absolute; left: 0; right: 0; top: 0; z-index: 1400;
  display: none; align-items: center; gap: 34px;
  padding: 14px 26px; background: var(--panel);
  border-bottom: 2px solid var(--border); font-family: var(--sans);
}
body.wallboard #wallboard-bar { display: flex; }
.wb-brand {
  font-family: var(--cond, var(--sans)); font-weight: 700; font-size: 20px;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text);
}
.wb-kpi { display: flex; flex-direction: column; gap: 1px; }
.wb-kpi .v {
  font-family: var(--mono); font-size: 34px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--text);
}
.wb-kpi .v.good { color: #008c44; }
.wb-kpi .v.warn { color: #b07800; }
.wb-kpi .v.bad  { color: #cc2200; }
body.dark-mode .wb-kpi .v.good { color: #35d17e; }
body.dark-mode .wb-kpi .v.warn { color: #ffc247; }
body.dark-mode .wb-kpi .v.bad  { color: #ff6a4d; }
/* Same state, same reasoning, 34px instead of 0.95rem — and likewise no
   body.dark-mode partner, because --dim has exactly one definition. Note that
   buildWallboard() already seeds all four of these with a bare em dash and no
   colour class; wb-incidents was the only one that abandoned that convention
   the moment a fetch resolved. This is that convention given a name, so it can
   survive a fetch that answered nothing. */
.wb-kpi .v.unknown { color: var(--dim); }
.wb-kpi .k {
  font-family: var(--mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--dim);
}
.wb-spacer { flex: 1; }
.wb-clock { font-family: var(--mono); font-size: 26px; font-variant-numeric: tabular-nums; color: var(--text); }
.wb-exit {
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--dim); border: 1px solid var(--border); background: var(--bg);
  border-radius: 3px; padding: 4px 9px; cursor: pointer;
}

/* Ticker along the bottom: one incident at a time, big enough to read from the
   far side of the room. A scrolling marquee would be unreadable; a slow cycle
   with a hard cut is not. */
#wallboard-ticker {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1400;
  display: none; align-items: center; gap: 16px;
  padding: 12px 26px; background: var(--panel); border-top: 2px solid var(--border);
  font-family: var(--sans);
}
body.wallboard #wallboard-ticker { display: flex; }
#wallboard-ticker .sev {
  font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; padding: 4px 10px; border-radius: 3px; flex-shrink: 0;
  background: var(--sev-none); color: #fff;
}
#wallboard-ticker .sev.critical { background: var(--sev-critical); }
#wallboard-ticker .sev.warning  { background: var(--sev-warning); color: #2b2000; }
#wallboard-ticker .sev.info     { background: var(--sev-info); }
#wallboard-ticker .sev.ok       { background: var(--sev-ok); }
/* NO DATA — the pill that needed this most. This bar repaints every four
   seconds from an in-memory array, so a wallboard whose source died a week ago
   went on asserting a green OK fifteen times a minute at a room that would
   never investigate, beside a live and correct clock that made the whole strip
   look freshly sourced. `ok` is now reachable only from a poll that answered.
   Grey rather than red on purpose: we are not claiming an outage, we are
   declining to claim anything. */
#wallboard-ticker .sev.unknown  { background: var(--sev-none); }
/* And the strip itself, so the state carries from the doorway instead of from
   an 11px pill: the rule above the bar goes grey and a grey edge runs down the
   left. inset box-shadow rather than border-left because the ticker is
   position:absolute with left:0/right:0 — a real border would move the content
   box only while stale, so the text would visibly jump at the exact moment we
   lost contact, which reads as a rendering fault rather than as information. */
#wallboard-ticker.stale {
  border-top-color: var(--sev-none);
  box-shadow: inset 5px 0 0 var(--sev-none);
}
#wallboard-ticker .what { font-size: 19px; color: var(--text); flex: 1; overflow: hidden;
                          text-overflow: ellipsis; white-space: nowrap; }
#wallboard-ticker .what .site { font-family: var(--mono); font-size: 15px; color: var(--accent); margin-left: 10px; }
#wallboard-ticker .age { font-family: var(--mono); font-size: 15px; color: var(--dim); flex-shrink: 0; }
#wallboard-ticker .pos { font-family: var(--mono); font-size: 11px; color: var(--dim); flex-shrink: 0; }

/* Operator identity, edited in place in the dock footer. Replaces a
   window.prompt, which blocked the page and left the acknowledge button dead
   when it was dismissed or suppressed. */
.cd-foot .cd-who-input {
  font-family: var(--mono); font-size: 10px; width: 110px;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--accent); border-radius: 2px; padding: 1px 4px;
}

/* ── 4Trak service state: shared badge, used on the map and on /manage ───────
   One class set, two surfaces, so a boat that reads "out of service" on the map
   reads the same on the manage page. The glyph carries the meaning on its own:
   these survive greyscale printing and every form of colour-vision deficiency,
   which a hue swap does not.

     out of service  solid teal, filled dot glyph    -- a stated fact
     unknown         dashed slate, hollow dot glyph  -- the absence of one
     in service      nothing at all                  -- the baseline

   NOT a replacement for the health colour. A vessel's marker keeps its up/down/
   unknown colour and wears this as a separate mark, because "tied up" and
   "unreachable" are different questions and an operator needs both answers at
   once. Collapsing them is how a real outage hides behind a mute. */
.svc-badge {
  display: inline-flex; align-items: center; gap: .3em;
  padding: .1em .45em; border-radius: 999px;
  font-size: .78em; font-weight: 600; line-height: 1.5;
  white-space: nowrap; vertical-align: middle;
}
.svc-badge::before { font-size: .9em; line-height: 1; }
.svc-badge.is-out {
  color: var(--svc-out); background: var(--svc-out-tint);
  border: 1px solid var(--svc-out);
}
.svc-badge.is-out::before { content: "\25CF"; }        /* ● filled: a stated fact */
.svc-badge.is-unknown {
  color: var(--svc-unknown); background: transparent;
  border: 1px dashed var(--svc-unknown);
}
.svc-badge.is-unknown::before { content: "\25CB"; }    /* ○ hollow: no answer */
/* In service renders nothing; the rule exists so a caller can set the class
   unconditionally without special-casing the baseline. */
.svc-badge.is-in { display: none; }

/* Map marker overlay. The badge sits on the corner of the vessel marker and
   never covers the marker's own health colour. */
.svc-mark {
  position: absolute; right: -2px; bottom: -2px;
  width: 11px; height: 11px; border-radius: 50%;
  box-sizing: border-box; pointer-events: none;
}
.svc-mark.is-out     { background: var(--svc-out); border: 1.5px solid #fff; }
.svc-mark.is-unknown { background: transparent; border: 1.5px dashed var(--svc-unknown); }
.svc-mark.is-in      { display: none; }
body.dark-mode .svc-mark.is-out { border-color: #0b1a24; }

/* Feed-health line. Shown when the roster could not be read at all, so an
   operator is told WHY every boat says unknown instead of guessing. */
.svc-feed-note {
  display: flex; align-items: center; gap: .4em;
  font-size: .82em; color: var(--svc-unknown);
  padding: .3em .5em; border-left: 2px dashed var(--svc-unknown);
}
