/* Reporting components - see TagHelpers/AppStatTagHelper.cs,
   AppChartTagHelper.cs, AppMatrixTagHelper.cs and wwwroot/js/app-report.js.

   BOOTSTRAP VARIABLES FOR EVERY STRUCTURAL COLOUR, exactly as app-grid.css
   requires: the theme flips at runtime via data-bs-theme on <html> with no
   reload, so a literal is a colour that survives into the wrong theme.

   THE ONE EXCEPTION IS THE SERIES PALETTE BELOW, and it is an exception
   because Bootstrap has no categorical scale to borrow - --bs-primary and
   friends are a STATUS palette (this is good, this is a warning), and reusing
   them to mean "program three" puts a red bar on screen that a reader
   reasonably takes as a problem. So the eight series colours are declared
   here as literals, and they are paid for the way site.css pays for
   .filter-bar: a full parallel [data-bs-theme="dark"] block, with no rule
   below this point referring to a colour any other way. */

:root {
  /* Categorical palette, light. Eight hues in a FIXED ORDER - the order is
     the colourblind-safety mechanism, not decoration, so do not resort these
     to taste. Validated as a set against a white surface: worst adjacent CVD
     separation dE 9.1 (protan), worst adjacent normal-vision dE 19.6.

     Three of them - aqua, yellow and magenta - sit below 3:1 contrast on a
     light surface. That is allowed here, and only here, because both reliefs
     are shipped by default and not as options: <app-chart> prints the value
     beside every bar (show-values) and renders the full data table under
     every chart (show-table). Turn BOTH of those off on a light-theme chart
     and this palette is no longer accessible. */
  --app-chart-1: #2a78d6; /* blue */
  --app-chart-2: #eb6834; /* orange */
  --app-chart-3: #1baf7a; /* aqua */
  --app-chart-4: #eda100; /* yellow */
  --app-chart-5: #e87ba4; /* magenta */
  --app-chart-6: #008300; /* green */
  --app-chart-7: #4a3aa7; /* violet */
  --app-chart-8: #e34948; /* red */
}

[data-bs-theme="dark"] {
  /* The same eight hues re-stepped for a dark surface - NOT an automatic
     lightening, and not the light values reused. Validated against Bootstrap's
     dark body background (#212529): all eight clear 3:1, worst adjacent CVD
     dE 8.4, worst adjacent normal-vision dE 19.3. */
  --app-chart-1: #3987e5;
  --app-chart-2: #d95926;
  --app-chart-3: #199e70;
  --app-chart-4: #c98500;
  --app-chart-5: #d55181;
  --app-chart-6: #008300;
  --app-chart-7: #9085e9;
  --app-chart-8: #e66767;
}

/* ── Series slots ────────────────────────────────────────────────────────
   Fill for a block mark, stroke for a drawn one. One rule per slot so the
   tag helper only has to emit .app-chart-sN. */
.app-chart-s1 { --app-series: var(--app-chart-1); }
.app-chart-s2 { --app-series: var(--app-chart-2); }
.app-chart-s3 { --app-series: var(--app-chart-3); }
.app-chart-s4 { --app-series: var(--app-chart-4); }
.app-chart-s5 { --app-series: var(--app-chart-5); }
.app-chart-s6 { --app-series: var(--app-chart-6); }
.app-chart-s7 { --app-series: var(--app-chart-7); }
.app-chart-s8 { --app-series: var(--app-chart-8); }

/* ════════════════════════════════════════════════════════════════════════
   Stat tiles
   ════════════════════════════════════════════════════════════════════════ */

.app-stat-group {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--app-stat-min, 12rem), 1fr));
  gap: 0.75rem;
  margin-bottom: 1.5rem;
  /* EVERY ROW THE SAME HEIGHT, not just every tile in a row. A grid row sizes
     to its tallest tile, so when the band wraps - four tiles becoming 2x2 on a
     phone - a single tile with an extra line of hint made its whole row taller
     than the one above it and the band stepped down the middle. Measured at
     375px: 76px against 92px. */
  grid-auto-rows: 1fr;
}

.app-stat-group-fixed {
  grid-template-columns: repeat(var(--app-stat-columns, 3), minmax(0, 1fr));
}

@media (max-width: 575.98px) {
  /* Two up on a phone rather than one: these are short numbers, and a single
     column turns a four-tile band into a full screen of scrolling before the
     report itself starts. */
  .app-stat-group,
  .app-stat-group-fixed {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.app-stat {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  background: var(--bs-body-bg);
  /* The accent is a left rule rather than a tinted panel: a tinted panel
     competes with the alerts on the same page for the same meaning. */
  border-left: 3px solid var(--bs-border-color);
}

.app-stat-primary { border-left-color: var(--bs-primary); }
.app-stat-success { border-left-color: var(--bs-success); }
.app-stat-warning { border-left-color: var(--bs-warning); }
.app-stat-danger { border-left-color: var(--bs-danger); }
.app-stat-info { border-left-color: var(--bs-info); }

.app-stat-emphasis {
  background: var(--bs-tertiary-bg);
}

.app-stat-link {
  text-decoration: none;
  color: inherit;
  transition: border-color 0.12s ease-in-out, background-color 0.12s ease-in-out;
}

.app-stat-link:hover,
.app-stat-link:focus-visible {
  background: var(--bs-tertiary-bg);
  color: inherit;
}

/* ── The chosen tile, when a band of tiles is also a selector ───────────
   Ringed and tinted, and the label takes the accent colour. The NUMBER is
   deliberately left at body colour and body size: these four tiles are a
   comparable set, and recolouring or enlarging the active one would make it
   read as the important figure rather than the current one. */
.app-stat-selected {
  border-color: var(--bs-primary);
  border-left-color: var(--bs-primary);
  background: var(--bs-tertiary-bg);
  /* Doubles the ring without moving anything - a border-width change here
     would shift every neighbour by a pixel as the selection moves. */
  box-shadow: inset 0 0 0 1px var(--bs-primary);
}

.app-stat-selected .app-stat-label,
.app-stat-selected .app-stat-icon {
  color: var(--bs-primary);
}

.app-stat-link.app-stat-selected:hover {
  background: var(--bs-tertiary-bg);
}

.app-stat-head {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 1.25rem;
}

.app-stat-icon {
  color: var(--bs-secondary-color);
  font-size: 0.875rem;
}

.app-stat-primary .app-stat-icon { color: var(--bs-primary); }
.app-stat-success .app-stat-icon { color: var(--bs-success); }
.app-stat-warning .app-stat-icon { color: var(--bs-warning); }
.app-stat-danger .app-stat-icon { color: var(--bs-danger); }
.app-stat-info .app-stat-icon { color: var(--bs-info); }

.app-stat-label {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.2;
}

.app-stat-value {
  /* Tabular figures so a column of tiles has its digits in the same places.
     Without this, "1,111" is visibly narrower than "8,888" and a row of
     numbers looks ragged for no reason a reader can name. */
  font-variant-numeric: tabular-nums;
  font-size: 1.875rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--bs-body-color);
}

.app-stat-emphasis .app-stat-value {
  font-size: 2.5rem;
}

.app-stat-value-suppressed {
  color: var(--bs-secondary-color);
  font-size: 1.5rem;
}

.app-stat-shield {
  font-size: 0.75rem;
  margin-left: 0.25rem;
  vertical-align: middle;
  color: var(--bs-secondary-color);
}

.app-stat-delta {
  display: inline-flex;
  align-items: center;
  font-size: 0.8125rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* Movement is coloured AND carries an arrow. Colour alone would leave the
   direction invisible to a red/green colourblind reader, which is roughly one
   man in twelve. */
.app-stat-delta-good { color: var(--bs-success-text-emphasis); }
.app-stat-delta-bad { color: var(--bs-danger-text-emphasis); }
.app-stat-delta-flat { color: var(--bs-secondary-color); }

.app-stat-hint {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  line-height: 1.3;
}

.app-stat-extra {
  margin-top: 0.375rem;
}

/* ════════════════════════════════════════════════════════════════════════
   Charts
   ════════════════════════════════════════════════════════════════════════ */

.app-chart {
  margin: 0 0 1.5rem;
}

.app-chart-caption {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.625rem;
}

.app-chart-heading {
  font-size: 1rem;
  font-weight: 600;
  color: var(--bs-body-color);
}

.app-chart-subheading {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.app-chart-empty {
  color: var(--bs-secondary-color);
  font-size: 0.875rem;
  margin: 0.5rem 0;
}

.app-chart-legend,
.app-chart-donut-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  padding: 0;
  margin: 0 0 0.625rem;
  list-style: none;
  font-size: 0.8125rem;
}

.app-chart-legend-item {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  /* Legend text is ink, never the series colour - the swatch beside it is
     what carries identity, and coloured text is harder to read for no gain. */
  color: var(--bs-body-color);
}

.app-chart-swatch {
  flex: 0 0 auto;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 2px;
  background: var(--app-series, var(--bs-secondary-color));
}

/* ── Horizontal bars ──────────────────────────────────────────────────── */

.app-chart-bars {
  display: grid;
  /* Three columns, not three flex children: this is what keeps every bar
     starting on the same vertical line however long the labels are. The
     label column is capped so one very long region name cannot take half the
     width away from the bars. */
  grid-template-columns: minmax(6rem, 12rem) 1fr auto;
  gap: 0.3125rem 0.75rem;
  align-items: center;
}

.app-chart-bar-row {
  display: contents;
}

.app-chart-bar-label {
  font-size: 0.8125rem;
  color: var(--bs-body-color);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-chart-track {
  position: relative;
  display: block;
  height: 1.125rem;
  border-radius: 3px;
  background: var(--bs-tertiary-bg);
}

.app-chart-fill {
  display: block;
  height: 100%;
  min-width: 2px;
  background: var(--app-series, var(--bs-primary));
  /* Rounded on the data end only. A bar rounded at the baseline end floats
     off its own axis. */
  border-radius: 0 4px 4px 0;
  transition: width 0.2s ease-out;
}

.app-chart-bar-value {
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--bs-body-color);
  white-space: nowrap;
}

/* ── Vertical columns ─────────────────────────────────────────────────── */

.app-chart-columns {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  /* Scrolls inside itself rather than crushing the columns. Twelve months at
     a readable width does not fit a phone, and a 6px-wide column is not a
     chart. */
  overflow-x: auto;
  padding-bottom: 0.25rem;
}

.app-chart-column {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  min-width: 2.5rem;
}

.app-chart-plot {
  display: flex;
  align-items: flex-end;
  width: 100%;
  height: var(--app-chart-height, 16rem);
  /* The baseline. Everything above it is the plot. */
  border-bottom: 1px solid var(--bs-border-color);
  overflow: hidden;
}

.app-chart-group {
  display: flex;
  align-items: flex-end;
  /* A 2px surface gap between adjacent fills, so two columns of similar
     colour still read as two marks. */
  gap: 2px;
  width: 100%;
  height: 100%;
}

.app-chart-column-fill {
  display: block;
  flex: 1 1 0;
  min-height: 2px;
  background: var(--app-series, var(--bs-primary));
  border-radius: 4px 4px 0 0;
}

.app-chart-stack {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  width: 100%;
  height: 100%;
}

.app-chart-segment {
  display: block;
  width: 100%;
  min-height: 2px;
  background: var(--app-series, var(--bs-primary));
}

.app-chart-stack > .app-chart-segment:first-child {
  border-radius: 4px 4px 0 0;
}

.app-chart-column-label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  text-align: center;
  line-height: 1.2;
  word-break: break-word;
}

.app-chart-column-value {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--bs-body-color);
}

/* AFTER the base rules above, not before. A media query adds no specificity,
   so a mobile override placed earlier in the file loses to the desktop rule
   that follows it, silently. */
@media (max-width: 575.98px) {
  /* Tighter columns and a smaller gap on a phone, so a twelve-month trend
     shows nine or ten months rather than seven. It still scrolls - crushing
     twelve columns into 354px leaves 26px each and turns every axis label
     into two lines - but it scrolls less, and app-report.js opens a
     scroll-to-end chart at the recent end, which is the half people came
     for. */
  .app-chart-columns {
    gap: 0.25rem;
  }

  .app-chart-column {
    min-width: 2.25rem;
  }

  .app-chart-column-label {
    font-size: 0.6875rem;
  }
}

/* ── Donut ────────────────────────────────────────────────────────────── */

.app-chart-donut-wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem;
}

.app-chart-donut-svg {
  flex: 0 0 auto;
  width: var(--app-chart-height, 12rem);
  height: var(--app-chart-height, 12rem);
  max-width: 100%;
}

.app-chart-donut-hole {
  fill: none;
  stroke: var(--bs-tertiary-bg);
  stroke-width: 5;
}

.app-chart-donut-slice {
  fill: none;
  stroke: var(--app-series, var(--bs-primary));
  stroke-width: 5;
  /* A surface-coloured ring between slices, so two adjacent wedges never
     merge into one shape. Same 2px spacer rule as the stacked bars. */
  paint-order: stroke;
}

.app-chart-donut-legend {
  flex: 1 1 12rem;
  flex-direction: column;
  /* nowrap, overriding the shared .app-chart-legend rule above, and it is not
     cosmetic. flex-wrap:wrap on a COLUMN container wraps into extra COLUMNS,
     and each line is then sized from its widest item rather than from the
     container - so the rows stopped obeying the list's width entirely (304px
     rows inside a 183px list) and pushed a horizontal scrollbar onto the whole
     page at phone width. The ellipsis below cannot fire while the row is free
     to be as wide as it likes. */
  flex-wrap: nowrap;
  gap: 0.3125rem;
  /* min-width:0 on BOTH the list and its rows, and neither is optional. A flex
     item defaults to min-width:auto, which refuses to shrink below its content
     - so a long program name pushes the legend past the column instead of
     ellipsing, taking the share figure off the side of the card with it. The
     ellipsis on .app-chart-legend-label cannot fire until every flex ancestor
     between it and the card is allowed to shrink. */
  min-width: 0;
}

.app-chart-donut-legend .app-chart-legend-item {
  justify-content: flex-start;
  gap: 0.5rem;
  min-width: 0;
}

.app-chart-legend-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-chart-legend-value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

/* ── Trend line ───────────────────────────────────────────────────────── */

/* ════════════════════════════════════════════════════════════════════════
   The value scale - shared by the trend line and the columns
   ════════════════════════════════════════════════════════════════════════

   Gridlines and their labels, as HTML positioned in PERCENTAGES of the plot
   box. Not SVG, for the reason the bar labels are not SVG - text in a viewBox
   scales with it, so the same chart would carry 9px ticks in a sidebar and
   30px on a projector - and not rows in the flow, because the column chart
   scrolls sideways and a value scale that scrolls away with the columns is
   not a scale.

   ONE IMPLEMENTATION FOR BOTH SHAPES. The trend explorer switches between
   line, columns and stacked in a click; a "100" rule that sat at a different
   height in each would be the first thing a reader noticed, and a moving axis
   is the fastest way to make somebody stop believing a chart. */

.app-chart-line-wrap,
.app-chart-columns-wrap {
  position: relative;
  /* The gutter the tick labels live in. Both the plot and the category axis
     under it are inside this padding, so the marks and their labels stay in
     the same coordinate space - which is the bug this whole block was written
     after finding. */
  padding-left: 2.75rem;
}

.app-chart-plotarea {
  position: relative;
}

.app-chart-yaxis {
  position: absolute;
  /* The plot band only: top of the wrap's border box, down by the plot's own
     height. The category labels below it are NOT in the scale. */
  top: 0;
  left: 0;
  right: 0;
  height: var(--app-chart-height, 16rem);
  pointer-events: none;
}

.app-chart-gridrule {
  position: absolute;
  left: 2.75rem;
  right: 0;
  border-top: 1px solid var(--bs-border-color);
  opacity: 0.55;
}

.app-chart-ytick {
  position: absolute;
  left: 0;
  width: 2.25rem;
  transform: translateY(-50%);
  text-align: right;
  color: var(--bs-secondary-color);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* The svg form, kept for any caller that draws its rules inside the picture. */
.app-chart-gridline {
  stroke: var(--bs-border-color);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  shape-rendering: crispEdges;
  opacity: 0.55;
}

/* Fills its box exactly, in both directions.
   The svg carries preserveAspectRatio="none" - see the long note in
   AppChartTagHelper.RenderLine for why, and for what pays for it. overflow is
   hidden rather than visible now: nothing round is drawn in here any more, so
   there is no marker to slice in half, and a clipped edge is better than a
   stray polygon vertex over the caption. */
.app-chart-line-svg {
  display: block;
  width: 100%;
  height: var(--app-chart-height, 16rem);
  /* visible, because a point sitting exactly on the top of the scale would
     otherwise have the upper half of its 2px stroke sliced off. Nothing round
     is drawn in here any more - the markers are HTML over the top - and every
     coordinate is clamped into the box, so there is nothing else that can
     escape. */
  overflow: visible;
}

/* ── The markers ──────────────────────────────────────────────────────────
   HTML, not SVG circles, so they stay round however the plot is stretched.
   The span is the HOVER TARGET - a generous box - and the visible disc is
   drawn by its pseudo-element in the middle of it. */
.app-chart-markers {
  position: absolute;
  inset: 0;
  /* The layer must not eat the pointer; only the markers in it may. */
  pointer-events: none;
}

.app-chart-line-wrap .app-chart-dot {
  position: absolute;
  width: 1.75rem;
  height: 1.75rem;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

.app-chart-line-wrap .app-chart-dot::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--app-series, var(--bs-primary));
  /* A surface ring so a marker sitting on the line is still a distinct mark. */
  box-shadow: 0 0 0 2px var(--bs-body-bg);
  transition: width 0.12s ease, height 0.12s ease, margin 0.12s ease;
}

/* The read-out is supposed to be easier to get than aiming at a 9px dot, and
   the mark should say when you have got it. */
.app-chart-line-wrap .app-chart-dot:hover::before {
  width: 15px;
  height: 15px;
  margin: -7.5px 0 0 -7.5px;
}

@media (prefers-reduced-motion: reduce) {
  .app-chart-line-wrap .app-chart-dot::before {
    transition: none;
  }
}

.app-chart-line {
  fill: none;
  stroke: var(--app-series, var(--bs-primary));
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* The stroke keeps its weight whatever the viewBox is scaled to. Without
     this a wide chart draws a hairline and a narrow one draws a rope. */
  vector-effect: non-scaling-stroke;
}

.app-chart-area {
  fill: var(--app-series, var(--bs-primary));
  fill-opacity: 0.14;
  stroke: none;
}

/* The svg form of the marker. Kept because nothing guarantees this component
   is the only caller that ever draws one, and it costs four lines; the line
   chart itself now uses the HTML marker above. */
.app-chart-dot {
  fill: var(--app-series, var(--bs-primary));
  /* A surface ring so a marker sitting on the line is still a distinct mark. */
  stroke: var(--bs-body-bg);
  stroke-width: 2;
}

.app-chart-line-axis {
  display: flex;
  justify-content: space-between;
  gap: 0.25rem;
  margin-top: 0.25rem;
}

/* WRAPS RATHER THAN ELLIPSING, and the difference is one specific label.
   The current month is labelled "Sep 26 (so far)" - the qualifier that stops
   a reader taking a two-day month as a collapse in growth - and under
   nowrap+ellipsis at twelve months across it came out "Sep 26 …", which
   truncates away the entire reason the label is longer than the others.
   Wrapping costs one line of height on that one label. */
.app-chart-axis-label {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  font-size: 0.6875rem;
  line-height: 1.2;
  color: var(--bs-secondary-color);
  text-align: center;
  overflow-wrap: break-word;
}

/* ── The numbers ──────────────────────────────────────────────────────── */

.app-chart-data {
  margin-top: 0.75rem;
  font-size: 0.875rem;
}

.app-chart-data > summary {
  cursor: pointer;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  width: fit-content;
}

.app-chart-data > summary:hover {
  color: var(--bs-body-color);
}

.app-chart-table {
  margin-top: 0.5rem;
  font-variant-numeric: tabular-nums;
}

/* ════════════════════════════════════════════════════════════════════════
   Cross-tab
   ════════════════════════════════════════════════════════════════════════ */

.app-matrix {
  margin-bottom: 1.5rem;
}

.app-matrix-caption {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.625rem;
}

.app-matrix-heading {
  font-size: 1rem;
  font-weight: 600;
}

.app-matrix-subheading,
.app-matrix-empty {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.app-matrix-table {
  font-variant-numeric: tabular-nums;
  margin-bottom: 0;
}

.app-matrix-table th,
.app-matrix-table td {
  white-space: nowrap;
}

/* THE ROW LABEL WRAPS AND IS CAPPED, and on a phone that is the difference
   between a usable cross-tab and none at all.

   Row labels here are real names - "Engage (Elementary/Middle School
   Robotics)" - and under the blanket nowrap below that came out 294px wide
   inside a 354px scroll box, leaving SIXTY PIXELS for every number in the
   table. The sticky column that exists to keep the labels in view had taken
   the whole viewport and pushed the data out of it, which is worse than not
   being sticky at all.

   Capped and wrapping in both directions: two lines of program name costs one
   row of height, and buys back the entire table.

   Note the selector. `.app-matrix-table th` above is 0-1-1 and would beat a
   bare `.app-matrix-label` at 0-1-0, so the nowrap would simply have stayed
   on. Qualifying with the table class is what overrides it - the same
   specificity trap as the heat cell further down, hit twice in one file. */
.app-matrix-table th.app-matrix-label,
.app-matrix-table th.app-matrix-corner {
  font-weight: 500;
  text-align: left;
  white-space: normal;
  min-width: 6rem;
  max-width: 16rem;
}

@media (max-width: 575.98px) {
  .app-matrix-table th.app-matrix-label,
  .app-matrix-table th.app-matrix-corner {
    max-width: 9rem;
  }
}

/* Keeps the row labels in place while the numbers scroll sideways. Without
   it the left-hand column leaves the screen and every remaining column is
   just digits with nothing to attach them to. */
.app-matrix-sticky .app-matrix-label,
.app-matrix-sticky .app-matrix-corner,
.app-matrix-sticky tfoot th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bs-body-bg);
}

.app-matrix-scroll {
  /* Own scroll container, so a wide cross-tab never pushes the page sideways. */
  overflow-x: auto;
}

/* The heat. --app-heat is a 0-1 fraction of the largest cell, set inline by
   the tag helper; the colour it tints with is Bootstrap's primary, so this
   needs no dark-theme counterpart - --bs-primary-rgb is redefined under
   data-bs-theme for us.

   THE LONG SELECTOR IS LOAD-BEARING. Bootstrap paints every table cell with
   `.table > :not(caption) > * > *`, which scores 0-1-1 - so a plain
   `.app-matrix-cell` (0-1-0) loses to it and the tint silently does nothing.
   That is exactly what happened: the cells carried a correct --app-heat of 1
   and 0.906 and still computed to solid white. Matching Bootstrap's own shape
   is what gets the specificity above it; do not "simplify" this back to one
   class.

   Capped at 0.3 alpha deliberately. A full-strength tint on the biggest cell
   makes the number inside it unreadable, which trades the table away to buy
   the heat map - and the entire point of shading a cross-tab rather than
   drawing one is that you keep both. */
.app-matrix-table > :not(caption) > * > td.app-matrix-cell {
  background-color: rgba(var(--bs-primary-rgb), calc(var(--app-heat, 0) * 0.3));
}

.app-matrix-zero {
  color: var(--bs-secondary-color);
}

.app-matrix-totals {
  font-weight: 600;
  border-top: 2px solid var(--bs-border-color);
}

/* Derived from the totals row above it, not measured, so it recedes. */
.app-matrix-column-share {
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
}

.app-matrix-column-share th {
  font-weight: 400;
}

.app-matrix-total-col {
  border-left: 1px solid var(--bs-border-color);
}

/* ════════════════════════════════════════════════════════════════════════
   Withheld values
   ════════════════════════════════════════════════════════════════════════
   A masked number must not look like a zero, an error, or a gap in the data.
   It gets muted type and, where it is a MARK rather than a number, a hatched
   stub - a bar that is visibly "not being shown" rather than one that is
   visibly short. */

.app-matrix-suppressed,
.app-chart-value-suppressed {
  color: var(--bs-secondary-color);
  font-style: italic;
  font-weight: 400;
}

/* A merged "withheld" mark - the donut slice and the stacked segment that
   stand in for values that are not being shown. Neutral, never a series
   colour: it is not a category, it is the absence of several. */
.app-chart-withheld {
  --app-series: var(--bs-secondary-color);
  background: var(--bs-secondary-bg);
  stroke: var(--bs-secondary-color);
}

.app-chart-fill-suppressed {
  background: repeating-linear-gradient(
    135deg,
    var(--bs-secondary-bg) 0,
    var(--bs-secondary-bg) 3px,
    transparent 3px,
    transparent 6px
  );
  border-radius: 3px;
}

/* ════════════════════════════════════════════════════════════════════════
   Control bars - see wwwroot/js/app-chart-tools.js and
   app-trend-explorer.js, and the Tools property on AppChartTagHelper.

   EVERY ONE OF THESE SHIPS hidden AND IS REVEALED BY SCRIPT. Nothing below
   styles a control that can be on screen without a script to run it - which
   is why there is no [hidden] override anywhere in this section and must
   never be one.

   Bootstrap does the buttons. .btn-check + .btn is already a segmented
   control with the app's own focus ring, its own theme and real radio
   semantics; the rules here are the frame around it and nothing else.
   ════════════════════════════════════════════════════════════════════════ */

.app-chart-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.625rem 1.25rem;
  margin-bottom: 0.875rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--bs-border-color-translucent);
}

/* A fieldset carries a default border, margin and padding in every browser,
   and a legend is a float in Bootstrap's reboot - both have to go before this
   reads as a toolbar rather than as a form. */
.app-chart-toolgroup {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.app-chart-toolgroup-label {
  float: none;
  width: auto;
  margin: 0 0 0.25rem;
  padding: 0;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bs-secondary-color);
}

.app-chart-toolgroup-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* The switch sits on the baseline of the buttons beside it rather than in its
   own block - form-check's bottom margin is what pushes it out of line. */
.app-chart-toolswitch {
  margin-bottom: 0;
  white-space: nowrap;
}

.app-chart-toolswitch .form-check-label {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

/* ── A legend row that does something ──────────────────────────────────────
   The static legend is a list of spans and stays one. These are the rows on a
   chart whose slices or series can be switched off, so they are real buttons
   with a pressed state - and they have to keep the flex layout the span
   version had, because the ellipsis on a long program name depends on every
   ancestor being allowed to shrink. */
.app-chart-legend-btn,
.app-trend-legend-btn {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  margin: 0;
  padding: 0.125rem 0.25rem;
  border: 0;
  border-radius: var(--bs-border-radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.app-chart-legend-btn:hover,
.app-trend-legend-btn:hover {
  background: var(--bs-tertiary-bg);
}

.app-chart-legend-btn:focus-visible,
.app-trend-legend-btn:focus-visible,
.app-trend-legend-all:focus-visible,
.app-matrix-sort:focus-visible {
  outline: 0;
  box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0)
    var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width, 0.25rem)
    var(--bs-focus-ring-color);
}

/* SWITCHED OFF IS NOT ONLY FADED. Opacity alone puts the whole state on
   colour and contrast, which is exactly what the palette notes in this file
   refuse to do anywhere else - so the label is struck through as well, and
   the button carries aria-pressed for anyone reading rather than looking. */
.app-chart-legend-off,
.app-trend-off {
  opacity: 0.5;
}

.app-chart-legend-off .app-chart-legend-label,
.app-trend-off .app-trend-legend-text {
  text-decoration: line-through;
}

.app-trend-legend-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-trend-legend-all {
  padding: 0.125rem 0.375rem;
  border: 0;
  border-radius: var(--bs-border-radius-sm);
  background: none;
  color: var(--bs-link-color);
  font-size: 0.8125rem;
  cursor: pointer;
}

.app-trend-legend-all:hover {
  background: var(--bs-tertiary-bg);
}

/* ── The donut's centre ────────────────────────────────────────────────────
   The hole was empty and it is the best real estate on a share chart: the one
   figure a donut never states is the total every wedge is a share of.

   pointer-events:none throughout - the ring underneath carries the hover
   read-out, and a text block over the middle of it would eat the hover on
   every slice that reaches the centre. */
.app-chart-donut-ring {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  line-height: 0;
}

.app-chart-donut-centre {
  position: absolute;
  inset: 22%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  text-align: center;
  line-height: 1.15;
  pointer-events: none;
}

.app-chart-donut-total {
  font-size: 1.5rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--bs-body-color);
}

.app-chart-donut-total-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bs-secondary-color);
}

/* ── Sortable cross-tab headings ──────────────────────────────────────────
   A button inside the <th>, never a click handler on the th itself: the th
   keeps scope="col" and the control is focusable and announced. The direction
   is a character rather than an icon font glyph, so it cannot come out as a
   box if the icon stylesheet is the thing that failed to load. */
.app-matrix-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--bs-border-radius-sm);
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.app-matrix-sort:hover {
  color: var(--bs-primary);
}

.app-matrix-sort-icon {
  font-size: 0.75em;
  opacity: 0.35;
}

.app-matrix-sorted .app-matrix-sort-icon {
  display: none;
}

.app-matrix-sorted .app-matrix-sort::after {
  content: "\25BC";
  font-size: 0.7em;
  color: var(--bs-primary);
}

.app-matrix-sorted-asc .app-matrix-sort::after {
  content: "\25B2";
}

/* ── The crosshair ────────────────────────────────────────────────────────
   An inset shadow rather than a background-color, and that is not a flourish:
   the cells already carry the heat tint as their background, and painting
   over it would delete the shading under the pointer - which is the one place
   a reader is looking. A shadow composites on top and leaves it.

   The selector matches Bootstrap's own `.table > :not(caption) > * > *`
   shape, for the reason spelled out on the heat rule above: anything shorter
   loses to it and does nothing at all. */
.app-matrix-table > :not(caption) > * > .app-matrix-hot-col,
.app-matrix-table > :not(caption) > .app-matrix-hot-row > * {
  box-shadow: inset 0 0 0 100vmax rgba(var(--bs-emphasis-color-rgb), 0.07);
}

/* Percentages are derived from the counts rather than measured, so they sit
   one step back the way the column-share row does. */
.app-matrix-as-percent > :not(caption) > * > td.app-matrix-cell {
  font-size: 0.8125rem;
}

/* ════════════════════════════════════════════════════════════════════════
   The trend explorer - see TagHelpers/AppTrendExplorerTagHelper.cs
   ════════════════════════════════════════════════════════════════════════ */

/* The plot keeps its height across a redraw. Without a floor, switching from
   twelve columns to a six-month line shortens the figure, the page reflows,
   and the toolbar the reader is clicking moves out from under the pointer -
   which on a control bar people use several times in a row is the single most
   irritating thing this component could do. */
.app-trend-plot {
  min-height: calc(var(--app-chart-height, 17rem) + 2.5rem);
}

.app-trend-group-end {
  /* Pushed to the far end of the bar: Reset is not one of the perspectives,
     it is the way out of all of them. */
  margin-inline-start: auto;
}

.app-trend-reset {
  padding: 0;
  font-size: 0.8125rem;
  text-decoration: none;
}

.app-trend-legend {
  margin-bottom: 0.5rem;
}

.app-trend-summary {
  margin: 0.5rem 0 0;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
}

.app-trend-note {
  margin: 0.5rem 0 0;
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  font-style: italic;
}

@media (max-width: 575.98px) {
  /* ── The control bar on a phone ──────────────────────────────────────
     Wrapped naturally rather than forced one group per row. Forcing 100%
     put four groups on four rows and pushed the chart - the thing the
     reader came for - most of a screen down. Smaller buttons instead:
     "Per month" and "Running total" still read at 0.75rem, and the
     narrow groups ("6 12 24" beside "Line Columns Stacked") pair up on
     one row on their own. */
  .app-chart-tools {
    gap: 0.5rem 0.75rem;
    padding-bottom: 0.5rem;
  }

  .app-chart-toolgroup {
    flex: 0 1 auto;
  }

  .app-chart-tools .btn {
    padding-inline: 0.5rem;
    font-size: 0.75rem;
  }

  .app-chart-toolgroup-label {
    font-size: 0.625rem;
  }

  .app-trend-group-end {
    margin-inline-start: 0;
  }

  /* ── The month axis on a phone ───────────────────────────────────────
     TWELVE LABELS DO NOT FIT IN 375px. Each one gets 24 pixels there, and
     "Oct 25" needs about 38 - so every label wraps to two lines, and the
     axis turns into a grey block that reads as damage rather than as
     dates. Measured before this rule existed.

     Every third label survives, counted FROM THE RIGHT so the most recent
     month is always one of them - the same rule, and the same reason, as
     the server-side thinning past fourteen categories. The kept ones are
     allowed to overflow their own slot, because the slots either side of
     them are only invisible, not gone, so there is room to spill into.

     :last-child is nth-last-child(3n+1) with n=0, so the current month is
     kept by the same selector rather than by a second one that could fall
     out of step with it. It reads right-aligned so its overflow goes
     inward instead of off the side of the page. */
  .app-chart-line-axis {
    /* The kept labels are out of flow, so nothing in here is guaranteed to
       give the row a height - at twenty-four months the thinned-out slots
       are empty spans. */
    min-height: 1rem;
  }

  .app-chart-line-axis > .app-chart-axis-label {
    visibility: hidden;
  }

  .app-chart-line-axis > .app-chart-axis-label:nth-last-child(3n + 1) {
    visibility: visible;
  }

  /* OUT OF FLOW, ANCHORED TO THE MIDDLE OF ITS OWN SLOT. Overflow always
     runs to the right in an ordinary box however it is aligned, so
     text-align cannot keep the last label on the page - it has to be
     positioned. Centred here, right-anchored on the last one. */
  .app-chart-line-axis > .app-chart-axis-label:nth-last-child(3n + 1) > span {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
  }

  .app-chart-line-axis > .app-chart-axis-label:last-child > span {
    left: auto;
    right: 0;
    transform: none;
  }

  /* The value gutter narrows with everything else - three digits at
     0.6875rem is about 26px, and the chart needs the width more. */
  .app-chart-line-wrap,
  .app-chart-columns-wrap {
    padding-left: 2.25rem;
  }

  .app-chart-gridrule {
    left: 2.25rem;
  }

  .app-chart-ytick {
    width: 1.875rem;
  }

  /* ── The donut's legend goes UNDER the ring on a phone ────────────────
     Side by side it fits - 154px of ring beside 183px of legend inside 354
     - and "fits" is the problem: every legend row is then ellipsed to
     "Engage (Elem…", which on a chart whose only labels ARE the legend
     leaves a picture of five colours and no categories. Full width below
     it, each row reads. */
  .app-chart-donut-wrap {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.875rem;
  }

  .app-chart-donut-legend {
    flex: 1 1 auto;
    width: 100%;
  }

  .app-chart-donut-ring {
    align-self: center;
  }

  /* A region name needs about 80px on a phone; the desktop cap of 12rem
     was taking 168 of 354 and leaving the bars 146. */
  .app-chart-bars {
    grid-template-columns: minmax(4.5rem, 8rem) 1fr auto;
    column-gap: 0.5rem;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   Hover read-out - see wwwroot/js/app-report.js
   ════════════════════════════════════════════════════════════════════════ */

.app-report-tip {
  position: fixed;
  z-index: 1085;
  max-width: 16rem;
  padding: 0.3125rem 0.5rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-sm);
  background: var(--bs-body-bg);
  color: var(--bs-body-color);
  box-shadow: var(--bs-box-shadow);
  font-size: 0.8125rem;
  line-height: 1.3;
  pointer-events: none;
  white-space: nowrap;
}

.app-report-tip[hidden] {
  display: none;
}

.app-report-hit {
  cursor: default;
}

.app-report-hit:hover .app-chart-fill,
.app-chart-segment.app-report-hit:hover,
.app-chart-column-fill.app-report-hit:hover {
  filter: brightness(1.1);
}

.app-chart-donut-slice.app-report-hit:hover {
  stroke-width: 6.5;
}

/* The invisible, generous hover target over a line marker. */
.app-chart-hotspot {
  fill: transparent;
  stroke: none;
}

@media (prefers-reduced-motion: reduce) {
  .app-chart-fill,
  .app-stat-link {
    transition: none;
  }
}

/* ════════════════════════════════════════════════════════════════════════
   Print - these pages get printed and pasted into decks
   ════════════════════════════════════════════════════════════════════════ */

/* ════════════════════════════════════════════════════════════════════════
   Heat map - see TagHelpers/AppHeatmapTagHelper.cs
   ════════════════════════════════════════════════════════════════════════
   A grid of fixed squares where the COLOUR is the reading. The tag helper
   emits a band class (.app-heatmap-slotN) and an inline --app-heat, a 0-1
   fraction of the ramp; everything about what those mean is here, which is
   what lets the same markup be a different palette in each theme.

   WHY THE TINT IS CAPPED AT 0.62 AND NOT 1. The obvious move on a component
   that has given up printing numbers in its cells is to let the strongest
   band run to full strength, and the first cut did. It breaks the `values`
   tool: turn the numbers back on and the top band's text is body-coloured
   over a saturated fill, at about 1.9:1. Every fix for that is worse - white
   text on the top bands inverts again between themes, and a per-band ink
   colour cannot be chosen from a class at all, because a five-band ramp and a
   three-band ramp both emit .app-heatmap-slot3 at different strengths. Capped
   here instead, body text stays legible over every band in both themes, and
   the ramp still runs from barely-there to solidly blue - which is as much
   dynamic range as a reader can actually use.

   MEASURED, not estimated, on the top band with the numbers switched on -
   composite the rgba over the body background and take the contrast against
   --bs-body-color: 5.69:1 on dark (#1552ac under #dee2e6) and 6.18:1 on light
   (#69a5fe under #212529). Both clear AA for normal text with room to spare,
   and the faintest band is 10.5:1 or better. Raising the cap is what spends
   that margin: the same measurement at full strength is 1.9:1. */

.app-heatmap {
  margin: 0;
}

.app-heatmap-caption {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.625rem;
}

.app-heatmap-heading {
  font-size: 1rem;
  font-weight: 600;
}

.app-heatmap-subheading,
.app-heatmap-empty {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.app-heatmap-scroll {
  /* Its own scroll container in both axes, so a wide grid never pushes the
     page sideways and a tall one can be capped with max-height without its
     column headings leaving with it. */
  overflow: auto;
  position: relative;
}

.app-heatmap-grid {
  display: grid;
  /* grid-template-columns is INLINE, from the tag helper: repeat() will not
     take a custom property as its count. See the note there. */
  gap: 2px;
  width: max-content;
  min-width: 100%;
}

/* ── The sticky frame ────────────────────────────────────────────────────
   Row labels hold the left edge, column headings hold the top, and the corner
   holds both - so it needs the highest stacking order of the three. Without
   this, a reader who has scrolled into the middle of a 41 x 24 grid is
   looking at a thousand unlabelled squares. */

.app-heatmap-corner,
.app-heatmap-row-head,
.app-heatmap-col-head {
  position: sticky;
  background: var(--bs-body-bg);
}

.app-heatmap-corner {
  left: 0;
  top: 0;
  z-index: 3;
}

.app-heatmap-col-head {
  top: 0;
  z-index: 2;
  height: var(--app-heatmap-head, 1.75rem);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  overflow: hidden;
}

.app-heatmap-row-head {
  left: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  padding-right: 0.5rem;
  font-size: 0.8125rem;
  min-width: 0;
}

.app-heatmap-row-head > span {
  /* One line, clipped. A row label that wraps makes its own row taller than
     every other row, and a grid whose rows are different heights is no longer
     something you can scan down a column of. The full text is on the hover
     read-out and in the data table. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Column headings on their side once they are words rather than a couple of
   characters. vertical-rl plus a half-turn gives bottom-to-top text, which is
   the direction that keeps a label's start next to its own column. */
.app-heatmap-vertical-heads .app-heatmap-col-head > span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-height: 100%;
}

/* ── The cells ───────────────────────────────────────────────────────────
   Square, from the same custom property that sizes the column track, so the
   two cannot drift apart. */

.app-heatmap-cell {
  height: var(--app-heatmap-cell, 2.25rem);
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  background-color: rgba(var(--bs-primary-rgb), calc(var(--app-heat, 0) * 0.62));
}

/* Zero is drawn as a hairline tray rather than as the palest tint. On a
   counting report zero is most of the grid, and a faint wash over three
   quarters of the cells is noise that makes the genuinely smallest band
   harder to find - which is the one thing the reader came for. */
.app-heatmap-slot0 {
  background-color: var(--bs-tertiary-bg);
}

/* The negative half of a diverging ramp. RED AGAINST BLUE, not red against
   green: red/green is the most common accessibility failure in reporting and
   it is invisible to the person who built it. Bootstrap's danger against
   Bootstrap's primary separates under every CVD simulation, because the two
   differ in lightness as well as in hue. */
.app-heatmap-down {
  background-color: rgba(var(--bs-danger-rgb), calc(var(--app-heat, 0) * 0.62));
}

/* A withheld cell gets NO tint - exactly the rule <app-matrix> follows, for
   exactly the same reason. The shading is drawn from the real value, so a
   masked cell with a visible tint announces roughly how big the hidden number
   is, which on a small-count mask is most of the way to saying it. */
.app-heatmap-suppressed,
.app-heatmap-absent {
  background-color: transparent;
  background-image: repeating-linear-gradient(
    135deg,
    var(--bs-secondary-bg) 0,
    var(--bs-secondary-bg) 3px,
    transparent 3px,
    transparent 6px
  );
  color: var(--bs-secondary-color);
  font-style: italic;
}

/* A gap is a gap. The hatching says "nothing here" and a number in it would
   contradict that. */
.app-heatmap-absent .app-heatmap-value {
  display: none;
}

/* Numbers in the cells are OFF unless the component asked for them or the
   reader switched them on. The text is always in the markup, so the toggle is
   a class flip rather than a re-render - see ShowValues. */
.app-heatmap-value {
  display: none;
}

.app-heatmap-values .app-heatmap-cell > .app-heatmap-value {
  display: block;
  line-height: 1;
}

/* The row-total column, deliberately outside the tinted grid: a row total is
   an order of magnitude larger than any cell in it, so tinting it on the same
   ramp would put every row's total in the top band and draw a solid stripe
   down the right-hand side of the picture. */
.app-heatmap-total {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-left: 0.625rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--bs-secondary-color);
}

.app-heatmap-total-head {
  justify-content: flex-end;
  padding-left: 0.625rem;
}

/* ── The crosshair ───────────────────────────────────────────────────────
   Set by wwwroot/js/app-heatmap.js on the cells sharing a row or a column
   with the pointer. AN OUTLINE, NOT A BACKGROUND CHANGE: the background is
   the data, and dimming or brightening it to show where the mouse is would
   move a cell into a different apparent band. */
.app-heatmap-crosshair {
  outline: 1px solid var(--bs-secondary-color);
  outline-offset: -1px;
}

.app-heatmap-hot {
  outline: 2px solid var(--bs-body-color);
  outline-offset: -1px;
}

.app-heatmap-head-hot {
  color: var(--bs-body-color);
  font-weight: 600;
}

/* ── The legend, which is the only way to decode the picture ───────────── */

.app-heatmap-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-top: 0.625rem;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.app-heatmap-legend-label {
  font-weight: 600;
  color: var(--bs-body-color);
}

.app-heatmap-legend-bands {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.625rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-heatmap-legend-band {
  display: flex;
  align-items: center;
  gap: 0.3125rem;
}

/* The swatch IS an .app-heatmap-cell, so it picks up the same ramp rule and
   cannot come out a different shade from the cells it explains. Only its size
   is overridden. */
.app-heatmap-legend-band > .app-heatmap-cell {
  width: 1.125rem;
  height: 1.125rem;
  flex: none;
}

.app-heatmap-legend-note {
  font-style: italic;
  flex-basis: 100%;
}

/* ════════════════════════════════════════════════════════════════════════
   Choropleth - see TagHelpers/AppGeomapTagHelper.cs
   ════════════════════════════════════════════════════════════════════════
   Deliberately thin. This component borrows the heat map's caption and legend
   markup class for class, because a choropleth IS a heat map whose cells are
   places - so everything below is only the parts a map needs and a grid does
   not. */

.app-geomap {
  margin: 0;
}

.app-geomap-canvas {
  position: relative;
  border-radius: 0.375rem;
  overflow: hidden;
  background: var(--bs-tertiary-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The placeholder line, and the several ways this can end up with no picture:
   no subscription key, Azure Maps blocked, the outline fetch failing. All of
   them land here rather than on a grey rectangle, because the figure around
   them is complete and the reader needs to know the gap is the map and not the
   numbers. */
.app-geomap-loading,
.app-geomap-unavailable {
  margin: 0;
  padding: 1.25rem;
  text-align: center;
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

/* What could not be placed. Not muted into invisibility: this is the line that
   stops a reader screenshotting a map that is quietly missing a third of the
   organization. */
.app-geomap-note {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: var(--bs-warning-text-emphasis);
}

.app-geomap-popup {
  padding: 0.5rem 0.625rem;
  font-size: 0.8125rem;
  line-height: 1.3;
  /* Azure Maps renders its popup into its own container outside this
     component's subtree, and that container carries the library's own light
     styling - so these two have to be stated rather than inherited, or the
     text is unreadable on the dark theme. */
  color: #212529;
}

.app-geomap-popup-warn {
  margin-top: 0.25rem;
  font-size: 0.6875rem;
  font-style: italic;
  color: #664d03;
}

/* ── Phone ───────────────────────────────────────────────────────────────
   The row-label track is the first thing to give: on a 375px screen an 11rem
   label column leaves barely four cells visible and the picture is gone. The
   labels stay sticky and readable, just narrower, and the total column - the
   one part of this that the data table repeats verbatim - goes.

   !important because it is beating a style attribute. That is the case it
   exists for, and it is why the track SIZES are custom properties rather than
   a literal track list: overriding one length here is possible, restating a
   repeat() whose count this stylesheet cannot know is not. */
@media (max-width: 575.98px) {
  .app-heatmap-grid {
    --app-heatmap-label: 6.5rem !important;
  }

  .app-heatmap-total,
  .app-heatmap-total-head {
    display: none;
  }
}

@media print {
  .app-stat,
  .app-chart,
  .app-matrix {
    break-inside: avoid;
  }

  /* CONTROLS DO NOT GO ON PAPER. A segmented control printed into a board
     deck is a picture of a button nobody can press, and on this page it is
     four of them above the figure they belong to. What IS on the paper is
     whatever perspective the reader had chosen when they hit print, which is
     exactly right - the picture, without the machinery. */
  .app-chart-tools,
  .app-trend-legend-all,
  .app-matrix-sort-icon {
    display: none !important;
  }

  /* The toggle buttons keep their text; only the affordance goes. */
  .app-chart-legend-btn,
  .app-trend-legend-btn,
  .app-matrix-sort {
    padding: 0;
    background: none;
  }

  .app-chart-columns,
  .app-matrix-scroll {
    /* On paper there is no scrolling, so let a wide chart be as wide as it
       needs to be rather than clipping the right-hand half of it silently. */
    overflow: visible;
  }

  /* Colour-adjust so the series colours and the heat tint actually reach the
     page instead of being dropped as "background graphics". */
  .app-chart-fill,
  .app-chart-column-fill,
  .app-chart-segment,
  .app-chart-swatch,
  .app-matrix-cell,
  .app-heatmap-cell {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  /* A HEAT MAP THAT PRINTS WITHOUT ITS COLOUR IS A GRID OF EMPTY BOXES -
     there is no value in the cells to fall back on, which is the one way this
     component is more fragile on paper than the cross-tab. The rule above is
     what stops that; this is the rest of it. */
  .app-heatmap-scroll {
    /* On paper there is no scrolling, so a wide grid is better wide than
       silently clipped at the right-hand edge. */
    overflow: visible;
    max-height: none !important;
  }

  .app-heatmap-corner,
  .app-heatmap-row-head,
  .app-heatmap-col-head {
    /* Sticky is meaningless without a scroll container and, worse, some
       engines lay a sticky element out over the row below it once the
       container stops scrolling. */
    position: static;
  }

  /* The legend is the only thing that makes the printed picture readable, so
     it must not be the thing that falls off the bottom of the page. */
  .app-heatmap-legend {
    break-inside: avoid;
  }
}
