/* Base styles — element selectors, page layout (.app, .main),
   sidebar + filter chrome (.filter-*, .sidebar*), and the few
   universal utility classes (.loading, .coverage-strip).

   Loaded after tokens.css so var(--...) references resolve. Sister
   sheets (components.css, charts.css, per-tab CSS) will arrive in
   later phases of the migration; see docs/FRONTEND-MIGRATION.md. */

/* The dark theme also slightly recolours the underlying basemap
   inside Leaflet popups so they don't read as pure white pop-ups
   against a dark page. */
html[data-theme="dark"] .leaflet-popup-content-wrapper,
html[data-theme="dark"] .leaflet-popup-tip {
  background: var(--card); color: var(--ink);
}
html[data-theme="dark"] .leaflet-container {
  background: var(--paper-2);
}
/* CartoDB Positron tiles look harsh on dark mode — apply a mild
   inversion + hue-shift so the basemap reads as a soft dark grey
   while keeping the marker dots legible. */
html[data-theme="dark"] .leaflet-tile-pane {
  filter: invert(1) hue-rotate(180deg) brightness(0.95) contrast(0.9);
}

* { box-sizing: border-box; }
/* "overflow-x: clip" rather than "hidden" — same belt-and-braces
   against horizontal page scroll, but unlike hidden it doesn't
   establish a scroll container, so position:sticky descendants
   (the tab bar, the sidebar) actually stick when you scroll. */
html, body { overflow-x: clip; }
/* Paint the page background on <html>, not just <body>. When a page is
   shorter than the viewport (e.g. the Home landing on a tall display),
   the area below <body> is the canvas — and with a transparent <html>
   the browser fills it with the OS default, which on a dark-mode OS is a
   dark grey/navy that reads as "stray blue space" below the last band.
   An explicit themed background here makes that area always match the
   page (cream in light, the dark surface in dark theme). */
html { background: var(--paper); }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: "Charter", "Iowan Old Style", "Cambria", Georgia, serif;
  font-weight: 600; letter-spacing: -0.01em; margin: 0;
}

/* Uppercase eyebrow label — used above section bodies and chart
   cards to name what comes next. Consolidates ~10 bespoke selectors
   across compare/themes/reports that all encoded this same recipe
   with arbitrary letter-spacing (0.04em / 0.05em / 0.06em) and
   font-size (10/11/12px) drift. Canonical values match the existing
   --track-eyebrow / --text-xs token usage on .sidebar h3 et al.

   Composes with the existing class on the element when the original
   needs to carry extras (border, flex layout, custom margin) — see
   .toolkit-area-title and .aar-insights-title for that pattern.
   Use .eyebrow--sm for 10px variants (e.g. dense picker labels). */
.eyebrow {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--ink-3);
  margin: 0 0 6px;
}
.eyebrow--sm { font-size: var(--text-nano); }

.loading {
  padding: 120px 28px; text-align: center; color: var(--ink-3);
  font-size: 14px;
}

/* ── Loading skeletons ──────────────────────────────────────────────
   .skel is the shared shimmer primitive (one recipe; callers add a
   sizing class). It backs the boot state painted from index.html and
   async chart mounts like the trust inspection-history timeline. The
   sweep is dropped under prefers-reduced-motion. */
.skel {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--paper-2);
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--ink) 6%, transparent),
    transparent
  );
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
@keyframes skel-shimmer { 100% { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
  .skel::after { animation: none; }
}

/* Boot state (index.html): the branded hero + a slim indeterminate
   progress bar, replaced wholesale by render() when the data lands.
   Deliberately NOT a body skeleton — the Home steps have very different
   shapes, so generic bands would misrepresent what's coming (see the
   index.html comment). */
.boot { min-height: 100vh; }
.boot-loading {
  padding: 40px 56px 80px;
  text-align: center;
}
.boot-progress {
  position: relative;
  height: 3px;
  width: min(360px, 60%);
  margin: 0 auto 16px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--paper-2);
}
.boot-progress > span {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 40%;
  border-radius: 2px;
  background: var(--accent);
  animation: boot-progress-slide 1.25s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes boot-progress-slide {
  0%   { left: -40%; }
  100% { left: 100%; }
}
.boot-status {
  color: var(--ink-3);
  font-size: 14px;
  margin: 0;
}
@media (prefers-reduced-motion: reduce) {
  .boot-progress > span { animation: none; left: 0; width: 100%; opacity: 0.4; }
}
@media (max-width: 720px) {
  .boot-loading { padding-left: var(--space-6); padding-right: var(--space-6); }
}

/* Trust inspection-history timeline placeholder
   (charts/timeline.js trustTimelineSkeletonHtml) — rows shaped like
   the master timeline so the section shows a cue, not a blank gap,
   while the trust shard loads. */
.tl-skeleton { display: flex; flex-direction: column; gap: 10px; padding: 8px 0; }
.tl-skeleton-row { height: 16px; width: 100%; }
.tl-skeleton-row:nth-child(2) { width: 86%; }
.tl-skeleton-row:nth-child(3) { width: 92%; }
.tl-skeleton-row:nth-child(4) { width: 78%; }
.tl-skeleton-row:nth-child(5) { width: 88%; }

/* Coverage strip — small, factual, sits under the pills on the
   Big-picture tab. Visible but not loud. */
.coverage-strip {
  font-size: 12px;
  color: var(--ink-3);
  line-height: 1.5;
  margin: 0 0 22px;
  padding: 8px 12px;
  border-left: 3px solid var(--rule);
  background: var(--paper-2);
  border-radius: 2px;
}
.coverage-strip--ok {
  border-left-color: var(--g-strong);
}
.coverage-strip--partial {
  border-left-color: var(--g-attention);
}

/* Two-column app layout — main content on the left, sidebar on
   the right. The sidebar can be collapsed via the Filters toggle
   in the tab bar; when collapsed, .app--sidebar-collapsed drops
   the sidebar column and the sidebar element itself is hidden. */
.app {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 280px;
  align-items: start;
}
.app--sidebar-collapsed {
  grid-template-columns: minmax(0, 1fr);
}
.app--sidebar-collapsed .sidebar { display: none; }
@media (max-width: 1000px) {
  /* Below 1000px there isn't room for a side-by-side layout.
     The sidebar becomes an overlay panel on the right that slides
     in over the content when the user taps "Show filters". This
     avoids pushing the main content down by hundreds of pixels
     whenever the filter panel is open. */
  .app, .app--sidebar-collapsed { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed !important;
    top: 0; right: 0;
    width: min(320px, 90vw); height: 100vh;
    border-left: 1px solid var(--rule) !important;
    border-right: none !important;
    box-shadow: -8px 0 24px rgba(0,0,0,0.08);
    z-index: 31;
  }
  .app--sidebar-collapsed .sidebar { display: none; }
  /* iOS Safari < 17.4 has a known scroll-bleed bug where touch-
     scrolling inside a modal <dialog> can still scroll the body
     underneath. Belt-and-braces: lock body overflow while the
     sidebar is open as an overlay. Modern browsers (and Safari
     17.4+) don't need this — the top-layer + ::backdrop pointer-
     events handle it — but the rule is cheap and silent on every
     other browser. Remove once iOS 17.3 share drops below ~3%. */
  body.sidebar-open { overflow: hidden; }
}

/* UA-default reset for <dialog class="sidebar"> + the column /
   overlay styling. Bare browser dialogs are centered, bordered and
   block-laid-out; we override those so the sidebar reads the same
   whether it's a permanent column (desktop, [open] non-modal) or an
   overlay (mobile, showModal()). border-left below is the column
   separator at desktop width; the mobile @media block re-asserts it
   alongside position:fixed.
   The :not([open]) display:none mirrors the historical
   .app--sidebar-collapsed .sidebar { display:none } — a closed
   dialog already takes zero space, but being explicit avoids the
   non-modal [open] case rendering a hidden block before applySidebar
   State runs. */
.sidebar {
  margin: 0;
  padding: 0;
  border: 0;
  max-width: none;
  max-height: none;
  color: inherit;
  background: var(--paper-2);
  position: sticky;
  top: 0;
  align-self: start;
  /* Fill the viewport vertically (not just up to content) so the
     cream background extends to the bottom edge even when the
     filter list is short. Inner content still scrolls when it
     exceeds 100vh. */
  height: 100vh;
  overflow-y: auto;
  /* Right-side now → border on the LEFT to separate from main. */
  border-left: 1px solid var(--rule);
  border-right: none;
  order: 2;  /* visually after main */
}
.sidebar:not([open]) { display: none; }
.sidebar::backdrop { background: rgba(0,0,0,0.32); }
.main { order: 1; }
.sidebar h3 {
  font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--ink-3);
  margin-bottom: 10px;
  display: inline;
}
.sidebar .filter-group { margin-bottom: 22px; }
.sidebar .filter-group:last-child { margin-bottom: 0; }
/* Collapsible filter group — chrome comes from `.disclosure
   .disclosure--quiet` in components.css. Only the bottom margin
   on the summary (separation from the filter list below it) is
   sidebar-specific and stays here. */
.filter-collapsible > summary { margin-bottom: 10px; }
.filter-collapsible-count {
  font-size: var(--text-nano);
  color: var(--ink-3);
  font-weight: 500;
  margin-left: 6px;
  text-transform: none;
  letter-spacing: 0;
}
/* Sidebar filter list — flex-column-gap recipe lives on
   `.stack .stack--gap-1` in components.css. */
/* @applies-to button */
.filter-row {
  /* Now a <button> rather than a role="button" div; reset the
     browser's default button chrome so the existing visual still
     holds. width:100% + text-align:left makes the button fill its
     column the same way the div did. */
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  display: flex; align-items: center; gap: var(--space-2);
  padding: 5px 8px;
  border-radius: var(--r-md);
  cursor: pointer;
  font-size: 13px;
  color: var(--ink-2);
  user-select: none;
  transition: background 0.1s;
}
.filter-row:hover { background: var(--surface-hover); color: var(--ink); }
.filter-row:focus-visible,
.ag-cell:focus-visible,
.ag-cell-head:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  background: var(--surface-hover);
}
.filter-row.on {
  background: var(--surface-strong); color: var(--surface-strong-ink);
}
.filter-row .swatch {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--c, var(--ink-3)); flex-shrink: 0;
}
.filter-row .label { flex: 1; }
.filter-row .count {
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  font-size: var(--text-xs);
}
.filter-row.on .count { color: rgba(255,255,255,.7); }
/* Trust-filter row — the sidebar's single-button "Trust: <trust> ·
   reset ×" filter group that only renders when state.trustFilter
   is set. The label sits a touch bolder than the standard
   filter-row label (it's the only label in its group, so visual
   weight here = filter group; mirrors how `.filter-row.on` lifts
   the row background). The `.count` slot inherits its colour and
   font-size from the existing `.filter-row .count` / `.filter-row.on
   .count` rules above — no extra declarations needed. */
.trust-filter-row__label { font-weight: 500; }

/* Filters header sits at the very top of the sidebar — a label
   and a Reset button on the right so users can clear all sidebar
   state in one click without scrolling. */
.sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 14px;
  margin-bottom: var(--space-1);
  border-bottom: 1px solid var(--rule);
}
.sidebar-head h2 {
  font-size: 14px;
  color: var(--ink);
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  font-weight: 600;
}
/* @applies-to button */
.reset-btn {
  border: 1px solid var(--rule);
  background: var(--card);
  color: var(--ink-2);
  border-radius: var(--r-md);
  padding: 6px 12px;
  font-size: var(--text-sm);
  cursor: pointer;
  font-family: inherit;
  transition: border-color var(--t-fast), color var(--t-fast), background var(--t-fast);
}
.reset-btn:hover { color: var(--ink); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 4%, var(--card)); }

/* Dashed-border quiet reset — used on the Themes tab's lens-filter
   bar where the reset is a secondary affordance under a stack of
   colourful lens chips. Solid border would compete with the chips. */
.reset-btn--dashed {
  border-style: dashed;
  background: transparent;
}
.reset-btn--dashed:hover {
  background: transparent;  /* no accent-bg bloom on the quiet variant */
}

/* Search input at the top of the sidebar. Matches against names,
   trust/LA, next-steps bullets, narrative paragraphs and the
   "About this inspection" fields. Debounced; matches are
   highlighted in the rendered card text. */
.filter-search {
  position: relative;
  margin: 0 0 18px;
}
.filter-search-input {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid var(--rule);
  background: var(--paper-2);
  border-radius: var(--r-md);
  padding: 7px 28px 7px 30px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237c8290' stroke-width='1.6'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M14 14l-3.6-3.6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 9px center;
  background-size: 14px;
}
.filter-search-input:focus {
  outline: none;
  border-color: var(--accent);
  background-color: var(--card);
}
.filter-search-input::placeholder { color: var(--ink-3); }
/* Inline-clear × inside the sidebar search input. Uses the shared
   .close-btn primitive (see components.css). Only the absolute
   positioning lives here — visual is parent-driven. */
.filter-search > .close-btn {
  position: absolute;
  right: 6px; top: 50%;
  transform: translateY(-50%);
  margin-left: 0;
  padding: 4px 6px;
}
.filter-search-hint {
  margin: 4px 2px 0;
  font-size: 11px;
  color: var(--ink-3);
  font-style: italic;
}
.filter-scope-hint {
  margin: 12px 2px 0;
  padding: 8px 10px;
  background: var(--paper-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-sm, 4px);
  font-size: var(--text-sm, 12px);
  color: var(--ink-2);
  line-height: 1.4;
}
.filter-scope-hint strong { color: var(--ink); }
.filter-scope-hint em { font-style: normal; font-weight: 500; }

/* ────────────────────────────────────────────────────────────
   Phase 0b-2: search highlight, grade-by-area filter grid,
   main content wrapper. Appended in order from the inline <style>.
   ──────────────────────────────────────────────────────────── */

/* Highlight rendered for search hits inside card text. Sits atop
   the card background but tinted with the accent so it stands
   out without screaming. */
mark {
  background: color-mix(in srgb, var(--accent) 28%, transparent);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}

/* Grade-by-area filter grid (and report-level grade quick-pick) */
.ag-grid {
  display: flex; flex-direction: column; gap: 2px;
}
.ag-row {
  display: grid;
  /* minmax(0, 1fr) instead of plain 1fr so a single wider cell can't push
     its column past its share. Without this, a longer string in one row's
     cell would expand the column for that whole grid track. */
  grid-template-columns: 88px repeat(5, minmax(0, 1fr));
  gap: 2px;
  align-items: center;
}
.ag-row.ag-header { margin-bottom: 2px; }
/* Thin divider between the six core area rows and the two optional
   rows (Early years, Sixth form) so the eye reads them as a separate
   group. */
.ag-row-divider {
  height: 1px;
  background: var(--rule);
  margin: 4px 0 2px;
  grid-column: 1 / -1;
}
.ag-label {
  font-size: 11px;
  color: var(--ink-2);
  font-weight: 500;
  padding-right: var(--space-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ag-cell-head {
  font-size: 9.5px; /* @approved: sub-nano AG-grid column header; midpoint between --text-9 (9px) and --text-nano (10px), no token at this size */
  text-align: center;
  padding: 3px 0;
  border-radius: 2px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
  user-select: none;
  transition: filter 0.1s, box-shadow 0.1s;
}
.ag-cell-head[data-area-col-grade]:hover {
  filter: brightness(0.92);
  box-shadow: 0 0 0 2px rgba(0,0,0,0.12);
}
.ag-cell-head.on {
  box-shadow: 0 0 0 2px var(--ink) inset;
}
.ag-cell-head--static {
  cursor: default;
}
.ag-cell-head--static:hover {
  filter: none;
  box-shadow: none;
}
/* Grade-coloured fill on the column-header cell. Selecting by the
   canonical GRADE_ORDER value (set on `data-grade` by the JS template)
   keeps the colour swap in CSS so dark-mode and theme overrides cascade
   from --g-* tokens without a per-grade inline style. Matches the
   `.ga-seg[data-grade]` and `.seg[data-grade]` patterns already in
   charts.css / tabs/reports.css. */
.ag-cell-head[data-grade="Exceptional"]        { background: var(--g-exceptional); color: var(--g-exceptional-ink); }
.ag-cell-head[data-grade="Strong standard"]    { background: var(--g-strong);      color: var(--g-strong-ink); }
.ag-cell-head[data-grade="Expected standard"]  { background: var(--g-expected);    color: var(--g-expected-ink); }
.ag-cell-head[data-grade="Needs attention"]    { background: var(--g-attention);   color: var(--g-attention-ink); }
.ag-cell-head[data-grade="Urgent improvement"] { background: var(--g-urgent);      color: var(--g-urgent-ink); }
/* @applies-to [role="button"] */
.ag-cell {
  height: 22px;
  border: 1px solid var(--rule);
  background: var(--card);
  border-radius: 2px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-nano);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  transition: all 0.1s;
  user-select: none;
}
.ag-cell:hover {
  border-color: var(--g);
  color: var(--ink);
}
.ag-cell.dim {
  opacity: 0.35;
  cursor: not-allowed;
}
.ag-cell.on {
  background: var(--g);
  /* --g-ink is set inline alongside --g so the count number on
     light grade fills (Expected, Attention) uses dark ink and
     clears WCAG AA. Falls back to #fff for any caller that
     hasn't migrated yet — matches the pre-AA pass behaviour. */
  color: var(--g-ink, #fff);
  border-color: var(--g);
  font-weight: 600;
}
.ag-hint {
  font-size: var(--text-xs);
  color: var(--ink-3);
  margin: 6px 0 0;
  font-style: italic;
}

/* Main content */
.main { padding: 0; min-width: 0; }
.main-inner { padding: 0 var(--page-gutter) 32px; max-width: var(--page-max); margin: 0 auto; }
/* Tighter side padding on narrow viewports so cards aren't visually
   squeezed between the screen edge and their own padding. The bottom
   padding also reserves a clearance gutter for the fixed "Ask Claude"
   pill (components.css .ask-popup, bottom-right) so it never sits over
   the last row of content: pill offset + pill height + breathing room. */
@media (max-width: 760px) {
  .main-inner { padding: 0 12px calc(var(--space-7) + var(--touch-target-min) + var(--space-6)); }
}
/* Map tab gets the full main width — no padding, no max-width — so
   it can fill the screen properly. Uses :has() so we don't need a
   JS hook to toggle a class. */
.main-inner:has(#tab-map:not([hidden])) {
  max-width: none;
  padding: 0;
}

/* ────────────────────────────────────────────────────────────
   Phase 0b-3: tab bar (.tabs / .tab / .site-title), the filter +
   theme toggle buttons that live in it, the active-filters strip
   sitting below it, the .sr-only skip-link, the universal
   :focus-visible default, and section.tab-content base.
   ──────────────────────────────────────────────────────────── */

/* Push the About tab to the right edge of the nav so it sits as an
   ancillary entry rather than alongside the analytic tabs. Doubled
   class for specificity: the `.tab` base rule further down sets the
   `margin` shorthand (the navy-pill inset), and being later in the
   file it would otherwise clobber this margin-left. */
.tab.tab--about { margin-left: auto; }

/* Filter-panel toggle — sits at the very right of the tab nav.
   Two-state button: when the sidebar is visible it offers "Hide
   filters", when collapsed it offers "Show filters". Icon is a
   simple drawn shape via CSS borders so we don't take a dep. */
.filter-toggle {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  font-family: inherit;
  font-size: 13px;
  padding: 16px 22px;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border-left: 1px solid var(--rule);
  font-weight: 500;
}
.filter-toggle:hover { color: var(--ink); }
.filter-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.filter-toggle-icon {
  display: inline-block;
  width: 14px; height: 12px;
  border: 1.5px solid currentColor;
  border-radius: 2px;
  position: relative;
}
/* Icon vertical line indicates the panel's open/closed edge. When
   the sidebar is open, draw the rightmost column filled; when
   closed, draw it empty. */
.filter-toggle-icon::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  right: 0;
  width: 4px;
  background: currentColor;
}
body.sidebar-collapsed .filter-toggle-icon::before {
  background: transparent;
}
/* Active-filter count badge — sits on the top-right corner of the
   toggle's icon when at least one global filter is set. Lets users
   see at a glance that the view is narrowed even when the sidebar
   is closed. Hidden when no filters are active. */
.filter-toggle {
  position: relative;
}
.filter-toggle-badge {
  position: absolute;
  top: var(--space-2);
  right: 10px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  border-radius: var(--r-full);
  background: var(--accent);
  /* --paper, not white: dark-mode --accent is a light blue where
     white fails AA (2.6:1) — same ink treatment as .tab.on /
     .toggle-btn.on / .chip--on (navy-chrome PR 863, sweep PR 869). */
  color: var(--paper);
  font-size: var(--text-nano);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 16px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--paper-2, var(--paper));
}
.filter-toggle-badge[hidden] { display: none; }

/* Active-filters strip — appears below the tab bar when at least
   one global filter is set. Compact horizontal row of chips, each
   naming the filter and offering a × to clear it. A "Reset all"
   pill at the end clears the lot. The whole strip hides itself
   (display:none via [hidden]) when no filters are active so the
   site reads as quiet at rest. */
.active-filters-strip {
  background: var(--paper-2, var(--paper));
  border-bottom: 1px solid var(--rule);
  /* Match .main-inner's centred column (var(--page-max), base.css:566)
     so the chips line up with the page content; var(--page-gutter) is
     the shared gutter. Background + border stay full-bleed. The
     ≤760px rule below drops the gutter to 12px. */
  padding: 8px calc(max(0px, (100% - var(--page-max)) / 2) + var(--page-gutter));
  display: flex; align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font-size: 12px;
}
.active-filters-strip[hidden] { display: none; }
.active-filters-strip-label {
  color: var(--ink-3);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 11px;
}
.af-chip {
  display: inline-flex; align-items: center;
  gap: var(--space-1);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-full);
  padding: 2px 4px 2px 10px;
  color: var(--ink-2);
  font-size: 12px;
}
.af-reset {
  border: 0; background: transparent;
  color: var(--accent);
  font: inherit; font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  padding: 2px 6px;
  margin-left: auto;
}
.af-reset:hover { text-decoration: underline; }
.af-reset:focus-visible {
  outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); border-radius: 3px;
}
@media (max-width: 760px) {
  .active-filters-strip { padding: 8px 12px; }
}

/* Theme (light/dark) toggle — sun in light mode, moon in dark mode.
   Both icons rendered as inline SVG via background-image so they
   pick up currentColor automatically. */
.theme-toggle {
  border: 0; background: transparent;
  cursor: pointer; padding: 12px 10px;
  color: var(--ink-3);
  display: inline-flex; align-items: center;
}
.theme-toggle:hover { color: var(--ink); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.theme-toggle-icon {
  width: 16px; height: 16px;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  /* Sun icon by default (light mode shows "I'd switch to dark"). */
  -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E");
}
html[data-theme="dark"] .theme-toggle-icon {
  /* In dark mode, show a moon icon ("I'd switch to light"). */
  -webkit-mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'/%3E%3C/svg%3E");
}

/* Tab bar */
.tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--rule);
  background: var(--card);
  position: sticky;
  top: 0;
  z-index: 20;
  align-items: stretch;
  /* Full-bleed band, contained content. The background + bottom border
     still run edge to edge (padding sits inside the border-box), but this
     padding centres the tabs + toggles into the same var(--page-max)
     column as the page content (.main-inner, base.css:566; mirrors the
     Home bands at styles/tabs/home.css:69). Without it the nav drifts
     ~100px left of the body at wide viewports. Collapses to 0 below
     var(--page-max), so narrow / mobile layouts are byte-identical to
     before. --page-max is a single shared token with .main-inner. */
  padding-inline: max(0px, calc((100% - var(--page-max)) / 2));
  /* On narrow viewports the cumulative tab width exceeds the
     viewport. Without overflow handling the tabs spill out and
     force the whole page to be horizontally scrollable, which
     breaks all per-tab content. Constrain the overflow to the
     tab bar itself so the rest of the layout stays normal. */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;     /* Firefox */
}
.tabs::-webkit-scrollbar { display: none; }   /* Chrome / Safari */
.tab, .site-title { flex-shrink: 0; }         /* keep labels on one line inside the scrollable bar */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0);
  white-space: nowrap; border: 0;
}
/* Skip-link: hidden until keyboard-focused, then jumps to the
   first focusable thing inside <main>. Bypasses the seven tabs +
   two toggle buttons in the nav for screen-reader and keyboard
   users. */
.sr-only-focusable:focus {
  position: fixed;
  top: var(--space-3); left: var(--space-3);
  width: auto; height: auto;
  margin: 0; clip: auto; overflow: visible;
  padding: 10px 16px;
  background: var(--accent);
  /* --paper, not white — see .filter-toggle-badge. */
  color: var(--paper);
  text-decoration: none;
  border-radius: var(--r-md);
  z-index: 1200;
  font-size: var(--text-md);
  font-weight: 500;
}

/* Universal keyboard focus indicator — every <button>, <summary>,
   and role="button" or tabindex="0" element gets a visible accent
   ring. Specific selectors below override (e.g. sticky tab-bar
   buttons want outline-offset: -2px so the ring sits inside the
   bar instead of clipping at its border). */
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[tabindex="0"]:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--r-sm);
}
/* Site title / home button sits at the left of the tab bar. Acts as
   a brand mark and a "reset to Big picture, clear URL hash" hook. */
.site-title {
  border: 0;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  font-family: "Charter", Georgia, serif;
  font-size: 17px;
  font-weight: 600;
  padding: 12px 22px 12px 28px;
  letter-spacing: -0.005em;
  border-right: 1px solid var(--rule);
  margin-right: 6px;
}
.site-title:hover { color: var(--accent); }
.site-title:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* @applies-to button */
.tab {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  /* Rounded-pill metrics for the navy-fill active state (home-
     rebuild "All navy" chrome): 6px vertical margin insets the
     fill from the bar edges; 11px vertical padding keeps overall
     bar height where the old 16px-pad + 3px-underline metrics
     had it. */
  padding: 11px 18px;
  margin: 6px 2px;
  border-radius: var(--r-sm);
  font-size: 14px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 500;
}
.tab:hover { color: var(--ink); background: var(--surface-hover); }
/* Active tab = solid navy fill + knockout text — replaces the 3px
   accent underline, matching the selection language of the
   toggle-groups. Text is --paper rather than --surface-strong-ink:
   dark mode flips --accent to a light blue where white fails AA
   (2.6:1); --paper reads near-white on navy in light mode (11.1:1)
   and near-black on the light blue in dark mode (7.0:1). Hover on
   the active tab keeps the fill. */
.tab.on,
.tab.on:hover {
  color: var(--paper);
  background: var(--accent);
  font-weight: 600;
}

section.tab-content { padding: 24px 0 0; }
section.tab-content > h2 { font-size: 22px; margin-bottom: var(--space-1); }

/* ────────────────────────────────────────────────────────────
   Final base sweep — cross-tab mobile touch-target bumps,
   section.tab-content > .sub / [hidden] base rules,
   prefers-reduced-motion safety override. This is the last block
   pulled out of the inline <style> in site/index.html, which is
   now deleted entirely.
   ──────────────────────────────────────────────────────────── */



/* Mobile touch-target bumps — the new chrome (cog summary, chip
   ×, profile chevrons, backlink) sits ~20–25px tall by default,
   which is fine on desktop but well under the 40px recommended
   touch target. At ≤1000px viewports, pad each up. */
@media (max-width: 1000px) {
  .chart-customise-summary { padding: 10px 12px; }
  .af-reset { padding: 8px 10px; }
  .profile-rank-nav { padding: 8px 12px; font-size: 14px; }
  .profile-backlink { padding: 8px 0; font-size: 14px; }
}

section.tab-content > .sub {
  color: var(--ink-3); font-size: 14px; margin-bottom: var(--space-6); max-width: 760px;
}
section.tab-content[hidden] { display: none; }

/* Netlify form-detection stub (index.html) — must stay invisible
   even if a stylesheet overrides [hidden]; the SPA destroys the
   node at first render anyway. */
.netlify-form-stub { display: none !important; }




/* Touch-target sizing — on narrow viewports (tablets / phones)
   bump the smallest interactive controls so finger taps don't
   miss. Apple/Google guidelines (and WCAG 2.5.5) suggest a 44px
   minimum; the desktop versions stay compact for mouse precision.

   Recipe — the grouped selector below owns the minimum-tap-size
   floor in one place, sized from the `--touch-target-min` design
   token (44px) so a future change to the floor tracks every opt-in
   control at once. The icon-only square buttons (sort arrows,
   copy-link) opt in here; pill-shaped controls reach a comfortable
   target through padding instead, since forcing a 44px square on a
   short text pill would distort the row. New icon-button tap targets
   should join this grouped selector rather than re-hardcode a size. */
@media (max-width: 1000px) {
  .sort-btn,
  .copy-link-btn,
  .profile-rank-nav {
    min-block-size: var(--touch-target-min);
    min-inline-size: var(--touch-target-min);
  }
  .sort-btn { padding: 6px 8px; }
  .map-pill { padding: 8px 12px; }
  .area-pill, .filter-pill { padding: 6px 12px; }
  .ag-cell { min-height: 32px; }
  .copy-link-btn { padding: 4px 8px; }
}

/* Respect prefers-reduced-motion. Drops every animation /
   transition to near-instant; the spinner and the segment-width
   transitions still complete, they just don't sweep. The
   "important" is needed because every specific rule sets its own
   transition / animation. */
@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Static error page rendered when viz_data.json fetch fails.
   Only visible when the whole app fails to boot. */
.load-error {
  max-width: 640px; margin: 80px auto; padding: var(--space-6);
  border: 1px solid var(--neg); border-radius: var(--r-lg);
  font: 16px/1.5 system-ui, sans-serif;
}
.load-error__detail {
  color: var(--ink-3);
  font-size: var(--text-base);
}
