/* ============================================================================
   components.css — cards, controls, charts, meters, packets, timelines, tables
   ========================================================================== */

/* ---- Cards & panels ------------------------------------------------------- */
.card {
  background: linear-gradient(180deg, var(--surface-1), var(--surface-0));
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  box-shadow: var(--shadow-1);
}
.card + .card { margin-top: 0; }
.card-title {
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-faint);
  margin-bottom: var(--sp-3);
  font-weight: 650;
}
.card.accent { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.subtle { color: var(--text-faint); font-size: var(--fs-sm); }
.mono { font-family: var(--font-mono); }

/* ---- Prose ---------------------------------------------------------------- */
.lead { font-size: var(--fs-md); color: var(--text-dim); }
.block-h { font-size: var(--fs-md); margin: var(--sp-4) 0 var(--sp-2); color: var(--text); }
.block-note { font-size: var(--fs-sm); color: var(--text-faint); }
.block-list li { color: var(--text-dim); }
.block-code {
  background: var(--surface-inset);
  border: 1px solid var(--border-faint);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  overflow-x: auto;
  font-size: var(--fs-sm);
  color: var(--accent);
  line-height: 1.7;
}
/* Wrap rather than pan. A <pre> with `overflow-x: auto` is a scroll container,
   and Safari does not put scroll containers in the sequential focus order, so
   any line long enough to overflow is unreachable by keyboard (2.1.1). Wrapping
   means the overflow does not happen in the first place; `overflow-x: auto`
   stays as the backstop for content that cannot break, which is why this still
   counts as a scroll container and still wants the tabindex/role/aria-label
   treatment in js/views/blocks.js that every other scroller here gets. */
.block-code code {
  background: none; border: none; padding: 0; color: inherit;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.kv-list { display: grid; gap: var(--sp-3); }
.kv-list dt { font-weight: 650; color: var(--text); }
.kv-list dd { color: var(--text-dim); margin-left: 0; }

.concept-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--sp-3); margin-top: var(--sp-3); }
.concept-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); }
.concept-tag { display: inline-block; font-size: var(--fs-xs); letter-spacing: 0.1em; font-weight: 700; color: var(--accent); margin-bottom: var(--sp-2); }
.concept-card p { font-size: var(--fs-sm); }

/* ---- Callouts ------------------------------------------------------------- */
.callout {
  display: flex; gap: var(--sp-3);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  margin: var(--sp-4) 0;
}
.callout-key { border-left-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--surface-1)); }
.callout-warn { border-left-color: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--surface-1)); }
.callout-note { border-left-color: var(--accent-2); }
.callout-tip { border-left-color: var(--info); }
.callout-icon { color: var(--accent); font-size: 1rem; line-height: 1.6; flex: none; }
.callout-warn .callout-icon { color: var(--warn); }
.callout-note .callout-icon { color: var(--accent-2); }
.callout-title { font-size: var(--fs-base); margin-bottom: var(--sp-1); }
.callout-body p { font-size: var(--fs-sm); }

.callout-chip {
  display: flex; flex-direction: column; gap: 2px;
  background: color-mix(in srgb, var(--covert) 10%, var(--surface-1));
  border: 1px solid color-mix(in srgb, var(--covert) 32%, var(--border));
  border-radius: var(--r-md);
  padding: var(--sp-3);
}
.chip-title { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--covert); font-weight: 700; }
.chip-body { font-size: var(--fs-sm); color: var(--text-dim); }

/* ---- Controls: slider ----------------------------------------------------- */
.control-group { display: flex; flex-direction: column; gap: var(--sp-4); }
.control-group-title { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint); }
.ctrl { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Was `opacity: 0.45` on the whole control. Opacity composites the entire
   subtree, so it took the LABEL to 2.78:1 and the help text to 2.06:1 against the
   card — and on a disabled control here the help text is the only thing that says
   WHY it is disabled ("Only applies to the data-area channel..."). SC 1.4.3
   exempts inactive components, so this was not a violation; it was still text the
   reader needs, rendered unreadable. The dimming moves to the furniture, which
   carries no text: the track and the aria-hidden value read-out. Label and help
   keep their normal tokens, 8.62:1 and 5.15:1. The disabled state stays visible
   through the dimmed track and is announced from the input's own `disabled`. */
.ctrl.is-disabled .ctrl-range,
.ctrl.is-disabled .ctrl-value { opacity: 0.45; }
.ctrl.is-disabled label { cursor: not-allowed; }
.ctrl-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.ctrl label { font-size: var(--fs-sm); color: var(--text-dim); font-weight: 500; }
.ctrl-value { font-size: var(--fs-sm); color: var(--accent); font-weight: 600; }
.ctrl-help { font-size: var(--fs-xs); color: var(--text-faint); }

/* The slider is the primary control of this exhibit — fourteen views build one —
   so it carries the most target-size risk. It used to BE six pixels tall: the
   input's own border box was `height: 6px`, and the 18x18px thumb overflowed it.
   Both are under the 24x24 CSS px minimum of WCAG 2.2 SC 2.5.8 Target Size
   (Minimum). (Strictly the old markup was rescued by the Spacing exception —
   sliders sit ~50-77px apart inside `.control-group`, so no two 24px circles
   intersect — but a 6px grab band is a real motor barrier whichever clause
   excuses it on paper, and any future layout that puts a button within 24px of a
   slider would turn the exception off.)

   The fix: the INPUT is now 24px tall and transparent, the visible 6px bar is
   drawn by the track pseudo-elements, and the thumb is a 24x24 target that
   PAINTS a 20px dot — a 12px accent core, a 3px --surface-0 ring, a 1px accent
   edge — via a radial gradient with hard stops, leaving the outer 2px
   transparent as pure hit area. Sizing the thumb with a border instead would
   have grown the target and the dot together.

   What the dot used to be is genuinely ambiguous, so this comment does not
   claim it is unchanged: the old thumb declared `width/height: 18px` with a 3px
   --surface-0 border, and the `*, *::before, *::after` box-sizing reset in
   base.css does NOT match a vendor pseudo-element — so whether that painted an
   18px dot (border-box) or a 24px one (content-box, the CSS default) is the
   UA's business, not this stylesheet's. The dot today is a definite 20px with a
   1px accent edge; it is a hair larger or a hair smaller than before depending
   on the engine, and either way the 24x24 target is now explicit rather than
   incidental. */
.ctrl-range {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 24px; border-radius: var(--r-pill);
  background: transparent;
  cursor: pointer;
  /* The old `outline-offset: 4px` existed to lift the focus ring off a 6px box.
     The box is now 24px, so the house :focus-visible treatment fits as-is. */
}
/* The bar, still 6px, but no longer invisible. It was --surface-3: 1.31:1
   against a card, so the extent of the slider — where it begins, where it ends,
   how far along it the thumb sits — could not be made out at all (WCAG 1.4.11
   Non-text Contrast). --border-control is 3.55:1 there, and using the same token
   as every other control boundary keeps the rail part of one visual language
   rather than a new colour. Both engines centre their own track inside the 24px
   input, so the bar is drawn here rather than on the input, which would need a
   background band that cannot take a pill radius. */
.ctrl-range::-webkit-slider-runnable-track {
  height: 6px; border-radius: var(--r-pill);
  background: var(--border-control);
}
.ctrl-range::-moz-range-track {
  height: 6px; border-radius: var(--r-pill);
  background: var(--border-control);
}
.ctrl-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 24px; height: 24px; border: none; border-radius: 50%;
  background: transparent;
  /* 12px accent core, 3px dark ring, 1px accent edge, 2px transparent hit area. */
  background-image: radial-gradient(circle at 50% 50%,
    var(--accent) 0 6px,
    var(--surface-0) 6px 9px,
    var(--accent) 9px 10px,
    transparent 10px);
  /* WebKit does not centre the thumb on a shorter track: (6px - 24px) / 2. */
  margin-top: -9px;
  cursor: grab;
}
.ctrl-range::-webkit-slider-thumb:active { cursor: grabbing; }
.ctrl-range::-moz-range-thumb {
  width: 24px; height: 24px; border: none; border-radius: 50%;
  background: transparent;
  background-image: radial-gradient(circle at 50% 50%,
    var(--accent) 0 6px,
    var(--surface-0) 6px 9px,
    var(--accent) 9px 10px,
    transparent 10px);
  cursor: grab;
}

/* ---- Controls: select ----------------------------------------------------- */
/* A field's fill is nearly the same colour as what surrounds it — --surface-2 is
   1.10:1 against a card, --surface-inset 1.03:1 against the header — so the
   border is the ONLY thing that says "this is a control". At --border-strong
   that border measured 1.79:1 and 1.97:1, which is the canonical failure of WCAG
   1.4.11 Non-text Contrast. --border-control clears 3:1 against every surface
   these sit on (see base.css). */
.ctrl-select {
  width: 100%; padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--r-md);
  cursor: pointer;
}
.seed-field input, .msg-input {
  width: 100%; padding: var(--sp-2) var(--sp-3);
  background: var(--surface-inset); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--r-md);
}
.msg-input:focus, .ctrl-select:focus, .seed-field input:focus { border-color: var(--accent); }
.msg-counter { font-size: var(--fs-xs); color: var(--text-faint); }
.msg-counter.at-limit { color: var(--warn); }

/* ---- Controls: switch ----------------------------------------------------- */
/* The real target is this <label>, which wraps the input, the track and the text.
   With no padding its height is max(track 22px, label line box 21px) = 22px, two
   pixels under the WCAG 2.2 SC 2.5.8 minimum; it was passing only on the Spacing
   exception, with 2px of margin. min-height puts it on the baseline instead, and
   changes nothing visually. */
.switch { display: flex; align-items: center; gap: var(--sp-3); cursor: pointer; min-height: 24px; }
.switch-input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The OFF state used to be --surface-3 with no edge: 1.31:1 against a card, so
   the track — where the switch begins, where it ends, which side the thumb is on
   — could not be made out at all, and what a reader saw was a loose dot beside a
   label. WCAG 1.4.11 covers the boundary of a component as well as its state,
   and this is the same token at the same ratio that the slider rail was fixed
   for. --border-control is 3.55:1 on a card and clears 3:1 on every surface a
   control sits on. Geometry is unchanged: box-sizing is border-box so the track
   stays 40x22, and the thumb's 3px inset is now 1px of border plus 2px of
   offset — absolute positioning resolves against the padding box. (The ON fill
   is 4.23:1 and the state itself is a position change, so 1.4.1 was never the
   problem here.) */
.switch-track {
  position: relative; width: 40px; height: 22px; flex: none;
  background: var(--surface-3); border: 1px solid var(--border-control);
  border-radius: var(--r-pill);
  transition: background var(--dur) var(--ease);
}
.switch-thumb {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  background: var(--text-dim); border-radius: 50%;
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
.switch-input:checked + .switch-track { background: color-mix(in srgb, var(--accent) 55%, var(--surface-3)); }
.switch-input:checked + .switch-track .switch-thumb { transform: translateX(18px); background: var(--accent); }
.switch-input:focus-visible + .switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch-label { font-size: var(--fs-sm); color: var(--text-dim); }

/* ---- Controls: segmented -------------------------------------------------- */
.segmented { display: inline-flex; background: var(--surface-inset); border: 1px solid var(--border-control); border-radius: var(--r-md); padding: 3px; gap: 2px; }
.seg-input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg-btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-radius: calc(var(--r-md) - 3px);
  font-size: var(--fs-sm); color: var(--text-dim); cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* 3px + 3px + a 1.6 line box on --fs-xs came to 24.4px — passing SC 2.5.8 by
   four tenths of a pixel, at the mercy of font metrics and line-height rounding.
   4px of padding gives it real margin. */
.seg-sm .seg-btn { padding: 4px var(--sp-2); font-size: var(--fs-xs); }
.seg-btn:hover { color: var(--text); }
/* WCAG 1.4.11 covers the STATES of a component, and "selected" used to be
   nothing but a fill swap from --surface-inset to --surface-3: 1.43:1, with a
   1.73:1 text change and a black drop shadow that does nothing on a dark
   surface. This is the control that switches the exhibit between Sender/Receiver
   and Defender mode and picks the channel in the Detection Console, so the state
   has to be unmistakable. The accent ring is the indicator — 10.31:1 against the
   group's --surface-inset track and 4.92:1 against the segment's own fill — and,
   being the presence of an edge rather than a change of hue, it is a non-colour
   cue as well. The lifted fill is redundant reinforcement. */
.seg-input:checked + .seg-btn {
  background: color-mix(in srgb, var(--accent) 18%, var(--surface-3));
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.seg-input:focus-visible + .seg-btn { outline: 2px solid var(--accent); outline-offset: 2px; }
.seg-icon { font-size: 0.9em; }

/* ---- Buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-md);
  /* --surface-2 is 1.10:1 against a card, so the border is the whole boundary —
     and .btn.ghost is transparent, where it is literally all there is (1.4.11).
     .btn.primary keeps its own transparent border: its accent fill is 10.39:1. */
  border: 1px solid var(--border-control);
  background: var(--surface-2); color: var(--text);
  font-size: var(--fs-sm); font-weight: 550; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.btn:hover { background: var(--surface-3); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: linear-gradient(140deg, var(--accent), var(--accent-deep)); border-color: transparent; color: var(--text-invert); font-weight: 650; }
.btn.primary:hover { filter: brightness(1.08); background: linear-gradient(140deg, var(--accent), var(--accent-deep)); }
.btn.ghost { background: transparent; }
/* Mixed with --border-control, not --border: this is an interactive boundary and
   has to clear 3:1 (SC 1.4.11). Against the decorative --border it measured 2.92:1. */
.btn.danger { border-color: color-mix(in srgb, var(--danger) 50%, var(--border-control)); color: var(--danger); background: transparent; }
.btn-icon { font-size: 1em; }

/* ---- Pipeline & bytes ----------------------------------------------------- */
.pipeline { display: flex; flex-direction: column; gap: var(--sp-3); }
.pipeline.full { gap: var(--sp-2); }
.pipe-stage { background: var(--surface-inset); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: var(--sp-3); }
.pipe-label { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: var(--sp-2); }
.pipe-label > span:first-child { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint); font-weight: 650; }
.pipe-meta { font-size: var(--fs-xs); color: var(--text-faint); }
.pipe-text { font-size: var(--fs-md); color: var(--accent); letter-spacing: 0.02em; }
.byte-row { display: flex; flex-wrap: wrap; gap: 4px; }
.byte-chip { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 2px 6px; font-size: var(--fs-sm); color: var(--text-dim); }

/* ---- Bit ribbon ----------------------------------------------------------- */
.bit-ribbon { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; }
.bit-ribbon.compact { gap: 2px; }
.bit-cell {
  --bit-color: var(--bit-0);
  /* Positioning context for .bit-error::after. Without it the badge resolved
     against the initial containing block and, all offsets being auto, painted at
     its static position — centred in the cell, on top of the digit — so the
     redundant non-colour cue for an error bit was not actually being delivered. */
  position: relative;
  display: grid; place-items: center;
  width: 20px; height: 22px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: 700;
  background: color-mix(in srgb, var(--bit-color) 20%, var(--surface-inset));
  border: 1px solid color-mix(in srgb, var(--bit-color) 45%, transparent);
  color: color-mix(in srgb, var(--bit-color) 85%, white);
  animation: bitpop var(--dur-fast) var(--ease);
}
.bit-ribbon.compact .bit-cell { width: 15px; height: 17px; font-size: 9px; }
.bit-0 { --bit-color: var(--bit-0); }
.bit-1 { --bit-color: var(--bit-1); }
.bit-error {
  --bit-color: var(--bit-err);
  outline: 2px solid var(--bit-err); outline-offset: 1px;
}
.bit-error::after {
  content: "✕";
  position: absolute; top: -4px; right: -3px;
  font-size: 8px; line-height: 1; color: var(--bit-err);
  pointer-events: none;
}
.bit-more { font-size: var(--fs-xs); color: var(--text-faint); margin-left: 4px; }
@keyframes bitpop { from { transform: scale(0.6); opacity: 0; } to { transform: scale(1); opacity: 1; } }

.flow-arrow { text-align: center; color: var(--text-faint); font-size: 1.1rem; line-height: 1; }

/* ---- Charts --------------------------------------------------------------- */
.chart { width: 100%; height: auto; overflow: visible; }
/* The axis was --border: 1.35:1 against a card, i.e. effectively invisible. Not
   a 1.4.11 failure — every chart carries role="img" with a substantive
   aria-label and repeats its values as adjacent text — but a chart whose axis
   cannot be seen is not much of a chart. --border-control is 3.55:1. */
.chart-axis { stroke: var(--border-control); stroke-width: 1; }
.chart-xlabel { fill: var(--text-faint); font-size: 9px; font-family: var(--font-mono); }
.chart-val { fill: var(--text-dim); font-size: 9px; font-family: var(--font-mono); }
.chart-figure { margin: 0; }
.chart-caption { font-size: var(--fs-xs); color: var(--text-faint); margin-bottom: var(--sp-2); text-transform: uppercase; letter-spacing: 0.08em; }
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-2); }
.legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-dim); }
.legend-chip { flex: none; }

/* ---- Meters --------------------------------------------------------------- */
.meter { display: flex; flex-direction: column; gap: 4px; }
.meter-head { display: flex; justify-content: space-between; align-items: baseline; }
.meter-label { font-size: var(--fs-sm); color: var(--text-dim); }
.meter-value { font-size: var(--fs-sm); color: var(--text); font-weight: 600; }
.meter-track { height: 8px; background: var(--surface-inset); border-radius: var(--r-pill); overflow: hidden; }
.meter-fill { height: 100%; border-radius: var(--r-pill); transition: width var(--dur) var(--ease); }

/* ---- Anomaly gauge -------------------------------------------------------- */
.anomaly-gauge { display: flex; flex-direction: column; gap: var(--sp-2); }
.anomaly-head { display: flex; justify-content: space-between; align-items: baseline; }
.anomaly-level { font-size: var(--fs-sm); font-weight: 750; letter-spacing: 0.04em; }
.level-low .anomaly-level { color: var(--anom-low); }
.level-moderate .anomaly-level { color: var(--anom-mod); }
.level-high .anomaly-level { color: var(--anom-high); }
.anomaly-score { font-size: var(--fs-sm); color: var(--text-dim); }
/* No overflow:hidden here, so the marker can protrude above the bands. */
.anomaly-track { position: relative; height: 12px; border-radius: var(--r-pill); display: flex; }
.anomaly-band { height: 100%; }
.band-low { width: 34%; background: color-mix(in srgb, var(--anom-low) 40%, var(--surface-inset)); border-radius: var(--r-pill) 0 0 var(--r-pill); }
.band-mod { width: 33%; background: color-mix(in srgb, var(--anom-mod) 40%, var(--surface-inset)); }
.band-high { width: 33%; background: color-mix(in srgb, var(--anom-high) 40%, var(--surface-inset)); border-radius: 0 var(--r-pill) var(--r-pill) 0; }
.anomaly-marker {
  position: absolute; top: -3px; width: 4px; height: 18px; border-radius: var(--r-pill);
  background: var(--text); box-shadow: 0 0 0 2px var(--surface-0);
  transform: translateX(-2px); transition: left var(--dur) var(--ease);
}
.anomaly-disclaimer {
  font-size: var(--fs-xs); letter-spacing: 0.06em; color: var(--text-faint);
  text-transform: uppercase; font-weight: 600;
}

/* ---- Metric rows ---------------------------------------------------------- */
.metric-list { display: flex; flex-direction: column; }
.metric-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border-faint); }
.metric-row:last-child { border-bottom: none; }
.metric-name { font-size: var(--fs-sm); color: var(--text-dim); }
.metric-val { font-size: var(--fs-sm); color: var(--text); font-weight: 600; font-family: var(--font-mono); }
.metric-val.hi { color: var(--warn); }

/* ---- Observations --------------------------------------------------------- */
.observation { border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3); background: var(--surface-1); }
.observation + .observation { margin-top: var(--sp-2); }
/* "Not triggered" is carried by the dashed border. It used to also be carried by
   `opacity: 0.75`, which dragged the --text-faint tag from 5.15:1 on --surface-1
   down to 3.47:1 (1.4.3). Opacity is never a safe way to de-emphasise text: it
   multiplies whatever margin the colour token has, so a token that passes on its
   own can still fail wherever someone fades it. The dashed border says "not
   triggered" by itself, and says it without colour. */
.observation.quiet { border-style: dashed; }
.obs-what { font-size: var(--fs-sm); color: var(--text); font-weight: 600; margin-bottom: 4px; }
.obs-line { font-size: var(--fs-xs); color: var(--text-dim); display: flex; gap: 6px; }
.obs-line .obs-tag { color: var(--text-faint); font-weight: 650; flex: none; text-transform: uppercase; letter-spacing: 0.06em; }
.obs-line.why .obs-tag { color: var(--accent-2); }
.obs-line.else .obs-tag { color: var(--warn); }

/* ---- Tables --------------------------------------------------------------- */
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-md); }
.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.data-table th, .data-table td { padding: var(--sp-2) var(--sp-3); text-align: left; border-bottom: 1px solid var(--border-faint); white-space: nowrap; }
/* This measured 4.08:1 against --surface-2 when --text-faint was #728398 —
   under the 4.5:1 that 11.52px uppercase body text needs (1.4.3). The token has
   since been lightened to #7c8da2, which brings the old pairing to 4.66:1, but a
   column header names the data underneath it and 0.16 of a point is not a margin
   worth shipping. --text-dim measures 7.80:1 here. */
.data-table thead th { position: sticky; top: 0; background: var(--surface-2); color: var(--text-dim); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em; z-index: 1; }
.data-table tbody tr:hover { background: var(--surface-1); }
.data-table td.mono, .data-table th.mono { font-family: var(--font-mono); }
.data-table tr.is-covert td { background: color-mix(in srgb, var(--covert) 9%, transparent); }
.data-table tr.is-covert:hover td { background: color-mix(in srgb, var(--covert) 14%, transparent); }
.data-table td .cellwrap { white-space: normal; }

/* status pills */
.pill { display: inline-block; padding: 1px 8px; border-radius: var(--r-pill); font-size: var(--fs-xs); font-weight: 650; border: 1px solid transparent; }
.pill-covert { color: var(--covert); background: color-mix(in srgb, var(--covert) 14%, transparent); border-color: color-mix(in srgb, var(--covert) 30%, transparent); }
.pill-normal { color: var(--text-dim); background: var(--surface-2); }
.pill-mod { color: var(--anom-mod); background: color-mix(in srgb, var(--anom-mod) 14%, transparent); }
.pill-high { color: var(--anom-high); background: color-mix(in srgb, var(--anom-high) 14%, transparent); }
.pill-ok { color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); }
.pill-lost { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }

/* entropy mini-bar in tables */
.ent-bar { display: inline-flex; align-items: center; gap: 6px; }
/* Inside a covert-marked row the amber tint warms the background enough to pull
   --text-faint to 3.84:1. The row highlight is the point, so lift the label instead. */
.data-table tr.is-covert .ent-bar .subtle { color: var(--text-dim); }
.ent-track { width: 46px; height: 6px; background: var(--surface-inset); border-radius: var(--r-pill); overflow: hidden; }
.ent-fill { height: 100%; background: var(--covert); }

/* ---- Packet cards (retired) -----------------------------------------------
   The `.packet` / `.packet-head` / `.packet-field` / `.packet-bit` block that
   used to live here styled markup no view builds any more — verified by
   collecting every class name rendered by all 24 sections in both view modes.
   Recorded rather than silently deleted because it also carried a
   `content: "DROPPED"` pseudo-element, which reads exactly like the live
   `.sim-note::before` case and would otherwise cost the next audit a 1.3.1 fix
   on markup that does not exist. The timeline dot below (`.tl-packet`) is the
   packet visualisation that survived.
   ------------------------------------------------------------------------ */

/* ---- Timeline (timing channel) -------------------------------------------- */
.timeline-wrap { overflow-x: auto; padding: var(--sp-5) 0 var(--sp-3); }
.timeline { position: relative; height: 150px; min-width: 100%; }
.timeline-axis { position: absolute; left: 0; right: 0; top: 74px; height: 2px; background: var(--border-strong); }
.tl-packet {
  position: absolute; top: 62px; width: 26px; height: 26px; margin-left: -13px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--accent) 90%, white), var(--accent-deep));
  box-shadow: var(--glow-accent);
  display: grid; place-items: center; font-size: 9px; color: var(--text-invert); font-weight: 700;
  z-index: 2;
}
/* A dropped packet is how the reader sees what the Packet-loss slider removed,
   and the merged gap after it is the module's whole teaching point — so the
   marker is a graphical object required to understand the content. Faded to
   `opacity: 0.5` it composited to 1.13:1 against the card: invisible (1.4.11).
   The dashed --danger ring is 6.32:1 against the card and the glyph is 4.84:1 on
   the fill; the ring is also a shape difference, not just a colour one, which
   the fade was not (1.4.1). The state is exposed to assistive tech as well:
   timingView.js appends ", dropped" to the packet's `title`, so the ring is
   reinforcement rather than the only carrier (1.3.1). */
.tl-packet.dropped {
  background: var(--surface-3);
  box-shadow: none;
  border: 2px dashed var(--danger);
  color: var(--danger);
}
.tl-gap { position: absolute; top: 96px; font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--font-mono); transform: translateX(-50%); }
.tl-bit {
  position: absolute; top: 112px; transform: translateX(-50%);
  width: 20px; height: 22px; display: grid; place-items: center;
  border-radius: var(--r-sm); font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-xs);
}
.tl-bit.b0 { color: var(--bit-0); background: color-mix(in srgb, var(--bit-0) 18%, var(--surface-inset)); }
.tl-bit.b1 { color: var(--bit-1); background: color-mix(in srgb, var(--bit-1) 18%, var(--surface-inset)); }
.tl-bit.err { outline: 2px solid var(--bit-err); }
.tl-bracket { position: absolute; top: 90px; height: 8px; border: 1px solid var(--border-strong); border-bottom: none; }
.tl-label { position: absolute; top: 30px; font-size: var(--fs-xs); color: var(--text-faint); }

.identical-banner {
  text-align: center; margin: var(--sp-4) 0;
}
.identical-banner strong { display: block; font-size: var(--fs-lg); color: var(--covert); letter-spacing: 0.02em; }
.identical-banner .sub { color: var(--text-dim); font-size: var(--fs-sm); }

/* signal dots animation */
.signal-dot {
  position: absolute; top: 68px; width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); box-shadow: var(--glow-accent);
}

/* ---- Mode banner ---------------------------------------------------------- */
.mode-banner {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--r-md);
  background: var(--surface-1); border: 1px solid var(--border);
  font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: var(--sp-4);
}
.mode-banner .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
.mode-banner.defender { border-color: color-mix(in srgb, var(--accent-2) 40%, var(--border)); }
.mode-banner.defender .dot { background: var(--accent-2); }

/* ---- Sim note / disclaimers ---------------------------------------------- */
.sim-note {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--text-faint);
  border: 1px dashed var(--border-strong); border-radius: var(--r-pill);
  padding: 3px 10px;
}
/* The word "SIMULATED" is the exhibit's central honesty claim, and it is now
   emitted from the DOM by simTag() in js/views/widgets.js — a claim of that
   weight should not depend on a stylesheet loading, or on an engine choosing to
   expose ::before text to the accessibility tree. The ::before below is the
   fallback for any `.sim-note` that does not carry the span, and stands down the
   moment one does, so the label neither doubles up nor disappears. */
.sim-note::before { content: "SIMULATED"; color: var(--accent); font-weight: 700; letter-spacing: 0.06em; }
.sim-note:has(.sim-tag)::before { content: none; }
.sim-tag { color: var(--accent); font-weight: 700; letter-spacing: 0.06em; }

/* ---- Stego ---------------------------------------------------------------- */
.stego-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: var(--sp-4); }
.img-frame { display: flex; flex-direction: column; gap: var(--sp-2); }
.img-frame canvas, .img-frame img { width: 100%; border-radius: var(--r-md); border: 1px solid var(--border); background: var(--surface-inset); image-rendering: pixelated; aspect-ratio: 1 / 1; object-fit: contain; }
.img-frame figcaption { font-size: var(--fs-xs); color: var(--text-faint); text-align: center; }
.img-frame .cap-title { color: var(--text-dim); font-weight: 600; }

/* ---- Glossary ------------------------------------------------------------- */
.glossary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr)); gap: var(--sp-3); }
.glossary-item { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); }
.glossary-term { font-size: var(--fs-base); font-weight: 650; color: var(--accent); margin-bottom: 4px; }
.glossary-def { font-size: var(--fs-sm); color: var(--text-dim); }
.glossary-see { margin-top: var(--sp-2); font-size: var(--fs-xs); color: var(--text-faint); }

/* ---- Quiz ----------------------------------------------------------------- */
.quiz-item { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--sp-4); margin-bottom: var(--sp-4); }
.quiz-q { font-size: var(--fs-md); font-weight: 600; margin-bottom: var(--sp-3); }
.quiz-choices { display: flex; flex-direction: column; gap: var(--sp-2); }
.quiz-choice {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-md);
  /* --surface-0 fill is 1.06:1 against the quiz card, so the border is the only
     boundary (1.4.11); --border-control is 3.16:1 against that fill. */
  border: 1px solid var(--border-control); background: var(--surface-0);
  text-align: left; cursor: pointer; font-size: var(--fs-sm); color: var(--text-dim);
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.quiz-choice:hover:not(:disabled) { background: var(--surface-2); }
.quiz-choice .qc-key { width: 22px; height: 22px; flex: none; display: grid; place-items: center; border-radius: var(--r-sm); background: var(--surface-3); font-family: var(--font-mono); font-size: var(--fs-xs); }
.quiz-choice.correct { border-color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--text); }
.quiz-choice.wrong { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--text); }
.quiz-choice.correct .qc-key { background: var(--ok); color: var(--text-invert); }
.quiz-choice.wrong .qc-key { background: var(--danger); color: var(--text-invert); }
.quiz-explain { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--text-dim); padding: var(--sp-3); background: var(--surface-inset); border-radius: var(--r-md); border-left: 3px solid var(--accent); }
.quiz-score { font-size: var(--fs-md); font-weight: 650; }

/* ---- Tradeoff triangle ---------------------------------------------------- */
.tradeoff { display: grid; grid-template-columns: 1fr; gap: var(--sp-4); }
.tri-meters { display: grid; gap: var(--sp-4); }

/* ---- Misc ----------------------------------------------------------------- */
.empty-note { color: var(--text-faint); font-size: var(--fs-sm); font-style: italic; padding: var(--sp-4); text-align: center; }
.recovered-box { background: var(--surface-inset); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3); font-family: var(--font-mono); font-size: var(--fs-md); word-break: break-all; }
.recovered-box.ok { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); color: var(--ok); }
.recovered-box.err { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); }
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(90px, 100%), 1fr)); gap: var(--sp-2); }
.stat-tile { background: var(--surface-inset); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: var(--sp-3); text-align: center; }
.stat-tile .st-val { font-size: var(--fs-lg); font-weight: 700; font-family: var(--font-mono); color: var(--text); }
.stat-tile .st-lab { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px; }
.stat-tile.good .st-val { color: var(--ok); }
.stat-tile.bad .st-val { color: var(--danger); }

