/* =============================================================================
   HF Hub — Components (slice 2: controls)
   -----------------------------------------------------------------------------
   Buttons, badges and form inputs. Every colour, radius, duration and easing
   comes from tokens.css, and this file declares no hex values.

   Two dimension families are in play, and they are kept separate on purpose:
     - Control sizing (heights, control padding, toggle/knob, badge padding,
       choice size) consumes the --control-* / --toggle-* / --badge-* / --choice-*
       tokens. These are ergonomic constants, decoupled from the spacing scale.
     - Genuine spacing (gaps between controls, the label→field margin, padding
       inside the drop zone) still consumes --space-*.

   Hairline BORDER widths (1px / 2px) are the one intentional literal: they match
   the convention already set in base.css (1px rules, the 2px focus ring) and are
   stroke weights, not layout spacing.
============================================================================= */

/* =============================================================================
   BUTTONS
   Four variants × three sizes. Every variant is correct in both themes purely
   by consuming semantic tokens. Focus is inherited from base.css :focus-visible
   — buttons never define their own ring.
============================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  height: var(--control-h-md);
  padding: 0 var(--control-pad-x-md);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  /* Body size + family from the token; weight lifted to 500, sentence case. */
  font: var(--type-body);
  font-weight: 500;
  text-transform: none;
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
  cursor: pointer;
  transition:
    background-color var(--motion-micro) var(--ease),
    border-color var(--motion-micro) var(--ease),
    color var(--motion-micro) var(--ease);
}
.btn svg {
  width: 1em;
  height: 1em;
  flex: none;
}

/* --- Sizes ---------------------------------------------------------------- */
.btn--sm {
  height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font: var(--type-body-sm);
  font-weight: 500;
}
.btn--md {
  height: var(--control-h-md);
  padding: 0 var(--control-pad-x-md);
}
.btn--lg {
  height: var(--control-h-lg);
  padding: 0 var(--control-pad-x-lg);
}

/* --- Variants ------------------------------------------------------------- */
.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.btn--primary:not(:disabled):not(.is-loading):hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

.btn--secondary {
  color: var(--text-primary);
  border-color: var(--border-default);
}
.btn--secondary:not(:disabled):not(.is-loading):hover {
  background: var(--surface-hover);
  border-color: var(--border-strong);
}

.btn--ghost {
  color: var(--text-secondary);
  border-color: transparent;
}
.btn--ghost:not(:disabled):not(.is-loading):hover {
  background: var(--surface-hover);
  color: var(--text-primary);
}

.btn--destructive {
  color: var(--negative);
  border-color: var(--negative);
}
.btn--destructive:not(:disabled):not(.is-loading):hover {
  background: var(--negative);
  border-color: var(--negative);
  color: var(--accent-contrast);
}

/* --- Disabled ------------------------------------------------------------- */
/* Hover effects are already suppressed by the :not(:disabled) guards above. */
.btn:disabled,
.btn.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* --- Icon-only ------------------------------------------------------------ */
/* Square at the size height; aspect-ratio derives width from height. */
.btn--icon {
  padding-inline: 0;
  aspect-ratio: 1;
}

/* --- Loading -------------------------------------------------------------- */
/* Hide the label but keep it in flow (visibility) so the width never jumps.
   The arc is a three-quarter ring in the current text colour. Markup also sets
   aria-busy="true". */
.btn.is-loading {
  position: relative;
  pointer-events: none;
}
.btn.is-loading .btn__label {
  visibility: hidden;
}
.btn.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 1em;
  height: 1em;
  border: 2px solid;
  border-color: currentColor currentColor currentColor transparent; /* 3/4 arc */
  border-radius: var(--radius-pill);
  animation: hf-spin var(--motion-data) linear infinite;
}
@keyframes hf-spin {
  to { transform: rotate(360deg); }
}
/* Reduced motion: freeze on the static three-quarter arc. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-loading::after { animation: none; }
}

/* =============================================================================
   BADGES
   A pill in the micro type style. Background is a ~12% tint of the functional
   colour mixed against the current page surface, so it re-tints automatically
   on theme change; text is the solid functional colour. Registered domain
   meanings are named so a page never invents its own status colour.
============================================================================= */
.badge {
  --badge-color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--badge-pad-y) var(--badge-pad-x);
  border-radius: var(--radius-pill);
  font: var(--type-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase; /* micro is uppercase-by-style */
  background: color-mix(in srgb, var(--badge-color) 12%, var(--surface-page));
  color: var(--badge-color);
}

/* Functional meanings */
.badge--positive { --badge-color: var(--positive); }
.badge--warning  { --badge-color: var(--warning); }
.badge--negative { --badge-color: var(--negative); }
.badge--info     { --badge-color: var(--info); }
.badge--neutral {
  --badge-color: var(--text-secondary);
  background: var(--surface-hover); /* solid, not a functional tint */
}

/* Registered domain meanings → functional mapping (single source of status colour) */
.badge--offer-accepted,
.badge--live,
.badge--compliant       { --badge-color: var(--positive); }

.badge--under-offer,
.badge--awaiting-landlord,
.badge--beta,
.badge--due             { --badge-color: var(--warning); }

.badge--overdue,
.badge--error           { --badge-color: var(--negative); }

.badge--available,
.badge--edited          { --badge-color: var(--info); }

.badge--dev,
.badge--auto,
.badge--stale {
  --badge-color: var(--text-secondary);
  background: var(--surface-hover);
}

/* Leading-dot variant: drops the pill fill for dense table cells, keeping only
   a small functional-colour dot before the label. */
.badge--dot {
  background: transparent;
  padding-inline: 0;
}
.badge--dot::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: var(--radius-pill);
  background: var(--badge-color);
  flex: none;
}

/* =============================================================================
   REPORT-ROW WRITE-BACK  (referrals inbox, slice B1 — the first mutating report page)
   -----------------------------------------------------------------------------
   The shared bits of the write-back pattern, promoted out of the referrals inbox so the styleguide can
   demonstrate them from one source and later mutating pages consume them rather than re-inventing:
     · two TERMINAL status-badge weights (built on .badge) so a status column separates live from finished;
     · the "empty but real" state labels (Unattributed / Unassigned read as states, never blank cells);
     · the drill-down ACTIONS card, its inline write-error line, and the in-flight busy lock.
   Page-specific composition (the detail grid, the definition list, the event history) stays page-local.
============================================================================= */
/* Active pipeline stages take .badge--neutral; the two terminal states are weighted apart — a win reads
   positive, a dead lead reads quiet — so the column separates still-live from finished at a glance. */
.ref-status--done { --badge-color: var(--positive); }
.ref-status--lost { --badge-color: var(--text-tertiary); }

/* Empty-but-real labels: a muted italic so an unattributed referrer or an unassigned row reads as a genuine
   state, never a blank cell. */
.ref-quiet { color: var(--text-tertiary); font-style: italic; }
.ref-sub   { color: var(--text-tertiary); font-size: 0.9em; }

/* The drill-down actions card — the "do something" zone, set apart from the read-only detail beside it. */
.ref-actions {
  padding: var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-4);
}
.ref-action__label { display: block; margin-bottom: var(--space-1); }
.ref-action__row { display: grid; gap: var(--space-2); }
/* Inline, per-record write error — visible, leaving the rest of the row unchanged; collapses when empty. */
.ref-actions__error { margin: 0; color: var(--negative); font: var(--type-body-sm); }
.ref-actions__error:empty { display: none; }
/* While a write is in flight the actions dim and stop accepting input, so a second click cannot race the first. */
.ref-busy { opacity: 0.6; pointer-events: none; }
/* Empty-report message — a fresh inbox says so rather than showing a bare table. */
.ref-empty { margin: var(--space-7) 0; text-align: center; color: var(--text-secondary); }

/* =============================================================================
   FORM INPUTS
============================================================================= */
/* Label: the label type style but NOT uppercased, sitting above its field. */
.field-label {
  display: block;
  font: var(--type-label);
  letter-spacing: var(--track-label);
  text-transform: none;
  color: var(--text-secondary);
  margin-bottom: var(--space-1); /* 4px clearance */
}

/* Shared field style for text inputs, selects and textareas. */
.field {
  width: 100%;
  height: var(--control-h-md);
  padding: 0 var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-primary);
  font: var(--type-body);
  transition:
    border-color var(--motion-micro) var(--ease),
    background-color var(--motion-micro) var(--ease);
}
.field::placeholder { color: var(--text-tertiary); }
.field:hover { border-color: var(--border-strong); }
/* The 2px --focus-ring outline + 1px offset is inherited from base.css; the
   field additionally moves its border to the accent on focus. */
.field:focus-visible { border-color: var(--border-accent); }
.field:disabled {
  background: var(--surface-sunken);
  color: var(--text-tertiary);
  cursor: not-allowed;
}

select.field { cursor: pointer; }

/* Textareas are multi-line: auto height, symmetric padding, vertical resize. */
.field--textarea {
  height: auto;
  min-height: var(--space-8); /* 64px */
  padding: var(--space-3);
  resize: vertical;
}

/* Error state + message. Styleguide markup wires aria-describedby field→message. */
.field--error { border-color: var(--negative); }
.field-error {
  font: var(--type-body-sm);
  color: var(--negative);
  margin-top: var(--space-1);
}

/* Warning variant of the field state — the same shape as the error state on the warning token, for a value
   that is advisable rather than blocking (a figure still being decided). It never sets aria-invalid, since
   the value is not wrong; the message is associated the same way. */
.field--warn { border-color: var(--warning); }
.field-warning {
  font: var(--type-body-sm);
  color: var(--warning);
  margin-top: var(--space-1);
}

/* Checkboxes & radios, tinted with the accent. */
.checkbox,
.radio {
  width: var(--choice-size);
  height: var(--choice-size);
  accent-color: var(--accent);
  cursor: pointer;
}

/* Toggle switch, built from a checkbox: pill track that fills with the accent
   when checked; the knob slides at the standard duration. Track, knob and the
   centring inset all resolve from the control-sizing tokens, so the knob stays
   centred and the travel stays correct if those tokens are retuned. */
.toggle {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: var(--toggle-w);
  height: var(--toggle-h);
  border: none;
  border-radius: var(--radius-pill);
  background: var(--border-default);
  cursor: pointer;
  transition: background-color var(--motion-standard) var(--ease);
}
.toggle::before {
  content: "";
  position: absolute;
  /* (track - knob) / 2 centres the knob within the track. */
  top: calc((var(--toggle-h) - var(--toggle-knob)) / 2);
  left: calc((var(--toggle-h) - var(--toggle-knob)) / 2);
  width: var(--toggle-knob);
  height: var(--toggle-knob);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  transition: transform var(--motion-standard) var(--ease);
}
.toggle:checked { background: var(--accent); }
/* Travel = track width − track height (= width − knob − 2×inset). */
.toggle:checked::before { transform: translateX(calc(var(--toggle-w) - var(--toggle-h))); }
.toggle:disabled { opacity: 0.45; cursor: not-allowed; }

/* Choice row: a checkbox, radio or toggle sitting inline with its label. Appears
   on almost every form, so it is a real class rather than a repeated inline style. */
.choice {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font: var(--type-body);
  cursor: pointer;
}
.choice.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
/* A row of choices. */
.choice-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* File drop zone. */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-6);
  border: 1px dashed var(--border-default);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  text-align: center;
  transition:
    border-color var(--motion-micro) var(--ease),
    background-color var(--motion-micro) var(--ease);
}
.dropzone.is-dragover {
  border-color: var(--border-accent);
  background: var(--surface-selected);
}

/* =============================================================================
   METRIC
   Three stacked elements — label, value, delta — with NO card, border or shadow.
   The value (display-2 Presti 700) is the dominant element by a clear margin.
   The delta colour is driven entirely by attribute selectors on the root, so a
   server-rendered metric is the right colour before any script runs.
============================================================================= */
.metric {
  --metric-delta-color: var(--text-secondary); /* default / flat */
  display: flex;
  flex-direction: column;
}

.metric__label {
  font: var(--type-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase; /* label style is uppercase-by-style */
  color: var(--text-secondary);
}

.metric__value {
  /* Query container for the sparkline (see the sparkline block). It lives here
     rather than on .metric because .metric is a subgrid in a row, and a query
     container establishes layout containment, which would disable that subgrid. */
  container-type: inline-size;
  display: flex;
  align-items: baseline;        /* sparkline sits on the value's baseline */
  gap: var(--space-3);
  margin-top: var(--space-2);   /* rhythm: label → value */
  font: var(--type-display-2);
  letter-spacing: var(--track-display-2);
  color: var(--text-primary);
}

.metric__delta {
  display: flex;
  align-items: baseline;
  margin-top: var(--space-1);   /* rhythm: value → delta */
  font: var(--type-body-sm);
  color: var(--text-secondary);
  white-space: nowrap;          /* a delta must never wrap across two lines */
}
/* The arrow + amount take the functional colour and are the information, so they
   stay fully visible; the phrase is context, so it truncates with an ellipsis if
   the column is too narrow (metric.js adds a title with the full text when so). */
.metric__delta-figure {
  flex: none;
  color: var(--metric-delta-color);
}
.metric__delta-note {
  flex: 0 1 auto;
  min-width: 0;
  margin-left: var(--space-1);
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-secondary);
}

/* Small SVG triangle (currentColor), not a text arrow. Overrides base.css's
   `svg { display: block }` so it flows inline with the amount. */
.metric__arrow {
  display: inline-block;
  width: 0.72em;
  height: 0.72em;
  vertical-align: -0.05em;
  margin-right: var(--space-1);
}
/* Down = the same triangle rotated; flat = an en-dash instead of any arrow. */
.metric[data-direction="down"] .metric__arrow { transform: rotate(180deg); }
.metric[data-direction="flat"] .metric__arrow,
.metric[data-direction="flat"] .metric__amount { display: none; }
.metric__delta-figure::before { content: "\2013"; display: none; } /* en-dash */
.metric[data-direction="flat"] .metric__delta-figure::before { display: inline; }

/* --- Delta colour logic (polarity × direction) ---------------------------- */
/* Polarity defaults to up-good when the attribute is absent, hence the
   :not([data-polarity="up-bad"]) guards below. */
.metric[data-direction="up"]:not([data-polarity="up-bad"]),
.metric[data-polarity="up-bad"][data-direction="down"] {
  --metric-delta-color: var(--positive);
}
.metric[data-direction="down"]:not([data-polarity="up-bad"]),
.metric[data-polarity="up-bad"][data-direction="up"] {
  --metric-delta-color: var(--negative);
}
.metric[data-direction="flat"] {
  --metric-delta-color: var(--text-secondary);
}

/* --- Target comparison ----------------------------------------------------
   A metric measured against a fixed GOAL rather than against a previous period.
   Same delta slot, same --metric-delta-color hook, same render path — this adds a
   new way to DECIDE the colour, not a new way to draw it.

   OPT-IN AND ADDITIVE: a tile with no data-target-state is untouched, so every
   existing metric in the app renders exactly as before.

   MET IS GREEN, BELOW IS AMBER — deliberately not --negative. A period delta that
   went the wrong way is a fact about what happened; a daily target not yet met is a
   gap to close, and red overstates it on a figure that is short by one with hours
   of the day left. --warning is the system's "attention, not alarm" tone.

   MUTUALLY EXCLUSIVE WITH A TREND DELTA. One tile has one --metric-delta-color, so
   it can answer "how did this move?" or "is this on target?", not both. A target
   tile therefore carries NO data-direction — and note the polarity × direction
   rules above are (0,3,0) to these (0,2,0), so if both were set the direction
   would win. Setting both is a caller error, not a supported combination.

   The met/below decision is computed by the caller (HFMetric.targetState does the
   arithmetic) and written into the attribute, exactly as data-direction already is
   — so the colour is correct from the markup before any script runs. */
.metric[data-target-state="met"]   { --metric-delta-color: var(--positive); }
.metric[data-target-state="below"] { --metric-delta-color: var(--warning); }

/* --- No-data state -------------------------------------------------------- */
/* An em-dash value and a short phrase, both --text-tertiary. Deliberate, not
   broken: appears on new accounts and thin data. */
.metric--nodata .metric__value,
.metric--nodata .metric__delta {
  color: var(--text-tertiary);
}

/* --- TV mode: the value at display-1 ------------------------------------- */
.metric--tv .metric__value {
  font: var(--type-display-1);
  letter-spacing: var(--track-display-1);
}

/* --- Graceful value size step-down ---------------------------------------- */
/* metric.js sets data-value-length from the rendered value's character count
   (short ≤6, medium 7–9, long ≥10). Long financial values step down instead of
   overflowing — precision is kept, never abbreviated to a rounded "k". Short
   values stay at display-2. TV steps down from display-1 by the same ratio
   (display-1 / display-2 = 3.5 / 2.5 = 1.4), since TV has no horizontal scroll. */
.metric[data-value-length="medium"] .metric__value { font-size: var(--metric-value-md); }
.metric[data-value-length="long"] .metric__value { font-size: var(--metric-value-lg); }
.metric--tv[data-value-length="medium"] .metric__value { font-size: calc(var(--metric-value-md) * 1.4); }
.metric--tv[data-value-length="long"] .metric__value { font-size: calc(var(--metric-value-lg) * 1.4); }

/* --- Sparkline ------------------------------------------------------------ */
.metric__spark {
  /* Shrinks before anything else: the number keeps its intrinsic width (flex's
     default min-width: auto protects it), while min-width: 0 lets the sparkline
     give way. Overflow is hidden because the drawing is inset by the dot radius
     in metric.js, so nothing needs to spill. */
  flex: 0 1 var(--spark-w);
  min-width: 0;
  width: var(--spark-w);
  height: var(--spark-h);
  overflow: hidden;
}
.metric__spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.metric__spark-dot { fill: var(--accent); }

/* A sparkline squashed to nothing communicates nothing, so it disappears rather
   than compressing. The container is the value row, which is inset from the
   metric by its --space-6 padding, so ~140px here is a metric of ~200px. */
@container (max-width: 140px) {
  .metric__spark { display: none; }
}

/* =============================================================================
   METRIC ROW
   A row of metrics separated by hairline vertical rules — a 1px --border-subtle
   left border on every metric but the first, with --space-6 of padding either
   side of that border. No cards, no gaps beyond that padding.

   Alignment: the row is a grid of three shared rows (label / value / delta) and
   each metric is a subgrid spanning them, so every label, value and delta lines
   up across the row however many lines a single label wraps to. Browsers without
   subgrid keep the flex fallback below (self-contained metrics, no cross-metric
   alignment) rather than something broken.

   Wrapping: 2 columns below 900, 1 column below 640. The rules resolve on wrap
   with no orphaned border at the start of a wrapped line — the wrapped states are
   self-contained metrics in a known-column grid where :nth-child(odd) starts each
   wrapped row.
============================================================================= */
.metric-row {
  display: flex;
}
.metric-row > .metric {
  flex: 1 1 0;
  min-width: 0;
  padding-inline: var(--space-6);
  border-left: 1px solid var(--border-subtle);
}
.metric-row > .metric:first-child {
  padding-left: 0;
  border-left: none;
}

/* Subgrid alignment (progressive enhancement over the flex fallback above). */
@supports (grid-template-rows: subgrid) {
  .metric-row {
    display: grid;
    grid-auto-flow: column;
    /* Columns size to their content — a six-figure value takes the room it needs,
       a single digit gives it up — while auto-max tracks stretch to absorb any
       leftover, so the right edge is never ragged. Subgrid rows (below) are
       unaffected, so labels / values / deltas still align across the row. */
    grid-auto-columns: minmax(min-content, auto);
    justify-content: stretch;
    grid-template-rows: auto auto auto;
  }
  .metric-row > .metric {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    align-items: start;
  }
}

@media (max-width: 900px) {
  /* Two self-contained columns; the vertical rule sits on column two (even), so
     column one (odd) starts each wrapped row with no orphaned border. Overrides
     the subgrid above at this width. */
  .metric-row {
    display: grid;
    grid-auto-flow: row;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: none;
    row-gap: var(--space-5);
  }
  .metric-row > .metric {
    display: flex;
    grid-row: auto;
    grid-template-rows: none;
  }
  .metric-row > .metric:nth-child(odd) {
    padding-left: 0;
    border-left: none;
  }
}

@media (max-width: 640px) {
  .metric-row {
    grid-template-columns: 1fr;
    row-gap: 0;
  }
  /* Stacked: no vertical rules; a top hairline separates instead. */
  .metric-row > .metric {
    padding-inline: 0;
    border-left: none;
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
  }
  .metric-row > .metric:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
  }
}

/* =============================================================================
   TABLE
   The shared data table — structure, sorting, density and responsive reflow.
   Deliberately spare: no zebra striping, no outer border, no vertical column
   rules, no shadow. Numbers line up because base.css sets tabular figures
   globally; right-alignment of numeric columns is the only extra needed.
============================================================================= */
.table {
  width: 100%;
  border-collapse: separate;   /* keeps the sticky header's border on the cell */
  border-spacing: 0;
  color: var(--text-primary);
}

/* Density → row height. data-density may sit on the table or any ancestor; the
   cells read --row-h with a comfortable fallback, so nothing needs it set. */
[data-density="compact"]     { --row-h: var(--row-h-compact); }
[data-density="comfortable"] { --row-h: var(--row-h-comfortable); }
[data-density="spacious"]    { --row-h: var(--row-h-spacious); }

/* --- Header --------------------------------------------------------------- */
.table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  height: var(--row-h, var(--row-h-comfortable));
  padding-inline: var(--space-3);
  text-align: left;
  vertical-align: middle;
  white-space: nowrap;
  font: var(--type-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  background: var(--surface-page);   /* opaque when stuck so rows don't bleed through */
  border-bottom: 1px solid var(--border-default);
}

/* The sort trigger is a real <button>, so focus + keyboard come from base.css.
   It inherits the header's label type via base.css's `button { font: inherit }`. */
.table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}
.table__caret {
  width: 0.7em;
  height: 0.7em;
  flex: none;
  color: var(--accent);
  opacity: 0;                        /* inactive columns: hidden */
  transition: opacity var(--motion-micro) var(--ease),
              transform var(--motion-micro) var(--ease);
}
.table th[aria-sort] .table__caret { opacity: 1; }                    /* active column */
.table th[aria-sort="descending"] .table__caret { transform: rotate(180deg); }
/* Discoverable, not noisy: low opacity on hover/focus of an inactive header. */
.table th:not([aria-sort]) .table__sort:hover .table__caret,
.table th:not([aria-sort]) .table__sort:focus-visible .table__caret { opacity: 0.35; }

/* --- Body ----------------------------------------------------------------- */
.table tbody td {
  height: var(--row-h, var(--row-h-comfortable));
  padding-inline: var(--space-3);
  vertical-align: middle;
  font: var(--type-body);
  color: var(--text-primary);
}
/* Separators sit between rows only — none before the first or after the last. */
.table tbody tr + tr > td { border-top: 1px solid var(--border-subtle); }
.table tbody tr { transition: background-color var(--motion-micro) var(--ease); }
.table tbody tr:hover { background: var(--surface-hover); }

/* Numeric / currency columns right-align (JS tags them from the header type);
   text and date columns stay left-aligned by default. */
.table .is-numeric { text-align: right; }

/* --- Column tag ----------------------------------------------------------- */
/* A quiet, small marker inside a column header that qualifies what the column
   means — e.g. an ATTRIBUTION basis ("assigned", "contact") on a pivot whose
   column attributes differently from its neighbours, so the difference is on the
   exact column that would otherwise be misread, not only in a note. Lowercase,
   tertiary, smaller than the header. */
.table__col-tag { font-weight: 400; font-size: 0.85em; color: var(--text-tertiary); }

/* --- Totals row (tfoot) --------------------------------------------------- */
/* A pivot's totals row. It lives in a <tfoot>, which HFTable never sorts (it orders
   tBodies[0] only), so it stays pinned at the foot under any column sort. A stronger
   top rule and medium weight separate it from the data. It holds the SUM OF THE
   VISIBLE rows — which differs from a source's own grand total whenever records fall
   outside the breakdown; that difference is surfaced by the data-quality warning below.
   While a search or a column filter narrows the body, sortable-table.js sets [hidden] here
   and the row is removed rather than recomputed: its label is the page's ("All managers"),
   and a filtered total under it would be a correct figure making a false claim. Stated
   explicitly below rather than left to the user-agent cascade — an author `display` on a
   component silently defeats [hidden], which is a bug this stylesheet has had before. */
.table tfoot[hidden] { display: none; }
.table tfoot td {
  height: var(--row-h, var(--row-h-comfortable));
  padding-inline: var(--space-3);
  vertical-align: middle;
  font: var(--type-body);
  font-weight: 600;
  color: var(--text-primary);
  border-top: 2px solid var(--border-strong);
}

/* --- Data-quality warning ------------------------------------------------- */
/* A QUIET, actionable note beneath a table: records that fell OUTSIDE the shown
   breakdown, with a control to open them for review. Informational tone — a soft
   surface and an accent left rule — NEVER the red error alert: it is a gap to
   investigate, not a fault. The open control is a link-style button. */
.hf-data-warning {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--border-accent);
  background: var(--surface-hover);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
}
.hf-data-warning__open {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}
.hf-data-warning__open:hover { text-decoration: none; }
.hf-data-warning__open:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
/* An author `display: flex` BEATS the user-agent's `[hidden] { display: none }` — author origin wins over UA
   origin regardless of specificity — so without this rule every one of these notes stayed visible when its JS
   set hidden, painting an empty tinted bar with an accent rule and nothing in it. There is no global [hidden]
   reset in this stylesheet (the shell menu and the drill-down overlay each carry their own), so any component
   that sets an explicit `display` must restate it. */
.hf-data-warning[hidden] { display: none; }

/* --- Empty state ---------------------------------------------------------- */
/* A designated row the component reveals when there are no body rows. The page
   supplies the sentence (never "No data") and an optional single action. */
.table__empty > td {
  padding: var(--space-7) var(--space-4);
  text-align: center;
}
.table__empty-message {
  margin: 0 auto var(--space-3);
  max-width: 48ch;
  font: var(--type-body);
  color: var(--text-secondary);
}
.table__empty-message:last-child { margin-bottom: 0; }

/* --- Search toolbar (injected above the table; strip leaves room for future
       filter / export controls without the search having to move) ----------- */
.table-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
}
.table-search {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 40ch;                 /* caps its width so other controls can sit right */
}
.table-search .field {
  height: var(--control-h-sm);     /* the small control height */
  width: 100%;
  padding-left: var(--space-6);    /* room for the magnifier */
  padding-right: var(--space-6);   /* room for the clear control */
}
.table-search .field::-webkit-search-cancel-button,
.table-search .field::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;                /* use our own clear, not the native one */
}
.table-search__icon {
  position: absolute;
  left: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  width: 1em;
  height: 1em;
  color: var(--text-tertiary);
  pointer-events: none;
}
.table-search__clear {
  position: absolute;
  right: var(--space-1);
  top: 50%;
  transform: translateY(-50%);
  display: none;                   /* only shown while there is a query */
  align-items: center;
  justify-content: center;
  width: var(--space-5);
  height: var(--space-5);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: color var(--motion-micro) var(--ease),
              background-color var(--motion-micro) var(--ease);
}
.table-search__clear:hover { color: var(--text-primary); background: var(--surface-hover); }
.table-search__clear svg { width: 0.8em; height: 0.8em; }
.table-search.has-query .table-search__clear { display: inline-flex; }
.table-search-count {
  margin: 0;
  color: var(--text-secondary);    /* label type comes from the .label class */
}
.table-search-count:empty { display: none; }  /* collapse when no query is active */

/* Clear-all for the per-column filters. Link-styled rather than a button block: it is an undo, subordinate to
   the search field beside it. Shown only while at least one filter is applied — see updateFilterBar. */
.table-toolbar__clear-filters {
  margin-left: auto;                 /* pushed to the trailing edge, away from the search */
  border: 0;
  background: none;
  padding: 0;
  font: var(--type-body-sm);
  font-weight: 600;
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}
.table-toolbar__clear-filters:hover { text-decoration: none; }
.table-toolbar__clear-filters:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.table-toolbar__clear-filters[hidden] { display: none; }

/* =============================================================================
   PER-COLUMN FILTER
   -----------------------------------------------------------------------------
   A trigger in each column header, and a popover holding either a checkbox picker
   (few distinct values) or a text search (many). Which kind is chosen by the data,
   in sortable-table.js — the CSS styles both and knows about neither.

   THE POPOVER IS PORTALED TO <body> AND POSITIONED IN VIEWPORT COORDINATES, and it
   has to be. Inside the drill-down panel the obvious approach fails twice over:
   .drilldown__panel carries a transform, which makes it the containing block for
   position:fixed descendants (so "fixed" would mean the panel, not the viewport),
   and overflow:hidden, which clips them at the panel's edge. Anchoring inside the
   scrolling .drilldown__body is worse still — the popover would scroll away from a
   header that is itself sticky. Rendering in <body>, where no ancestor transforms,
   makes position:fixed mean the viewport and behave identically on a page-level
   table and inside the panel, which is the requirement.

   z-index sits above .drilldown (60) and below .skip-link (200).
============================================================================= */
.table__filter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4em; height: 1.4em;
  margin-left: var(--space-1);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  vertical-align: middle;
  transition: color var(--motion-micro) var(--ease), background-color var(--motion-micro) var(--ease);
}
.table__filter:hover { color: var(--text-primary); background: var(--surface-hover); }
.table__filter:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 1px; }
.table__filter-icon { width: 0.75em; height: 0.75em; }
/* An applied filter reads in the header WITHOUT opening anything: the glyph fills and takes the accent, and
   the header takes a tint. Never colour alone — the trigger's accessible name states what is applied. */
.table__filter.is-active { color: var(--accent); }
.table__filter.is-active .table__filter-icon { fill: currentColor; }
.table thead th.is-filtered { background: var(--surface-selected); }

.table-filter-pop {
  position: fixed;
  z-index: 90;
  min-width: 13rem;
  max-width: min(20rem, calc(100vw - 2 * var(--space-4)));
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-modal);
}
.table-filter-pop__head { margin: 0; color: var(--text-secondary); }
/* The list scrolls rather than growing past the viewport. The picker threshold keeps it short in practice, so
   this is the guard for a narrow window rather than the normal case. */
.table-filter-pop__body { max-height: min(18rem, 50vh); overflow-y: auto; }
.table-filter-pop__list { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; }
.table-filter-pop__opt {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-1);
  border-radius: var(--radius-sm);
  font: var(--type-body-sm);
  color: var(--text-primary);
  cursor: pointer;
  /* Values are shown exactly as the cell reads, so a long one wraps rather than being clipped to a stub the
     reader cannot tell apart from its neighbour. */
  overflow-wrap: anywhere;
}
.table-filter-pop__opt:hover { background: var(--surface-hover); }
.table-filter-pop__opt input { flex: none; }
.table-filter-pop__foot { display: flex; justify-content: flex-end; gap: var(--space-2); }

/* Match highlight — inherited colour, no extra weight; only ever a text wrapper. */
.table mark {
  background: var(--surface-selected);
  color: inherit;
  font-weight: inherit;
  border-radius: var(--radius-sm);
}

/* No-results state — distinct meaning from the empty state, shared layout. */
.table__noresults > td {
  padding: var(--space-7) var(--space-4);
  text-align: center;
}
.table__noresults-message {
  margin: 0 auto var(--space-3);
  max-width: 48ch;
  font: var(--type-body);
  color: var(--text-secondary);
}

/* --- Drill-down rows ------------------------------------------------------ */
/* A chevron button toggles the child; the whole parent row is clickable too
   (guarded in JS against interactive targets and text selection). */
.table__expand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-5);
  height: var(--space-5);
  margin-right: var(--space-2);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  vertical-align: middle;
  transition: transform var(--motion-standard) var(--ease),
              color var(--motion-micro) var(--ease),
              background-color var(--motion-micro) var(--ease);
}
.table__expand:hover { color: var(--text-primary); background: var(--surface-hover); }
.table__expand svg { width: 0.7em; height: 0.7em; }
.table__expand[aria-expanded="true"] { transform: rotate(180deg); }
.table__has-drill { cursor: pointer; }

/* The child region sits on --surface-sunken, inset from the left. Its height is
   animated by transitioning a wrapper's grid rows 0fr -> 1fr — a <tr> can't be
   given a reliable height, so the row itself is never animated. Reduced motion is
   honoured by the global block in base.css. */
/* Closed: genuinely zero height — no background, no border, and no row-height
   floor. Only the chevron indicates the row can expand. Specific enough to beat
   the body-cell height/padding and the row-separator rules. A parent and its
   child are one unit, so there is never a separator between them; the row that
   follows the drill-down still draws its own separator via `tr + tr`. */
.table tbody tr.table__drilldown > td {
  height: auto;
  padding: 0;
  border-top: none;
}
/* Background only while open, so a closed row shows no tinted band. */
.table tbody tr.table__drilldown.is-open > td {
  background: var(--surface-sunken);
}
.table__drilldown-inner {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--motion-standard) var(--ease);
}
.table__drilldown.is-open .table__drilldown-inner { grid-template-rows: 1fr; }
.table__drilldown-content {
  overflow: hidden;
  min-height: 0;
}
/* The padding lives on the nested child table, not on the overflow-hidden wrapper,
   so the 0fr grid collapse clips it and the closed state is truly zero height. */
.table__drilldown-content > .table {
  padding: var(--space-4) var(--space-4) var(--space-4) var(--space-6);
}

/* --- Data-error rows ------------------------------------------------------ */
/* Shown as a fix-list but excluded from every aggregate. A subtle --negative tint
   mixed against the surface (so it adapts across themes) plus a dot; the reason
   rides on the row title and the dot's aria label, never as visible text. */
.table__error > td {
  background: color-mix(in srgb, var(--negative) 8%, var(--surface-page));
}
.table__error:hover > td {
  background: color-mix(in srgb, var(--negative) 14%, var(--surface-page));
}
.table__error-dot {
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  margin-right: var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--negative);
  vertical-align: middle;
}
.table__error-footer {
  margin-top: var(--space-3);
  color: var(--negative);
}

/* --- Reflow sort bar (built by JS; shown only in the stacked layout) ------- */
.table__sortbar { display: none; }
.table__sortbar-field { flex: 1 1 auto; }
.table__sortbar-dir .table__caret { opacity: 1; }
.table__sortbar-dir.is-desc .table__caret { transform: rotate(180deg); }

/* --- Responsive: stack each row into a block below 640 -------------------- */
@media (max-width: 640px) {
  /* Search stays visible, goes full width, and sits above the sort bar; the count
     drops onto its own line so it still reads clearly. */
  .table-search { max-width: none; flex: 1 1 100%; }
  .table-search-count { flex: 1 1 100%; }
  .table__sortbar {
    display: flex;
    align-items: flex-end;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
  }
  .table thead { display: none; }               /* header row hidden when stacked */
  .table,
  .table tbody { display: block; width: 100%; }
  .table tbody tr { display: block; padding: var(--space-3) 0; }
  .table tbody tr + tr { border-top: 1px solid var(--border-subtle); } /* row separator */
  .table tbody td {
    display: flex;
    justify-content: space-between;
    gap: var(--space-4);
    height: auto;
    padding: var(--space-1) 0;
  }
  .table tbody tr + tr > td { border-top: none; } /* separator moved to the row */
  .table tbody td::before {
    content: attr(data-label);                    /* column name from the cell contract */
    flex: none;
    font: var(--type-label);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
    color: var(--text-secondary);
    text-align: left;
  }
  /* The empty / no-results rows keep a centred message, not a label / value pair. */
  .table tbody tr.table__empty { padding: 0; border-top: none; }
  .table tbody tr.table__empty > td { display: block; }
  .table tbody tr.table__empty > td::before { content: none; }

  /* Drill-down rows: the nested child table reflows itself; the wrapper cell just
     stays a block with no label. */
  .table tbody tr.table__drilldown { padding: 0; border-top: none; }
  .table tbody tr.table__drilldown > td { display: block; padding: 0; }
  .table tbody tr.table__drilldown > td::before { content: none; }
}

/* Visually-hidden helper for captions that carry accessible names only. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* =============================================================================
   CHART
   The shared wrapper chrome and static SVG rendering for line, bar and donut.
   Charts are drawn in real pixel space by charts.js and redrawn on resize, so
   nothing is stretched. Colours come from the categorical --chart-* palette and
   the semantic tokens; no hex here. Draw-on animation is a later slice — the only
   motion is hover feedback and the loading shimmer.
============================================================================= */
.chart {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* A CONTAINER, so the head can respond to how wide THIS CHART is rather than how wide the window is.
     That distinction is the whole bug: in a small-multiples grid a chart is ~450px on a 1600px viewport,
     so a viewport media query never fires and the legend stays crammed beside the title. Same mechanism
     the metric component already uses. */
  container: chart / inline-size;
}

/* --- Head: title / subtitle + legend -------------------------------------- */
.chart__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}
.chart__title { margin: 0; color: var(--text-primary); }
.chart__subtitle { margin: var(--space-1) 0 0; color: var(--text-secondary); }
.chart__legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  color: var(--text-secondary);
  text-align: right;
}
/* The swatch aligns to the label's FIRST LINE, not to the middle of the whole label. With align-items:
   center a label that wraps to two lines pushed its swatch down to the block's centre — i.e. into the gap
   BETWEEN the two lines — which is the misalignment seen on charts with long series names. flex-start plus
   half the leading reproduces centred alignment exactly for the single-line case ((1.2em − 0.6em) / 2 is
   the offset centring already produced), so nothing changes until the label actually wraps. 1.2em is the
   --type-label line-height; the swatch is sized in the same em, so the two stay locked together. */
.chart__legend-item {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--space-1);
  color: var(--text-secondary);
}
.chart__swatch {
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  margin-top: calc((1.2em - 0.6em) / 2);
  border-radius: var(--radius-sm);
  flex: none;
}

/* --- Notice (caps / cropped-axis) ----------------------------------------- */
.chart__notice { margin: 0; color: var(--warning); }

/* --- Chart area ----------------------------------------------------------- */
.chart__area {
  position: relative;
  width: 100%;
  background: var(--surface-page);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.chart__area:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.chart__plot,
.chart__skeleton {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.chart__skeleton { display: none; }

/* SVG marks */
.chart__gridline { stroke: var(--border-subtle); stroke-width: 1; }
.chart__axis-label {
  fill: var(--text-secondary);
  font: var(--type-label);
  letter-spacing: var(--track-label);
}
.chart__line {
  fill: none;
  stroke-width: 2;                 /* fine, not heavy */
  stroke-linejoin: round;
  stroke-linecap: round;
}
.chart__dot { stroke: var(--surface-page); stroke-width: 1.5; }
.chart__bar { stroke: none; }
.chart__arc { stroke: none; }
.chart__axis-break { stroke: var(--text-tertiary); stroke-width: 1; fill: none; }
.chart__hoverband { fill: transparent; }
/* A SELECTABLE point (the chart was given an onSelect and this index has data). The pointer cursor and
   the faint band say "this opens something" before it is clicked, and — just as important — their
   ABSENCE on a gap month says the opposite without a word. Only selectable bands get the class, so an
   unselectable point cannot accidentally look interactive. */
.chart__hoverband.is-selectable { cursor: pointer; }
.chart__hoverband.is-selectable:hover { fill: var(--surface-hover); }

/* --- Draw-on entrance ------------------------------------------------------
   The keyframes for this live here, with the component, not in motion.css: they
   are transitions bound to the chart's own SVG marks (a line's dashoffset, a bar
   column's scaleY, a final dot's opacity), not a reusable page-level entrance
   pattern — nothing outside a chart would ever opt in. charts.js owns the timing:
   it decides WHEN to animate (once, on first data, on scroll-in), sets each mark's
   start and end values plus its stagger delay, and adds .is-drawing to arm the
   transition — so these rules stay inert on every resize redraw. The donut sweep
   and centre count-up are driven by requestAnimationFrame in charts.js instead, as
   a growing filled arc can't be expressed as a CSS transition without changing the
   geometry. Bar columns scale from the zero baseline (their group's bottom edge).
   The stroke and the column grow both run over --motion-draw on --ease-data — the
   long, gentle data-draw timing the donut shares in JS — so all four types complete
   together. The final-point dot is a supporting detail: a longer, gentler fade than
   feedback (--motion-data), delayed by charts.js until its own line has finished. */
.chart__bar-group { transform-box: fill-box; transform-origin: center bottom; }
.chart__line.is-drawing { transition: stroke-dashoffset var(--motion-draw) var(--ease-data); }
.chart__dot.is-drawing { transition: opacity var(--motion-data) var(--ease); }
.chart__bar-group.is-drawing { transition: transform var(--motion-draw) var(--ease-data); }

/* Reduced motion: charts.js already skips the entrance entirely (it never adds
   .is-drawing and draws the final state, centre figure included), so this is
   belt-and-braces against any stray transition. */
@media (prefers-reduced-motion: reduce) {
  .chart__line.is-drawing,
  .chart__dot.is-drawing,
  .chart__bar-group.is-drawing { transition: none; }
}

/* Donut centre — Presti display-3, the metric value treatment. */
.chart__donut-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  text-align: center;
}
.chart__donut-value { color: var(--text-primary); line-height: 1; }
.chart__donut-label { color: var(--text-tertiary); margin-top: var(--space-1); }

/* --- Tooltip (raised surface, popover shadow, flips within bounds) -------- */
.chart__tooltip {
  position: absolute;
  z-index: 2;
  max-width: 60%;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-popover);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.chart__tooltip-title { margin: 0 0 var(--space-1); color: var(--text-secondary); }
.chart__tooltip-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
  margin: 0;
  color: var(--text-primary);
  white-space: nowrap;
}
.chart__tooltip-val { margin-left: auto; padding-left: var(--space-3); font-variant-numeric: tabular-nums; }

/* --- Foot: last-refreshed + optional status ------------------------------- */
.chart__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.chart__refreshed { margin: 0; color: var(--text-tertiary); }
.chart__status { margin: 0; color: var(--text-secondary); }

/* --- Empty & error states ------------------------------------------------- */
.chart__empty,
.chart__error {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-5);
  text-align: center;
}
.chart__empty { color: var(--text-secondary); }
.chart__error-msg { margin: 0; color: var(--negative); }

/* --- Refresh hairline (indeterminate) ------------------------------------- */
.chart__refresh-bar {
  position: absolute;
  top: 0;
  left: 0;
  height: 2px;
  width: 30%;
  display: none;
  background: var(--accent);
  border-radius: var(--radius-pill);
}

/* --- Loading skeleton shapes + shimmer ------------------------------------ */
.chart__skel-grid { stroke: var(--surface-hover); stroke-width: 1; }
.chart__skel-shape { fill: var(--surface-hover); stroke: none; }
.chart__shimmer {
  position: absolute;
  inset: 0;
  display: none;
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent 35%, var(--surface-raised) 50%, transparent 65%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  opacity: 0.6;
  /* One slow sweep, same direction and speed on every chart so several loading at
     once read as one system, on the dedicated ambient-loop token. */
  animation: hf-chart-shimmer var(--motion-ambient) linear infinite;
}
@keyframes hf-chart-shimmer {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}

/* =============================================================================
   CHART STATES — driven by data-state on the figure
============================================================================= */
/* Loading (never had data): full skeleton on a sunken surface, chrome as blocks. */
.chart[data-state="loading"] .chart__area { background: var(--surface-sunken); }
.chart[data-state="loading"] .chart__plot { display: none; }
.chart[data-state="loading"] .chart__skeleton { display: block; }
.chart[data-state="loading"] .chart__shimmer { display: block; }
.chart[data-state="loading"] .chart__donut-center { display: none; }
.chart[data-state="loading"] .chart__title,
.chart[data-state="loading"] .chart__subtitle,
.chart[data-state="loading"] .chart__refreshed {
  color: transparent;
  background: var(--surface-hover);
  border-radius: var(--radius-sm);
}
.chart[data-state="loading"] .chart__title { width: 45%; }
.chart[data-state="loading"] .chart__subtitle { width: 60%; }
.chart[data-state="loading"] .chart__refreshed { width: 30%; }
.chart[data-state="loading"] .chart__legend::before,
.chart[data-state="loading"] .chart__legend::after {
  content: "";
  display: inline-block;
  width: var(--space-8);
  height: 0.7em;
  background: var(--surface-hover);
  border-radius: var(--radius-pill);
}

/* Refreshing (has data): keep the chart, dim it, run a thin hairline. Never blank
   a chart the user is currently reading. */
.chart[data-state="refreshing"] .chart__plot { opacity: 0.5; }
.chart[data-state="refreshing"] .chart__refresh-bar {
  display: block;
  animation: hf-chart-refresh var(--motion-ambient) var(--ease) infinite;
}
@keyframes hf-chart-refresh {
  0%   { left: 0;    width: 15%; }
  50%  { left: 42%;  width: 30%; }
  100% { left: 100%; width: 15%; }
}

/* Empty & error replace the plot area. */
.chart[data-state="empty"] .chart__plot,
.chart[data-state="empty"] .chart__donut-center,
.chart[data-state="error"] .chart__plot,
.chart[data-state="error"] .chart__donut-center { display: none; }
.chart[data-state="empty"] .chart__empty { display: flex; }
.chart[data-state="error"] .chart__error { display: flex; }

/* --- Responsive: legend drops under the title on narrow figures -----------
   Keyed on the CHART's own width, not the viewport's. The old viewport media query only fired on a phone,
   so a chart sitting in a small-multiples grid column — narrow on a wide screen — kept a squeezed legend
   beside a title that had wrapped to two lines, and the two read as misaligned. Below 560px the title and
   legend stop competing for one row and the legend takes its own full-width line.
   This SUBSUMES the old max-width:640px viewport rule: a phone-width chart is itself narrow, so the
   container query fires there too. */
@container chart (max-width: 560px) {
  .chart__head { flex-direction: column; gap: var(--space-2); }
  .chart__legend { text-align: left; }
}

/* --- Reduced motion: static skeleton, no shimmer, no hairline animation --- */
@media (prefers-reduced-motion: reduce) {
  .chart__shimmer { animation: none; opacity: 0; }
  .chart[data-state="refreshing"] .chart__refresh-bar { animation: none; width: 100%; left: 0; opacity: 0.5; }
}

/* =============================================================================
   REPORT SCAFFOLD (hf-report.js) — the shared chrome of a Team Reports page:
   the freshness line, the staleness banner (page-level analogue of the table's
   per-row data-error), the row-count note, a flagged cell, an in-cell sub-line,
   quiet loading/empty text, and a wrapping table cell. Domain-free — a page picks
   these classes in its row/metric builders; the scaffold styles them once.
============================================================================= */
.hf-report-freshness { display: flex; align-items: baseline; gap: var(--space-2) var(--space-3); flex-wrap: wrap; margin: 0 0 var(--space-4); color: var(--text-secondary); font: var(--type-body-sm); }
.hf-report-stale { margin: 0 0 var(--space-5); padding: var(--space-3) var(--space-4); background: var(--surface-sunken); border-left: 3px solid var(--warning); border-radius: var(--radius-md); color: var(--text-primary); font: var(--type-body-sm); }
.hf-report-stale strong { font-weight: 600; }
.hf-report-note { margin-top: var(--space-3); color: var(--text-secondary); font: var(--type-body-sm); }
.hf-report-flag { color: var(--warning); font-weight: 700; }
/* Compliance alert banner (hf-report.js alert mechanism): louder than the stale banner — a NEGATIVE edge,
   because it means action is outstanding, not merely that the data is old. */
.hf-report-alert { display: flex; align-items: baseline; gap: var(--space-2) var(--space-3); flex-wrap: wrap; margin: 0 0 var(--space-5); padding: var(--space-3) var(--space-4); background: var(--surface-sunken); border-left: 3px solid var(--negative); border-radius: var(--radius-md); color: var(--text-primary); font: var(--type-body-sm); }
.hf-report-alert strong { font-weight: 600; }
.hf-report-alert-action { background: none; border: 0; padding: 0; color: var(--text-accent); font: inherit; cursor: pointer; text-decoration: underline; }
.hf-report-subline { display: block; color: var(--text-tertiary); font: var(--type-micro); }
.hf-report-muted { color: var(--text-secondary); }

/* --- Grouped-severity report (shared; rendered by hf-report.js `table.groups`; demonstrated in the styleguide) --
   A compliance report split into severity-ranked, collapsible groups instead of one flat list with a status
   column. The weight descends through the LEFT ACCENT RULE + the count treatment ONLY — headings are uniform and
   there are no filled blocks. A page maps each of its groups to one step of this severity vocabulary:
     negative-strong (gravest) → negative → warning-strong → warning → positive / neutral (reassurance). */
.hf-groups { display: flex; flex-direction: column; gap: var(--space-6); margin-top: var(--space-6); }

/* The accent spine runs the WHOLE group (header + its table); content clears it via padding-left. */
.hf-group { padding-left: var(--space-4); }
.hf-group--negative-strong { border-left: 3px solid var(--negative); }
.hf-group--negative        { border-left: 2px solid var(--negative); }
.hf-group--warning-strong  { border-left: 2px solid var(--warning); }
.hf-group--warning         { border-left: 1px solid color-mix(in srgb, var(--warning) 45%, var(--border-default)); }
.hf-group--positive        { border-left: 1px solid color-mix(in srgb, var(--positive) 55%, var(--border-default)); }
.hf-group--neutral         { border-left: 1px solid var(--border-default); }

/* The head is a flex ROW OF TWO SIBLING CONTROLS: the header button (chevron + name) expands the group, and
   the count opens its records in the drill-down panel. They were one button until the count became a drill
   target — a button inside a button is invalid, and browsers resolve it by hoisting the inner one out. */
.hf-group__head { display: flex; align-items: center; gap: var(--space-2); }
.hf-group__header {
  display: flex; align-items: center; gap: var(--space-2);
  flex: 1 1 auto; min-width: 0;                 /* takes the row, leaving the count its intrinsic width */
  padding: var(--space-2) 0; margin: 0;
  background: none; border: 0; cursor: pointer; text-align: left; color: var(--text-primary);
}
.hf-group__chevron {
  width: 0.7em; height: 0.7em; flex: none; color: var(--text-tertiary);
  transition: transform var(--motion-standard) var(--ease);
}
.hf-group__header[aria-expanded="true"] .hf-group__chevron { transform: rotate(90deg); }

/* Uniform heading type across every group — equal siblings; severity reads through the rule + count, not type. */
.hf-group__name { font: var(--type-body); font-weight: 600; }

/* Count. The GRAVEST group (negative-strong) shows a plain coloured number so it reads most plainly; every other
   group shows a small tinted pill in its own colour. */
/* Scoped to .hf-group (rather than the bare class) deliberately: the count now also carries .drill-value, whose
   `font: inherit` sits later in this file and would otherwise reset the weight below to the head's. The count
   only ever appears inside a group, so the extra specificity costs nothing and pins the typography. */
.hf-group .hf-group__count { margin-left: var(--space-1); font-weight: 600; }
.hf-group--negative-strong .hf-group__count { color: var(--negative); }
.hf-group:not(.hf-group--negative-strong) .hf-group__count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6em; padding: 0 var(--space-2); border-radius: var(--radius-pill); font: var(--type-label); font-weight: 600;
}
.hf-group--negative .hf-group__count       { background: color-mix(in srgb, var(--negative) 14%, var(--surface-page)); color: var(--negative); }
.hf-group--warning-strong .hf-group__count { background: color-mix(in srgb, var(--warning) 15%, var(--surface-page)); color: var(--text-accent); }
.hf-group--warning .hf-group__count        { background: color-mix(in srgb, var(--warning) 8%, var(--surface-page));  color: var(--text-secondary); }
.hf-group--positive .hf-group__count       { background: color-mix(in srgb, var(--positive) 14%, var(--surface-page)); color: var(--positive); }
.hf-group--neutral .hf-group__count        { background: var(--surface-sunken); color: var(--text-secondary); }

/* Collapsible body — the system's accordion motion (grid rows 0fr → 1fr; reduced motion honoured globally).
   overflow:hidden lets the 0fr collapse clip the body, but it ALSO makes an open body an inner scroll/clip
   container: if the 1fr row is ever computed taller than its <table> child, the clip traps an empty band the page
   scrolls into (a second scroll context). So once a group has finished opening the clip is dropped (.is-settled,
   toggled by hf-report.js on the grid-rows transitionend — and immediately for reduced motion / groups open at
   first paint): the open group grows to fit its content and the PAGE is the only scroll context. The clip is
   re-applied the instant a close begins, so the collapse still animates cleanly. */
.hf-group__body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--motion-standard) var(--ease); }
.hf-group__body.is-open { grid-template-rows: 1fr; }
.hf-group__body-inner { overflow: hidden; min-height: 0; }
.hf-group__body.is-settled .hf-group__body-inner { overflow: visible; }
.hf-group__body-inner > .table-scroll,
.hf-group__body-inner > .table { margin-top: var(--space-2); }

/* Empty under a filter: a single quiet, unalarming line naming the group — not a heavy header over a zero, not
   gone (which reads as not-loaded). No accent spine. */
.hf-group--empty { padding-left: 0; border-left: 0; }
.hf-group__empty { margin: 0; padding: var(--space-2) 0; border-top: 1px solid var(--border-subtle); color: var(--text-secondary); font: var(--type-body-sm); }

/* Interactive headline metric — the scaffold renders .metric--action for a metric that jumps to its group;
   pointer + hover + keyboard focus, consistent with the system's controls. */
.metric--action { cursor: pointer; border-radius: var(--radius-sm); transition: background-color var(--motion-micro) var(--ease); }
.metric--action:hover { background: var(--surface-hover); }
.metric--action:focus-visible { outline: 2px solid var(--border-accent); outline-offset: -2px; }
.table td.hf-report-wrap { white-space: normal; min-width: 200px; }

/* =============================================================================
   REPORT INDEX
   A way IN to a team's individual reports from its landing page — a responsive band of
   destination CARDS, each a report label plus a one-line description of what it holds,
   with a chevron marking it as a way out. Each card sits on --surface-raised with a
   --border-default edge so it reads as a destination, not a faint outline — both resolve
   per theme (raised = white on the off-white page in light, a lighter panel on the darker
   page in dark), and hover firms the edge to --border-strong. Sits directly under the page
   title, before the metrics; the reports it links are deliberately kept out of the sidebar
   (nav:false in lib/pages.js) and reached from here.

   Layout: auto-fit columns, min 260 / max 360px, so the band fills the row on wide
   content, collapses to fewer columns and stacks on narrow — with no media query, so it
   reflows to whatever content width the shell gives it (full width in drawer mode below
   900px, reduced beside the sidebar above it). The 360px CAP is load-bearing: a LONE
   card stays 360px rather than stretching the full content width. Cards in a row equalise
   to the tallest (grid stretch + the link filling its cell), so uneven description lengths
   give aligned card bottoms, not a ragged band. min(100%, 260px) keeps a card from
   overflowing a container narrower than 260px.

   The consuming page filters the links to what the viewer can open and hides the section
   when none remain — both are page decisions, not the component's.
============================================================================= */
.report-index {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 360px));
  gap: var(--space-4);
}
.report-index__item { display: flex; }   /* flex cell so its link stretches to the row height */

.report-index__link {
  flex: 1;
  display: flex;
  align-items: flex-start;               /* content to the top; short cards leave space below, not centred */
  gap: var(--space-3);
  padding: var(--space-4);
  text-decoration: none;
  color: var(--text-primary);
  background: var(--surface-raised);        /* lift the card off the page surface so it reads as a destination */
  border: 1px solid var(--border-default);  /* a definite edge, not the faint --border-subtle hairline */
  border-radius: var(--radius-md);
  transition: background-color var(--motion-micro) var(--ease), border-color var(--motion-micro) var(--ease), color var(--motion-micro) var(--ease);
}
.report-index__link:hover { border-color: var(--border-strong); }
.report-index__link:hover .report-index__label { color: var(--accent); }
.report-index__link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }

/* Text column grows and wraps; min-width:0 lets a long description wrap rather than force the chevron out. */
.report-index__text { display: flex; flex-direction: column; gap: var(--space-1); flex: 1 1 auto; min-width: 0; }
.report-index__label { font: var(--type-body); font-weight: 500; color: var(--text-primary); transition: color var(--motion-micro) var(--ease); }
.report-index__desc { font: var(--type-body-sm); color: var(--text-secondary); }

.report-index__chevron {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 1px;                        /* sits against the label's cap height */
  color: var(--text-tertiary);
  transition: color var(--motion-micro) var(--ease), transform var(--motion-micro) var(--ease);
}
.report-index__link:hover .report-index__chevron { color: var(--accent); transform: translateX(2px); }

/* =============================================================================
   DRILL-DOWN PANEL
   A modal-style panel that slides in from the right over the page, holding a title, a
   record count, and a table of the records behind whatever figure was clicked. It is the
   shared way to open the records behind any figure on the four team reports, so it lives
   here rather than being hand-rolled on one page. Driven by public/js/drilldown.js
   (window.HFDrilldown); see the styleguide "Drill-down panel" entry.

   Scroll architecture (the load-bearing part). The overlay is fixed and out of flow, so it
   never pushes the page or adds a page scrollbar. The panel is height:100% of the viewport
   with overflow:hidden — it never grows past the viewport. The ONLY scroll context is
   .drilldown__body; the head (title + count + close) is flex:none and stays put. The shared
   .table thead sticks at top:0 of .drilldown__body — it uses a LITERAL top:0 (not the page's
   --app-header-h), so inside this scroll container it needs no offset; only its opaque
   background is overridden to the panel surface. overscroll-behavior:contain stops scroll
   chaining to the page behind, which drilldown.js also makes inert ([inert] on .shell).
============================================================================= */
.drilldown { position: fixed; inset: 0; z-index: 60; display: flex; justify-content: flex-end; }
.drilldown[hidden] { display: none; }

.drilldown__scrim {
  position: absolute; inset: 0;
  background: var(--surface-inverse);      /* the same scrim token the shell drawer overlay uses */
  opacity: 0;
  transition: opacity var(--motion-standard) var(--ease);
}
.drilldown.is-open .drilldown__scrim { opacity: 0.5; }

.drilldown__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Wide enough for a four-column table with an address column to read without sideways scroll, while still
     leaving the page visible behind it on a desktop. Below the 900px drawer breakpoint it goes full width. */
  width: min(880px, 92vw);
  max-width: 100%;
  height: 100%;
  background: var(--surface-raised);
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-modal);
  transform: translateX(100%);
  transition: transform var(--motion-entrance) var(--ease);
  overflow: hidden;                        /* the panel itself never scrolls — only its body does */
}
@media (max-width: 900px) { .drilldown__panel { width: 100%; } }   /* drawer mode: effectively full width */
.drilldown.is-open .drilldown__panel { transform: translateX(0); }

.drilldown__head {
  flex: none;
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  /* Horizontal padding matches the main content column (--space-6) so content is not tight against the edge. */
  padding: var(--space-5) var(--space-6) var(--space-4);
  border-bottom: 1px solid var(--border-default);
}
.drilldown__titles { flex: 1 1 auto; min-width: 0; }
.drilldown__title { margin: 0; }           /* .h3 supplied in markup — subordinate to the page title */
.drilldown__count { margin: var(--space-1) 0 0; font: var(--type-body-sm); color: var(--text-secondary); }

.drilldown__close {
  flex: none;
  width: var(--control-h-md); height: var(--control-h-md);
  display: grid; place-items: center;
  background: none;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color var(--motion-micro) var(--ease), color var(--motion-micro) var(--ease);
}
.drilldown__close:hover { background: var(--surface-hover); color: var(--text-primary); }
.drilldown__close:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.drilldown__close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }

.drilldown__body {
  flex: 1 1 auto;
  min-height: 0;                           /* lets the flex child shrink so its own overflow engages */
  overflow-y: auto;                        /* THE single scroll context inside the panel */
  overscroll-behavior: contain;            /* no scroll chaining to the page behind */
  padding: 0 var(--space-6) var(--space-6); /* sides match the content column; top:0 keeps the sticky header flush */
}
/* Sticky header inside the panel: the shared rule sticks it at top:0 of THIS scroll box; only the opaque
   background needs to change from the page surface to the panel surface so rows don't bleed through. */
.drilldown__body .table thead th { top: 0; background: var(--surface-raised); }

.drilldown__empty {
  margin: 0;
  padding: var(--space-7) var(--space-4);
  text-align: center;
  font: var(--type-body-sm);
  color: var(--text-secondary);
}

/* Loading state — for records that have to be FETCHED (a chart-point drill re-runs the report for that
   month), so the panel is never a frozen empty dialog. Shimmer bars in the shape of the table that is
   coming, rather than a spinner: the reader sees the panel is working AND roughly what will arrive, and
   the body does not jump when the rows land. The record count is hidden while loading — a count we do
   not yet know must never render as a zero. */
.drilldown__loading {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-4);
}
.drilldown__loading-bar {
  height: 14px;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--surface-hover) 37%, var(--surface-sunken) 63%);
  background-size: 400% 100%;
  animation: hf-drill-shimmer var(--motion-ambient) linear infinite;
}
/* Staggered widths so it reads as rows of text rather than a progress bar. */
.drilldown__loading-bar:nth-child(3n) { width: 78%; }
.drilldown__loading-bar:nth-child(3n + 1) { width: 92%; }
@keyframes hf-drill-shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}
/* Reduced motion: keep the placeholder, drop the movement — matching the chart skeleton's treatment. */
@media (prefers-reduced-motion: reduce) {
  .drilldown__loading-bar { animation: none; }
}

/* Drill target — a figure that OPENS the panel. Visibly interactive at rest (a dotted underline in the link
   idiom) so it never looks identical to a plain number, firming to the accent on hover. A real <button>, so it
   is Tab-reachable and activates on Enter/Space. Metric tiles reuse the existing .metric--action affordance. */
.drill-value {
  font: inherit; color: inherit;
  background: none; border: 0; padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--text-tertiary);
  text-underline-offset: 0.2em;
  transition: color var(--motion-micro) var(--ease), text-decoration-color var(--motion-micro) var(--ease);
}
.drill-value:hover { color: var(--accent); text-decoration-color: var(--accent); text-decoration-style: solid; }
.drill-value:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }

/* =============================================================================
   DATE RANGE PICKER
   A from/to pair (native <input type="date">, matching the filter bar's range) plus quick
   presets. Driven by public/js/date-range.js (window.HFDateRange); see the styleguide
   "Date range picker" entry. Presets are a wrapping row of pill toggles (the active one
   marked with .is-active + aria-pressed); the inputs wrap below the 900px drawer breakpoint
   where the control has less width. Layout only — all behaviour (default month-to-date,
   presets, the inverted-range guard, state + URL) lives in the JS.
============================================================================= */
.date-range { display: flex; flex-direction: column; gap: var(--space-4); }

.date-range__presets { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.date-range__preset {
  height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  background: none;
  color: var(--text-secondary);
  font: var(--type-body-sm);
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--motion-micro) var(--ease), border-color var(--motion-micro) var(--ease), color var(--motion-micro) var(--ease);
}
.date-range__preset:hover { border-color: var(--border-strong); color: var(--text-primary); }
.date-range__preset:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.date-range__preset.is-active { border-color: var(--border-accent); background: var(--surface-selected); color: var(--text-primary); }

/* From / To inputs. flex-wrap stacks them when the width is tight (drawer mode below 900px). The native
   date input is full-width by default (.field width:100%); here it sizes to its own intrinsic width. */
.date-range__inputs { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.date-range__field { display: flex; flex-direction: column; }
.date-range__field .field { width: auto; min-width: 9rem; }

/* =============================================================================
   PERSON SELECTOR
   -----------------------------------------------------------------------------
   A single "who" control — All + each team member — the sibling of the date range
   picker in a report's control area, used to narrow a page to one person. A labelled
   native <select> (the people are authored per page, so the component stays generic).
   Driven by public/js/pm-select.js (window.HFPmSelect); see the styleguide "Person
   selector" entry. Layout only — behaviour (event, URL sync) lives in the JS.
============================================================================= */
.pm-select__field { display: flex; flex-direction: column; gap: var(--space-1); }
.pm-select__field .field { width: auto; min-width: 12rem; }

/* =============================================================================
   VIEW TOGGLE
   -----------------------------------------------------------------------------
   A segmented control switching the SAME data between presentations — Visual
   (charts) vs Data (tables) on a team landing page. The third member of the
   report control set, alongside the date range picker and the person selector:
   those two choose WHICH figures, this one chooses HOW they are drawn.

   A track (the bordered pill) holding real <button>s, the active one raised onto
   a surface rather than merely recoloured — so the selected view reads as a
   physical position in the group, which is what makes a segmented control legible
   at a glance. Driven by public/js/view-toggle.js (window.HFViewToggle); see the
   styleguide "View toggle" entry. Layout only — behaviour (event, URL sync) lives
   in the JS.

   aria-pressed is the accessible state and .is-active its visual counterpart, the
   same split the date range picker's presets use. Both are set by the JS; the CSS
   keys off aria-pressed so the visual can never drift from what is announced.
============================================================================= */
.view-toggle {
  display: inline-flex;
  gap: var(--space-1);
  padding: 3px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
}
.view-toggle__btn {
  height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--text-secondary);
  font: var(--type-body-sm);
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--motion-micro) var(--ease), color var(--motion-micro) var(--ease);
}
.view-toggle__btn:hover { color: var(--text-primary); }
.view-toggle__btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Keyed off aria-pressed, not .is-active, so the appearance cannot drift from the announced state. */
.view-toggle__btn[aria-pressed="true"] {
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

/* =============================================================================
   PINNED BAND — a section whose figures IGNORE the page's date control
   -----------------------------------------------------------------------------
   Most bands on a report answer "what does the selected range say". A pinned band
   does not: its figures are fixed to a window of their own — next month from today
   on the forward-looking metrics — and change when the calendar changes, never when
   the reader changes the date control above.

   That is a genuine misreading hazard, and a quiet one. Someone selects last month,
   reads down the page, and takes every figure as belonging to that month; the pinned
   band silently is not. So it is marked THREE ways, deliberately redundant: this
   visual treatment (an accent edge and a lifted surface, so it reads as a different
   kind of thing before it is read at all), a __flag chip INSIDE the heading — inside,
   so a screen reader announces the caveat as part of the heading rather than as a
   detached note it might never reach — and prose in the band itself.

   Do not use this merely to emphasise a section. It means one specific thing: the
   date control does not reach these figures.
============================================================================= */
.band-pinned {
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-accent);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}
.band-pinned__flag {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
  font: var(--type-label);
  vertical-align: middle;
  white-space: nowrap;
}

/* =============================================================================
   CHART GRID (small multiples)
   -----------------------------------------------------------------------------
   One chart per metric, laid out so a band of related metrics can be compared by
   eye. auto-fit + minmax means the column count follows the available width with
   no breakpoints.

   440px floor, not 320: eighteen monthly points in a 320px column left roughly
   15px per point, which is not enough room for a line to show its shape — the
   chart reads as a squiggle rather than a trend. 440px is the width at which the
   plot area (column minus the 52px of axis gutter) gives a monthly series about a
   year and a half of legible spacing.

   max-width caps the grid at THREE columns however wide the screen. That is a
   readability bound and a tidiness one: a six-chart band divides exactly by 1, 2
   and 3, so the last row is never ragged at any width, whereas a fourth column
   would leave four-then-two. A band whose count does not divide by three will
   still leave a short last row — set an explicit column count on that grid rather
   than widening this one.

   Do NOT pair this with a shared axis.max unless every chart in the band measures
   THE SAME THING (e.g. one metric across four managers). Six different metrics on
   one scale is what the axis.max option must not be used for: the largest metric
   sets a ceiling that flattens all the others into the baseline. See charts.js.
============================================================================= */
.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr));
  gap: var(--space-5);
  max-width: 1500px;   /* three 440px+ columns plus gaps — never a fourth */
}

/* =============================================================================
   INSUFFICIENT HISTORY
   -----------------------------------------------------------------------------
   What a metric shows INSTEAD of a chart when it has too few months to plot. Three
   points is not a trend, and drawing one anyway invites a reader to see direction
   in noise — so below the promotion threshold the metric shows its current value
   and states plainly how much history exists.

   It is deliberately NOT the chart component's empty state: that reads as "this
   chart failed", where this is the opposite — the figure is fine, there is simply
   not yet enough of it to draw. Same footprint as a chart figure so a grid of small
   multiples does not jump as metrics graduate into charts one by one.

   The threshold belongs to the PAGE (one shared rule across every metric in the
   band, so metrics graduate on their own as data accumulates) — never per-metric,
   and never hardcoded here.
============================================================================= */
.chart-insufficient {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}
.chart-insufficient__title { margin: 0; }
.chart-insufficient__value { margin: var(--space-2) 0 0; }
.chart-insufficient__label { margin: 0; color: var(--text-secondary); }
/* The history statement is the point of the component — it is what makes the absent chart honest
   rather than merely missing, so it sits apart from the figure with a rule above it. */
.chart-insufficient__note {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
  color: var(--text-secondary);
}
