/*
 * Arrow Fresh Reporting Centre - stylesheet.
 *
 * Structure, in order:
 *   1. Theme tokens (light, then the two dark scopes)
 *   2. Base and layout
 *   3. Sidebar navigation
 *   4. Filter bar and the two controls
 *   5. Cards, stats, tables
 *   6. Charts and tooltips
 *   7. Loading / empty / error states
 *   8. Auth screens
 *   9. Narrow screens
 *
 * Every colour is a custom property. If you are changing a colour and
 * you are not in section 1, you are in the wrong place.
 */

/* ================================================================== *
 * 1. THEME TOKENS
 *
 * The single source of truth for every colour, radius, space and
 * shadow in the product. Values are declared exactly three times:
 * once for light (:root), once for the OS dark setting, once for the
 * manual dark toggle. Nothing below section 1 may invent a value.
 *
 * The ladders, and why they are what they are:
 *
 *  Surfaces  Four levels, each a measured step apart in CIE L*, so the
 *            page reads as stacked planes rather than one flat sheet.
 *            sunken < page < card < raised. Light mode is capped by
 *            white, so its steps are ~4 and ~3.5 L*; dark has headroom
 *            and runs ~5.5 each. "raised" is hover, popovers and table
 *            headers, anything lifting toward the reader. "sunken" is
 *            the opposite: command blocks, callouts and notes that sit
 *            INTO a card.
 *
 *  Text      primary / secondary / tertiary all clear 4.5:1 on both
 *            card and page in both themes. Tertiary is the small-caps
 *            label colour and used to be 3.5:1 in light mode, which is
 *            why those labels read as faint. It is 5.07:1 now.
 *
 *  Elevation Light mode gets a real, soft shadow. Dark mode does not:
 *            a blurred black shadow over a dark page is mud, so depth
 *            there comes from the lighter surface plus a stronger
 *            border. The exception is --elevation-overlay, for things
 *            that genuinely float free of the page (drawer, modals,
 *            popovers), which need separating in both themes.
 *
 *  Charts    Every chart colour is a token, the heatmap ramp included.
 *            No chart may hold a literal hex value. See js/theme.js.
 * ================================================================== */

:root {
  color-scheme: light;

  /*
   * LIGHT IS WHITE ON OFF-WHITE. Surfaces separate by tone, not by line:
   * the card is white on an off-white page (1.08:1) and that is the whole
   * separation. Borders and gridlines are recessive and exempt from any
   * contrast floor; the previous 3:1 floor on them is what made the theme
   * unusable, and mirroring dark's lightness distances is what crushed it.
   * The floors that remain apply to text (4.5:1 on card and on page) and
   * to data marks a reader must tell apart (3:1 on card). Every ratio
   * below is computed against the surface named, and dev/verify.mjs
   * measures them rather than trusting the comment.
   */
  --surface-sunken:  #efefec;   /* the sidebar tone, and the segmented-control track */
  --surface-page:    #f6f6f4;
  --surface-card:    #ffffff;   /* 1.08:1 on page */
  --surface-raised:  #fafaf9;   /* hover */

  --border-subtle:   #e3e3df;   /* 1.29:1 on card, deliberately below any floor */
  --border-strong:   #8f8f8a;   /* 3.25:1 on card: inputs, focus rings, control edges only */

  /* --- Text: 17.4:1, 6.7:1, 5.1:1 on card; 4.7:1 for muted on page. --- */
  --text-primary:    #1a1a1a;
  --text-secondary:  #5c5c58;
  --text-tertiary:   #6e6e68;   /* caps labels, axis labels, "Data as of" */
  --text-on-accent:  #ffffff;

  /* --- Accent: the one brand colour. Chrome only, never a series. Kept
         on the same blue as series-1 so the theme has one blue. --- */
  --accent:          #2e63c9;
  --accent-hover:    #2554ad;
  --accent-subtle:   color-mix(in srgb, var(--accent) 10%, transparent);

  /* --- Semantic. Amber is reserved for a genuine alert and nothing
         else, which is why "today so far" no longer borrows it. --- */
  --positive:        #1b7a3d;   /* 5.4:1 */
  --negative:        #b8382b;   /* 5.8:1 */
  --warning:         #603600;
  --neutral:         #6a6962;

  --positive-subtle: color-mix(in srgb, var(--positive) 12%, transparent);
  --negative-subtle: color-mix(in srgb, var(--negative) 10%, transparent);
  --warning-subtle:  color-mix(in srgb, var(--warning-mark) 18%, transparent);
  --neutral-subtle:  color-mix(in srgb, var(--neutral) 10%, transparent);

  /* The amber a warning chip is tinted and bordered with. The ink stays
     --warning, which is dark enough to read against that tint. */
  --warning-mark:    #fab219;

  /* --- Charts. Series 1 is the current period, 5.6:1 on card. --- */
  --chart-series-1:  #2e63c9;
  --chart-series-2:  #9e3400;
  --chart-series-3:  #006f6a;
  --chart-series-4:  #623bad;
  --chart-series-5:  #a01b57;
  --chart-series-6:  #644c00;

  /*
   * The comparison series: a grey, 3.25:1 on card, dashed at the same
   * weight as the current series. Grey, not ochre, in light: the dash is
   * what says "reference, not measurement", and a second hue on a white
   * card competed with the one that mattered. Dark keeps its amber.
   */
  --chart-comparison: #161616;  /* 3.23:1 on series-1, 18.1:1 on card */

  --chart-grid:        #ebebe8;   /* 1.19:1; gridlines, exempt from any floor */
  --chart-axis:        #c4c4bf;   /* 1.75:1; the axis line only, labels use --text-tertiary */
  --chart-axis-label:  var(--text-tertiary);
  --chart-crosshair:   var(--text-tertiary);
  --chart-tooltip-bg:  var(--surface-card);
  --chart-tooltip-border: var(--border-strong);

  /* No data: a true neutral grey just below the card, so it recedes and
     does not read as the bottom step of the blue ramp beside it. */
  --chart-empty-cell:  #ebebe8;

  /* Sequential ramp, pale -> saturated, in even ~10 L* steps. */
  --chart-ramp-1:    #c9defe;
  --chart-ramp-2:    #a9c5ee;
  --chart-ramp-3:    #8babdd;
  --chart-ramp-4:    #6d93cc;
  --chart-ramp-5:    #4f7abb;
  --chart-ramp-6:    #3262aa;
  --chart-ramp-7:    #104999;

  /* Scrollbars. The thumb is a control, so it keeps 3:1 on its track. */
  --scrollbar-track:       #efefec;
  --scrollbar-thumb:       #7a7a76;
  --scrollbar-thumb-hover: #5c5c58;

  --focus:           var(--accent);

  /* One shadow. Cards, selected pills and the stuck filter bar carry it;
     nothing else does. Overlays (menus, dialogs) keep a lift so they read
     as floating over the page rather than painted on it. */
  --elevation-1:       0 1px 2px rgba(0, 0, 0, 0.04);
  --elevation-2:       0 1px 2px rgba(0, 0, 0, 0.04);
  --elevation-overlay: 0 2px 4px rgba(0, 0, 0, 0.06), 0 12px 32px rgba(0, 0, 0, 0.12);

  /* --- Radius. Cards are md. Nothing invents its own. --- */
  --radius-xs:       4px;
  --radius-sm:       8px;
  --radius-md:       12px;
  --radius-lg:       16px;
  --radius-pill:     999px;

  /* --- Space: 4 / 8 / 12 / 16 / 24 / 32 / 48 --- */
  --space-1:         4px;
  --space-2:         8px;
  --space-3:         12px;
  --space-4:         16px;
  --space-5:         24px;

  /*
   * The sidebar's left inset, in ONE place. Group headings reach it by two
   * different routes -- the report ones through padding on their toggle
   * button, Settings as a bare <p> with no button under it -- and while
   * each route carried its own number they drifted apart. See .nav-group-toggle.
   */
  --nav-inset:       16px;
  --space-6:         32px;
  --space-7:         48px;

  /* The report never grows past this. Past roughly 1500px a bar chart
     puts its category labels a screen away from the marks. */
  --content-max:     1500px;

  /* --- Motion. Restrained and fast. ---
     Everything an interaction produces lands inside 250ms. The one
     deliberate exception is the chart draw-on, which is a first
     impression rather than a response to a click. */
  --motion-fast:     150ms;
  --motion-base:     200ms;
  --motion-chart:    400ms;
  --ease-out:        cubic-bezier(0.2, 0, 0, 1);

  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/*
 * Dark values live in both scopes: the media query covers the OS
 * setting, the attribute covers the manual toggle, and the :not() guard
 * lets an explicit "light" choice beat an OS set to dark.
 *
 * Only tokens that actually differ are restated. Anything defined by
 * reference above (--accent-subtle, --chart-axis-label, the empty cell)
 * re-resolves on its own against the new values.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --surface-sunken:  #0a0a09;
    --surface-page:    #101010;
    --surface-card:    #1c1c1a;
    --surface-raised:  #272725;

    --border-subtle:   rgba(255, 255, 255, 0.10);
    --border-strong:   rgba(255, 255, 255, 0.20);

    --text-primary:    #f7f7f5;
    --text-secondary:  #c2c1b9;
    --text-tertiary:   #9b9a91;
    --text-on-accent:  #101010;

    --accent:          #4f95ea;
    --accent-hover:    #6ba8f0;
    --accent-subtle:   color-mix(in srgb, var(--accent) 16%, transparent);

    --positive:        #4cb84c;
    --negative:        #f07a76;
    --warning:         #f0b429;
    --neutral:         #94938a;

    --positive-subtle: color-mix(in srgb, var(--positive) 16%, transparent);
    --negative-subtle: color-mix(in srgb, var(--negative) 14%, transparent);
    --warning-subtle:  color-mix(in srgb, var(--warning-mark) 16%, transparent);
    --neutral-subtle:  color-mix(in srgb, var(--neutral) 14%, transparent);

    --warning-mark:    #f0b429;

    --chart-series-1:  #a9caf7;   /* 10.1:1 on card; pale so a reference can sit 3:1 under it */
    --chart-series-2:  #f07a3d;
    --chart-series-3:  #2fb3a8;
    --chart-series-4:  #ac90f2;
    --chart-series-5:  #f0709f;
    --chart-series-6:  #d2a83c;

    --chart-comparison: #8f6528;  /* 3.30:1 on card, 3.07:1 on series-1, dimmer than it */

    --chart-grid:      #2e2e2b;
    --chart-axis:      #3d3d39;
    --chart-empty-cell: #2b2b2b;

    /* Ramping from near-white down to blue leaves the low end with no
       contrast against a dark card. This runs the other way: a dark
       desaturated blue-grey up to a bright accent. */
    --chart-ramp-1:    #1d2a36;
    --chart-ramp-2:    #1f3b55;
    --chart-ramp-3:    #215073;
    --chart-ramp-4:    #216792;
    --chart-ramp-5:    #2280b3;
    --chart-ramp-6:    #2d9bd4;
    --chart-ramp-7:    #57b8f0;
    /* Added, not changed: the dark palette itself is untouched. */
    --scrollbar-track:       #0a0a09;
    --scrollbar-thumb:       #5d5d5b;
    --scrollbar-thumb-hover: #797977;


    --elevation-1:       none;
    --elevation-2:       none;
    --elevation-overlay: 0 2px 6px rgba(0, 0, 0, 0.5), 0 16px 40px rgba(0, 0, 0, 0.55);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --surface-sunken:  #0a0a09;
  --surface-page:    #101010;
  --surface-card:    #1c1c1a;
  --surface-raised:  #272725;

  --border-subtle:   rgba(255, 255, 255, 0.10);
  --border-strong:   rgba(255, 255, 255, 0.20);

  --text-primary:    #f7f7f5;
  --text-secondary:  #c2c1b9;
  --text-tertiary:   #9b9a91;
  --text-on-accent:  #101010;

  --accent:          #4f95ea;
  --accent-hover:    #6ba8f0;
  --accent-subtle:   color-mix(in srgb, var(--accent) 16%, transparent);

  --positive:        #4cb84c;
  --negative:        #f07a76;
  --warning:         #f0b429;
  --neutral:         #94938a;

  --positive-subtle: color-mix(in srgb, var(--positive) 16%, transparent);
  --negative-subtle: color-mix(in srgb, var(--negative) 14%, transparent);
  --warning-subtle:  color-mix(in srgb, var(--warning-mark) 16%, transparent);
  --neutral-subtle:  color-mix(in srgb, var(--neutral) 14%, transparent);

  --warning-mark:    #f0b429;

  --chart-series-1:  #a9caf7;   /* 10.1:1 on card; pale so a reference can sit 3:1 under it */
  --chart-series-2:  #f07a3d;
  --chart-series-3:  #2fb3a8;
  --chart-series-4:  #ac90f2;
  --chart-series-5:  #f0709f;
  --chart-series-6:  #d2a83c;

  --chart-comparison: #8f6528;  /* 3.30:1 on card, 3.07:1 on series-1, dimmer than it */

  --chart-grid:      #2e2e2b;
  --chart-axis:      #3d3d39;
  --chart-empty-cell: #2b2b2b;

  --chart-ramp-1:    #1d2a36;
  --chart-ramp-2:    #1f3b55;
  --chart-ramp-3:    #215073;
  --chart-ramp-4:    #216792;
  --chart-ramp-5:    #2280b3;
  --chart-ramp-6:    #2d9bd4;
  --chart-ramp-7:    #57b8f0;
    /* Added, not changed: the dark palette itself is untouched. */
  --scrollbar-track:       #0a0a09;
  --scrollbar-thumb:       #5d5d5b;
  --scrollbar-thumb-hover: #797977;


  --elevation-1:       none;
  --elevation-2:       none;
  --elevation-overlay: 0 2px 6px rgba(0, 0, 0, 0.5), 0 16px 40px rgba(0, 0, 0, 0.55);
}

/*
 * LIGHT-ONLY TOKENS. Three surfaces the light theme names that dark never
 * needed: the sidebar tone, the active nav item, and the segmented-control
 * track. Scoped to light exactly as the dark blocks are scoped to dark, so
 * they never resolve in dark and their consumers fall back to what dark has
 * always used. The two dark blocks above are byte-identical to what they
 * were before the light theme was redone.
 */
:root {
  --surface-sidebar: #efefec;   /* the sidebar; 1px --border-subtle on its right edge */
  --surface-active:  #e2e2de;   /* active nav item, 1.13:1 on the sidebar */
  --surface-track:   #efefec;   /* segmented-control track; the selected pill is the card */
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface-sidebar: initial;
    --surface-active:  initial;
    --surface-track:   initial;
  }
}
:root[data-theme="dark"] {
  --surface-sidebar: initial;
  --surface-active:  initial;
  --surface-track:   initial;
}

/* ================================================================== *
 * 2. BASE AND LAYOUT
 * ================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-primary);
  background: var(--surface-page);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p { margin: 0; }

button { font: inherit; color: inherit; }

/*
 * Focus is never subtle. Two solid pixels of accent, offset so the ring
 * sits outside the control rather than on top of its own border.
 */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/*
 * The one global transition, and it names its properties.
 *
 * `transition: all` would sweep up width, height and everything else the
 * browser has to reflow to interpolate, which is how a hover state ends
 * up costing a layout pass on every frame. These four paint-only
 * properties plus transform are the whole vocabulary.
 */
@media (prefers-reduced-motion: no-preference) {
  a,
  button,
  input,
  select,
  textarea,
  .card,
  .nav-link,
  .data-table tbody tr,
  .data-table th,
  .popover-row,
  .site-row,
  .badge,
  .live-tile,
  .user-trigger,
  .user-menu-item {
    transition:
      background-color var(--motion-fast) var(--ease-out),
      border-color var(--motion-fast) var(--ease-out),
      color var(--motion-fast) var(--ease-out),
      box-shadow var(--motion-fast) var(--ease-out),
      transform var(--motion-fast) var(--ease-out);
  }
}

/*
 * Hover lift. One pixel, which is enough to read as "this responds" and
 * little enough that a grid of cards does not appear to breathe.
 * transform, not margin: it does not reflow anything around it.
 */
@media (prefers-reduced-motion: no-preference) {
  .button:hover:not(:disabled),
  .row-button:hover,
  .export-trigger:hover {
    transform: translateY(-1px);
    box-shadow: var(--elevation-2);
  }

  .button:active:not(:disabled),
  .row-button:active,
  .export-trigger:active {
    transform: translateY(0);
    box-shadow: none;
  }

  /* Cards carry controls, so they answer the pointer. Stat tiles do not,
     and lifting them would promise a click that is not there. */
  .card:hover {
    transform: translateY(-1px);
    box-shadow: var(--elevation-2);
  }
}

/*
 * The theme crossfade.
 *
 * Applied only while a theme change is actually in flight, by a class
 * that js/ui.js adds for the length of the fade. Because there is no
 * transition at rest, there is nothing to suppress on first paint: the
 * page cannot animate on load, so the usual "no-transition-on-boot"
 * flag is unnecessary. fill and stroke are in the list so the charts
 * cross over with everything else.
 */
@media (prefers-reduced-motion: no-preference) {
  :root.theme-switching,
  :root.theme-switching *:not(.chart-tooltip) {
    transition:
      background-color var(--motion-base) var(--ease-out),
      border-color var(--motion-base) var(--ease-out),
      color var(--motion-base) var(--ease-out),
      fill var(--motion-base) var(--ease-out),
      stroke var(--motion-base) var(--ease-out) !important;
  }
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.screen { min-height: 100vh; }
.screen[hidden] { display: none; }

/*
 * The sidebar is a fixed panel and the main column is offset by its width.
 * Doing it this way rather than as a grid column means one transform hides
 * it, and the same markup serves both behaviours: on a wide screen closing
 * it gives the width back to the charts, on a narrow one it slides over the
 * top as a drawer.
 */
:root { --sidebar-width: 244px; }

.shell { min-height: 100vh; }

.shell--message { display: grid; place-items: center; }

.main { margin-left: var(--sidebar-width); }

body.nav-closed .main { margin-left: 0; }

@media (prefers-reduced-motion: no-preference) {
  .main { transition: margin-left 0.18s ease; }
  .sidebar { transition: transform 0.18s ease; }
}

/*
 * .main spans whatever the sidebar leaves, so the sticky filter bar can
 * reach both edges. The reading width is capped by .main-col inside it.
 *
 * container-type makes .main a query container, so the KPI grid responds
 * to the width the CARDS actually have rather than to the viewport. That
 * matters because the sidebar is 244px, 104px or nothing depending on
 * size, and a viewport query cannot know which. Tooltips and modals are
 * appended to <body>, so the containment here never traps them.
 */
.main {
  min-width: 0;
  padding: 0 0 var(--space-7);
  container-type: inline-size;
  container-name: main;
}

.main-col {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--space-6);
}

.main-col--head { padding-top: var(--space-5); }

.main-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}

.main-head-left {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  min-width: 0;
}

/*
 * Optical alignment, not a guess: the burger is 38px and the title's
 * first line box is about 32px, so the button is pulled up by half the
 * difference to put its centre on the title's centre. The negative left
 * margin sets the bars, not the button's padding box, against the page
 * edge so the burger and the title below it share a left edge.
 */
.main-head-left .burger { margin-top: -3px; margin-left: -10px; }

.main-head-titles { min-width: 0; }

.section-title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* When the REPORT was read. Quiet, permanent, and nowhere near the live
   ticker's stamp, because they measure two different things. */
.data-stamp {
  margin-top: var(--space-1);
  font-size: 11.5px;
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}

.data-stamp[hidden] { display: none; }

.section-body { display: grid; gap: var(--space-5); }

/*
 * Grid and flex children default to min-width:auto, which means they
 * refuse to shrink below their content. A wide table then pushes the
 * whole page sideways instead of scrolling inside its own container.
 * These three lines are what keep the page from overflowing.
 */
.section-body > *,
.card,
.admin-head { min-width: 0; }

.table-wrap { max-width: 100%; }

/* ================================================================== *
 * 3. SIDEBAR - navigation, not filters.
 *
 * Deliberately quiet: text rows with a current-page marker. No pills,
 * no chips, no bordered boxes, nothing that could be mistaken for a
 * control that changes the data.
 * ================================================================== */

.sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-5) 0;
  background: var(--surface-sidebar, var(--surface-card));
  border-right: 1px solid var(--border-subtle);
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--sidebar-width);
  z-index: 70;
  overflow-y: auto;
  overscroll-behavior: contain;
}

body.nav-closed .sidebar {
  transform: translateX(-100%);
  /* Out of sight has to mean out of the tab order too. The inert attribute
     in app.js does the same job for browsers that support it. */
  visibility: hidden;
}

/* ---- The burger ----
   Drawn with borders. Like every other affordance here it is not a typed
   glyph, so nothing depends on an icon font being present. */

.burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 38px;
  height: 38px;
  margin-left: -6px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.burger:hover { background: var(--surface-raised); border-color: var(--border-subtle); }

.burger-bars,
.burger-bars::before,
.burger-bars::after {
  display: block;
  width: 17px;
  height: 1.5px;
  background: var(--text-secondary);
  border-radius: 1px;
}

.burger-bars { position: relative; }

.burger-bars::before,
.burger-bars::after {
  content: '';
  position: absolute;
  left: 0;
}

.burger-bars::before { top: -5.5px; }
.burger-bars::after { top: 5.5px; }

/* Sits over the report while the drawer is open on a narrow screen. */
.nav-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 65;
  background: color-mix(in srgb, #000 50%, transparent);
}

/* The identity is typographic. There is no logo mark, by design. */
.sidebar-brand { padding: 0 var(--space-4); }
.auth-brand { padding: 0; }

.brand-name {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.brand-sub {
  margin-top: 1px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/*
 * Two named groups rather than one long list plus an orphan.
 *
 * Six identical text rows are slow to scan, and Admin sitting alone
 * under a rule at the bottom read as a page footer rather than as
 * somewhere to go. A heading says "these are settings", which a dividing
 * line never did on its own. This product carries no icon marks by
 * design, so the grouping does the work icons would have done.
 */
.nav-group { display: grid; gap: 2px; }

/* The report groups take the slack so the account menu stays at the
   bottom. NOT :first-of-type, which matches on element type: the first
   div in the sidebar is the brand block, so that selector matched
   nothing and the foot floated up under the nav. */
.nav-group--reports {
  flex: 1;
  display: grid;
  align-content: start;
  gap: var(--space-3);
}

/* --- Collapsible groups ---
 *
 * The header is a real button, so Enter and Space activate it and the
 * focus ring is the browser's own. The chevron rotates; nothing else
 * moves, because the list is shown and hidden rather than animated -
 * interpolating height costs a layout pass per frame and the sidebar is
 * the last place worth spending that.
 */
.nav-group-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--nav-inset) var(--space-1);
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
}

.nav-group-toggle:hover .nav-group-label { color: var(--text-secondary); }

/*
 * The chevron is drawn from two borders and is ALREADY rotated 45deg to
 * make the arrow. Setting `transform` here replaces that rotation rather
 * than adding to it, which turns the arrow back into a corner of a
 * square. Both states therefore carry the 45deg and add their own.
 */
.nav-group-chev { margin-right: 2px; }

/*
 * BOTH states are qualified by the attribute, not just one. A bare
 * .nav-group-chev ties with .chev on specificity and sits earlier in the
 * file, so it lost and the shut state kept pointing down.
 */
.nav-group-toggle[aria-expanded="false"] .nav-group-chev {
  transform: rotate(-45deg);
  margin-bottom: 0;
}

.nav-group-toggle[aria-expanded="true"] .nav-group-chev {
  transform: rotate(45deg);
  margin-bottom: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .nav-group-chev { transition: transform var(--motion-fast) var(--ease-out); }
}

/*
 * A collapsed group still has to say the current page is inside it,
 * because the accent bar it normally carries has nowhere to sit. The
 * label takes the accent and a dot marks the group.
 */
.nav-group.has-current .nav-group-label { color: var(--accent); }

.nav-group.has-current .nav-group-toggle[aria-expanded="false"] .nav-group-label::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 7px;
  vertical-align: middle;
  border-radius: 50%;
  background: var(--accent);
}

/* The wrapper is the flex child now, so the marker has something
   positioned to travel inside. */

.nav-group-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
}

/* Settings is a group in its own right, so it gets air above it rather
   than a rule that reads as the end of the page. */
.nav-group--utility { margin-top: var(--space-4); }

/*
 * SETTINGS IS A <p>, NOT A BUTTON, so it has no .nav-group-toggle to take
 * its left inset from and was sitting at x=0 -- flush against the sidebar's
 * clipping edge, with the S on the boundary and reading as "ETTINGS". The
 * other three headings were at 16px, so it was also simply out of line.
 *
 * Same variable as the toggle, so the two routes cannot drift again.
 */
.nav-group--utility .nav-group-label { padding-inline: var(--nav-inset); }

.nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  align-content: start;
}

/*
 * Required, and for the same reason .filter-bar[hidden] is: a `display`
 * rule beats the hidden attribute's UA default, so a collapsed group
 * stayed on screen and its links kept their rectangles. The nav marker
 * measures rectangles, so it happily parked itself against a link nobody
 * could see.
 */
.nav-list[hidden] { display: none; }

/* Full-bleed rows. A navigation item spans the whole column and carries a
   proper hit area, rather than floating as a small label in empty space. */
.nav-link {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border-left: 3px solid transparent;
  font-size: 14.5px;
  color: var(--text-secondary);
  text-decoration: none;
}

.nav-link:hover { background: var(--surface-raised); color: var(--text-primary); }

/*
 * The current section is marked by a 3px accent bar on the leading edge,
 * not by a wash. At these surface levels a background tint alone is a
 * couple of percent of lightness and disappears in daylight; the bar is
 * unambiguous and survives both themes.
 */
.nav-link.is-current {
  color: var(--text-primary);
  font-weight: 600;
  /* Tinted with the accent rather than lifted to the raised surface. A
     surface step is a couple of percent of lightness here and reads as
     nothing; the tint and the bar are the same signal said twice. */
  background: var(--surface-active, var(--accent-subtle));
}

/* Hovering something already current must not read as a second state. */
.nav-link.is-current:hover { background: var(--surface-active, var(--accent-subtle)); }

/*
 * The active marker is ONE element that slides, not a border that blinks
 * off one row and on to another. Every row still reserves its 3px, so
 * nothing shifts underneath it as it travels.
 *
 * Driven by transform alone: app.js measures the active link and sets a
 * translateY. Height is set without a transition, because the rows are
 * all the same height and animating height would be a layout property.
 */
.nav-groups {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.nav-marker {
  position: absolute;
  left: 0;
  top: 0;
  width: 3px;
  border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
  background: var(--accent);
  pointer-events: none;
  opacity: 0;
}

.nav-marker.is-visible { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .nav-marker { transition: transform var(--motion-base) var(--ease-out), opacity var(--motion-fast) linear; }
}

/* --- The account menu ---
 *
 * Sign out was the loudest control in the product: full width, high
 * contrast, bottom left, and the least-used action in it. Nothing about
 * signing out deserves more weight than the report. It and the theme
 * control now sit behind the email address, which is what somebody
 * actually looks for when they want either.
 *
 * The panel is IN THE FLOW, not absolutely positioned, because .sidebar
 * scrolls and an absolute panel would be clipped by that scroll
 * container. Sitting above the trigger in source order, it opens upward
 * on its own.
 */
.sidebar-foot {
  display: grid;
  gap: var(--space-2);
  margin: 0 var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
}

.user-trigger {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 40px;
  padding: var(--space-2);
  text-align: left;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
}

.user-trigger:hover {
  background: var(--surface-raised);
  border-color: var(--border-subtle);
  color: var(--text-primary);
}

.user-trigger[aria-expanded="true"] {
  background: var(--surface-raised);
  border-color: var(--border-subtle);
  color: var(--text-primary);
}

.user-trigger-text {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One of these two shows, depending on how much room the sidebar has. */
.user-trigger-short { display: none; }

.user-trigger .chev { margin: 0; color: var(--text-tertiary); }
.user-trigger[aria-expanded="true"] .chev { transform: rotate(45deg); margin-top: 0; margin-bottom: 2px; }

.user-menu {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-2);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
}

.user-menu[hidden] { display: none; }

.user-menu-identity { padding: var(--space-1) var(--space-2) 0; }

.user-menu-section {
  display: grid;
  gap: 5px;
  padding-top: var(--space-2);
  border-top: 1px solid var(--border-subtle);
}

.user-menu-label {
  padding-inline: var(--space-2);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

/* A menu item, not a call to action. */
.user-menu-item {
  width: 100%;
  min-height: 34px;
  padding: var(--space-2);
  text-align: left;
  font-size: 13px;
  color: var(--text-primary);
  background: none;
  border: 0;
  border-top: 1px solid var(--border-subtle);
  border-radius: 0 0 var(--radius-xs) var(--radius-xs);
  cursor: pointer;
}

.user-menu-item:hover { background: var(--surface-card); }
.user-menu-item--signout:hover { color: var(--negative); }

.account-email {
  font-size: 12px;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.account-role {
  font-size: 12px;
  color: var(--text-tertiary);
  text-transform: capitalize;
}

.theme-toggle {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
}

.theme-option {
  padding: 4px 2px;
  font-size: 12px;
  background: none;
  border: 0;
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  cursor: pointer;
}

.theme-option:hover { background: var(--surface-raised); }

.theme-option.is-active {
  background: var(--surface-raised);
  color: var(--text-primary);
  font-weight: 600;
}

/* ---- The rail ----
 *
 * Between the drawer breakpoint and a comfortably wide screen, a 244px
 * sidebar is eating width the charts need while the report is already
 * cramped. It narrows to a rail instead of disappearing, so navigation
 * stays one click away rather than behind the burger.
 *
 * It is a TYPOGRAPHIC rail, not an icon rail. This product carries no
 * logo mark and no icon marks by design, and six invented glyphs for
 * Overview, Shops, Products, Add-ons, Timing and Payments would be six
 * new marks plus a guessing game. Shortening the labels keeps the words
 * that already name the sections and needs no new vocabulary.
 *
 * Widths only; nothing here changes what is in the DOM, so the burger,
 * the inert handling and the drawer all behave exactly as before.
 */
@media (min-width: 881px) and (max-width: 1180px) {
  :root { --sidebar-width: 104px; }

  .sidebar { gap: var(--space-4); }
  .sidebar-brand { padding: 0 var(--space-2); }
  .brand-name { font-size: 12.5px; line-height: 1.25; }
  /* The line under the name is the first thing that will not fit. */
  .brand-sub { display: none; }

  .nav-link {
    padding: var(--space-2) var(--space-2) var(--space-2) 9px;
    font-size: 11.5px;
    line-height: 1.3;
  }

  /* Retune the shared inset rather than padding the label, which stacked
     on top of the toggle's own padding for the three that have one and
     left Settings on 8px against their 24px. */
  :root { --nav-inset: var(--space-2); }
  .nav-group-label { font-size: 10px; }

  .sidebar-foot {
    margin: 0 var(--space-2);
    gap: var(--space-2);
    padding-top: var(--space-3);
  }

  /* Three options across 104px is unreadable, so they stack. */
  .theme-toggle { grid-template-columns: 1fr; }
  .theme-option { padding: 5px 2px; font-size: 11px; }

  /* The address does not fit and is not what the rail is for. It is
     still named in full inside the menu the trigger opens. */
  .user-trigger-email { display: none; }
  .user-trigger-short { display: inline; }
  .user-trigger { padding: var(--space-2) 6px; }
  .user-menu { padding: 5px; }
  .user-menu-identity .account-email { font-size: 11px; }
}

/* ================================================================== *
 * 4. TOOLBAR - date range, scope buttons, and the today tile.
 * ================================================================== */

/*
 * Sticky, but only DRESSED as a bar once it is actually holding position.
 * Sitting in place it is transparent and reads as part of the page; stuck,
 * it takes a background, a rule and a shadow so the date range and shop
 * selection stay readable over the report scrolling beneath it.
 *
 * .filter-sentinel above it is what tells us which of the two it is.
 */
/*
 * One real pixel, pulled straight back out of the flow. A zero-height
 * target leaves isIntersecting ambiguous - the intersection rectangle is
 * empty whether the element is on screen or not - and it latched to the
 * stuck state under repeated viewport changes. A pixel makes the answer
 * unambiguous and the negative margin keeps the layout identical.
 */
.filter-sentinel { height: 1px; margin-bottom: -1px; }

.filter-bar {
  position: sticky;
  top: 0;
  z-index: 30;
  margin-bottom: var(--space-5);
  background: transparent;
  border-bottom: 1px solid transparent;
  box-shadow: none;
}

@media (prefers-reduced-motion: no-preference) {
  .filter-bar { transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }
}

.filter-bar.is-stuck {
  background: color-mix(in srgb, var(--surface-page) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom-color: var(--border-subtle);
  box-shadow: var(--elevation-2);
}

/* The controls line up with the cards below, whatever the bar spans. */
.filter-bar-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--space-3) var(--space-6);
}

/* A display rule beats the [hidden] attribute's UA display:none, so a
   bar that sets display:flex will not hide without this. Sections that
   filter nothing (Admin) hide the whole bar. */
.filter-bar[hidden] { display: none; }

.filter-controls { display: flex; flex-wrap: wrap; gap: var(--space-3); }

.control-wrap { position: relative; }

/*
 * Scope buttons. Each named group applies on click and opens nothing.
 * Only Custom opens the shop selector, and it is the only one carrying a
 * chevron, so the difference is visible before you click.
 */
.scope-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 3px;
  padding: 3px;
  background: var(--surface-track, var(--surface-raised));
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.scope-button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 6px 14px;
  white-space: nowrap;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
}

.scope-button:hover { background: var(--surface-card); color: var(--text-primary); }

.scope-button.is-active {
  background: var(--surface-card);
  color: var(--text-primary);
  font-weight: 600;
  box-shadow: var(--elevation-1);
}

.scope-button.is-active .chev { color: var(--text-primary); }

.control {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 232px;
  padding: 8px 12px;
  text-align: left;
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.control:hover { border-color: var(--accent); }

.control-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.control-body { display: grid; flex: 1; min-width: 0; }

.control-value {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.control-detail {
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * Chevrons and ticks are drawn with borders rather than typed as
 * characters, so nothing in the interface depends on a glyph or an emoji
 * font. .chev points down by default.
 */
.chev {
  display: inline-block;
  flex: none;
  width: 6px;
  height: 6px;
  margin-bottom: 2px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  color: var(--text-tertiary);
}

.chev--up { margin-bottom: 0; margin-top: 3px; transform: rotate(-135deg); }
.chev--tiny { width: 4px; height: 4px; border-width: 1.25px; }
.chev--hidden { visibility: hidden; }

.tick {
  align-self: center;
  justify-self: center;
  width: 5px;
  height: 9px;
  margin-top: -2px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(45deg);
}

.tick--hidden { visibility: hidden; }

/* --- Popovers --- */

.popover {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  width: 330px;
  max-height: 70vh;
  overflow-y: auto;
  padding: 6px;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-2);
}

.popover--shops { width: 340px; }
.popover[hidden] { display: none; }

.popover-list { display: grid; gap: 1px; }

.popover-row {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 8px;
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-primary);
}

/* Hover stays a ghost wash so it never competes with the selected row. */
.popover-row:hover { background: var(--surface-raised); }

.popover-row.is-selected .popover-row-label { font-weight: 600; }

.popover-row-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.popover-row-detail { font-size: 12px; color: var(--text-tertiary); white-space: nowrap; }

.popover-row--check { grid-template-columns: 18px 1fr auto; }
.popover-checkbox { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); }

.popover-group-heading {
  padding: 10px 8px 4px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.popover-search-wrap { padding: 4px 4px 8px; }

.popover-search,
.inline-search {
  width: 100%;
  padding: 8px 10px;
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}

.popover-divider {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border-subtle);
}

.popover-footer {
  display: grid;
  gap: 8px;
  padding: 10px 8px 6px;
  margin-top: 6px;
  border-top: 1px solid var(--border-subtle);
}

.popover-footer--tight { padding-bottom: 2px; }

.popover-footer-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.date-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.date-field { display: grid; gap: 3px; font-size: 12px; color: var(--text-secondary); }

.date-input {
  padding: 7px 8px;
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}

.popover-error { font-size: 12px; color: var(--negative); }
.popover-empty { padding: 14px 8px; font-size: 13px; color: var(--text-secondary); }

/* --- The live ticker ---
 *
 * Not a report figure and dressed so it cannot be mistaken for one. A
 * solid neutral border rather than the dashed accent it used to carry:
 * the accent said "selected" and the amber before it said "warning",
 * and it is neither. What marks it out is the pulsing dot and the word
 * Live, which is the vocabulary of an instrument rather than a total.
 *
 * It hides itself when the report is already about today, so [hidden]
 * has to beat this display rule the same way it does on .filter-bar.
 */
.live-tile {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  text-align: left;
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.live-tile[hidden] { display: none; }

.live-tile:hover { border-color: var(--border-strong); background: var(--surface-raised); }

.live-head { display: inline-flex; align-items: center; gap: 6px; }

.live-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
  white-space: nowrap;
}

/* The one thing on the screen that is allowed to move on its own. It is
   a state indicator, not decoration, so it is drawn in CSS like every
   other affordance here rather than typed as a character. */
.live-dot {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--positive);
}

@media (prefers-reduced-motion: no-preference) {
  .live-dot { animation: live-pulse 2s ease-in-out infinite; }
}

@keyframes live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: 0.45; transform: scale(0.82); }
}

.live-body { display: grid; }
.live-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.live-detail { font-size: 12px; color: var(--text-secondary); }

/* The reading's own time. Without it two screens read a minute apart
   look like a dashboard disagreeing with itself. */
.live-stamp {
  font-size: 11px;
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  align-self: flex-end;
}

/* A figure that could not be re-read is not dressed as a live one. */
.live-tile.is-stale .live-dot {
  background: var(--text-tertiary);
  animation: none;
}

/* ================================================================== *
 * 5. CARDS, STATS, TABLES
 * ================================================================== */

.card {
  padding: var(--space-4);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-1);
}

.card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  /* A bare title is one 15px line, so it wants less air beneath it than a
     title with a second line under it did. :has() says that without
     threading a class through every call site; a browser without :has()
     keeps the roomier gap, which is the safe way to be wrong. */
  margin-bottom: var(--space-3);
}

.card-head:has(.card-subtitle) { margin-bottom: var(--space-4); }

.card-title { font-size: 15px; font-weight: 600; }
.card-subtitle { margin-top: 2px; font-size: 12.5px; color: var(--text-secondary); }

/* A title carrying a definition in its tooltip. Marked, because a tooltip
   nobody knows is there is not documentation; quietly, because it is a
   footnote and not a control. */
.card-title--hinted {
  text-decoration: underline dotted var(--border-strong);
  text-underline-offset: 3px;
  cursor: help;
}

/*
 * A real grid with a fixed column count, not auto-fit.
 *
 * Overview holds five tiles, one of which is the hero and spans two
 * columns, so six columns divide exactly and the row fills edge to edge.
 * auto-fit used to size columns off a 190px minimum, which is why the
 * hero came out at roughly twice its neighbours by accident rather than
 * on purpose, and why the row rarely ended flush.
 *
 * Counts step 5 -> 3 -> 2 -> 1 tiles per row. The queries are on the
 * .main CONTAINER, so they follow the width the cards really have.
 */
.stat-row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-3);
}

/* Exactly one hero figure per view. The double-width tile is deliberate:
   a full year of revenue is eleven characters and was running out of its
   own card. Declared here, above the container queries, because the
   single-column rule below turns it off and both have equal specificity:
   whichever comes last would otherwise win. */
.stat--hero { grid-column: span 2; }

/* Four equal tiles: Timing, Payments and Add-ons. 4 -> 2 -> 1. */
.stat-row--compact { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* Admin's pair of tiles shares a flex row with the action buttons, so it
   sizes to two columns rather than leaving half a four-column grid empty. */
.stat-row.admin-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@container main (max-width: 1249px) {
  /* Six columns here would put the tiles under about 175px. */
  .stat-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@container main (max-width: 799px) {
  .stat-row,
  .stat-row--compact { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@container main (max-width: 559px) {
  .stat-row,
  .stat-row--compact,
  .stat-row.admin-stats { grid-template-columns: minmax(0, 1fr); }
  /* One column: there is no second column left to span. */
  .stat--hero { grid-column: auto; }
}

.stat {
  padding: var(--space-4);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-1);
}

.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.stat-value {
  margin-top: 4px;
  font-size: 26px;
  overflow-wrap: anywhere;
  font-weight: 600;
  letter-spacing: -0.02em;
  /* Proportional figures: tabular-nums makes big numbers look gappy. */
}

.stat--hero .stat-value {
  font-size: 34px;
  white-space: nowrap;
}

@container main (min-width: 800px) {
  .stat--hero .stat-value { font-size: 38px; }
}

.stat-delta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px;
  margin-top: 5px;
  font-size: 12.5px;
  color: var(--text-secondary);
}

.stat-delta-value { font-weight: 600; }
.stat-delta--up .stat-delta-value { color: var(--positive); }
.stat-delta--down .stat-delta-value { color: var(--negative); }
.stat-delta--flat .stat-delta-value { color: var(--text-secondary); }
.stat-delta--none { color: var(--text-tertiary); }
.stat-delta-abs { color: var(--text-tertiary); }

.stat-row--compact .stat-value { font-size: 20px; }

.stat-footnote {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-tertiary);
}


/* --- Tables --- */

.table-wrap { overflow-x: auto; }

/*
 * The scroll box a sticky header sticks inside.
 *
 * .table-wrap already scrolled - overflow-x:auto makes overflow-y compute
 * to auto too - but it had no height, so a sticky <th> had nothing to
 * stick against and scrolled away with the page. This box is only given a
 * height once the table is long enough to need one.
 */
.table-scroll { overflow: auto; }

.table-scroll--tall {
  max-height: min(70vh, 620px);
  /* A hairline so the box reads as a scroll area rather than a cut-off. */
  border-bottom: 1px solid var(--border-subtle);
}

.table-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-2);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.data-table th, .data-table td {
  padding: 8px 12px;
  text-align: left;
  border-bottom: 1px solid var(--border-subtle);
  white-space: nowrap;
}

.data-table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-card);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-tertiary);
  font-weight: 600;
  /* The header must not blur into the first row as it passes beneath. */
  box-shadow: inset 0 -1px 0 var(--border-strong);
}

/* --- Density. A reading preference, shared and remembered. --- */
.data-table--compact th,
.data-table--compact td { padding: 3px var(--space-3); }

.data-table--compact { font-size: 12.5px; }

.data-table td { font-variant-numeric: tabular-nums; }
.data-table td:first-child { font-variant-numeric: normal; }

.data-table tbody tr:hover { background: var(--surface-raised); }
.data-table tbody tr:last-child td { border-bottom: 0; }

.align-right { text-align: right !important; }

.th-button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  text-transform: inherit;
  letter-spacing: inherit;
}

/*
 * The affordance. Every one of these is clickable, and until now the
 * only column that looked it was the one already sorted.
 */
.th-button {
  border-radius: var(--radius-xs);
  cursor: pointer;
}

.th-button:hover { color: var(--text-primary); }
.data-table th.is-sorted .th-button { color: var(--text-primary); }
.th-button .chev { color: currentColor; }

.data-table th:hover { background: var(--surface-raised); }

/*
 * The direction mark always occupies its space, so a header does not
 * shift when it becomes the sorted one. It is invisible on a column that
 * is neither sorted nor pointed at, half-strength on hover as a hint,
 * and full strength on the column actually doing the sorting.
 */
.th-chev { opacity: 0; }
.data-table th:hover .th-chev { opacity: 0.45; }
.th-button:focus-visible .th-chev { opacity: 0.45; }
.th-chev--on,
.data-table th:hover .th-chev--on { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .th-chev { transition: opacity 0.12s ease; }
}

.table-more { margin-top: var(--space-3); }

.inline-search-wrap { display: grid; gap: 6px; }
.inline-search-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.inline-search-row .inline-search { flex: 1 1 260px; max-width: 420px; }
.inline-filter { display: flex; }
/* Required, for the same reason .filter-bar[hidden] is: the display rule
   above would otherwise beat the attribute. Rolled-up add-ons hide it. */
.inline-filter[hidden] { display: none; }

/* A labelled control on the same rail as the kind filter. The label is the
   question ("Add-ons"), the options are the answers, and the hint on each
   option says what it changes, exactly as the Categories controls do. */
.inline-control { display: flex; align-items: center; gap: 10px; }
.inline-filter-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* ---- Categories: the two grouping controls ----
 *
 * Side by side on one rail, each labelled, each option carrying its own
 * explanation as a tooltip on the button rather than as a paragraph under
 * the heading. A control that needs explaining explains itself where the
 * hand already is.
 */
.category-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
}

.category-control { display: flex; align-items: center; gap: 10px; }

.category-control-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
}

/* The category name is the handle that opens its trend, so it reads as a
   control without becoming a second column of buttons. */
.category-open { text-align: left; }

/* ---- Compare: the period list ----
 *
 * One row per period: its colour key, its number, the same date picker
 * the rest of the app uses, and a remove control that is disabled rather
 * than hidden at the minimum of two, so the row does not change shape as
 * periods come and go.
 */

.compare-controls { display: grid; gap: var(--space-3); }

/* Four chart names is more than one line holds on a narrow column, so
   the toggle wraps rather than pushing the page sideways. */
.compare-controls .measure-toggle { flex-wrap: wrap; }

.period-list { display: grid; gap: var(--space-2); }

.period-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

/* The chart choice sits on the same rail as the periods, so the whole
   card reads as one set of controls rather than two stacked ones. */
.period-row--chart {
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border-subtle);
}

/* Matches the legend key in the chart, so the row and the line are
   obviously the same period without reading a word. */
.period-key {
  width: 14px;
  height: 3px;
  border-radius: 2px;
  flex: none;
}

.period-head {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 96px;
}

.period-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
}

.period-remove { margin-left: auto; }

/* The category applies to every period at once, so it sits on the chart
   rail rather than on any one period's row. */
.period-name--category { margin-left: var(--space-4); }
.category-select { min-width: 190px; max-width: 260px; padding: 7px 10px; }
.period-remove[disabled] { opacity: 0.4; cursor: not-allowed; }

.period-actions { display: flex; }

.period-cell { display: inline-flex; align-items: center; gap: 8px; }

/* --- Comparison periods on a chart header ---
 *
 * Chips, one per comparison window, labelled with the dates the API
 * resolved. The key is a short dashed rule in the period's own colour,
 * so the chip and the line it stands for read as one thing. Add period
 * is a chip-shaped button so the row scans as one control.
 */
.period-chips {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 4px 2px 8px;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-secondary);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  white-space: nowrap;
}

.chip-key {
  width: 14px;
  height: 0;
  border-top: 2px dashed currentColor;
  border-color: var(--chart-comparison);
}

.chip-label { color: var(--text-primary); }

.chip-remove {
  width: 18px;
  height: 18px;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  color: var(--text-tertiary);
  background: none;
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}
.chip-remove:hover, .chip-remove:focus-visible { color: var(--text-primary); background: var(--surface-active, var(--accent-subtle)); }
.chip-remove[disabled] { opacity: 0.35; cursor: not-allowed; }

.chip--add {
  padding: 2px 10px;
  color: var(--text-secondary);
  background: transparent;
  border-style: dashed;
  cursor: pointer;
}
.chip--add:hover, .chip--add:focus-visible { color: var(--text-primary); border-color: var(--border-strong); }
.chip--add[disabled] { opacity: 0.45; cursor: not-allowed; }

.chip-add-wrap { display: inline-flex; }
.popover--periods { right: 0; left: auto; min-width: 260px; }

.chart-notes {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  font-size: 12px;
  color: var(--text-secondary);
}

/* A stat tile carrying a tooltip says so with the same dotted rule a
   hinted card title uses, so the affordance is one thing everywhere. */
.stat--hinted .stat-label { text-decoration: underline dotted var(--border-strong); text-underline-offset: 3px; cursor: help; }
.stat--hinted:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }

/*
 * The change against period 1: absolute, percentage, and a direction mark
 * that is drawn rather than typed, so the sign survives a mono print and
 * a colourblind reader. Never colour alone.
 */
.delta-cell {
  display: inline-flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}

.delta-mark {
  width: 0;
  height: 0;
  align-self: center;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}

.delta-cell--up .delta-mark { border-bottom: 5px solid var(--positive); }
.delta-cell--down .delta-mark { border-top: 5px solid var(--negative); }
/* Flat has no direction to point in, so it keeps the width and draws a bar. */
.delta-cell--flat .delta-mark {
  width: 8px;
  height: 2px;
  border: 0;
  background: var(--text-tertiary);
}

.delta-cell--up .delta-abs { color: var(--positive); }
.delta-cell--down .delta-abs { color: var(--negative); }
.delta-abs { font-weight: 600; }
.delta-pct { color: var(--text-secondary); }

/*
 * What the chart cannot say for itself: unequal lengths, a period running
 * into the future, a period that took nothing. Warnings, not blocks.
 */
.compare-notes {
  display: grid;
  gap: 4px;
  margin: var(--space-3) 0 0;
  padding: 0 0 0 var(--space-4);
  font-size: 12.5px;
  color: var(--text-secondary);
}


/*
 * Add-on marking.
 *
 * A muted bar plus the word, never a second hue: the chart's colour is
 * already carrying which measure is plotted, and adding a colour for
 * "kind" would have two different meanings sharing one channel.
 */
.bar-tag {
  font-size: 10px;
  fill: var(--chart-axis-label);
  font-family: var(--sans);
  letter-spacing: 0.04em;
}

.product-cell { display: inline-flex; align-items: baseline; gap: 8px; }

.tag {
  padding: 1px 6px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
  border-radius: var(--radius-xs);
  color: var(--text-tertiary);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
}

/* Three kinds, told apart by weight and wording rather than by hue, which
   is already carrying the measure the chart is plotting. */
.tag--both {
  color: var(--text-secondary);
  border-color: var(--border-strong);
  background: transparent;
}
/* --- Buttons --- */

.button {
  padding: 8px 14px;
  font-weight: 500;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-card);
  color: var(--text-primary);
  cursor: pointer;
}

.button:hover { background: var(--surface-raised); }

.button--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-on-accent);
  font-weight: 600;
}

.button--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.button--primary:disabled { opacity: 0.6; cursor: default; }

/* A real action, but not the main one on the panel. */
.button--secondary {
  background: var(--surface-raised);
  border-color: var(--border-strong);
  color: var(--text-primary);
  font-weight: 600;
}

.button--secondary:hover { border-color: var(--accent); }

.button--quiet { background: none; border-color: var(--border-subtle); color: var(--text-secondary); }
.button--block { width: 100%; }

.link-button {
  padding: 4px 0;
  background: none;
  border: 0;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  justify-self: start;
}

/* ================================================================== *
 * 6. CHARTS
 * ================================================================== */

.plot { width: 100%; }

/* A period with no trade is a fact, not an error, so it is stated
   quietly and centred rather than left as a bare axis frame. */
.chart-empty {
  display: grid;
  place-items: center;
  min-height: 140px;
  padding: var(--space-5);
}

.chart-empty-text {
  color: var(--text-tertiary);
  font-size: 13px;
}
.chart-svg { display: block; width: 100%; overflow: visible; }
.chart-svg:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.axis-text {
  font-size: 11px;
  fill: var(--chart-axis-label);
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
}

.bar-label {
  font-size: 12px;
  fill: var(--text-secondary);
  font-family: var(--sans);
}

.bar-value {
  font-size: 12px;
  fill: var(--text-primary);
  font-family: var(--sans);
  font-variant-numeric: tabular-nums;
}

.chart-hit { cursor: crosshair; }
.crosshair { pointer-events: none; }

.bar-group { cursor: default; }

@media (prefers-reduced-motion: no-preference) {
  .bar-mark { transition: opacity var(--motion-fast) var(--ease-out); }
}
.bar-group:hover .bar-mark,
.bar-group:focus-visible .bar-mark { opacity: 0.82; }
.bar-group:focus { outline: none; }
.bar-group:focus-visible .bar-mark { stroke: var(--text-primary); stroke-width: 1.5; }

.cell-group { cursor: default; }
.cell-group:hover .cell-mark { stroke: var(--text-primary); stroke-width: 1.5; }
.cell-group:focus { outline: none; }
.cell-group:focus-visible .cell-mark { stroke: var(--text-primary); stroke-width: 2; }

/* The legend sits above the plot, outside it. Nothing drawn in the chart
   may reach it: see the headroom rule in niceTicks(). */
.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
  font-size: 12.5px;
  color: var(--text-secondary);
}

.legend-item { display: inline-flex; align-items: center; gap: 7px; }
.legend-line-svg { flex: none; overflow: visible; }
.legend-swatch { width: 11px; height: 11px; border-radius: var(--radius-xs); }

.ramp-legend { display: flex; align-items: center; gap: var(--space-2); }

/* Was 11px against a 92x9 sliver, which is a legend you have to go
   looking for. It is the only key the heatmap has. */
.ramp-legend-label { font-size: 12.5px; color: var(--text-secondary); }

.ramp-legend-bar {
  width: 140px;
  height: 14px;
  border-radius: var(--radius-xs);
  background: linear-gradient(to right,
    var(--chart-ramp-1), var(--chart-ramp-2), var(--chart-ramp-3),
    var(--chart-ramp-4), var(--chart-ramp-5), var(--chart-ramp-6), var(--chart-ramp-7));
}

/* --- Tooltip --- */

.chart-tooltip {
  position: fixed;
  z-index: 200;
  pointer-events: none;
  min-width: 148px;
  /* Wide enough for a period overlay's rows, which carry a range, a real
     date and a change on one line. Short tooltips are unaffected: this is
     a cap, not a width. */
  max-width: 340px;
  padding: 9px 11px;
  background: var(--chart-tooltip-bg);
  border: 1px solid var(--chart-tooltip-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--elevation-overlay);
  font-size: 12.5px;
}

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

.tt-title {
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--text-secondary);
}

.tt-row {
  display: grid;
  /* Fourth column for the change against the first series; empty on
     every tooltip that does not overlay periods. */
  grid-template-columns: auto auto 1fr auto;
  align-items: baseline;
  gap: 7px;
  padding: 1px 0;
}

.tt-change { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.tt-change--up { color: var(--positive); }
.tt-change--down { color: var(--negative); }

.tt-key { width: 12px; height: 2px; border-radius: 1px; align-self: center; }

/* The delta has no series of its own, but it keeps the key column so the
   numbers above it stay in one line. */
.tt-key--none { background: none; }

/*
 * The difference between the two series. Ruled off, because it is a
 * derived figure rather than a third reading.
 */
.tt-row--delta {
  margin-top: 5px;
  padding-top: 5px;
  border-top: 1px solid var(--border-subtle);
}

.tt-row--delta .tt-value { color: var(--text-secondary); }
.tt-row--up .tt-value { color: var(--positive); }
.tt-row--down .tt-value { color: var(--negative); }

/* Value leads, label follows - the reader already knows the series. */
.tt-value { font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.tt-label { color: var(--text-secondary); }

/* ================================================================== *
 * 6b. MOTION: ENTRANCES, LOADING, TOAST
 *
 * Every block here is inside prefers-reduced-motion: no-preference. The
 * reduce block in section 7 is the backstop, but nothing should have to
 * rely on it.
 * ================================================================== */

/*
 * KPI entrance. Staggered fade and an 8px rise, 40ms apart.
 *
 * FIRST LOAD ONLY. views.js adds .stat-row--enter the first time a
 * section renders in this session and never again, because tiles that
 * re-animate on every date change turn a filter into a light show, and
 * the figures are what people are trying to read.
 */
@media (prefers-reduced-motion: no-preference) {
  .stat-row--enter > .stat {
    animation: kpi-enter var(--motion-base) var(--ease-out) both;
  }

  .stat-row--enter > .stat:nth-child(1) { animation-delay: 0ms; }
  .stat-row--enter > .stat:nth-child(2) { animation-delay: 40ms; }
  .stat-row--enter > .stat:nth-child(3) { animation-delay: 80ms; }
  .stat-row--enter > .stat:nth-child(4) { animation-delay: 120ms; }
  .stat-row--enter > .stat:nth-child(5) { animation-delay: 160ms; }
  .stat-row--enter > .stat:nth-child(6) { animation-delay: 200ms; }
}

@keyframes kpi-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/*
 * Chart entrance. Bars grow from their baseline, lines draw left to
 * right. Longer than the 250ms everything else lives inside, because
 * this is a first impression rather than an answer to a click.
 *
 * Applied by charts.js on a container's FIRST draw only. A redraw from a
 * resize or a theme change does not re-run it, and hovering never
 * redraws at all, so a tooltip cannot re-trigger it.
 */
@media (prefers-reduced-motion: no-preference) {
  .chart-enter .bar-mark {
    /* fill-box, or the scale happens about the SVG origin and the bar
       flies in from the corner. */
    transform-box: fill-box;
    transform-origin: left center;
    animation: bar-grow var(--motion-chart) var(--ease-out) both;
  }

  .chart-enter .bar-mark--column {
    transform-origin: bottom center;
    animation-name: column-grow;
  }

  /*
   * pathLength is set to 1 on the path, so one unit is the whole line
   * whatever its real length.
   *
   * ONLY lines that are not already dashed. The comparison series is
   * dashed on purpose - it is the backdrop the current period is read
   * against - and a CSS stroke-dasharray beats the presentation
   * attribute that makes it so, which quietly turned it solid.
   */
  .chart-enter .series-line:not([stroke-dasharray]) {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: line-draw var(--motion-chart) var(--ease-out) both;
  }

  /* The comparison fades in rather than drawing, keeping its dashes. */
  .chart-enter .series-line[stroke-dasharray] {
    animation: cell-in var(--motion-chart) var(--ease-out) both;
  }

  /* A range bar spans from x1 to x2; scaling it from one end would
     claim the shop opened at midnight and crept forward. */
  .chart-enter .range-mark {
    animation-name: cell-in;
    transform: none;
  }

  .chart-enter .cell-mark {
    animation: cell-in var(--motion-base) var(--ease-out) both;
  }
}

@keyframes bar-grow    { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes column-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes line-draw   { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes cell-in     { from { opacity: 0; } to { opacity: 1; } }

/*
 * A filter change dims the figures it is replacing instead of blanking
 * the page. The old numbers stay legible and in place, which is the
 * difference between "working" and "gone".
 */
.section-host.is-refreshing {
  opacity: 0.4;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .section-host { transition: opacity var(--motion-fast) var(--ease-out); }
}

/* --- Skeletons ---
 * Only on a first load, where there is nothing to dim. Grey blocks in
 * the shape of what is coming, so the page does not jump when it lands.
 */
.skeleton { display: grid; gap: var(--space-5); }

.skeleton-row {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-3);
}

@container main (max-width: 1249px) { .skeleton-row { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@container main (max-width: 799px)  { .skeleton-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container main (max-width: 559px)  { .skeleton-row { grid-template-columns: minmax(0, 1fr); } }

.skeleton-tile {
  height: 96px;
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.skeleton-tile--hero { grid-column: span 2; }

.skeleton-card {
  padding: var(--space-4);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.skeleton-block { background: var(--surface-raised); border-radius: var(--radius-xs); }
.skeleton-block--title { width: 180px; height: 15px; margin-bottom: var(--space-2); }
.skeleton-block--sub { width: 260px; height: 12px; margin-bottom: var(--space-5); }
.skeleton-block--plot { height: 220px; }
.skeleton-block--row { height: 34px; margin-top: var(--space-2); }

@media (prefers-reduced-motion: no-preference) {
  .skeleton-tile,
  .skeleton-block { animation: skeleton-pulse 1.4s ease-in-out infinite; }
}

@keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

/* --- Toast ---
 * An export that works is silent otherwise: the file lands in a folder
 * the page cannot see, so the only feedback is the browser's own.
 */
.toast-area {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  z-index: 400;
  display: grid;
  gap: var(--space-2);
  justify-items: end;
  pointer-events: none;
}

.toast {
  padding: 10px var(--space-4);
  font-size: 13px;
  color: var(--text-primary);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: var(--elevation-overlay);
  max-width: 320px;
}

@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-in var(--motion-base) var(--ease-out) both; }
  .toast.is-leaving { animation: toast-out var(--motion-fast) var(--ease-out) both; }
}

@keyframes toast-in  { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; } }

@media (max-width: 560px) {
  .toast-area { right: var(--space-4); left: var(--space-4); bottom: var(--space-4); justify-items: stretch; }
  .toast { max-width: none; }
}

/* ================================================================== *
 * 7. LOADING / EMPTY / ERROR
 * ================================================================== */

.state {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
  padding: var(--space-7) var(--space-5);
  text-align: center;
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.state-title { font-weight: 600; }
.state-detail { max-width: 46ch; color: var(--text-secondary); font-size: 13px; }

.state--error { border-color: color-mix(in srgb, var(--negative) 40%, var(--border-subtle)); }
.state--error .state-title { color: var(--negative); }

.state-spinner {
  width: 20px; height: 20px;
  border: 2px solid var(--chart-grid);
  border-top-color: var(--accent);
  border-radius: 50%;
}

/*
 * The one animation that survives a reduced-motion preference, because
 * it is the only one carrying information rather than decorating
 * something: a still spinner says "stopped". It just turns slowly.
 */
@media (prefers-reduced-motion: no-preference) {
  .state-spinner { animation: spin 0.8s linear infinite; }
}

@keyframes spin { to { transform: rotate(360deg); } }

/*
 * The backstop.
 *
 * Every motion block above is already gated on no-preference, so this
 * should never be the thing doing the work. It is here because one
 * missed gate is a vestibular problem for somebody, and because
 * animation needs killing as well as transition: the two are separate
 * properties and a rule that only names transitions leaves every
 * keyframe running.
 *
 * The spinner keeps turning, slowly. It is the only animation that
 * carries information rather than decorating something.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition: none !important;
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }

  .state-spinner {
    animation: spin 2.4s linear infinite !important;
    animation-duration: 2.4s !important;
    animation-iteration-count: infinite !important;
  }
}

/* ================================================================== *
 * 8. AUTH SCREENS
 * ================================================================== */

.screen--auth {
  display: grid;
  place-items: center;
  padding: 30px 20px;
  background: var(--surface-page);
}

.auth-panel {
  width: 100%;
  max-width: 380px;
  display: grid;
  gap: var(--space-4);
  padding: var(--space-5);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--elevation-overlay);
}

.auth-brand { padding: 0; }
.auth-title { font-size: 20px; font-weight: 600; }
.auth-lead { color: var(--text-secondary); font-size: 13px; }
.auth-foot { margin-top: 4px; font-size: 11.5px; color: var(--text-tertiary); }

.auth-form { display: grid; gap: 13px; }
.field { display: grid; gap: 4px; }
.field-label { font-size: 12.5px; color: var(--text-secondary); }

.field-input {
  padding: 9px 11px;
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}

.field-input:focus { border-color: var(--accent); }

.form-error {
  padding: 8px 10px;
  font-size: 13px;
  color: var(--negative);
  background: var(--negative-subtle);
  border-radius: var(--radius-sm);
}

.form-notice {
  padding: 8px 10px;
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
}

/* ================================================================== *
 * 9. EXPORTS, BADGES AND THE ADMIN TABLE
 * ================================================================== */

.card-head-tools {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* --- Chart export menu ---
 *
 * One trigger per card instead of three buttons. It is invisible until
 * the card is hovered or something inside it takes focus, so a page of
 * six charts is not also a page of eighteen buttons.
 *
 * Opacity rather than display, so the button keeps its space and the
 * card header does not reflow when you point at it.
 */
.export-controls { display: flex; gap: var(--space-1); }

.export-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  background: none;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  cursor: pointer;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .export-trigger { transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease; }
}

.card:hover .export-trigger,
.card:focus-within .export-trigger,
.export-trigger[aria-expanded="true"] { opacity: 1; }

/* Keyboard users must never lose it, whatever the hover state says. */
.export-trigger:focus-visible { opacity: 1; }

.export-trigger:hover {
  opacity: 1;
  color: var(--text-primary);
  border-color: var(--border-strong);
  background: var(--surface-raised);
}

/*
 * No hover on touch, so there is no way to reveal it. Always on there.
 * A media query, not a device sniff: this asks what the input can do.
 */
@media (hover: none) {
  .export-trigger { opacity: 1; }
}

.export-menu {
  width: 190px;
  left: auto;
  right: 0;
  padding: var(--space-1);
}

.export-menu-title {
  padding: 6px 8px 4px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.export-option {
  display: block;
  width: 100%;
  padding: 7px 8px;
  text-align: left;
  font-size: 13px;
  color: var(--text-primary);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.export-option:hover { background: var(--surface-raised); }
.export-option:disabled { color: var(--negative); cursor: default; }

.card-tools { display: flex; }

/*
 * Measure toggle. Sits with its chart, not in the toolbar, because it
 * changes how one chart reads rather than what the page is about. Styled
 * like the theme control so it does not read as another filter.
 */
.measure-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface-track, var(--surface-raised));
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
}

.measure-option {
  padding: 4px 11px;
  font-size: 12px;
  white-space: nowrap;
  background: none;
  border: 0;
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  cursor: pointer;
}

.measure-option:hover { background: var(--surface-card); color: var(--text-primary); }

.measure-option.is-active {
  background: var(--surface-card);
  color: var(--text-primary);
  font-weight: 600;
  box-shadow: var(--elevation-1);
}


/* --- Invite badges ---
   Weighted deliberately: an expired invite has to be findable at a glance
   down a column of thirty rows, and an accepted one has to stay out of
   the way. Colour is never the only signal; each badge carries its own
   word, and the dot is decoration on top of that. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
}

.badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.badge-count {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}

.badge--negative {
  color: var(--negative);
  background: var(--negative-subtle);
  border-color: color-mix(in srgb, var(--negative) 40%, transparent);
}

.badge--warning {
  color: var(--warning);
  background: var(--warning-subtle);
  border-color: color-mix(in srgb, var(--warning-mark) 45%, transparent);
}

.badge--quiet {
  color: var(--text-tertiary);
  background: var(--neutral-subtle);
  border-color: var(--border-subtle);
  font-weight: 500;
}

.badge--quiet .badge-dot { opacity: 0.55; }

.badge--inline { margin-left: 8px; font-size: 11px; padding: 1px 7px; }

/* --- Admin table --- */

.account-cell { display: inline-flex; align-items: center; }
.account-cell-email { font-variant-numeric: normal; }
.muted-cell { color: var(--text-tertiary); }

/* Muted with colour, never with opacity. A translucent cell in the pinned
   first column lets the scrolled-away columns show through it. */
.data-table tbody tr.is-disabled-row td { color: var(--text-tertiary); }
.data-table tbody tr.is-disabled-row .account-cell-email { text-decoration: line-through; }

.lazy-card:empty { display: none; }
.lazy-card { margin-top: var(--space-4); }

.section-note {
  padding: var(--space-3) var(--space-4);
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.section-note strong { color: var(--text-primary); }

/* --- Error panel detail --- */

.state-diagnostic {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text-tertiary);
  padding: 5px 10px;
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
}

.state-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }

/* ================================================================== *
 * 10. MODALS AND THE ADMIN COMPOSERS
 * ================================================================== */

body.has-modal { overflow: hidden; }

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 20px;
  overflow-y: auto;
  background: color-mix(in srgb, #000 50%, transparent);
}

.modal {
  width: 100%;
  max-width: 480px;
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-overlay);
}

.modal--wide { max-width: 620px; }

.modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5) var(--space-3);
  border-bottom: 1px solid var(--border-subtle);
}

.modal-title { font-size: 16px; font-weight: 600; }
.modal-subtitle { margin-top: 2px; font-size: 12.5px; color: var(--text-secondary); }
.modal-body { padding: var(--space-4) var(--space-5) var(--space-5); }
.modal-lead { font-size: 13px; color: var(--text-secondary); }
.modal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

.modal-close {
  flex: none;
  width: 28px; height: 28px;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.modal-close:hover { background: var(--surface-raised); }

/* An X drawn rather than typed, like the chevrons and ticks. */
.modal-close-mark { position: relative; width: 11px; height: 11px; }
.modal-close-mark::before,
.modal-close-mark::after {
  content: '';
  position: absolute;
  left: 0; top: 5px;
  width: 11px; height: 1.5px;
  background: var(--text-secondary);
}
.modal-close-mark::before { transform: rotate(45deg); }
.modal-close-mark::after { transform: rotate(-45deg); }

.stack { display: grid; gap: var(--space-4); }

/* --- Composed commands --- */

.command-block {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.command-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.command-text {
  margin: 0;
  padding: 12px;
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  /* A command must read exactly as it will be pasted. Wrapping it breaks
     paths across lines and makes the line continuation ambiguous, so it
     scrolls instead. */
  white-space: pre;
  overflow-x: auto;
}

.command-block .button { justify-self: start; }

/* --- Callouts --- */

.callout {
  display: grid;
  gap: 6px;
  padding: 12px 14px;
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--neutral);
  border-radius: var(--radius-sm);
}

.callout-title { font-weight: 600; color: var(--text-primary); }
.callout-list { font-size: 12.5px; color: var(--text-primary); overflow-wrap: anywhere; }

.callout--warning {
  border-left-color: var(--warning-mark);
  background: color-mix(in srgb, var(--warning-mark) 10%, var(--surface-card));
}

.callout--danger {
  border-left-color: var(--negative);
  background: color-mix(in srgb, var(--negative) 9%, var(--surface-card));
}

.callout--danger .callout-title { color: var(--negative); }

/* --- The permissions diff --- */

.diff {
  display: grid;
  gap: 8px;
  padding: 14px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.diff-title { font-weight: 600; font-size: 13px; }

.diff-grid {
  display: grid;
  grid-template-columns: auto 1fr 1fr;
  gap: 6px 14px;
  font-size: 13px;
  align-items: start;
}

.diff-key { color: var(--text-tertiary); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
.diff-from { color: var(--text-secondary); text-decoration: line-through; text-decoration-color: var(--text-tertiary); }
.diff-to { color: var(--text-secondary); }

.diff-to.is-changed {
  color: var(--text-primary);
  font-weight: 600;
  background: var(--positive-subtle);
  border-radius: var(--radius-xs);
  padding: 0 5px;
  margin: 0 -5px;
}

/* --- Row actions --- */

.row-actions-wrap { display: grid; gap: 4px; justify-items: start; }
.row-actions { display: flex; gap: 4px; }

.row-button {
  padding: 4px 10px;
  font-size: 12px;
  color: var(--text-secondary);
  background: var(--surface-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
}

.row-button:hover { background: var(--surface-raised); color: var(--text-primary); }

/* Delete is not a peer of Disable. Quieter, and on its own line. */
.row-button--quiet {
  padding: 2px 4px;
  font-size: 11.5px;
  color: var(--text-tertiary);
  background: none;
  border: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.row-button--quiet:hover { color: var(--negative); background: none; }

.button--danger {
  background: var(--negative);
  border-color: var(--negative);
  color: var(--text-on-accent);
  font-weight: 600;
}

.button--danger:hover { filter: brightness(1.06); background: var(--negative); }

/* --- Forms in the composers --- */

.field-hint { font-size: 12px; color: var(--text-tertiary); }
.field-input--area { font-family: var(--mono); font-size: 12px; resize: vertical; }

.radio-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 3px 0; }
.radio-row input { accent-color: var(--accent); }

.site-list {
  max-height: 190px;
  overflow-y: auto;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 4px;
}

.site-list.is-disabled { opacity: 0.45; pointer-events: none; }

.site-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  font-size: 13px;
  border-radius: var(--radius-xs);
}

.site-row:hover { background: var(--surface-raised); }
.site-row-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.site-row-group { font-size: 11px; color: var(--text-tertiary); }

/* --- Invite email preview --- */

.email-preview {
  display: grid;
  gap: 6px;
  padding: 14px;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.email-field-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.email-subject { font-weight: 600; font-size: 13.5px; }

.email-body {
  margin: 0;
  padding: 12px;
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* --- Admin header and rows --- */

.admin-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

/* The grid inside needs min-width:0 or its items keep their min-content
   width, the row refuses to shrink, and the New user button is pushed off
   the right edge of the page. */
.admin-stats { flex: 1 1 320px; min-width: 0; }

/* Create the account, then send the link: one flow, one place. */
.admin-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
.admin-stats .stat { min-width: 0; }
.admin-head > .button { flex: 0 0 auto; }

.next-step {
  display: grid;
  gap: 8px;
  justify-items: start;
  padding-top: 12px;
  border-top: 1px solid var(--border-subtle);
}

.next-step-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.data-table tbody tr.is-attention-row {
  background: color-mix(in srgb, var(--negative) 6%, transparent);
  box-shadow: inset 3px 0 0 var(--negative);
}

/*
 * Pinned identity column.
 *
 * The accounts table is wider than the page, and the column that scrolls
 * out of sight first is the email address. Acting on a row you cannot
 * identify is exactly the mistake this screen exists to prevent, so the
 * first column stays put. The cells inherit their row's background so the
 * attention tint and the hover wash carry across the pin.
 */
.data-table--sticky tbody tr { background: var(--surface-card); }

.data-table--sticky tbody tr.is-attention-row {
  background: color-mix(in srgb, var(--negative) 6%, var(--surface-card));
}

.data-table--sticky tbody tr:hover { background: var(--surface-raised); }

.data-table--sticky th:first-child,
.data-table--sticky td:first-child {
  position: sticky;
  left: 0;
  z-index: 2;
  background: inherit;
}

.data-table--sticky thead th:first-child { background: var(--surface-card); z-index: 3; }

/* A hairline that shows the column is pinned rather than just adjacent. */
.data-table--sticky th:first-child::after,
.data-table--sticky td:first-child::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-subtle);
}

/* Long site lists wrap instead of forcing the table wider. */
.data-table td.cell-wrap {
  white-space: normal;
  min-width: 150px;
  max-width: 230px;
}

.section-note { display: grid; gap: var(--space-3); }

/* --- Admin footnotes ---
   Two prose panels side by side under the accounts table, closing the
   empty band a single full-width table left on a wide monitor. They
   stack the moment there is not room for two readable measures. */
/* One panel now that the "this screen changes nothing" note has gone. It
   holds the full width, but the definitions inside it are capped so the
   dd column does not run to a 1440px measure. */
.admin-footnotes {
  display: grid;
  gap: var(--space-3);
}
.admin-footnotes .note-list { max-width: 90ch; }

.note-heading {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

.note-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  font-size: 12.5px;
}

.note-list dt {
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
}

.note-list dd {
  margin: 0;
  color: var(--text-secondary);
}

@container main (max-width: 559px) {
  /* Two columns stop being two columns when the term takes the width. */
  .note-list { grid-template-columns: minmax(0, 1fr); }
  .note-list dd { margin-bottom: var(--space-2); }
}

/* ================================================================== *
 * 11. NARROW SCREENS
 * ================================================================== */

@media (max-width: 880px) {
  /*
   * Below this width the sidebar becomes a drawer over the content. There
   * is no room to push the report sideways, and the horizontal scrolling
   * nav row this used to be was a poor substitute for the real thing: it
   * hid the account, the theme control and the sign-out behind a swipe.
   */
  .main { margin-left: 0; padding-bottom: var(--space-6); }

  /*
   * A period row stacks. The date picker carries a 232px floor and a full
   * range reading, "04/08/2026 - 31/08/2026 (28 days)", which is not
   * something to truncate: it is the whole point of the row. Side by side
   * with the label and the remove control it took the page past 390px, so
   * on a phone the picker gets the width to itself.
   */
  .period-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .period-row .control-wrap { width: 100%; }
  .period-row .control { width: 100%; min-width: 0; }
  .period-remove { margin-left: 0; align-self: flex-start; }
  .main-col { padding-inline: var(--space-4); }
  .main-col--head { padding-top: 0; }

  body:not(.nav-closed) .nav-backdrop { display: block; }
  .sidebar { box-shadow: var(--elevation-overlay); }

  /*
   * The burger has to stay reachable as the report scrolls, so the header
   * is sticky here. The filter bar gives up its stickiness in exchange:
   * two sticky bars on a phone leave almost no room for the figures.
   *
   * The WRAPPER is what sticks, not the header inside it. A sticky
   * element can only travel within its own parent, so sticking the
   * header itself would pin it to a box barely taller than the header
   * and it would scroll away with the first swipe.
   */
  .main-col--head {
    position: sticky;
    top: 0;
    z-index: 40;
    margin-bottom: var(--space-3);
    background: color-mix(in srgb, var(--surface-page) 92%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border-subtle);
  }

  .main-head {
    flex-wrap: wrap;
    align-items: center;
    margin: 0;
    padding: 10px 0;
  }

  .main-head-left { align-items: center; }
  /* Centred against the title here, so no optical nudge is wanted. */
  .main-head-left .burger { margin-top: 0; margin-left: -10px; }
  .section-title { font-size: 18px; }
  .filter-bar {
    position: static;
    margin-bottom: var(--space-4);
  }

  .filter-bar.is-stuck {
    background: transparent;
    border-bottom-color: transparent;
    box-shadow: none;
    backdrop-filter: none;
  }

  .filter-bar-inner { padding: var(--space-3) var(--space-4); }

  .control { min-width: 0; flex: 1 1 200px; }
  .popover { width: min(330px, calc(100vw - 32px)); }
}

@media (max-width: 560px) {
  .filter-controls { width: 100%; }
  .scope-bar { width: 100%; }
  .scope-button { flex: 1 1 auto; justify-content: center; padding: 6px 10px; }
  .live-tile { width: 100%; }

  .stat--hero .stat-value { font-size: 30px; }

  .card { padding: 14px 14px 16px; }
  .card-head { flex-wrap: wrap; gap: 10px; }

  /* A dialog on a phone is the whole screen, not a floating card. */
  .modal-overlay { padding: 0; align-items: stretch; }
  .modal, .modal--wide { max-width: none; min-height: 100%; border: 0; border-radius: 0; }

  .main-head > .button { font-size: 12px; padding: 6px 10px; }
}



/* ================================================================== *
 * 12. PRINT
 *
 * "Save all as PDF" is window.print(). The browser's own print-to-PDF
 * renders the charts as vectors, paginates properly, and costs this
 * codebase nothing: no library, no bundler, no build step.
 *
 * Print always uses the light palette. Paper is white, and a dark theme
 * would otherwise waste a cartridge and read badly.
 * ================================================================== */

@media print {
  :root, :root[data-theme="dark"] {
    color-scheme: light;
    --surface-page: #ffffff;
    --surface-card: #ffffff;
    --surface-raised: #f4f4f2;
    --text-primary: #000000;
    --text-secondary: #333333;
    --text-tertiary: #595959;
    --chart-grid: #d8d8d3;
    --chart-axis: #9a9a94;
    --border-subtle: rgba(0, 0, 0, 0.18);
    --border-strong: rgba(0, 0, 0, 0.3);
    --accent: #1c5cab;
    --chart-series-1: #1c5cab;
    --chart-series-2: #a8461a;
    --chart-comparison: #b0761f;
    --chart-empty-cell: #e4e4e4;
    --surface-sunken: #f4f4f2;

    /* Paper takes the light ramp whatever the screen was showing. */
    --chart-ramp-1: #dde8f8;
    --chart-ramp-2: #bcd4f0;
    --chart-ramp-3: #97bbe8;
    --chart-ramp-4: #6d9edd;
    --chart-ramp-5: #4680d2;
    --chart-ramp-6: #2c63b5;
    --chart-ramp-7: #1a4586;

    --elevation-1: none;
    --elevation-2: none;
    --elevation-overlay: none;
  }

  @page { margin: 14mm; size: A4 portrait; }

  body { background: #fff; }

  /* Chrome that means nothing on paper. */
  .sidebar,
  .filter-bar,
  .no-print,
  .export-controls,
  .table-more,
  .theme-toggle,
  .popover { display: none !important; }

  .shell { display: block; }
  .main { padding: 0; }
  /* Paper sets its own measure with @page margins; the screen cap and
     gutters would just narrow it further. */
  .main-col { max-width: none; padding-inline: 0; }
  .main-col--head { padding-top: 0; }

  /* The filter selection still has to be on the page, or the figures have
     no context a week later. app.js writes this block before printing. */
  .print-header { display: block !important; }

  .card,
  .stat,
  .state {
    border: 1px solid var(--border-subtle);
    box-shadow: none;
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
  .stat--hero .stat-value { font-size: 26px; }
  .section-body { gap: 12px; }

  .data-table { font-size: 10.5px; }
  .data-table th { position: static; }

  a[href]::after { content: none !important; }
}

/* Only ever visible on paper. */
.print-header { display: none; }

@media print {
  .print-header {
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 1px solid #000;
  }
  .print-title { font-size: 14px; font-weight: 600; }
  .print-meta { font-size: 10px; color: #333; }
}

/* ---- Scrollbars ---------------------------------------------------
 *
 * ALWAYS VISIBLE, NEVER HOVER-REVEALED. A scrollbar that appears on hover
 * hides the fact that a region scrolls at all, which is a worse failure
 * than an ugly one: you cannot look for something you have not been told
 * is there. It may brighten on hover. It does not vanish when idle.
 *
 * THE THUMB IS 3:1 AGAINST ITS OWN TRACK, the same floor as any other
 * non-text control. That is why the track is a real colour rather than
 * transparent: over a transparent track the ratio would depend on
 * whatever surface happened to be behind it, and in dark mode that lands
 * around 2:1 on a card. A named track makes the contrast a fact rather
 * than a coincidence.
 *
 * BOTH ENGINES. scrollbar-width and scrollbar-color are the standard
 * properties, honoured by Firefox; the ::-webkit- pseudo-elements cover
 * Chromium and Safari, which ignore the standard ones. Styling one and
 * leaving the other on the platform default is how a theme ends up
 * looking finished on the machine it was built on.
 */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}

::-webkit-scrollbar { width: 10px; height: 10px; }

::-webkit-scrollbar-track { background: var(--scrollbar-track); }

::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--radius-pill);
  /* Inset by the track colour, so the thumb reads as a rounded bar with
     air around it rather than a block filling the gutter. The border is
     the track colour, not transparent: transparent would composite the
     thumb over whatever is behind and undo the contrast above. */
  border: 2px solid var(--scrollbar-track);
}

::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }

/* No arrow buttons, and no pale square where the two bars meet. */
::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
::-webkit-scrollbar-corner { background: var(--scrollbar-track); }

@media (prefers-reduced-motion: no-preference) {
  ::-webkit-scrollbar-thumb { transition: background var(--motion-fast) var(--ease-out); }
}

/*
 * Reserve the gutter on anything that scrolls only sometimes, so content
 * does not jump sideways the moment a bar appears. Not on the sidebar,
 * which always scrolls at its own length, and not on <body>, which would
 * reserve a gutter on every page whether or not one is needed.
 */
.main,
.table-wrap,
.table-scroll { scrollbar-gutter: stable; }
