* { box-sizing: border-box; }

/* =========================================================================
 * THEME — light / dark via :root[data-theme] attribute.
 *
 * Default (no attribute, or data-theme="dark") = dark mode.
 * data-theme="light" = light mode.
 *
 * The semantic variable names describe what the colour MEANS (background,
 * border, status-ok, accent-warm) rather than what it looks like. That way
 * a `border: 1px solid var(--border-subtle)` is correct in both modes; the
 * variable resolves to the right hex per theme.
 *
 * JS-emitted SVG colours (edge fills, node fills, heat ramps) are not
 * styled via CSS — they're written as attribute values by D3. Those read
 * the same variables via the cssVar() helper in meshscope.js so theme
 * switching also re-tints the live diagram.
 * ======================================================================= */
:root,
:root[data-theme="dark"] {
  /* Backgrounds — surfaces layered front-to-back */
  --bg-base:            #0b0e14;  /* page + graph canvas */
  --bg-card:            #11151c;  /* header / cards / settings panels */
  --bg-sidebar:         #0e1218;  /* right sidebar */
  --bg-overlay:         #161b24;  /* modals */
  --bg-input:           #1c2230;  /* buttons, inputs */
  --bg-elevated:        #2c3344;  /* hover states for input chrome */

  /* Borders */
  --border-subtle:      #1f2530;  /* default row / panel borders */
  --border-muted:       #3a4256;  /* divider lines */
  --border-strong:      #5a6378;  /* emphasised borders */

  /* Text — primary→muted gradient */
  --text-primary:       #e1e5ee;
  --text-secondary:     #d3d8e3;
  --text-tertiary:      #b1b8c6;
  --text-muted:         #7c8499;
  --text-disabled:      #6b7488;
  --text-faded:         #4a5366;
  --text-inverse:       #11151c;  /* on warm-accent backgrounds */

  /* Status palette — keep semantics, vary brightness per theme */
  --status-ok:          #2ecc71;
  --status-ok-soft:     #51cf66;
  --status-warn:        #f4a14a;
  --status-err:         #e74c3c;
  --status-info:        #5dade2;

  /* Accents — orange used for "selected", cool blue for idle edges */
  --accent-warm:        #ffb85c;  /* selected node ring, h1, kind chips */
  --accent-warm-soft:   #f4a14a;
  --accent-cool:        #2c5b9c;  /* idle edges, secondary borders */
  --accent-cool-soft:   #4dabf7;

  /* Edge/flow colours (also used by D3 attributes via cssVar()) */
  --edge-idle:          #2c5b9c;
  --edge-warm:          #f4a14a;
  --edge-error:         #e74c3c;
  --edge-discovered:    #6b7488;

  /* Node fills (D3-emitted; vary by group via --node-group-* in JS) */
  --node-stroke:        #0b0e14;
  --node-fill-source:   #5DCAA5;
  --node-fill-gateway:  #4dabf7;
  --node-fill-workflow: #9775fa;
  --node-fill-shared:   #74c0fc;
  --node-fill-ndhif:    #ffb85c;

  /* Glow / drop-shadow tints */
  --glow-selected:      rgba(255, 184, 92, 0.8);
  --glow-active:        rgba(46, 204, 113, 0.5);
  --glow-active-strong: rgba(46, 204, 113, 0.95);

  color-scheme: dark;
}

:root[data-theme="light"] {
  /* Backgrounds */
  --bg-base:            #fafbfc;
  --bg-card:            #ffffff;
  --bg-sidebar:         #f1f4f9;
  --bg-overlay:         #f7f9fc;
  --bg-input:           #ffffff;
  --bg-elevated:        #e8eef5;

  /* Borders */
  --border-subtle:      #dde2eb;
  --border-muted:       #c1c8d3;
  --border-strong:      #8a93a3;

  /* Text */
  --text-primary:       #1a1f29;
  --text-secondary:     #2c3344;
  --text-tertiary:      #4a5366;
  --text-muted:         #6b7488;
  --text-disabled:      #98a0b3;
  --text-faded:         #b1b8c6;
  --text-inverse:       #ffffff;

  /* Status — bolder shades read better on white than the dark variants */
  --status-ok:          #16a34a;
  --status-ok-soft:     #22c55e;
  --status-warn:        #d97706;
  --status-err:         #c0392b;
  --status-info:        #2563eb;

  /* Accents */
  --accent-warm:        #c2410c;  /* deeper amber for contrast on white */
  --accent-warm-soft:   #d97706;
  --accent-cool:        #2563eb;
  --accent-cool-soft:   #3b82f6;

  /* Edges */
  --edge-idle:          #94a3b8;
  --edge-warm:          #d97706;
  --edge-error:         #c0392b;
  --edge-discovered:    #94a3b8;

  /* Node fills */
  --node-stroke:        #ffffff;
  --node-fill-source:   #16a34a;
  --node-fill-gateway:  #2563eb;
  --node-fill-workflow: #7c3aed;
  --node-fill-shared:   #0891b2;
  --node-fill-ndhif:    #c2410c;

  /* Glows are softer on light backgrounds */
  --glow-selected:      rgba(194, 65, 12, 0.45);
  --glow-active:        rgba(22, 163, 74, 0.35);
  --glow-active-strong: rgba(22, 163, 74, 0.55);

  color-scheme: light;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg-base);
  color: var(--text-primary);
  overflow: hidden;
}

header {
  height: 60px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20px;
  border-bottom: 1px solid var(--border-subtle);
  background: var(--bg-card);
}

header .title h1 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.5px;
  display: inline-block;
}
header .title #project-subtitle {
  margin-left: 12px;
  color: var(--text-muted);
  font-size: 13px;
}

.status {
  display: flex;
  gap: 24px;
  font-size: 12px;
}
.status-item label {
  display: block;
  color: var(--text-muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-bottom: 2px;
}
.status-item span {
  color: var(--text-primary);
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.header-btn {
  background: var(--bg-input);
  color: var(--text-primary);
  border: 1px solid var(--accent-cool);
  border-radius: 3px;
  padding: 3px 10px;
  font-size: 12px;
  font-family: ui-monospace, SFMono-Regular, monospace;
  cursor: pointer;
  transition: background 120ms;
}
.header-btn:hover { background: var(--accent-cool); }
.header-btn.active { background: var(--status-warn); color: var(--bg-card); border-color: var(--status-warn); }

/* Cog button + settings popover — opened from the header. Holds
 * controls that used to live inline (Layout / Replay / Pulses /
 * Gateway / Workflow / Offline) so the top bar stays tidy. */
.cog-btn {
  font-size: 16px;
  padding: 1px 8px;
  line-height: 1.2;
}
.settings-popover {
  position: fixed;
  top: 56px;                    /* immediately under the 60 px header */
  right: 18px;
  z-index: 30;
  min-width: 280px;
  padding: 12px;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.35);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.settings-popover[hidden] { display: none; }
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  min-height: 26px;
}
.settings-row > label {
  color: var(--text-muted);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  font-weight: 600;
  white-space: nowrap;
}

/* Filter chip group — Gateway / Workflow filters in the header. */
.chip-group {
  display: inline-flex;
  gap: 2px;
  background: var(--bg-input);
  border: 1px solid var(--border-subtle);
  border-radius: 3px;
  padding: 1px;
}
.chip-group .chip {
  background: transparent;
  color: var(--text-muted);
  border: 0;
  border-radius: 2px;
  padding: 2px 8px;
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, monospace;
  cursor: pointer;
  transition: background 120ms, color 120ms;
}
.chip-group .chip:hover { color: var(--text-primary); background: var(--bg-elevated); }
.chip-group .chip.active {
  background: var(--status-warn);
  color: var(--text-inverse);
  font-weight: 600;
}

/* Filtered-out elements (hidden by Gateway/Workflow filters or the
 * Offline toggle). display:none rather than opacity:0 so they don't
 * catch hover events through the hitbox. */
.node.filtered-out,
.edge.filtered-out,
.edge-hitbox.filtered-out,
.group-box.filtered-out { display: none; }

/* Tab strip — sits between the header and main. Each tab represents one
 * of the views described in service-mesh-spec.md. Tabs share the topology
 * data and sidebar so switching views never loses selection state. */
#view-tabs {
  display: flex;
  gap: 2px;
  padding: 0 20px;
  background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border-subtle);
  height: 36px;
  align-items: stretch;
}
.view-tab-sep {
  align-self: center;
  color: var(--border-muted);
  opacity: 0.6;
  margin: 0 8px;
  pointer-events: none;
  user-select: none;
}
.view-tab {
  background: transparent;
  color: var(--text-muted);
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 0 14px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.4px;
  cursor: pointer;
  transition: color 120ms, border-color 120ms, background 120ms;
}
.view-tab:hover { color: #c6cbd6; background: var(--bg-card); }
.view-tab.active {
  color: var(--accent-warm);
  border-bottom-color: var(--accent-warm);
  background: var(--bg-card);
}

main {
  display: flex;
  height: calc(100vh - 96px);  /* 60 header + 36 tabs */
}

#view-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.view-panels {
  flex: 1;
  position: relative;
  min-height: 0;
}
.view-panel {
  position: absolute;
  inset: 0;
  display: none;
}
.view-panel.active { display: block; }
.view-panel > svg, .view-panel > .matrix-host, .view-panel > .waterfall-host, .view-panel > .vitals-host {
  width: 100%;
  height: 100%;
  display: block;
}

/* Toolbar — sits above every view, never below, never hidden (spec §3a).
 * One shared bar in the DOM; meshscope-views.js swaps its content when
 * the active tab changes. */
#view-toolbar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 8px 16px;
  border-bottom: 1px solid var(--color-border-tertiary);
  background: var(--color-background-primary);
  font-size: 12px;
  color: var(--color-text-secondary);
  min-height: 38px;
}
#view-toolbar:empty { display: none; }
#view-toolbar .legend-group {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
#view-toolbar .legend-item {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}
#view-toolbar .legend-dot {
  width: 10px; height: 10px; border-radius: 50%;
  display: inline-block; margin-right: 6px; vertical-align: -1px;
}
#view-toolbar .legend-bar {
  width: 22px; height: 3px; border-radius: 2px;
  display: inline-block; margin-right: 6px; vertical-align: 2px;
}
#view-toolbar .legend-swatch {
  width: 12px; height: 12px; border-radius: 2px;
  display: inline-block; margin-right: 6px; vertical-align: -2px;
}
#view-toolbar .legend-sep {
  opacity: 0.4;
  user-select: none;
}
#view-toolbar .legend-hint {
  font-style: italic;
  color: var(--color-text-tertiary);
}
/* Synthetic-data badge — surfaced on views whose data isn't measured
 * (currently scatter / vitalsres / heatmap, which use deterministically
 * generated per-pod numbers because compose doesn't have a "pod" concept).
 * Amber to read as a soft warning without screaming. Tooltip explains
 * exactly what's stubbed. */
#view-toolbar .legend-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  border-radius: 3px;
  background: var(--status-warn);
  color: var(--text-inverse);
  cursor: help;
}
#view-toolbar .legend-badge::before {
  content: "⚠";
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0;
}

#graph {
  flex: 1;
  background: var(--bg-base);
  cursor: grab;
}
#graph:active {
  cursor: grabbing;
}

aside#sidebar {
  width: 320px;
  border-left: 1px solid var(--border-subtle);
  padding: 16px 20px;
  overflow-y: auto;
  font-size: 13px;
  background: var(--bg-sidebar);
}

aside#sidebar h3 {
  margin: 16px 0 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  color: var(--text-muted);
}
aside#sidebar h3:first-of-type { margin-top: 0; }

#selected-content em { color: var(--text-disabled); }

#selected-content .selected-name {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 4px;
}
#selected-content .selected-kind {
  color: var(--text-tertiary);
  font-size: 12px;
  font-weight: 500;
  margin-bottom: 2px;
}
#selected-content .selected-group {
  color: var(--text-muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-bottom: 12px;
}
#selected-content table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
#selected-content table td {
  padding: 4px 0;
  border-bottom: 1px solid var(--border-subtle);
}
#selected-content table td:first-child {
  color: var(--text-muted);
  width: 50%;
}
#selected-content table td:last-child {
  text-align: right;
  font-family: ui-monospace, SFMono-Regular, monospace;
}
#selected-content tr.metric-warn td:last-child { color: var(--status-warn); font-weight: 600; }
#selected-content small { color: var(--text-disabled); font-size: 10px; }

.legend {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: 12px;
}
.legend li {
  padding: 4px 0;
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-tertiary);
}
.dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  display: inline-block;
}
.dot-green { background: var(--status-ok); box-shadow: 0 0 6px var(--status-ok); }
.dot-red   { background: var(--status-err); box-shadow: 0 0 6px var(--status-err); }
.dot-grey  { background: var(--text-faded); }
.line {
  width: 24px;
  height: 3px;
  border-radius: 2px;
  display: inline-block;
}
.line-cool  { background: var(--accent-cool); }
.line-warm  { background: var(--status-warn); }
.line-error { background: var(--status-err); }
.line-discovered {
  background: linear-gradient(to right, var(--text-disabled) 50%, transparent 50%);
  background-size: 6px 100%;
}

/* Graph internals */
.node circle {
  stroke: var(--bg-base);
  stroke-width: 2;
  cursor: pointer;
  transition: r 400ms ease, fill 400ms ease;
}
.node text {
  fill: var(--text-secondary);
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--bg-base);
  stroke-width: 4;
}
.node text .node-kind {
  font-size: 12px;
  font-weight: 600;
  fill: var(--text-primary);
}
.node text .node-name {
  font-size: 11px;
  font-weight: 500;
  fill: var(--text-tertiary);
}
.node.selected circle {
  stroke: var(--accent-warm);
  stroke-width: 3;
  filter: drop-shadow(0 0 10px rgba(255, 184, 92, 0.8));
}

/* "Active" = the node is currently passing traffic. Pulses a soft halo
 * coloured to the node's fill so a reviewer can immediately see which
 * services in the topology are alive right now. The halo fades when the
 * traffic stops flowing (active class is removed by the JS poll loop). */
.node.active circle {
  animation: ndhx-pulse 1.6s ease-in-out infinite;
}
@keyframes ndhx-pulse {
  0%, 100% {
    filter: drop-shadow(0 0 4px rgba(46, 204, 113, 0.5));
  }
  50% {
    filter: drop-shadow(0 0 18px rgba(46, 204, 113, 0.95));
  }
}
/* Selected + active: keep the orange ring + the green pulse together. */
.node.selected.active circle {
  animation: ndhx-pulse-selected 1.6s ease-in-out infinite;
}
@keyframes ndhx-pulse-selected {
  0%, 100% {
    filter: drop-shadow(0 0 6px rgba(255, 184, 92, 0.7))
            drop-shadow(0 0 4px rgba(46, 204, 113, 0.5));
  }
  50% {
    filter: drop-shadow(0 0 10px rgba(255, 184, 92, 0.9))
            drop-shadow(0 0 18px rgba(46, 204, 113, 0.95));
  }
}

.edge {
  fill: none;
  stroke-linecap: round;
  /* Hover/click events are caught by .edge-hitbox (drawn underneath),
   * not the visible line — so the user gets a generous corridor to
   * grab without the visible stroke needing to be wide. */
  pointer-events: none;
  /* Idle edges fade into the background so active (warmly tinted)
   * edges visually dominate. Hover/select still bumps opacity back
   * to 1 via .edge.edge-hover / .edge.selected below. */
  opacity: 0.35;
  transition: opacity 200ms ease;
}
/* Active edges (rate > 0 → coloured by source) — full opacity. The JS
 * sets stroke from edgeColor(); if the colour is anything other than
 * the idle cool var, the edge is in the "active" visual band. We can't
 * detect that in CSS, so the JS adds a `data-active` attribute when
 * appropriate (see updateEdges in meshscope.js). Fallback: any edge
 * with non-cool stroke colour gets emphasised at the class level. */
.edge[data-active="true"] { opacity: 1; }
/* Invisible wide-stroked path traced along each edge, used solely to
 * catch tooltip + click events. Stroke-width sets the corridor size. */
.edge-hitbox {
  fill: none;
  stroke: transparent;
  stroke-width: 14;
  pointer-events: stroke;
  cursor: pointer;
}
/* Discovered (Tempo service-graph) edges: dashed line + lower opacity so
 * they're visually distinct from the YAML-declared "intended" topology. */
.edge.discovered {
  stroke-dasharray: 5 4;
}

/* Discovered nodes: smaller stroke, slightly translucent fill so they
 * read as "auto-detected" vs. the heavy declared nodes. */
.node.discovered circle {
  stroke: var(--border-strong);
  stroke-dasharray: 2 2;
  fill-opacity: 0.7;
}
.node.discovered text {
  fill: var(--text-disabled);
  font-style: italic;
}

.pulse {
  fill: #fff;
  filter: drop-shadow(0 0 4px currentColor);
  pointer-events: none;
}

/* Trace replay pulses — bigger, glowy, clickable. Each represents one
 * real Tempo trace. Click → opens the trace in Grafana Explore (Tempo).
 */
.trace-pulse {
  filter: drop-shadow(0 0 8px currentColor);
}
.trace-pulse:hover {
  r: 7;
  filter: drop-shadow(0 0 12px currentColor);
}

/* Group boxes — rounded borders that wrap the nodes belonging to each
 * `group` (or `groups: [outer, inner]`) value. Outer boxes are solid
 * and slightly heavier; inner sub-boxes are dashed and lighter so the
 * nesting reads at a glance. Both sit underneath edges and nodes.
 *
 * Interactivity: each box has TWO rects — a thick invisible hitbox
 * underneath, and a thin visible border on top. The hitbox catches all
 * drag/hover events, giving the user a generous corridor to grab even
 * though the visible line is slim. The hollow interior (fill:none) lets
 * clicks inside fall through to the nodes/edges. */
.group-box-hitbox {
  fill: none;
  stroke: transparent;
  stroke-width: 18;
  pointer-events: stroke;
  cursor: grab;
}
.group-box-border {
  fill: none;
  pointer-events: none;
}
.group-box.depth-0 .group-box-border {
  stroke: var(--border-muted);
  stroke-width: 1.6;
}
.group-box.depth-1 .group-box-border {
  stroke: var(--bg-elevated);
  stroke-width: 1.4;
  stroke-dasharray: 4 3;
}
/* Hover on the hitbox lights up the visible border. `~` is the general
 * sibling combinator — works because the border is rendered after the
 * hitbox in document order. */
.group-box.depth-0 .group-box-hitbox:hover ~ .group-box-border {
  stroke: var(--border-strong);
}
.group-box.depth-1 .group-box-hitbox:hover ~ .group-box-border {
  stroke: var(--text-faded);
}
.group-box-label {
  font-size: 10px;
  fill: var(--text-disabled);
  text-transform: uppercase;
  letter-spacing: 1.2px;
  font-weight: 600;
  cursor: grab;
}
.group-box.depth-0 .group-box-label {
  fill: var(--text-tertiary);
  font-size: 11px;
  letter-spacing: 1.6px;
}
.group-box.dragging .group-box-hitbox,
.group-box.dragging .group-box-label {
  cursor: grabbing;
}

/* Hover tooltip on edges — shows from→to + current rate + errors. */
.edge-tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--bg-card);
  border: 1px solid var(--accent-cool);
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 12px;
  color: var(--text-primary);
  font-family: ui-monospace, SFMono-Regular, monospace;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
  max-width: 260px;
  z-index: 10;
}
.edge-tooltip strong {
  color: var(--accent-warm);
  font-weight: 600;
}
.edge-tooltip .err {
  color: var(--status-err);
}

/* Hover state mirrored from .edge-hitbox via JS — same visual treatment
 * as the old `.edge:hover`, just driven by class instead of CSS pseudo. */
.edge.edge-hover {
  opacity: 1 !important;
  stroke-width: 5 !important;
}
.edge.selected {
  opacity: 1 !important;
  stroke-width: 6 !important;
  filter: drop-shadow(0 0 6px rgba(255, 184, 92, 0.7));
}

/* PromQL block in the edge sidebar */
.query-block {
  background: var(--bg-base);
  border: 1px solid var(--border-subtle);
  border-radius: 3px;
  padding: 6px 8px;
  margin: 4px 0;
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 11px;
  color: var(--text-tertiary);
  white-space: pre-wrap;
  word-break: break-all;
}

/* Tempo deep-link button */
.action-link {
  display: inline-block;
  margin-top: 6px;
  padding: 6px 10px;
  background: var(--accent-cool);
  color: #fff;
  text-decoration: none;
  border-radius: 3px;
  font-size: 12px;
  font-weight: 500;
  transition: background 120ms;
}
.action-link:hover {
  background: var(--accent-cool-soft);
}

/* =====================================================================
 *  Spec views (Layered, Sankey, Matrix, Waterfall, Particles, Vitals,
 *  Pipes, Heatmap, Scatter, Vitals+Res) — see service-mesh-spec.md
 *  §3 + §3a. (Radial / View 8 was dropped during spec design.)
 *
 *  Token discipline: every colour used by a spec view comes from a
 *  custom property declared in this block. Light-mode override below
 *  flips the surface/text/fill tokens but keeps semantic ring/flow
 *  colours intact (status is status regardless of theme). Only the
 *  spec views consume these tokens — the legacy graph keeps its own
 *  hardcoded dark palette so toggling theme doesn't affect it.
 *
 *  Convention from the spec:
 *    --status-<state>          ring / accent (border around node)
 *    --status-<state>-fill     interior of node (light + dark variant)
 *    --flow-<grade>            edge / ribbon body (ok / warn / bad)
 *    --flow-<grade>-bright     brighter variant for particles + span fills
 *    --tps-<bucket>            traffic-matrix TPS ramp (low → max)
 *    --color-background-*      surfaces (primary / secondary / tertiary)
 *    --color-text-*            text (primary / secondary / tertiary)
 *    --color-border-*          dividers / scaffolding lines
 * ===================================================================== */
:root {
  /* Surfaces — dark mode (default) */
  --color-background-primary:   #0E0E0D;
  --color-background-secondary: #1B1B1B;
  --color-background-tertiary:  #2C2C2A;

  /* Text */
  --color-text-primary:   #F0EFEC;
  --color-text-secondary: #B4B2A9;
  --color-text-tertiary:  #888780;

  /* Borders */
  --color-border-primary:   #555452;
  --color-border-secondary: #3A3937;
  --color-border-tertiary:  #2C2C2A;

  /* Semantic — health status (rings, accents) */
  --status-healthy:   #3B6D11;
  --status-degraded:  #BA7517;
  --status-unhealthy: #A32D2D;
  --status-unknown:   #888780;

  /* Semantic — health fill (node interiors, dark mode) */
  --status-healthy-fill:   #173404;
  --status-degraded-fill:  #412402;
  --status-unhealthy-fill: #501313;
  --status-unknown-fill:   #2C2C2A;

  /* Semantic — traffic / flow (edges, particles, ribbons) */
  --flow-ok:   #5DCAA5;
  --flow-warn: #BA7517;
  --flow-bad:  #A32D2D;

  /* Brighter variants for particles, span fills, animated elements.
   * Spec §3a deliberately keeps --flow-ok-bright = --flow-ok — OK
   * doesn't need a brighter accent variant. The asymmetry is intended:
   * only warn/bad get a louder "look at me" colour. */
  --flow-ok-bright:   #5DCAA5;
  --flow-warn-bright: #EF9F27;
  --flow-bad-bright:  #E24B4A;

  /* Matrix TPS ramp (View 3) */
  --tps-0:    rgba(124, 132, 153, 0.18);
  --tps-low:  #173404;
  --tps-med:  #27500A;
  --tps-high: #639922;
  --tps-max:  #97C459;

  /* Resource saturation ramp (Views 9, 10, 11). Thresholds:
   *   < 60%   → ok        — comfortable
   *   60–75%  → elevated  — busy but fine
   *   75–90%  → warn      — needs scaling
   *   > 90%   → crit      — danger zone
   * Brighter variants drive cell bottom-bars and pod dots so the
   * cell backgrounds stay legible while the "worst pod" indicator
   * pops. Spec §3a. */
  --sat-ok:       #27500A;
  --sat-elevated: #639922;
  --sat-warn:     #BA7517;
  --sat-crit:     #A32D2D;
  --sat-ok-bright:       #5DCAA5;
  --sat-elevated-bright: #97C459;
  --sat-warn-bright:     #EF9F27;
  --sat-crit-bright:     #E24B4A;

  /* Highlight tint for selected / hover indicators that aren't status */
  --highlight-accent: var(--accent-warm);

  /* Backwards-compatibility aliases — the legacy view's CSS still
   * references these names, and removing them would visually break it.
   * Spec views never reference these; they go through the tokens above. */
  --spec-bg: var(--color-background-primary);
  --spec-fg: var(--color-text-primary);
  --spec-fg-secondary: var(--color-text-secondary);
  --spec-fg-tertiary: var(--color-text-tertiary);
}

@media (prefers-color-scheme: light) {
  :root {
    /* Surfaces flip to off-white. Status / flow accents stay saturated
     * so semantic meaning survives — only the things that should
     * literally be "the page" flip. */
    --color-background-primary:   #FAFAF7;
    --color-background-secondary: #F1EFE8;
    --color-background-tertiary:  #E8E5DC;
    --color-text-primary:   #1F1E1B;
    --color-text-secondary: #555452;
    --color-text-tertiary:  #888780;
    --color-border-primary:   #888780;
    --color-border-secondary: #B4B2A9;
    --color-border-tertiary:  #E8E5DC;
    /* Node fills swap to pastel tints so the saturated ring still pops. */
    --status-healthy-fill:   #EAF3DE;
    --status-degraded-fill:  #FAEEDA;
    --status-unhealthy-fill: #FCEBEB;
    --status-unknown-fill:   #F1EFE8;
    /* Matrix idle cell becomes a tinted neutral so light mode still has
     * visible cell separation against the off-white background. */
    --tps-0: rgba(85, 84, 82, 0.10);
  }
}

/* Per-view scratch SVG. Spec §3a `.view-svg` — shared base styling so
 * every spec graph has the same canvas surface, border-radius, and
 * font stack. */
.spec-graph {
  background: var(--color-background-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  border-radius: 6px;
}

/* Group / region boundary — dashed thin rectangles around each layer's
 * nodes. Spec §3a calls these out as scaffolding: visible but recessive,
 * so the eye registers them only when it chooses to. */
.group-rect {
  fill: none;
  stroke: var(--color-border-tertiary);
  stroke-width: 0.5;
  stroke-dasharray: 3 3;
  rx: 6;
}
.group-label {
  font-size: 10px;
  font-weight: 500;
  fill: var(--color-text-tertiary);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Status colors as classes — node + matrix axis dots both use these. */
.status-healthy   .ring { stroke: var(--status-healthy); }
.status-healthy   .fill { fill: var(--status-healthy-fill); }
.status-degraded  .ring { stroke: var(--status-degraded); }
.status-degraded  .fill { fill: var(--status-degraded-fill); }
.status-unhealthy .ring { stroke: var(--status-unhealthy); }
.status-unhealthy .fill { fill: var(--status-unhealthy-fill); }
.status-unknown   .ring { stroke: var(--status-unknown); }
.status-unknown   .fill { fill: var(--status-unknown-fill); }

/* Layered / Particles / Pipes node shapes — shared `.spec-node` styling. */
.spec-node .ring {
  stroke-width: 2.5;
  fill: transparent;
}
.spec-node .fill { /* the inner disk */ }
.spec-node text.title {
  font-size: 11.5px;
  font-weight: 500;
  fill: var(--color-text-primary);
  text-anchor: middle;
  pointer-events: none;
}
.spec-node text.sub {
  font-size: 9.5px;
  font-weight: 400;
  fill: var(--color-text-tertiary);
  text-anchor: middle;
  pointer-events: none;
}
.spec-node circle.hit { fill: transparent; cursor: pointer; }

/* (Column / ring labels formerly used .spec-axis-label; unified onto
 * .group-label which has the same definition per spec §3a.) */

/* Spec edges — color is set per-edge by JS based on err. Thickness too. */
.spec-edge {
  fill: none;
  stroke-linecap: round;
  pointer-events: stroke;
  cursor: pointer;
}
.spec-edge.idle {
  stroke-dasharray: 2 4;
  opacity: 0.18;
}
@media (prefers-reduced-motion: no-preference) {
  .spec-edge.flowing {
    stroke-dasharray: 6 6;
    animation: spec-flow var(--flow-dur, 1.8s) linear infinite;
  }
}
@keyframes spec-flow {
  to { stroke-dashoffset: -24; }
}

/* High-traffic ring pulse on healthy nodes (tps > 100). Cheap CSS-only. */
@media (prefers-reduced-motion: no-preference) {
  .spec-node.high-traffic .pulse-ring {
    animation: spec-node-pulse 1.8s ease-out infinite;
    transform-origin: center;
    transform-box: fill-box;
  }
}
@keyframes spec-node-pulse {
  0%   { r: 14; opacity: 0.4; }
  100% { r: 22; opacity: 0; }
}

/* Hover fade — when a node has focus, all non-incident edges and non-
 * neighbour nodes dim. Class is toggled on the parent <svg> while the
 * relevant child carries `.is-focused` / `.is-incident`. */
.spec-graph.has-focus .spec-node           { opacity: 0.18; transition: opacity 180ms; }
.spec-graph.has-focus .spec-node.is-focused,
.spec-graph.has-focus .spec-node.is-incident { opacity: 1; }
.spec-graph.has-focus .spec-edge           { opacity: 0.08; transition: opacity 180ms; }
.spec-graph.has-focus .spec-edge.is-incident { opacity: 0.9; }

/* Tooltip — `.tip-card` per spec §3a (used by every spec view). */
.tip-card {
  position: absolute;
  pointer-events: none;
  background: var(--color-background-primary);
  border: 0.5px solid var(--color-border-secondary);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 12px;
  color: var(--color-text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
  min-width: 160px;
  max-width: 220px;
  opacity: 0;
  transition: opacity 0.12s;
  z-index: 10;
}
.tip-card .tip-title    { font-weight: 500; margin-bottom: 4px; color: var(--color-text-primary); }
.tip-card .tip-subtitle { font-size: 11px; color: var(--color-text-tertiary); margin-bottom: 6px; }
.tip-card .tip-row      { display: flex; justify-content: space-between; gap: 12px; margin-top: 2px; }
.tip-card .tip-row .k   { color: var(--color-text-secondary); }
.tip-card .tip-row .v   { font-variant-numeric: tabular-nums; font-weight: 500; }
.tip-card .tip-status-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 6px; vertical-align: 1px;
}
.tip-card .tip-status-dot.status-healthy   { background: var(--status-healthy); }
.tip-card .tip-status-dot.status-degraded  { background: var(--status-degraded); }
.tip-card .tip-status-dot.status-unhealthy { background: var(--status-unhealthy); }
.tip-card .tip-status-dot.status-unknown   { background: var(--status-unknown); }

/* =====================================================================
 *  Traffic matrix (View 4)
 * ===================================================================== */
.matrix-host {
  overflow: auto;
  padding: 12px 16px 16px;
  background: var(--color-background-primary);
}
.matrix-grid {
  display: grid;
  gap: 1px;
  background: var(--border-subtle);
  border: 1px solid var(--border-subtle);
  width: max-content;
}
.matrix-corner {
  background: var(--color-background-primary);
  padding: 6px 8px;
  font-size: 10px;
  color: var(--color-text-tertiary);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  position: sticky;
  top: 0; left: 0;
  z-index: 2;
}
.matrix-col-head, .matrix-row-head {
  background: var(--bg-sidebar);
  padding: 6px 8px;
  font-size: 11px;
  color: var(--color-text-primary);
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.matrix-col-head {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  justify-content: flex-end;
  position: sticky;
  top: 0;
  z-index: 1;
  min-height: 120px;
}
.matrix-row-head {
  position: sticky;
  left: 0;
  z-index: 1;
  justify-content: flex-start;
}
.matrix-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-block;
}
.matrix-cell {
  width: 28px;
  height: 28px;
  background: var(--tps-0);
  position: relative;
  cursor: pointer;
}
.matrix-cell.ramp-0 { background: var(--tps-0); }
.matrix-cell.ramp-1 { background: var(--tps-low); }
.matrix-cell.ramp-2 { background: var(--tps-med); }
.matrix-cell.ramp-3 { background: var(--tps-high); }
.matrix-cell.ramp-4 { background: var(--tps-max); }
.matrix-cell.err-warn { box-shadow: inset 0 0 0 1px var(--flow-warn); }
.matrix-cell.err-bad  { box-shadow: inset 0 0 0 1.5px var(--flow-bad); }
.matrix-cell.diag { background: repeating-linear-gradient(45deg, rgba(124,132,153,0.12) 0 4px, transparent 4px 8px); cursor: default; }

.matrix-host.dimming .matrix-cell:not(.is-active) { opacity: 0.25; }
.matrix-host.dimming .matrix-col-head:not(.is-active),
.matrix-host.dimming .matrix-row-head:not(.is-active) { opacity: 0.4; }
.matrix-col-head.is-active, .matrix-row-head.is-active { color: var(--accent-warm); }

/* Legend swatches for matrix */
.legend .cell {
  width: 16px;
  height: 16px;
  border-radius: 2px;
  display: inline-block;
}
.legend .cell.cell-ramp-0 { background: var(--tps-0); }
.legend .cell.cell-ramp-1 { background: var(--tps-low); }
.legend .cell.cell-ramp-2 { background: var(--tps-med); }
.legend .cell.cell-ramp-3 { background: var(--tps-high); }
.legend .cell.cell-ramp-4 { background: var(--tps-max); }
.legend .cell.cell-border-warn { background: transparent; box-shadow: inset 0 0 0 1px var(--flow-warn); }
.legend .cell.cell-border-err  { background: transparent; box-shadow: inset 0 0 0 1.5px var(--flow-bad); }

/* Legend dot variants for the spec status palette */
.legend .dot.dot-status-healthy   { background: var(--status-healthy); }
.legend .dot.dot-status-degraded  { background: var(--status-degraded); }
.legend .dot.dot-status-unhealthy { background: var(--status-unhealthy); }
.legend .dot.dot-status-unknown   { background: var(--status-unknown); }
.legend .dot.dot-span-ok    { background: var(--flow-ok-bright);   box-shadow: inset 0 0 0 1.5px var(--flow-ok); }
.legend .dot.dot-span-slow  { background: var(--flow-warn-bright); box-shadow: inset 0 0 0 1.5px var(--flow-warn); }
.legend .dot.dot-span-error { background: var(--flow-bad-bright);  box-shadow: inset 0 0 0 1.5px var(--flow-bad); }
.legend .line.line-flow-ok   { background: var(--flow-ok); }
.legend .line.line-flow-warn { background: var(--flow-warn); }
.legend .line.line-flow-bad  { background: var(--flow-bad); }

/* =====================================================================
 *  Trace waterfall (View 5)
 *
 *  Layout is a two-pane shell: left = scrollable list of recent traces
 *  with a filter input at the top; right = waterfall for the selected
 *  trace. The list updates incrementally (diff-based) so new traces
 *  appear at the top without disturbing your selection or scroll
 *  position. Selection survives across polls.
 * ===================================================================== */
.waterfall-host {
  height: 100%;
  background: var(--color-background-primary);
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 12px;
  color: var(--color-text-primary);
  overflow: hidden;  /* the children scroll, not the host */
}
.waterfall-empty { color: var(--color-text-tertiary); padding: 24px; text-align: center; line-height: 1.6; }

.wf-shell {
  display: grid;
  grid-template-columns: 320px 1fr;
  height: 100%;
}
.wf-left {
  border-right: 1px solid var(--border-subtle);
  display: flex;
  flex-direction: column;
  min-height: 0;  /* allow inner ul to scroll */
}
.wf-filter-wrap {
  padding: 10px 10px 8px;
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  gap: 6px;
  align-items: center;
}
.wf-filter {
  flex: 1;
  background: var(--bg-card);
  color: var(--color-text-primary);
  border: 1px solid var(--bg-elevated);
  border-radius: 3px;
  padding: 6px 8px;
  font-family: inherit;
  font-size: 12px;
}
.wf-filter::placeholder { color: var(--color-text-tertiary); }
.wf-filter:focus { outline: none; border-color: var(--accent-cool-soft); }
.wf-list-meta {
  font-size: 10px;
  color: var(--color-text-tertiary);
  padding: 6px 12px;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.wf-pause-btn {
  background: transparent;
  border: 1px solid var(--bg-elevated);
  color: var(--color-text-tertiary);
  font-family: inherit;
  font-size: 10px;
  padding: 1px 6px;
  border-radius: 2px;
  cursor: pointer;
}
.wf-pause-btn.active { color: var(--accent-warm); border-color: var(--accent-warm); }
.wf-list {
  list-style: none;
  padding: 0;
  margin: 0;
  overflow-y: auto;
  flex: 1;
  min-height: 0;
}
.wf-item {
  padding: 8px 12px 8px 10px;
  border-bottom: 1px solid var(--bg-overlay);
  cursor: pointer;
  transition: background 120ms, border-left-color 120ms;
  border-left: 2px solid transparent;
}
.wf-item:hover { background: var(--bg-card); }
.wf-item.selected {
  background: var(--bg-input);
  border-left-color: var(--accent-warm);
}
.wf-item-hdr {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--color-text-primary);
}
.wf-item-svc {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wf-item-dur {
  color: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.wf-item.is-slow .wf-item-dur { color: var(--flow-warn-bright); }
.wf-item.is-error .wf-item-dur { color: var(--flow-bad-bright); }
.wf-item-meta {
  font-size: 10.5px;
  color: var(--color-text-tertiary);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wf-item-meta .traceid {
  color: var(--color-text-tertiary);
  opacity: 0.65;
  margin-left: 6px;
}
.wf-item.is-new {
  animation: wf-flash 700ms ease-out;
}
@keyframes wf-flash {
  0%   { background: rgba(77, 171, 247, 0.20); }
  100% { background: transparent; }
}

.wf-right {
  overflow: auto;
  padding: 14px 18px 18px;
  min-width: 0;
}
.wf-trace-hdr {
  margin-bottom: 14px;
  font-size: 12px;
  color: var(--color-text-secondary);
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  align-items: baseline;
}
.wf-trace-hdr strong { color: var(--accent-warm); font-weight: 500; }
.wf-trace-hdr .wf-trace-id {
  color: var(--color-text-tertiary);
  font-size: 11px;
}
.wf-trace-link {
  margin-left: auto;
  color: var(--accent-cool-soft);
  font-size: 11px;
  text-decoration: none;
}
.wf-trace-link:hover { text-decoration: underline; }

.waterfall-grid {
  display: grid;
  grid-template-columns: 340px 1fr;
  gap: 4px 12px;
  align-items: center;
}
.waterfall-label {
  color: var(--color-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.waterfall-label .svc { color: var(--color-text-primary); font-weight: 500; }
.waterfall-label .op  { color: var(--color-text-tertiary); margin-left: 6px; }
.waterfall-track {
  position: relative;
  height: 18px;
  background: rgba(124,132,153,0.08);
  border-radius: 2px;
}
.waterfall-bar {
  position: absolute;
  top: 1px; bottom: 1px;
  border-radius: 2px;
  border: 1px solid;
  cursor: pointer;
}
.waterfall-bar.status-ok    { background: var(--flow-ok-bright);   border-color: var(--flow-ok); }
.waterfall-bar.status-slow  { background: var(--flow-warn-bright); border-color: var(--flow-warn); }
.waterfall-bar.status-error { background: var(--flow-bad-bright);  border-color: var(--flow-bad); }
.waterfall-dur {
  position: absolute;
  top: 0;
  font-size: 11px;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  line-height: 18px;
}

/* Hide legacy-only header buttons unless the legacy view is active. */
body:not([data-view="legacy"]) .legacy-only { display: none; }

/* =====================================================================
 *  Sankey ribbons (View 2)
 *
 *  Same columns as the layered graph, but each node is a vertical bar
 *  whose height encodes its total throughput, and each edge is a closed
 *  curved ribbon whose width on each side equals the edge's TPS. The
 *  effect: at a glance the user sees where volume concentrates rather
 *  than the directed-edge view of who-calls-whom.
 * ===================================================================== */
.sankey-bar {
  cursor: pointer;
}
.sankey-bar rect.fill { stroke: none; }
.sankey-bar rect.ring {
  fill: transparent;
  stroke-width: 2;
}
/* text-anchor is decided per-node in JS (source labels go LEFT of the
 * bar, sink labels go RIGHT, middle labels go RIGHT-with-halo). The CSS
 * deliberately does NOT set text-anchor so the SVG attribute wins.
 * paint-order: stroke + a thick bg-coloured stroke creates a halo so
 * middle-column labels stay legible where they cross ribbons. */
.sankey-bar text.title {
  font-size: 11.5px;
  font-weight: 500;
  fill: var(--color-text-primary);
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--color-background-primary);
  stroke-width: 3;
}
.sankey-bar text.sub {
  font-size: 9.5px;
  fill: var(--color-text-tertiary);
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--color-background-primary);
  stroke-width: 3;
}
.sankey-bar.status-healthy   rect.ring { stroke: var(--status-healthy); }
.sankey-bar.status-healthy   rect.fill { fill: var(--status-healthy-fill); }
.sankey-bar.status-degraded  rect.ring { stroke: var(--status-degraded); }
.sankey-bar.status-degraded  rect.fill { fill: var(--status-degraded-fill); }
.sankey-bar.status-unhealthy rect.ring { stroke: var(--status-unhealthy); }
.sankey-bar.status-unhealthy rect.fill { fill: var(--status-unhealthy-fill); }
.sankey-bar.status-unknown   rect.ring { stroke: var(--status-unknown); }
.sankey-bar.status-unknown   rect.fill { fill: var(--status-unknown-fill); }

.sankey-ribbon {
  stroke: none;
  pointer-events: fill;
  cursor: pointer;
  transition: opacity 180ms;
}
.sankey-ribbon.flow-ok    { fill: var(--flow-ok);   fill-opacity: 0.55; }
.sankey-ribbon.flow-warn  { fill: var(--flow-warn); fill-opacity: 0.60; }
.sankey-ribbon.flow-bad   { fill: var(--flow-bad);  fill-opacity: 0.65; }
.sankey-ribbon.idle       { fill: var(--color-text-tertiary); fill-opacity: 0.10; }

.spec-graph.has-focus .sankey-bar           { opacity: 0.25; transition: opacity 180ms; }
.spec-graph.has-focus .sankey-bar.is-focused,
.spec-graph.has-focus .sankey-bar.is-incident { opacity: 1; }
.spec-graph.has-focus .sankey-ribbon        { fill-opacity: 0.06; }
.spec-graph.has-focus .sankey-ribbon.is-incident { fill-opacity: 0.85; }

/* =====================================================================
 *  View 5 — Particle flow
 *
 *  Same five-column topology as View 1, but the edges step back to act
 *  as faint guide lines while individual particles carry the visual
 *  weight. Spawn cadence + animation are driven by JS (single rAF
 *  loop, capped at 150 active particles) so styles here are intentionally
 *  minimal — they describe the rest pose, not the motion.
 * ===================================================================== */
.particle-guide {
  fill: none;
  stroke: var(--color-text-tertiary);
  stroke-width: 0.6;
  opacity: 0.4;
  pointer-events: stroke;
  cursor: pointer;
}
.particle-guide.hot {
  stroke-width: 1.2;
  opacity: 0.55;
}
.particle {
  pointer-events: none;
  filter: drop-shadow(0 0 3px currentColor);
}
.particle.ok      { fill: var(--flow-ok-bright);  color: var(--flow-ok-bright); }
.particle.failed  { fill: var(--flow-bad-bright); color: var(--flow-bad-bright); }
/* Reduced-motion fallback: when the user opts out, the particle view
 * degrades to the same animated dashed flow used in View 1 so it stays
 * informative without the per-frame motion budget. JS detects this and
 * falls back; the CSS just disables the rAF-driven .particle elements
 * if they ever leak through. */
@media (prefers-reduced-motion: reduce) {
  .particle { display: none; }
}

/* =====================================================================
 *  View 6 — Heartbeat vitals panel
 *
 *  A CSS grid of compact monitor cards, sorted by severity (unhealthy
 *  first). Each card is a self-contained component: header (name + sub),
 *  pulsing dot top-right, three KPI cells, and a two-line ECG sparkline.
 *  Updates are driven by JS shifting samples left every 800 ms — the
 *  SVG path d-attribute is rewritten in place; no transitions needed.
 * ===================================================================== */
.vitals-host {
  height: 100%;
  overflow: auto;
  padding: 14px 18px 18px;
  background: var(--color-background-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.vitals-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px;
}
.vitals-card {
  background: var(--color-background-secondary);
  border: 1px solid var(--color-border-tertiary);
  border-left-width: 3px;
  border-radius: 4px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 120px;
}
.vitals-card.status-healthy   { border-left-color: var(--status-healthy); }
.vitals-card.status-degraded  { border-left-color: var(--status-degraded); }
.vitals-card.status-unhealthy { border-left-color: var(--status-unhealthy); }
.vitals-card.status-unknown   { border-left-color: var(--status-unknown); }

.vitals-card-hdr {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 6px;
}
.vitals-card-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--color-text-primary);
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vitals-card-sub {
  font-size: 10px;
  font-weight: 400;
  color: var(--color-text-tertiary);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vitals-pulse-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 3px;
  position: relative;
}
.vitals-pulse-dot.status-healthy   { background: var(--status-healthy); }
.vitals-pulse-dot.status-degraded  { background: var(--status-degraded); }
.vitals-pulse-dot.status-unhealthy { background: var(--status-unhealthy); }
.vitals-pulse-dot.status-unknown   { background: var(--status-unknown); }

/* Heartbeat — expanding ring fading to transparent. Speed varies by
 * status: fast (0.9s) = stressed, slow (1.4s) = healthy, slowest
 * (2.2s) = degraded — the same vocabulary used everywhere else in the
 * app. Static for unknown — nothing to monitor. */
@media (prefers-reduced-motion: no-preference) {
  .vitals-pulse-dot.status-healthy   { animation: vitals-pulse 1.4s ease-out infinite; }
  .vitals-pulse-dot.status-degraded  { animation: vitals-pulse 2.2s ease-out infinite; }
  .vitals-pulse-dot.status-unhealthy { animation: vitals-pulse 0.9s ease-out infinite; }
}
@keyframes vitals-pulse {
  0%   { box-shadow: 0 0 0 0 currentColor; opacity: 1; }
  70%  { box-shadow: 0 0 0 8px transparent; opacity: 0.6; }
  100% { box-shadow: 0 0 0 10px transparent; opacity: 0.4; }
}

.vitals-kpis {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}
.vitals-kpi {
  display: flex;
  flex-direction: column;
}
.vitals-kpi-lbl {
  font-size: 9px;
  font-weight: 400;
  color: var(--color-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.vitals-kpi-val {
  font-size: 13px;
  font-weight: 500;
  color: var(--color-text-primary);
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, monospace;
}
.vitals-kpi.warn .vitals-kpi-val { color: var(--flow-warn-bright); }
.vitals-kpi.bad  .vitals-kpi-val { color: var(--flow-bad-bright); }

.vitals-ecg {
  width: 100%;
  height: 32px;
  display: block;
  margin-top: 4px;
}
.vitals-ecg path.tps  { fill: none; stroke-width: 1; opacity: 0.9; }
.vitals-ecg path.lat  { fill: none; stroke-width: 1; opacity: 0.5; }
.vitals-ecg path.divider { stroke: var(--color-border-secondary); stroke-width: 0.4; opacity: 0.4; }
.vitals-card.status-healthy   .vitals-ecg path.tps,
.vitals-card.status-healthy   .vitals-ecg path.lat   { stroke: var(--status-healthy); }
.vitals-card.status-degraded  .vitals-ecg path.tps,
.vitals-card.status-degraded  .vitals-ecg path.lat   { stroke: var(--status-degraded); }
.vitals-card.status-unhealthy .vitals-ecg path.tps,
.vitals-card.status-unhealthy .vitals-ecg path.lat   { stroke: var(--status-unhealthy); }
.vitals-card.status-unknown   .vitals-ecg path.tps,
.vitals-card.status-unknown   .vitals-ecg path.lat   { stroke: var(--status-unknown); }

/* =====================================================================
 *  View 7 — Pipes & pressure
 *
 *  Tank metaphor for non-technical viewers. Tanks fill from the bottom
 *  by utilization (0–100%), with a wavy water surface and white fill
 *  percentage rendered inside. Pipes have a dark outer casing and a
 *  coloured inner flow channel whose dash pattern animates by TPS.
 * ===================================================================== */
.pipe-tank-casing {
  fill: var(--color-background-secondary);
  rx: 6;
}
.pipe-tank-outline {
  fill: none;
  stroke-width: 1.5;
  rx: 6;
}
.pipe-tank-outline.status-healthy   { stroke: var(--status-healthy); }
.pipe-tank-outline.status-degraded  { stroke: var(--status-degraded); }
.pipe-tank-outline.status-unhealthy { stroke: var(--status-unhealthy); }
.pipe-tank-outline.status-unknown   { stroke: var(--status-unknown); }
.pipe-tank-water {
  /* fill is set inline based on utilization bucket — green / amber / red.
   * Slight vertical gradient via `<linearGradient>` defined per-tank in
   * JS so each tank can have its own colour band without a shared defs
   * collision. */
}
.pipe-tank-label {
  font-size: 11px;
  font-weight: 500;
  fill: var(--color-text-primary);
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--color-background-primary);
  stroke-width: 3;
}
.pipe-tank-sub {
  font-size: 9.5px;
  font-weight: 400;
  fill: var(--color-text-tertiary);
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--color-background-primary);
  stroke-width: 3;
}
.pipe-tank-fillpct {
  font-size: 10px;
  font-weight: 500;
  fill: #fff;
  text-anchor: middle;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, monospace;
  pointer-events: none;
}
.pipe-casing {
  fill: none;
  stroke: var(--color-background-tertiary);
  stroke-linecap: round;
}
.pipe-flow {
  fill: none;
  stroke-linecap: round;
  stroke-dasharray: 4 4;
}
.pipe-flow.flow-ok    { stroke: var(--flow-ok); }
.pipe-flow.flow-warn  { stroke: var(--flow-warn); }
.pipe-flow.flow-bad   { stroke: var(--flow-bad); }
.pipe-flow.idle       { stroke: var(--color-text-tertiary); opacity: 0.25; stroke-dasharray: 2 4; }
@media (prefers-reduced-motion: no-preference) {
  .pipe-flow:not(.idle) {
    animation: pipe-flow var(--flow-dur, 1.8s) linear infinite;
  }
}
@keyframes pipe-flow { to { stroke-dashoffset: -16; } }

.pipes-hit {
  fill: transparent;
  cursor: pointer;
  pointer-events: all;
}

/* =====================================================================
 *  Numbers that change every poll get tabular-nums everywhere — without
 *  this, every refresh visibly jiggles the layout as digit glyphs swap
 *  in and out. Spec §3a calls this out explicitly.
 * ===================================================================== */
.tip-card .v,
.matrix-cell,
.waterfall-dur,
.wf-item-dur,
.vitals-kpi-val,
.pipe-tank-fillpct {
  font-variant-numeric: tabular-nums;
}

/* =====================================================================
 *  Accessibility — visible focus rings on interactive elements (spec §6).
 *  Mouse focus stays invisible via :focus-visible; keyboard focus draws
 *  an offset outline using the highlight accent. Covers tabs, the filter
 *  input, the pause button, every list row, and the spec-view hit
 *  targets (the invisible <circle class="hit"> overlay on each node).
 * ===================================================================== */
.view-tab:focus-visible,
.wf-filter:focus-visible,
.wf-pause-btn:focus-visible,
.wf-item:focus-visible,
.vitals-card:focus-visible,
.matrix-cell:focus-visible,
.matrix-row-head:focus-visible,
.matrix-col-head:focus-visible {
  outline: 2px solid var(--highlight-accent);
  outline-offset: 2px;
  border-radius: 3px;
}
.spec-node .hit:focus-visible,
.pipes-hit:focus-visible,
.spec-edge:focus-visible,
.sankey-bar:focus-visible,
.sankey-ribbon:focus-visible {
  outline: 2px solid var(--highlight-accent);
}

/* Reduced-motion overrides — spec §3a motion vocabulary. Every infinite
 * animation should disable here. Live numeric updates remain (they're
 * information, not decoration). */
@media (prefers-reduced-motion: reduce) {
  .spec-edge.flowing,
  .pipe-flow,
  .vitals-pulse-dot,
  .spec-node.high-traffic .pulse-ring,
  .wf-item.is-new {
    animation: none !important;
  }
}

/* =====================================================================
 *  View 9 — Resource heatmap (spec §4)
 *
 *  Services × four metric columns (CPU, Memory, Pool/Saturation, Net).
 *  Per cell: tinted background by max-pod saturation tier, centered avg
 *  value, 4 px bottom bar scaled to the worst-pod value, optional "↑NN"
 *  hot-spot indicator in the corner. Services sort by their worst
 *  metric so problems float to the top.
 * ===================================================================== */
.heatmap-host {
  height: 100%;
  overflow: auto;
  padding: 12px 16px 16px;
  background: var(--color-background-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: 12px;
  color: var(--color-text-primary);
}
.heatmap-grid {
  display: grid;
  grid-template-columns: 220px 36px repeat(4, 1fr);
  gap: 4px 8px;
  align-items: center;
  min-width: 600px;
}
.heatmap-col-head {
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-tertiary);
  padding: 6px 8px 4px;
  border-bottom: 1px solid var(--color-border-tertiary);
}
.heatmap-svc {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
  color: var(--color-text-primary);
  min-width: 0;
}
.heatmap-svc .svc-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.heatmap-svc .svc-sub {
  color: var(--color-text-tertiary);
  font-size: 10px;
  margin-left: 6px;
}
.heatmap-status-dot {
  width: 8px; height: 8px; border-radius: 50%;
  flex-shrink: 0;
}
.heatmap-status-dot.status-healthy   { background: var(--status-healthy); }
.heatmap-status-dot.status-degraded  { background: var(--status-degraded); }
.heatmap-status-dot.status-unhealthy { background: var(--status-unhealthy); }
.heatmap-status-dot.status-unknown   { background: var(--status-unknown); }

.heatmap-replicas {
  font-size: 10px;
  color: var(--color-text-tertiary);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.heatmap-cell {
  position: relative;
  height: 40px;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  overflow: hidden;
  background: var(--cell-bg, transparent);
  border: 1px solid var(--color-border-tertiary);
  transition: opacity 180ms;
}
.heatmap-cell.sat-ok       { --cell-bg: rgba(39, 80, 10, 0.25);  border-color: rgba(39, 80, 10, 0.55); }
.heatmap-cell.sat-elevated { --cell-bg: rgba(99, 153, 34, 0.25); border-color: rgba(99, 153, 34, 0.55); }
.heatmap-cell.sat-warn     { --cell-bg: rgba(186, 117, 23, 0.30); border-color: rgba(186, 117, 23, 0.65); }
.heatmap-cell.sat-crit     { --cell-bg: rgba(163, 45, 45, 0.32); border-color: rgba(163, 45, 45, 0.7); }
.heatmap-cell .cell-value {
  font-weight: 500;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
  line-height: 1;
}
.heatmap-cell .cell-spike {
  position: absolute;
  top: 2px;
  right: 4px;
  font-size: 9px;
  font-weight: 500;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}
.heatmap-cell .cell-spike.high { color: var(--sat-crit-bright); }
.heatmap-cell .cell-maxbar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 4px;
  background: var(--max-bar-bg, var(--sat-ok-bright));
}
.heatmap-cell.sat-ok       .cell-maxbar { --max-bar-bg: var(--sat-ok-bright); }
.heatmap-cell.sat-elevated .cell-maxbar { --max-bar-bg: var(--sat-elevated-bright); }
.heatmap-cell.sat-warn     .cell-maxbar { --max-bar-bg: var(--sat-warn-bright); }
.heatmap-cell.sat-crit     .cell-maxbar { --max-bar-bg: var(--sat-crit-bright); }

.heatmap-host.dimming .heatmap-cell:not(.is-active) { opacity: 0.32; }

/* =====================================================================
 *  View 10 — Instance scatter (spec §4)
 *
 *  Per-pod 2D scatter of CPU (x) vs Memory (y). Dot color = service,
 *  dot size = pod's share of service TPS. A faint red rectangle marks
 *  the danger zone (top-right quadrant); dashed amber threshold lines
 *  cross at 75 / 75.
 * ===================================================================== */
.scatter-host { /* uses .spec-graph svg directly */ }
.scatter-axis-line { stroke: var(--color-border-secondary); stroke-width: 0.5; }
.scatter-axis-tick { stroke: var(--color-border-tertiary); stroke-width: 0.5; }
.scatter-axis-label {
  font-size: 10px;
  fill: var(--color-text-tertiary);
  font-variant-numeric: tabular-nums;
}
.scatter-axis-title {
  font-size: 10px;
  font-weight: 500;
  fill: var(--color-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.scatter-danger-zone {
  fill: var(--flow-bad);
  fill-opacity: 0.07;
}
.scatter-threshold {
  stroke: var(--flow-warn);
  stroke-width: 0.6;
  stroke-dasharray: 4 3;
  fill: none;
  opacity: 0.6;
}
.scatter-pod-halo {
  fill: currentColor;
  fill-opacity: 0.15;
}
.scatter-pod {
  fill: currentColor;
  stroke: var(--color-background-primary);
  stroke-width: 0.6;
  cursor: pointer;
  transition: opacity 180ms;
}
.scatter-host.dimming .scatter-pod:not(.is-active),
.scatter-host.dimming .scatter-pod-halo:not(.is-active) { opacity: 0.15; }
.scatter-cluster-label {
  font-size: 10px;
  font-weight: 500;
  fill: var(--color-text-secondary);
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--color-background-primary);
  stroke-width: 3;
}

/* =====================================================================
 *  View 11 — Vitals + resources (extends View 6's .vitals-card)
 *
 *  Adds a bottom "resource strip" with two side-by-side mini-meters
 *  (CPU + Memory). Each meter shows avg + ↑max in saturation colour,
 *  plus a horizontal bar that scales to the max value, coloured by
 *  saturation tier. Dashed 0.5 px divider separates from the ECG.
 * ===================================================================== */
.vitals-card .vitals-replicas {
  font-size: 10px;
  font-weight: 400;
  color: var(--color-text-tertiary);
  margin-left: 6px;
  font-variant-numeric: tabular-nums;
}

.vitals-resstrip {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 0.5px dashed var(--color-border-tertiary);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.vitals-resmeter {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.vitals-resmeter-hdr {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 10px;
  color: var(--color-text-tertiary);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.vitals-resmeter-vals {
  font-size: 11px;
  font-weight: 500;
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
  letter-spacing: 0;
}
.vitals-resmeter-vals.sat-ok       { color: var(--sat-ok-bright); }
.vitals-resmeter-vals.sat-elevated { color: var(--sat-elevated-bright); }
.vitals-resmeter-vals.sat-warn     { color: var(--sat-warn-bright); }
.vitals-resmeter-vals.sat-crit     { color: var(--sat-crit-bright); }
.vitals-resmeter-track {
  height: 3px;
  background: var(--color-background-tertiary);
  border-radius: 2px;
  overflow: hidden;
}
.vitals-resmeter-fill {
  height: 100%;
  background: var(--sat-ok-bright);
}
.vitals-resmeter-fill.sat-ok       { background: var(--sat-ok-bright); }
.vitals-resmeter-fill.sat-elevated { background: var(--sat-elevated-bright); }
.vitals-resmeter-fill.sat-warn     { background: var(--sat-warn-bright); }
.vitals-resmeter-fill.sat-crit     { background: var(--sat-crit-bright); }
