/* ==========================================================================
   THUGS(red) Security Check — charts.css
   --------------------------------------------------------------------------
   Statistics presentation. Requires base.css (tokens) and, for .stat-tile /
   .meter / .table, components.css.

   HOW THESE CHARTS ARE BUILT
   Charts are **inline SVG generated by PHP**. There is no charting library
   and no JS dependency. The site's CSP forbids inline styles, so the markup
   ships *geometry only* — x/y/width/height/d/points/r/cx/cy attributes, which
   are presentation attributes, not styles — plus the classes below. Every
   colour, stroke width, font size, opacity and dash pattern lives here.

   THE METHOD (dataviz skill), APPLIED
   The colour work in this file is computed, not eyeballed. The eight
   categorical slots below were derived by holding each of the main site's
   ANSI hue angles and snapping lightness/chroma into the mode's band, then
   enumerating slot orderings and keeping only those that clear every gate in
   BOTH themes. Measured results:

     dark  (surface #0e1217)  worst adjacent CVD ΔE 12.7  · normal ΔE 24.8
     light (surface #fbfcfd)  worst adjacent CVD ΔE 11.4  · normal ΔE 23.3

   against a target of ΔE >= 8 (CVD, OKLab ×100, Machado 2009 severity 1.0)
   and a hard floor of >= 15 unsimulated. All sixteen marks clear 3:1 against
   their own surface, so no relief clause is in play. Lightness band and
   chroma floor pass in both modes.

   SERIES CAP FOR ALL-PAIRS FORMS. Scatter, bubble and small-multiples put
   any two marks side by side, so they are gated on all pairs, not just
   neighbours. Slots 1-3 clear that gate (dark 14.1 CVD / 26.7 normal; light
   11.4 / 23.7). **Past three series in those forms, fold the tail into
   "Other" or facet — do not add a fourth colour.** Bars, stacks and lines
   use the adjacent pairlist and may run to all eight.

   NON-NEGOTIABLES ENFORCED BELOW
   - Assign slots 1..N in fixed order. Never cycle, never generate a 9th.
     A 9th category folds into .series-other (de-emphasis grey).
   - Colour follows the entity, never its rank. Filtering must not repaint
     the survivors.
   - One axis. Never two y-scales on one plot.
   - Sequential = one hue, light→dark. Diverging = two hues + neutral grey
     midpoint. Never a rainbow, never a hue at the midpoint.
   - Text wears text tokens, never a series colour.
   - Status tokens (--ok/--warn/--bad/--info) are reserved. When a series
     *means* detected/not-detected it wears status; when it is just
     "series 4" it wears categorical. Never both in one chart.
   - Grid and axes are solid hairlines, one step off the surface. Never
     dashed — dashing reads as "threshold".
   - A legend is present for two or more series; a single series has none
     (the title names it). Direct labels are selective, never on every point.
   - Every chart has a table-view twin (.chart-table).

   PRODUCT SEMANTICS, WHICH INVERT THE USUAL INSTINCT
   "detected" is GOOD (the customer's tooling worked) and wears --ok/green.
   "not detected" is BAD (it missed the simulated attack) and wears
   --bad/red. Legend wording must say so: use "detected (good)" and
   "not detected (missed)" rather than bare colour keys. The sequential heat
   ramp is therefore a GREEN ramp — darker/paler is a worse detection rate,
   brighter/deeper green is better.
   ========================================================================== */

/* ================================================= 1. CHART-ONLY TOKENS === */

:root {
  /* The surface a chart actually renders on — .card-body's background. The
     validator was run against exactly these two values. */
  --chart-surface: #0e1217;

  /* Chrome. Recessive by construction. */
  --chart-grid-color: var(--line);
  --chart-axis-color: #2a333f;
  --chart-ink:        var(--fg);
  --chart-ink-dim:    var(--fg-dim);
  --chart-ink-faint:  var(--fg-faint);
  --chart-ink-strong: var(--fg-bright);

  /* ---- categorical: eight slots, FIXED ORDER, never cycled -------------
     1 amber   2 magenta  3 azure   4 red
     5 teal    6 orange   7 violet  8 green
     Slots 1-3 lead deliberately: red and green are reserved status hues in
     this product, so they are pushed down the order and never open an
     identity palette. All eight sit at OKLCH L 0.635 so no slot shouts
     louder than another. */
  --series-1: #ae830a;
  --series-2: #e83699;
  --series-3: #078eef;
  --series-4: #f63341;
  --series-5: #03a092;
  --series-6: #d3690c;
  --series-7: #b057f3;
  --series-8: #02a55b;

  /* The folded tail. De-emphasis grey — not a ninth identity. */
  --series-other: #6d7887;

  /* ---- sequential: ONE hue, five ordered steps -------------------------
     Green, because magnitude here is "detection rate" and more green is
     better news. Step 1 is the weakest bin and still clears 2:1 on the
     surface, so even the lowest bin is visible. --heat-0 is "no data" and
     is intentionally the surface itself plus a hairline. */
  --heat-0: transparent;
  --heat-1: #005d31;
  --heat-2: #007c43;
  --heat-3: #019d56;
  --heat-4: #01bf6a;
  --heat-5: #01e27e;

  /* A second sequential context (volume / report counts) takes the next
     available hue as its own one-hue ramp, never a second colour inside the
     green ramp. */
  --heat-alt-1: #624900;
  --heat-alt-2: #82620b;
  --heat-alt-3: #a47c0d;
  --heat-alt-4: #c8980e;
  --heat-alt-5: #edb40e;

  /* ---- diverging: azure <-> red, neutral grey midpoint -----------------
     For "better / worse than the cohort average". Warm/cool poles so they
     read as opposite; the midpoint is grey so it reads as "nothing".
     Green<->red was rejected: it is the classic CVD collision.
     Arm step 1 sits nearest the midpoint, step 3 at the extreme. */
  --div-up-1:   #0877ca;
  --div-up-2:   #0c96fb;
  --div-up-3:   #66b3ff;
  --div-mid:    #39414d;
  --div-down-1: #d7152e;
  --div-down-2: #fc454d;
  --div-down-3: #ff8480;

  /* Mark geometry — referenced by the CSS below and by the PHP that emits
     the geometry, so both sides agree. */
  --mark-bar-max: 24px;
  --mark-line-w:  2px;
  --mark-dot-r:   4px;   /* >= 8px diameter */
  --mark-gap:     2px;   /* surface gap between touching fills */
}

:root[data-theme="light"] {
  --chart-surface: #fbfcfd;

  --chart-grid-color: var(--line);
  --chart-axis-color: #b3bcc9;

  --series-1: #846308;
  --series-2: #b91075;
  --series-3: #046bb7;
  --series-4: #c50526;
  --series-5: #129689;
  --series-6: #a24e04;
  --series-7: #8938c3;
  --series-8: #0b7d45;

  --series-other: #7c8694;

  --heat-0: transparent;
  --heat-1: #15c971;
  --heat-2: #12ac61;
  --heat-3: #0f9150;
  --heat-4: #00773f;
  --heat-5: #005d31;

  --heat-alt-1: #e4ac06;
  --heat-alt-2: #c69608;
  --heat-alt-3: #aa8009;
  --heat-alt-4: #8f6b09;
  --heat-alt-5: #745709;

  --div-up-1:   #3fa3fe;
  --div-up-2:   #0877ca;
  --div-up-3:   #025592;
  --div-mid:    #dfe3ea;
  --div-down-1: #ff6363;
  --div-down-2: #de2234;
  --div-down-3: #a7001e;
}


/* ================================================== 2. FIGURE / WRAPPER === */

/* <figure class="chart-figure">
     <h3 class="chart-title">…</h3>
     <p class="chart-subtitle">…</p>
     <svg class="chart" viewBox="0 0 640 280" role="img"
          aria-labelledby="c1-t c1-d">…</svg>
     <ul class="chart-legend">…</ul>
     <figcaption class="chart-note">…</figcaption>
   </figure>

   The container is never given a fixed height: the x-axis band must be
   inside it, or the card grows a tiny nested scrollbar. */
.chart-figure {
  margin: 0;
  min-width: 0;
}

.chart-figure + .chart-figure { margin-top: 1.6rem; }

.chart-title {
  margin: 0 0 .15rem;
  color: var(--chart-ink-strong);
  font-family: var(--mono);
  font-size: .92rem;
  font-weight: 700;
  letter-spacing: .02em;
}

.chart-subtitle {
  margin: 0 0 .7rem;
  color: var(--chart-ink-dim);
  font-size: .78rem;
}

.chart-note {
  margin: .7rem 0 0;
  color: var(--chart-ink-faint);
  font-size: .74rem;
}

/* The SVG itself. Scales with its container; the viewBox does the work. */
.chart {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;             /* end-labels may sit outside the viewBox */
  font-family: var(--mono);      /* inherited by every <text> below */
}

.chart-sm    { max-width: 420px; }
.chart-donut { max-width: 230px; margin-inline: auto; }
.chart-gauge { max-width: 280px; margin-inline: auto; }

/* Small multiples: facet instead of adding a fourth all-pairs colour. */
.chart-facets {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.chart-facet { min-width: 0; }

.chart-facet-title {
  margin: 0 0 .2rem;
  color: var(--chart-ink-dim);
  font-size: .74rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}


/* ====================================================== 3. GRID & AXES === */

/* Solid hairlines, one step off the surface, never dashed. */
.chart-grid,
.chart-grid line,
.chart-grid path {
  fill: none;
  stroke: var(--chart-grid-color);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* The baseline / axis rule is one step stronger than the grid — it is
   structure, not reference. */
.chart-axis,
.chart-axis line,
.chart-axis path {
  fill: none;
  stroke: var(--chart-axis-color);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* A genuine threshold (a target detection rate, the cohort average) is the
   ONE thing allowed to be dashed, because here dashing means what it looks
   like it means. Always direct-label it. */
.chart-threshold {
  fill: none;
  stroke: var(--chart-ink-faint);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

.chart-threshold-label {
  fill: var(--chart-ink-faint);
  font-size: 9px;
  letter-spacing: .02em;
}

/* Axis text and tick labels wear text tokens. Never a series colour. */
.chart-label {
  fill: var(--chart-ink-dim);
  font-size: 10px;
}

.chart-tick {
  fill: var(--chart-ink-faint);
  font-size: 9px;
  font-variant-numeric: tabular-nums;   /* ticks are a column of numbers */
}

.chart-tick-y { text-anchor: end; }
.chart-tick-x { text-anchor: middle; }

/* Rotated x tick, for long category names. The transform is a presentation
   attribute on the element; this only sets the anchor. */
.chart-tick-rot { text-anchor: end; }

.chart-axis-title {
  fill: var(--chart-ink-faint);
  font-size: 9px;
  letter-spacing: .07em;
  text-transform: uppercase;
}

/* Selective direct labels. NEVER a number on every point — the endpoint,
   the extreme, or the one series the story is about. */
.chart-value {
  fill: var(--chart-ink-strong);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.chart-value-dim { fill: var(--chart-ink-dim); font-weight: 400; }

/* A label set INSIDE a coloured fill is the one place text may leave the
   text tokens — pick by the fill's luminance so it always clears contrast.
   Only emit it when the text measurably fits; otherwise put the value at the
   bar end, or leave it to the legend and the table view. */
.chart-value-in         { font-size: 10px; font-weight: 700; }
.chart-value-in.on-dark  { fill: #ffffff; }
.chart-value-in.on-light { fill: #0b0e12; }


/* ============================================= 4. CATEGORICAL SLOT PAINT === */

/* One class per slot, setting fill (SVG shapes), stroke (SVG lines) and
   background-color (HTML legend swatches). Whichever property does not
   apply to a given element is simply ignored, so one rule covers all three
   and there is exactly one hex per slot. */
.series-1 { fill: var(--series-1); stroke: var(--series-1); background-color: var(--series-1); }
.series-2 { fill: var(--series-2); stroke: var(--series-2); background-color: var(--series-2); }
.series-3 { fill: var(--series-3); stroke: var(--series-3); background-color: var(--series-3); }
.series-4 { fill: var(--series-4); stroke: var(--series-4); background-color: var(--series-4); }
.series-5 { fill: var(--series-5); stroke: var(--series-5); background-color: var(--series-5); }
.series-6 { fill: var(--series-6); stroke: var(--series-6); background-color: var(--series-6); }
.series-7 { fill: var(--series-7); stroke: var(--series-7); background-color: var(--series-7); }
.series-8 { fill: var(--series-8); stroke: var(--series-8); background-color: var(--series-8); }
.series-other { fill: var(--series-other); stroke: var(--series-other); background-color: var(--series-other); }

/* Short aliases matching the main site's naming, so a chart authored there
   drops in unchanged. */
.chart-s1 { fill: var(--series-1); stroke: var(--series-1); background-color: var(--series-1); }
.chart-s2 { fill: var(--series-2); stroke: var(--series-2); background-color: var(--series-2); }
.chart-s3 { fill: var(--series-3); stroke: var(--series-3); background-color: var(--series-3); }
.chart-s4 { fill: var(--series-4); stroke: var(--series-4); background-color: var(--series-4); }
.chart-s5 { fill: var(--series-5); stroke: var(--series-5); background-color: var(--series-5); }
.chart-s6 { fill: var(--series-6); stroke: var(--series-6); background-color: var(--series-6); }
.chart-s7 { fill: var(--series-7); stroke: var(--series-7); background-color: var(--series-7); }
.chart-s8 { fill: var(--series-8); stroke: var(--series-8); background-color: var(--series-8); }
.chart-s-other { fill: var(--series-other); stroke: var(--series-other); background-color: var(--series-other); }

/* Status paint, for marks whose identity IS the outcome. A chart uses either
   these or the categorical slots — never both.

   These are SCOPED to chart elements on purpose. `.is-ok` / `.is-bad` are
   also state classes on .timeline-item, .stat-delta and .meter-fill in
   components.css; an unscoped paint rule here would repaint those as solid
   colour blocks. */
.chart-bar.is-ok, .chart-bar-h.is-ok, .chart-heat-cell.is-ok, .chart-arc.is-ok,
.chart-line.is-ok, .chart-dot.is-ok, .chart-area.is-ok,
.chart-swatch.is-ok, .chart-scale-step.is-ok, .bar-h-fill.is-ok {
  fill: var(--ok); stroke: var(--ok); background-color: var(--ok);
}

.chart-bar.is-warn, .chart-bar-h.is-warn, .chart-heat-cell.is-warn, .chart-arc.is-warn,
.chart-line.is-warn, .chart-dot.is-warn, .chart-area.is-warn,
.chart-swatch.is-warn, .chart-scale-step.is-warn, .bar-h-fill.is-warn {
  fill: var(--warn); stroke: var(--warn); background-color: var(--warn);
}

.chart-bar.is-bad, .chart-bar-h.is-bad, .chart-heat-cell.is-bad, .chart-arc.is-bad,
.chart-line.is-bad, .chart-dot.is-bad, .chart-area.is-bad,
.chart-swatch.is-bad, .chart-scale-step.is-bad, .bar-h-fill.is-bad {
  fill: var(--bad); stroke: var(--bad); background-color: var(--bad);
}

.chart-bar.is-info, .chart-bar-h.is-info, .chart-heat-cell.is-info, .chart-arc.is-info,
.chart-line.is-info, .chart-dot.is-info, .chart-area.is-info,
.chart-swatch.is-info, .chart-scale-step.is-info, .bar-h-fill.is-info {
  fill: var(--info); stroke: var(--info); background-color: var(--info);
}

.chart-bar.is-neutral, .chart-bar-h.is-neutral, .chart-heat-cell.is-neutral,
.chart-arc.is-neutral, .chart-line.is-neutral, .chart-dot.is-neutral,
.chart-area.is-neutral, .chart-swatch.is-neutral, .chart-scale-step.is-neutral,
.bar-h-fill.is-neutral {
  fill: var(--neutral); stroke: var(--neutral); background-color: var(--neutral);
}

/* Emphasis: highlight one, grey the rest. This — not eight hues — is the
   answer when the story is a single series. */
.chart-muted { fill: var(--series-other); stroke: var(--series-other); opacity: .75; }
.chart-emph  { fill: var(--accent); stroke: var(--accent); }


/* ============================================================= 5. BARS === */

/* Columns (vertical) and bars (horizontal) share this paint.

   GEOMETRY THE PHP MUST EMIT
   Bars cap at 24px thick — never fill the band; the leftover is air. The
   data-end is rounded 4px, the baseline end stays square, which a plain
   <rect rx="4"> cannot express. Emit a <path> instead. For an upward column
   of width w, height h, at (x, y_top), baseline at y_top + h:

     M x,(y_top+h) V (y_top+4) A 4,4 0 0 1 (x+4),y_top
     H (x+w-4) A 4,4 0 0 1 (x+w),(y_top+4) V (y_top+h) Z

   Adjacent bars are separated by a 2px gap in the surface — narrow the rect,
   never stroke it. A stroke around a mark adds ink that is not data. */
.chart-bar {
  stroke: none;
  transition: opacity var(--t-fast) ease;
}

/* DEFAULT PAINT IS DECLARED IN :where(), WHICH HAS ZERO SPECIFICITY.
   That is deliberate and it applies to every mark default in this file: a
   slot class (.series-3) or a status class (.is-bad) is a plain 0-1-0
   selector, and if the default were written the same way the later rule
   would simply win and every bar in a chart would come out slot-1 amber.
   Wrapping the fallback in :where() lets any single class beat it, so the
   mark's own class always decides the colour. Do not "simplify" these. */
:where(.chart-bar) { fill: var(--series-1); }

/* A <rect> fallback. Rounds both ends; acceptable for a 4px-tall sparkbar,
   not for a full column chart. */
rect.chart-bar { rx: 2; }

/* Stacked segments: the 2px separation is a gap in the surface, produced by
   the geometry, not a stroke. Nothing to declare here beyond the reminder. */
.chart-bar-stack > .chart-bar { stroke: none; }

/* Grouped bars: the group is a <g>, each member a .chart-bar with its own
   slot class. The 2px gap sits between members and a wider gap between
   groups; both come from the geometry. */
.chart-bar-group { }

/* Horizontal bar chart: identical paint, and the value rides the tip. */
:where(.chart-bar-h) { fill: var(--series-1); }

.chart-bar-h-label {
  fill: var(--chart-ink-dim);
  font-size: 10px;
  text-anchor: end;
  dominant-baseline: middle;
}

.chart-bar-h-value {
  fill: var(--chart-ink-strong);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  dominant-baseline: middle;
}

/* An HTML horizontal bar list — cheaper than SVG for a simple top-N and it
   is its own table view. Width comes from the data-pct rules generated at
   the end of components.css, so no inline style is needed. */
.bar-h-list {
  display: grid;
  gap: .45rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.bar-h-row {
  display: grid;
  grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: .6rem;
  font-size: .8rem;
  min-width: 0;
}

.bar-h-name {
  color: var(--chart-ink-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar-h {
  position: relative;
  height: 10px;
  border-radius: 0 2px 2px 0;
  background: var(--line-soft);
  overflow: hidden;
}

.bar-h-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 0 2px 2px 0;
}

:where(.bar-h-fill) { background-color: var(--series-1); }

.bar-h-num {
  color: var(--chart-ink-strong);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  min-width: 3.5em;
  text-align: right;
}

@media (max-width: 480px) {
  .bar-h-row { grid-template-columns: minmax(0, 1fr) auto; }
  .bar-h { grid-column: 1 / -1; }
}


/* ======================================================= 6. LINE / TREND === */

.chart-line {
  fill: none;
  stroke-width: var(--mark-line-w);
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Area under a line: the series hue as a ~10% wash, never a saturated
   block. fill-opacity so one slot class still supplies the hue. */
.chart-area {
  stroke: none;
  fill-opacity: .10;
}

:where(.chart-line) { stroke: var(--series-1); }
:where(.chart-area) { fill: var(--series-1); }

/* Markers >= 8px across, each with a 2px ring in the surface colour so it
   stays legible where it crosses a line or another dot. */
.chart-dot {
  stroke: var(--chart-surface);
  stroke-width: 2;
  r: 4;
}

:where(.chart-dot) { fill: var(--series-1); }

.chart-dot-end { r: 5; }

/* A projected / incomplete tail is the second legitimate dash. */
.chart-line-projected { stroke-dasharray: 5 4; opacity: .8; }

/* Crosshair for the hover layer. */
.chart-crosshair {
  stroke: var(--chart-ink-faint);
  stroke-width: 1;
  pointer-events: none;
  opacity: 0;
}

.chart:hover .chart-crosshair,
.chart:focus-within .chart-crosshair { opacity: .7; }


/* ==================================================== 7. DONUT / GAUGE === */

/* Posture score. Drawn as stroked arcs on a transparent circle: one <circle>
   for the track, one per segment. The 2px separation between segments is a
   gap in the arc (a stroke-dasharray offset), never a stroke around it. */
.chart-arc {
  fill: none;
  stroke-width: 15;
  stroke-linecap: butt;
}

.chart-arc-track {
  fill: none;
  stroke: var(--line);
  stroke-width: 15;
}

.chart-arc-thin,
.chart-arc-thin ~ .chart-arc-track { stroke-width: 9; }

/* The number in the hole is the point of the chart. Display face is fine
   here — it is the site's mono display face, not a decorative serif — and
   figures stay proportional at this size. */
.chart-hole-value {
  fill: var(--chart-ink-strong);
  font-family: var(--display);
  font-size: 26px;
  text-anchor: middle;
  dominant-baseline: middle;
  font-variant-numeric: normal;
}

.chart-hole-label {
  fill: var(--chart-ink-faint);
  font-size: 8px;
  letter-spacing: .09em;
  text-anchor: middle;
  text-transform: uppercase;
}

.chart-hole-sub {
  fill: var(--chart-ink-dim);
  font-size: 9px;
  text-anchor: middle;
}

/* A gauge's arc is coloured by grade band, and the grade letter is always
   printed alongside — colour never carries the band alone. */
.chart-arc.grade-a { stroke: var(--grade-a); }
.chart-arc.grade-b { stroke: var(--grade-b); }
.chart-arc.grade-c { stroke: var(--grade-c); }
.chart-arc.grade-d { stroke: var(--grade-d); }
.chart-arc.grade-e { stroke: var(--grade-e); }
.chart-arc.grade-f { stroke: var(--grade-f); }

/* Gauge tick marks at the band boundaries (40 / 60 / 75 / 90). */
.chart-gauge-tick {
  stroke: var(--chart-axis-color);
  stroke-width: 1;
}

.chart-gauge-tick-label {
  fill: var(--chart-ink-faint);
  font-size: 7px;
  text-anchor: middle;
  font-variant-numeric: tabular-nums;
}

/* Part-to-whole at a glance only, <= 6 segments. More than that, or values
   that are close, is a bar chart — see the anti-pattern list. */


/* ========================================================== 8. HEATMAP === */

/* Category × platform detection-rate matrix. One hue, five ordered bins,
   plus an explicit "no data" cell. The value is printed in every cell that
   is wide enough, and the whole matrix has a .chart-table twin, so the
   colour is never the only way to read a number.

   The 1px gap between cells comes from the cell geometry (a rect inset by
   1px on each side), so the grid reads without any stroke. */
.chart-heat { display: block; width: 100%; height: auto; }

.chart-heat-cell {
  stroke: none;
  transition: opacity var(--t-fast) ease;
}

.heat-0 {
  fill: var(--heat-0);
  stroke: var(--line);
  stroke-width: 1;
}

.heat-1 { fill: var(--heat-1); }
.heat-2 { fill: var(--heat-2); }
.heat-3 { fill: var(--heat-3); }
.heat-4 { fill: var(--heat-4); }
.heat-5 { fill: var(--heat-5); }

.heat-alt-1 { fill: var(--heat-alt-1); }
.heat-alt-2 { fill: var(--heat-alt-2); }
.heat-alt-3 { fill: var(--heat-alt-3); }
.heat-alt-4 { fill: var(--heat-alt-4); }
.heat-alt-5 { fill: var(--heat-alt-5); }

/* In-cell value. Bins 1-2 are dark in dark mode and pale in light mode, so
   the ink flips per bin, not per theme — PHP picks .on-dark / .on-light from
   the bin it just chose. */
.chart-heat-value {
  font-size: 9px;
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: middle;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.chart-heat-value.on-dark  { fill: #ffffff; }
.chart-heat-value.on-light { fill: #0b0e12; }

.chart-heat-row-label {
  fill: var(--chart-ink-dim);
  font-size: 9px;
  text-anchor: end;
  dominant-baseline: middle;
}

.chart-heat-col-label {
  fill: var(--chart-ink-dim);
  font-size: 9px;
  text-anchor: middle;
}

/* A sequential scale legend — mandatory for a heatmap, because the bins are
   a continuous scale and the reader needs the mapping. */
.chart-scale {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: .7rem 0 0;
  font-size: .72rem;
  color: var(--chart-ink-faint);
  flex-wrap: wrap;
}

.chart-scale-steps {
  display: flex;
  gap: 2px;
}

.chart-scale-step {
  width: 22px;
  height: 10px;
  border-radius: 1px;
}

.chart-scale-step.heat-0 { background: var(--surface); box-shadow: inset 0 0 0 1px var(--line); }
.chart-scale-step.heat-1 { background: var(--heat-1); }
.chart-scale-step.heat-2 { background: var(--heat-2); }
.chart-scale-step.heat-3 { background: var(--heat-3); }
.chart-scale-step.heat-4 { background: var(--heat-4); }
.chart-scale-step.heat-5 { background: var(--heat-5); }


/* ======================================================== 9. DIVERGING === */

/* Above / below the cohort average. Azure = better than the cohort, red =
   worse, grey = at the average. The sign is always in the label too. */
.div-up-1   { fill: var(--div-up-1);   background-color: var(--div-up-1); }
.div-up-2   { fill: var(--div-up-2);   background-color: var(--div-up-2); }
.div-up-3   { fill: var(--div-up-3);   background-color: var(--div-up-3); }
.div-mid    { fill: var(--div-mid);    background-color: var(--div-mid); }
.div-down-1 { fill: var(--div-down-1); background-color: var(--div-down-1); }
.div-down-2 { fill: var(--div-down-2); background-color: var(--div-down-2); }
.div-down-3 { fill: var(--div-down-3); background-color: var(--div-down-3); }

/* The zero line on a diverging bar chart is structure, so it wears the axis
   token and is a touch stronger than the grid. */
.chart-zero {
  stroke: var(--chart-ink-faint);
  stroke-width: 1;
  shape-rendering: crispEdges;
}


/* ========================================================= 10. SPARKLINE === */

/* A 12-point trend inside a .stat-tile. No axes, no labels, no legend: it is
   a shape, and the number beside it is the value. */
.sparkline {
  display: block;
  width: 100%;
  height: 28px;
  margin-top: .4rem;
  overflow: visible;
}

.sparkline-sm { height: 18px; }

.spark-line {
  fill: none;
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.spark-area {
  stroke: none;
  fill-opacity: .12;
}

:where(.spark-line) { stroke: var(--series-other); }
:where(.spark-area) { fill: var(--series-other); }

:where(.spark-dot) { fill: var(--accent); }

/* The current period is the accent; the history is de-emphasis. */
.spark-line.is-accent { stroke: var(--accent); }
.spark-dot {
  stroke: var(--chart-surface);
  stroke-width: 1.5;
  r: 2.6;
}

.spark-line.is-ok  { stroke: var(--ok); }
.spark-line.is-bad { stroke: var(--bad); }
.spark-dot.is-ok   { fill: var(--ok); }
.spark-dot.is-bad  { fill: var(--bad); }

/* A baseline for the sparkline's zero, if one is drawn. */
.spark-base {
  stroke: var(--line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}


/* ============================================================ 11. LEGEND === */

/* Present whenever there are two or more series; absent for one (the title
   names it). Doubles as the compact table view: this is where a share is
   readable as a number without hovering anything.

   Wording rule for this product: say "detected (good)" / "not detected
   (missed)", never a bare colour key. */
.chart-legend {
  list-style: none;
  margin: .8rem 0 0;
  padding: 0;
  font-size: .78rem;
}

.chart-legend li {
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: .55rem;
  padding: .22rem 0;
  border-top: 1px solid var(--line-soft);
}

.chart-legend li:first-child { border-top: 0; }

.chart-swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex: none;
}

/* A line-key rather than a block, for line charts. */
.chart-swatch-line {
  width: 12px;
  height: 0;
  border-radius: 0;
  border-top: 2px solid currentColor;
  background: none !important;
}

.chart-legend-name {
  color: var(--chart-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chart-legend-n {
  color: var(--chart-ink-strong);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.chart-legend-pct {
  color: var(--chart-ink-faint);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 3.8em;
}

/* An inline, one-row legend for a compact card. */
.chart-legend-inline {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem .9rem;
  margin: .6rem 0 0;
  padding: 0;
  list-style: none;
  font-size: .74rem;
  color: var(--chart-ink-dim);
}

.chart-legend-inline li {
  display: flex;
  align-items: center;
  gap: .35rem;
  border: 0;
  padding: 0;
}

/* An interactive legend row (toggling a series) must be a real button.
   Colour follows the entity, so hiding one series must not repaint the
   others — the slot class stays on the mark regardless.

   The button becomes the grid itself rather than `display: contents`, which
   drops a <button> out of the accessibility tree in several engines — the
   control would still be focusable but would stop being announced as a
   button. The <li> hands its layout down instead. */
.chart-legend li:has(> button) {
  display: block;
  padding: 0;
}

.chart-legend button {
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: .55rem;
  width: 100%;
  padding: .22rem 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}

.chart-legend li.is-off { opacity: .45; }


/* =========================================================== 12. TOOLTIP === */

/* Two ways to get a hover layer without a charting library, both CSP-safe:

   A. NATIVE — give every mark's <g class="chart-mark"> an SVG <title>
      child. The browser shows it on hover and assistive tech reads it. Zero
      CSS, zero JS. This is the default and it is enough for bar/heatmap.

   B. STYLED — PHP emits a <g class="chart-tooltip"> positioned with a
      transform attribute next to each mark; CSS reveals it on hover/focus
      of the parent .chart-mark. No JS, no inline style.

   Either way the tooltip only ENHANCES: every value is also in the legend,
   a direct label, or the .chart-table twin. */
.chart-mark { cursor: default; }

/* An invisible hit rect, sized in the geometry to at least ~24px, so a thin
   bar or an 8px dot is not a pinpoint target. */
.chart-hit {
  fill: transparent;
  stroke: none;
  cursor: pointer;
}

/* Emphasis on hover: dim the siblings, hold the hovered mark. Applied to the
   whole chart so it reads as one gesture. */
.chart:hover .chart-bar,
.chart:hover .chart-heat-cell { opacity: .55; }

.chart .chart-mark:hover .chart-bar,
.chart .chart-mark:hover .chart-heat-cell,
.chart .chart-mark:focus-within .chart-bar,
.chart .chart-mark:focus-within .chart-heat-cell { opacity: 1; }

/* SVG-native tooltip group (option B). */
g.chart-tooltip {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--t-fast) ease, visibility var(--t-fast) ease;
}

.chart-mark:hover > g.chart-tooltip,
.chart-mark:focus-within > g.chart-tooltip {
  opacity: 1;
  visibility: visible;
}

g.chart-tooltip rect {
  fill: var(--surface-2);
  stroke: var(--line);
  stroke-width: 1;
  rx: 4;
}

g.chart-tooltip text {
  fill: var(--chart-ink);
  font-size: 10px;
}

g.chart-tooltip .chart-tooltip-title {
  fill: var(--chart-ink-strong);
  font-weight: 700;
}

g.chart-tooltip .chart-tooltip-value {
  fill: var(--chart-ink-strong);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* HTML tooltip (for a JS-positioned overlay, should one be added later).
   Position is set through the CSSOM, which CSP does not restrict; nothing
   in the markup carries a style attribute. */
div.chart-tooltip {
  position: absolute;
  z-index: 60;
  min-width: 8rem;
  max-width: 18rem;
  padding: .45rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--chart-ink);
  box-shadow: var(--shadow-pop);
  font-size: .76rem;
  line-height: 1.45;
  pointer-events: none;
}

div.chart-tooltip[hidden] { display: none; }

.chart-tooltip-title {
  display: block;
  margin-bottom: .15rem;
  color: var(--chart-ink-strong);
  font-weight: 700;
}

.chart-tooltip-row {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-variant-numeric: tabular-nums;
}

.chart-tooltip-row .chart-swatch { margin-right: .1rem; }


/* ================================================= 13. TABLE-VIEW TWIN === */

/* Every chart has a WCAG-clean equivalent. Rendered collapsed behind a
   <details> so it costs no space but is always one click (and one tab stop)
   away — and it is fully present with JS off. */
.chart-table-toggle {
  margin-top: .7rem;
  font-size: .74rem;
}

.chart-table-toggle > summary {
  cursor: pointer;
  color: var(--chart-ink-faint);
  list-style: none;
}

.chart-table-toggle > summary::-webkit-details-marker { display: none; }

.chart-table-toggle > summary::before {
  content: "[+] ";
  color: var(--accent);
}

.chart-table-toggle[open] > summary::before { content: "[-] "; }

.chart-table-toggle > summary:hover,
.chart-table-toggle > summary:focus-visible { color: var(--chart-ink); }

.chart-table {
  width: 100%;
  margin-top: .5rem;
  border-collapse: collapse;
  font-size: .76rem;
  font-variant-numeric: tabular-nums;
}

.chart-table th,
.chart-table td {
  padding: .28rem .45rem;
  text-align: left;
  border-bottom: 1px solid var(--line-soft);
}

.chart-table thead th {
  color: var(--chart-ink-faint);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
}

.chart-table td.num,
.chart-table th.num { text-align: right; }

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


/* ==================================================== 14. LAYOUT / FILTERS = */

/* Filters live in ONE row above everything they scope (.toolbar in
   components.css) and every chart re-renders against the same slice. Never
   a filter inside a chart card. */
.chart-board {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
}

.chart-board > * { min-width: 0; }

.chart-board-wide { grid-column: 1 / -1; }

/* A chart sitting directly in a card body needs no extra padding. */
.card-body > .chart-figure:only-child { margin: 0; }


/* ============================================ 15. TEXTURE (OPT-IN ONLY) ===
   Where hue fails — full-severity CVD, greyscale print, forced-colors — a
   45°/135° hatch carries identity instead. Never on by default, never
   decorative.

   SVG patterns cannot be declared in CSS, so PHP must inline this <defs>
   block once per page (ids are the contract):

     <defs>
       <pattern id="tex-a" width="6" height="6" patternTransform="rotate(45)"
                patternUnits="userSpaceOnUse">
         <line x1="0" y1="0" x2="0" y2="6" stroke="currentColor"
               stroke-width="2"/>
       </pattern>
       <pattern id="tex-b" … patternTransform="rotate(135)" …/>
       <pattern id="tex-c" … rotate(45) with width="10" …/>
       <pattern id="tex-d" … rotate(135) with width="10" …/>
     </defs>

   Horizontal and vertical are deliberately absent: they read as gridlines
   and bars. On a value scale the rotation is ordered with magnitude so the
   texture can never misstate the value. */

:root[data-texture="on"] .chart-bar.tex-a,
:root[data-texture="on"] .chart-heat-cell.tex-a { fill: url(#tex-a); }
:root[data-texture="on"] .chart-bar.tex-b,
:root[data-texture="on"] .chart-heat-cell.tex-b { fill: url(#tex-b); }
:root[data-texture="on"] .chart-bar.tex-c,
:root[data-texture="on"] .chart-heat-cell.tex-c { fill: url(#tex-c); }
:root[data-texture="on"] .chart-bar.tex-d,
:root[data-texture="on"] .chart-heat-cell.tex-d { fill: url(#tex-d); }

@media print {
  .chart-bar.tex-a, .chart-heat-cell.tex-a { fill: url(#tex-a); }
  .chart-bar.tex-b, .chart-heat-cell.tex-b { fill: url(#tex-b); }
  .chart-bar.tex-c, .chart-heat-cell.tex-c { fill: url(#tex-c); }
  .chart-bar.tex-d, .chart-heat-cell.tex-d { fill: url(#tex-d); }

  /* Print on white: force the chrome to ink so a chart survives a mono
     printer, and always open the table view. */
  .chart-grid, .chart-grid line { stroke: #bbb; }
  .chart-axis, .chart-axis line { stroke: #666; }
  .chart-label, .chart-tick, .chart-value { fill: #000; }
  .chart-table-toggle[open] > summary,
  .chart-table-toggle > summary { color: #000; }
  .chart-table { display: table !important; }
}

@media (forced-colors: active) {
  /* Let the OS palette drive everything; the hatch is what keeps series
     apart once every fill collapses to one colour. */
  .chart-bar, .chart-heat-cell, .chart-arc, .chart-line, .chart-dot {
    forced-color-adjust: none;
  }

  .chart-bar.tex-a, .chart-heat-cell.tex-a { fill: url(#tex-a); }
  .chart-bar.tex-b, .chart-heat-cell.tex-b { fill: url(#tex-b); }
  .chart-bar.tex-c, .chart-heat-cell.tex-c { fill: url(#tex-c); }
  .chart-bar.tex-d, .chart-heat-cell.tex-d { fill: url(#tex-d); }
}


/* =========================================== 16. MOTION / REDUCED MOTION === */

/* Charts do not animate on load: a server-rendered SVG is already there, and
   an entrance animation on data is noise. The only transitions are the hover
   emphasis and the meter fill, both already reduced to 1ms by the global
   prefers-reduced-motion block in base.css. */
@media (prefers-reduced-motion: reduce) {
  .chart-bar,
  .chart-heat-cell,
  g.chart-tooltip { transition: none; }
}
