/* ============================================================================
 * Cadrify Design System — colors_and_type.css
 *
 * NASA modernism × British civic design × quiet operational software.
 * Import this in any HTML surface that should look like Cadrify.
 * ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Newsreader:ital,wght@0,400;1,400&display=swap');

:root {
  /* ---------------------------------------------------------------------- */
  /* Color — raw                                                            */
  /* ---------------------------------------------------------------------- */

  /* Ink (deep navy → near-paper) */
  --ink-900: #0E1A2B;
  --ink-800: #182438;
  --ink-700: #25334A;
  --ink-600: #3D485C;
  --ink-500: #5C6678;   /* slate */
  --ink-400: #8C94A3;
  --ink-300: #BDC2CC;
  --ink-200: #DCDFE5;
  --ink-100: #EBEDF1;
  --ink-050: #F4F5F8;

  /* Paper (warm, stone) */
  --paper:        #F5F2EC;
  --paper-soft:   #FAF8F3;
  --paper-warm:   #EDE7DA;
  --white:        #FFFFFF;

  /* Civic blue — primary action */
  --blue-700: #1F3F66;
  --blue-600: #2C5A8C;   /* default */
  --blue-500: #3E73AB;
  --blue-400: #6791BE;
  --blue-300: #90AFD1;
  --blue-200: #B9CEE4;
  --blue-100: #DCE7F2;
  --blue-050: #EEF3F9;

  /* Forest — active / positive */
  --forest-700: #21443A;
  --forest-600: #2F5D4F;
  --forest-500: #437A6B;
  --forest-200: #B8CFC6;
  --forest-100: #DBE7E2;
  --forest-050: #ECF2EF;

  /* Signal — warning · draft · needs-review (civic amber) */
  --signal-700: #7A5E10;
  --signal-600: #A38219;
  --signal-500: #C2A12E;
  --signal-200: #E5D696;
  --signal-100: #F0E6BC;
  --signal-050: #F8F2DA;

  /* Crimson — destructive only */
  --crimson-700: #7A1F1F;
  --crimson-600: #9B2C2C;
  --crimson-500: #BE3E3E;
  --crimson-400: #CE6A6A;
  --crimson-300: #DCB0B0;
  --crimson-200: #E4B9B9;
  --crimson-100: #F0D8D8;
  --crimson-050: #F7EAEA;

  /* Purple — categorical accent (Figma Color/Purple) */
  --purple-600: #7B4F8A;
  --purple-500: #9367A2;

  /* Score gradient, the canvass support-to-oppose scale used app-wide.
     One canonical scheme: bold enough to pop on the muted map basemap, but
     earthy/warm rather than neon so it stays on-brand. 1 = strong support,
     5 = firm against. 3 (undecided) is a neutral grey rather than gold, per
     the Figma refresh. Change here and it updates everywhere (CSS + inline
     legends; the Chart.js arrays mirror these same hexes in JS). */
  --score-1: #2E8B57;   /* strong support, sea green */
  --score-2: #64B896;   /* leaning support, sage   */
  --score-3: #E0A82E;   /* undecided, gold         */
  --score-4: #DD7A3D;   /* leaning against, terracotta */
  --score-5: #C8403B;   /* opposed, brick red      */
  /* This scale is canonical (the original app palette, restored 2026-07-17
     after a brief Figma-side deviation). */
  --score-none: #8C94A3; /* no data, ink-400        */
  --score-out: #BDC2CC;  /* knocked, no answer: ink-300, lighter than score-none */

  /* Translucent ink — for borders/scrims that need to blend on paper */
  --ink-12: rgba(14, 26, 43, 0.12);
  --ink-08: rgba(14, 26, 43, 0.08);
  --ink-20: rgba(14, 26, 43, 0.20);
  --ink-04: rgba(14, 26, 43, 0.04);
  --ink-scrim: rgba(14, 26, 43, 0.28);
  --ink-02: rgba(14, 26, 43, 0.02);   /* sub-hover tint — half of ink-04 */

  /* Backward-compat aliases — referenced in templates before canonical names were settled */
  --civic-blue:  var(--blue-600);      /* → var(--action-default) */
  --dur-normal:  var(--dur-base);      /* → 180ms */
  --fg-base:     var(--fg-default);
  --forest-300:  #7AADA0;              /* mid-step between forest-200 and forest-500 */
  --mono:        var(--font-mono);     /* → 'IBM Plex Mono', … */

  /* ---------------------------------------------------------------------- */
  /* Color — semantic                                                       */
  /* ---------------------------------------------------------------------- */
  /* The app canvas is white, per the ward/branch workspace redesign: cards
     and tables sit on the same white and structure comes from hairlines.
     Marketing pages opt back into var(--paper) locally — the brand keeps
     its paper, the product does not. */
  --bg-canvas:    #FFFFFF;
  --bg-canvas-soft: #FFFFFF;
  --surface-1:    var(--white);          /* cards, panels */
  --surface-2:    var(--paper-soft);     /* nav rails, subtle wells */
  --surface-3:    var(--paper-warm);     /* eyebrow strips, archival panels */

  --fg-default:   var(--ink-900);
  --fg-muted:     var(--ink-500);
  --fg-subtle:    var(--ink-400);
  --fg-inverse:   var(--paper-soft);

  --border-default: var(--ink-12);
  --border-soft:    var(--ink-08);
  --border-strong:  var(--ink-20);
  --border-focus:   var(--blue-600);

  --action-default:   var(--blue-600);
  --action-hover:     var(--blue-700);
  --action-pressed:   #163059;
  --action-on:        #FFFFFF;

  /* Forest fills — for backgrounds where white text must be readable.
     In light mode these equal forest-600/700 (dark forest, white text fine).
     In dark mode these are pinned to mid-dark values (see dark block) so
     that --forest-600 can be lifted for text/accents without breaking fills. */
  --forest-fill:       var(--forest-600);
  --forest-fill-hover: var(--forest-700);

  --status-active-fg: var(--forest-700);
  --status-active-bg: var(--forest-050);
  --status-active-border: var(--forest-200);

  --status-draft-fg:  var(--signal-700);
  --status-draft-bg:  var(--signal-050);
  --status-draft-border: var(--signal-200);

  --status-info-fg:   var(--blue-700);
  --status-info-bg:   var(--blue-050);
  --status-info-border: var(--blue-200);

  --status-error-fg:  var(--crimson-700);
  --status-error-bg:  var(--crimson-050);
  --status-error-border: var(--crimson-200);

  --status-neutral-fg: var(--ink-700);
  --status-neutral-bg: var(--ink-050);
  --status-neutral-border: var(--ink-200);

  /* Elevation, wells, overlays */
  --surface-overlay: var(--white);     /* popovers, dropdowns, tooltips — one step up */
  --bg-well:         var(--ink-050);   /* recessed wells, hover rows, tracks, disabled */
  --scrim:           var(--ink-scrim); /* modal/overlay backdrops */

  /* Form inputs */
  --input-bg:     var(--white);
  --input-border: var(--ink-12);
  --input-fg:     var(--ink-900);

  /* Code / mono chips */
  --fg-code: var(--ink-700);
  --bg-code: var(--ink-050);

  /* Data-viz chrome — read by Chart.js via getComputedStyle (chart-theme.js).
     The score scale (--score-1..5) is data. Dark mode only lifts it
     slightly for visibility. The chart furniture (grid, ticks, tooltip)
     adapts. */
  --chart-grid:       rgba(14, 26, 43, 0.10);
  --chart-axis:       var(--ink-400);
  --chart-tick:       var(--ink-500);
  --chart-tooltip-bg: var(--ink-900);
  --chart-tooltip-fg: var(--paper-soft);

  /* Categorical data series palette (Figma Color/Chart) */
  --chart-1: #4E86C4;
  --chart-2: #1F937A;
  --chart-3: #AE8617;
  --chart-4: #9B6FC7;
  --chart-5: #D2564E;

  /* Map — basemap tile filter + feature paint, read at layer-build time */
  --map-basemap-filter: saturate(.5) brightness(1.08) contrast(.92) sepia(.07);
  --map-water:  #F1F2F5;
  --map-stroke: #0E1A2B;
  --map-halo:   var(--paper);

  /* Party colours — real-world brand hues, identical in both themes.
     Canonical source; JS lookups read these via getComputedStyle. */
  --party-lab: #E4003B;
  --party-con: #0087DC;
  --party-ld:  #FAA61A;
  --party-grn: #02A95B;
  --party-snp: #FDF38E;
  --party-pc:  #005B54;
  --party-ref: #12B6CF;
  --party-ukip:#70147A;
  --party-rb:  #182438;
  --party-ind: #94A3B8;

  /* ---------------------------------------------------------------------- */
  /* Type                                                                   */
  /* ---------------------------------------------------------------------- */
  --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --font-editorial: 'Newsreader', 'Iowan Old Style', Georgia, serif;

  /* Type scale (approx 1.2 ratio) */
  --t-display:   32px;   /* page titles, splash */
  --t-h1:        24px;
  --t-h2:        19px;
  --t-h3:        16px;
  --t-body:      14px;
  --t-small:     13px;
  --t-mono:      12px;
  --t-eyebrow:   11px;

  --lh-tight:    1.15;
  --lh-snug:     1.3;
  --lh-body:     1.5;
  --lh-loose:    1.6;

  --tr-tight:    -0.02em;   /* display sizes */
  --tr-normal:   0;
  --tr-wide:     0.08em;    /* eyebrow ALL CAPS */

  /* ---------------------------------------------------------------------- */
  /* Spacing — 8px base, 4 as half-step                                     */
  /* ---------------------------------------------------------------------- */
  --space-1:   4px;
  --space-1-5: 6px;   /* half-step between space-1 and space-2 */
  --space-2:   8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  24px;
  --space-6:  32px;
  --space-7:  48px;
  --space-8:  64px;
  --space-9:  96px;

  /* ---------------------------------------------------------------------- */
  /* Radii — small, architectural                                           */
  /* ---------------------------------------------------------------------- */
  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-lg: 6px;
  --radius-full: 999px;   /* avatar exception, per Figma Radius collection */

  /* ---------------------------------------------------------------------- */
  /* Shadows — minimal                                                      */
  /* ---------------------------------------------------------------------- */
  --shadow-1: 0 1px 0 rgba(14,26,43,0.04), 0 1px 2px rgba(14,26,43,0.06);
  --shadow-2: 0 2px 4px rgba(14,26,43,0.06), 0 8px 16px rgba(14,26,43,0.06);
  --shadow-3: 0 12px 32px rgba(14,26,43,0.12);

  /* ---------------------------------------------------------------------- */
  /* Motion                                                                 */
  /* ---------------------------------------------------------------------- */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --dur-fast: 120ms;
  --dur-base: 180ms;
  --dur-slow: 240ms;
}

/* ============================================================================
 * Dark theme — remaps the SEMANTIC layer only. Components that reference
 * semantic tokens (--surface-*, --fg-*, --border-*, --status-*) flip for free.
 * Canvas is deep navy, never pure black; text is cool neutral, never pure
 * white, mirroring the light-mode "never pure white at canvas level" rule.
 *
 * The raw palette (--ink-*, --paper*, --white, --blue-*, etc.) stays fixed as
 * anchors EXCEPT the five translucent ink primitives, which flip to translucent
 * white so every hairline/hover/scrim built on them reads correctly on dark.
 * --party-* is data and keeps each party's real hue in both themes; the
 * score scale, and the few party colours that are themselves dark (PC,
 * UKIP, RB), are lifted for visibility on dark.
 * ========================================================================== */
html[data-theme="dark"] {
  /* Translucent primitives flip — highest-leverage single change */
  --ink-02:    rgba(255, 255, 255, 0.02);
  --ink-04:    rgba(255, 255, 255, 0.05);
  --ink-08:    rgba(255, 255, 255, 0.08);
  --ink-12:    rgba(255, 255, 255, 0.12);
  --ink-20:    rgba(255, 255, 255, 0.22);
  --ink-scrim: rgba(0, 0, 0, 0.55);

  /* Canvas & surfaces — step UP in lightness as elevation rises */
  --bg-canvas:      #0A111C;   /* deeper than ink-900; the "never pure black" canvas */
  --bg-canvas-soft: #0F1925;
  --surface-1:      #131F2E;   /* cards, panels, topbar */
  --surface-2:      #0F1925;   /* nav rails, wells, headers */
  --surface-3:      #1B2838;   /* eyebrow strips, archival/elevated */

  /* Foreground — cool neutral, never pure white */
  --fg-default: #E6EBF2;
  --fg-muted:   #9BA5B5;
  --fg-subtle:  #7E8A9A;
  --fg-inverse: #0E1A2B;       /* dark text on light chips */

  /* Borders — low-alpha light on dark */
  --border-default: rgba(223, 230, 240, 0.13);
  --border-soft:    rgba(223, 230, 240, 0.08);
  --border-strong:  rgba(223, 230, 240, 0.22);
  --border-focus:   #6BA0DC;

  /* Action — civic blue lifted for contrast on dark */
  --action-default: #4E86C4;
  --action-hover:   #6699D2;
  --action-pressed: #3D74B2;
  --action-on:      #08111C;   /* dark text on the lighter blue fill */

  /* Forest / brand — two-tier approach.
     --forest-600/700 are lifted for text and accents (~6:1 on dark surfaces).
     --forest-fill/fill-hover are darker values reserved for button/fill
     backgrounds, where white text needs ≥4.5:1. Direct usages of --forest-600
     as colour text now pop visibly; button rules are overridden below. */
  --forest-050: #111D17;
  --forest-100: #162B23;
  --forest-200: #243D33;
  --forest-500: #7CC4A5;
  --forest-600: #6BAE95;   /* text/accents: ~6:1 on dark surfaces */
  --forest-700: #5D9B81;   /* secondary accent */
  --forest-fill: #4A7E69;       /* btn fills: white text 4.7:1 */
  --forest-fill-hover: #3D6B57; /* btn hover fills: white text 6.1:1 */

  /* Signal — lifted amber; no dark override existed before. */
  --signal-050: #1A1608;
  --signal-100: #231E0C;
  --signal-200: #2E2810;
  --signal-500: #DFBE3F;
  --signal-600: #D4A827;   /* text/accents: ~6:1 on dark surfaces */
  --signal-700: #B08C1A;   /* darker signal accent */

  /* Status — dark-tinted backgrounds, light-tinted foregrounds */
  --status-active-fg:     #6FB6A1;
  --status-active-bg:     #14302A;
  --status-active-border: rgba(79, 146, 129, 0.38);

  --status-draft-fg:      #D6B44C;
  --status-draft-bg:      #2C2611;
  --status-draft-border:  rgba(207, 169, 58, 0.38);

  --status-info-fg:       #7FB0E0;
  --status-info-bg:       #13283F;
  --status-info-border:   rgba(80, 137, 198, 0.40);

  --status-error-fg:      #E08A8A;
  --status-error-bg:      #2E1717;
  --status-error-border:  rgba(207, 106, 106, 0.38);

  --status-neutral-fg:    #AEB8C7;
  --status-neutral-bg:    #1A2433;
  --status-neutral-border:rgba(223, 230, 240, 0.18);

  /* Elevation, wells, inputs, code */
  --surface-overlay: #1B2838;
  --bg-well:         #0D1622;
  --scrim:           rgba(0, 0, 0, 0.6);
  --input-bg:        #16202F;
  --input-border:    rgba(255, 255, 255, 0.16);
  --input-fg:        #E7E3DA;
  --fg-code:         #B7C2D0;
  --bg-code:         #16202F;

  /* Data-viz furniture */
  --chart-grid:       rgba(255, 255, 255, 0.08);
  --chart-axis:       #8C94A3;
  --chart-tick:       #BCC4D0;
  --chart-tooltip-bg: #243246;
  --chart-tooltip-fg: #E7E3DA;

  /* Categorical data series palette (Figma Color/Chart) — lifted for dark */
  --chart-1: #5590CE;
  --chart-2: #26A084;
  --chart-3: #B08A1E;
  --chart-4: #9F73C9;
  --chart-5: #D4605A;

  /* Score, lifted for visibility on the dark basemap. 3 (undecided) is a
     neutral grey rather than gold, per the Figma refresh. */
  --score-1: #44B87A;   /* strong support, brighter sea green */
  --score-2: #8FCFB0;   /* leaning support, lifted sage */
  --score-3: #EAB830;   /* undecided, lifted gold */
  --score-4: #E38A4A;   /* leaning against, lifted terracotta */
  --score-5: #D85454;   /* opposed, lifted brick red */
  --score-out: #4E5A6E; /* knocked, no answer: recedes toward the dark bg-well */

  /* Party colours stay each party's real colour in both themes, but the
     handful that are themselves dark (Plaid teal, UKIP purple, Restore
     Britain navy) vanish against the dark basemap, so those lift here the
     same way the score scale does. Hue is preserved, only lightness moves. */
  --party-pc:   #1FA08F; /* Plaid Cymru teal, lifted from #005B54 */
  --party-ukip: #A34FB4; /* UKIP purple, lifted from #70147A */
  --party-rb:   #6E86B8; /* Restore Britain navy, lifted from #182438 */

  /* Map — dark basemap via invert+hue-rotate, dark water, light strokes */
  --map-basemap-filter: brightness(.62) invert(.92) contrast(.92) hue-rotate(180deg) saturate(.8);
  --map-water:  #0B1420;
  --map-stroke: #C2C9D3;
  --map-halo:   #0B1420;

  /* Shadows deepen — light-mode shadows are invisible on dark */
  --shadow-1: 0 1px 0 rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.5);
  --shadow-2: 0 2px 4px rgba(0,0,0,0.4), 0 8px 16px rgba(0,0,0,0.5);
  --shadow-3: 0 12px 32px rgba(0,0,0,0.6);
}

/* Belt-and-braces: also follow the OS for any surface that renders before the
   head script runs or outside an authenticated page (where data-theme is unset
   but data-bs-theme may be). Only applies when no explicit theme is set. */
html[data-theme="dark"] body { color-scheme: dark; }

/* Forest button fills — --forest-600 is now lifted for text readability, so
   button backgrounds that need white text must use --forest-fill instead.
   !important is required to beat any inline <style> block that also sets
   background on these classes (e.g. the marketing nav's .nav .btn-brand). */
html[data-theme="dark"] .btn-brand,
html[data-theme="dark"] .btn-success,
html[data-theme="dark"] .bg-brand {
  background-color: var(--forest-fill) !important;
  border-color: var(--forest-fill) !important;
}
html[data-theme="dark"] .btn-brand:hover,
html[data-theme="dark"] .btn-brand:focus,
html[data-theme="dark"] .btn-brand:active,
html[data-theme="dark"] .btn-check:checked + .btn-brand,
html[data-theme="dark"] .btn-success:hover,
html[data-theme="dark"] .btn-success:focus,
html[data-theme="dark"] .btn-success:active {
  background-color: var(--forest-fill-hover) !important;
  border-color: var(--forest-fill-hover) !important;
}
html[data-theme="dark"] .btn-outline-success:hover {
  background-color: var(--forest-fill) !important;
  border-color: var(--forest-fill) !important;
}
html[data-theme="dark"] .gotv-bar { background: var(--forest-fill); }

/* Print — never let dark mode leak onto paper or PDF exports. Force the
   light canvas/surface/ink anchors regardless of the active theme. */
@media print {
  html, html[data-theme="dark"] {
    --bg-canvas: #FFFFFF;
    --bg-canvas-soft: #FFFFFF;
    --surface-1: #FFFFFF;
    --surface-2: #FFFFFF;
    --surface-3: #FFFFFF;
    --fg-default: #0E1A2B;
    --fg-muted: #5C6678;
    --border-default: rgba(14,26,43,0.12);
    --ink-12: rgba(14,26,43,0.12);
    --ink-08: rgba(14,26,43,0.08);
  }
}

/* ============================================================================
 * Semantic type — apply to elements directly OR via these utility classes
 * ========================================================================== */

html, body {
  background: var(--bg-canvas);
  color: var(--fg-default);
  font-family: var(--font-sans);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'cv11', 'ss01', 'ss03';
}

.display, h1.display {
  font: 500 var(--t-display)/var(--lh-tight) var(--font-sans);
  letter-spacing: var(--tr-tight);
  color: var(--fg-default);
}

h1, .t-h1 {
  font: 600 var(--t-h1)/var(--lh-snug) var(--font-sans);
  letter-spacing: -0.01em;
  margin: 0;
}

h2, .t-h2 {
  font: 600 var(--t-h2)/var(--lh-snug) var(--font-sans);
  margin: 0;
}

h3, .t-h3 {
  font: 600 var(--t-h3)/var(--lh-snug) var(--font-sans);
  margin: 0;
}

.t-body  { font: 400 var(--t-body)/var(--lh-body) var(--font-sans); }
.t-small { font: 400 var(--t-small)/var(--lh-body) var(--font-sans); color: var(--fg-muted); }
.t-mono  { font: 400 var(--t-mono)/var(--lh-snug) var(--font-mono); letter-spacing: 0.01em; }

.t-eyebrow {
  /* Sans semibold per Figma type/eyebrow (Inter Semi Bold 11/+8%). Mono is
     reserved for IDs, timestamps and metadata, not section labels. */
  font: 600 var(--t-eyebrow)/1 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: var(--tr-wide);
  color: var(--fg-muted);
}

.t-editorial {
  font: 400 var(--t-h2)/1.45 var(--font-editorial);
  font-style: italic;
  color: var(--fg-default);
}

/* Mono used inline for IDs/timestamps inside body copy */
code, kbd, .mono-inline {
  font: 400 0.92em/1.2 var(--font-mono);
  letter-spacing: 0.01em;
  color: var(--fg-code);
}

a {
  color: var(--action-default);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
a:hover { color: var(--action-hover); }

hr {
  border: 0;
  border-top: 1px solid var(--border-default);
  margin: var(--space-5) 0;
}

::selection { background: var(--blue-100); color: var(--ink-900); }

/* ── GOTV teller coverage timeline ──────────────────────────────────────── */
.gotv-axis, .gotv-row { display: flex; align-items: stretch; }
.gotv-axis-label, .gotv-row-label { flex: 0 0 220px; padding-right: 12px; }
.gotv-row-action { flex: 0 0 110px; padding-left: 12px; display: flex; align-items: center; }
.gotv-axis-track { position: relative; flex: 1 1 auto; height: 20px; }
.gotv-track {
  position: relative; flex: 1 1 auto; height: 38px;
  background: var(--bg-well);
  border: 1px solid var(--border-default); border-radius: 2px; overflow: hidden;
}
.gotv-row { margin-bottom: 8px; }
.gotv-row-label { display: flex; flex-direction: column; justify-content: center; }
.gotv-row-meta { font-family: var(--mono, 'IBM Plex Mono', monospace); font-size: 12px; }
.gotv-tick { position: absolute; top: 0; bottom: 0; transform: translateX(-50%); }
.gotv-tick-label { font-family: var(--mono, 'IBM Plex Mono', monospace); font-size: 12px; color: var(--fg-muted); }
.gotv-gap {
  position: absolute; top: 0; bottom: 0;
  background: var(--crimson-100); border-right: 1px solid var(--crimson-200);
}
.gotv-bar {
  position: absolute; top: 3px; bottom: 3px;
  background: var(--forest-600); color: var(--white); border-radius: 2px;
  display: flex; align-items: center; gap: 4px;
  padding: 0 6px; min-width: 0; overflow: hidden;
}
.gotv-bar-text { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gotv-bar-remove {
  flex: 0 0 auto; background: transparent; border: 0; color: var(--white);
  font-size: 15px; line-height: 1; padding: 0 2px; cursor: pointer;
}
.gotv-bar-remove:hover { color: var(--crimson-100); }
.gotv-bar-code {
  flex: 0 0 auto; background: transparent; border: 0; color: var(--white);
  line-height: 1; padding: 0 2px; cursor: pointer; display: inline-flex;
  align-items: center;
}
.gotv-bar-code:hover { opacity: 0.75; }

/* ── Design-system panel — replaces Bootstrap .card ─────────────────────
   Use .ds-panel / .ds-panel__header / .ds-panel__body / .ds-panel__footer.
   Matches the 4px radius, 1px Ink/12% hairline, white surface spec. */
.ds-panel {
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
}
.ds-panel__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 10px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-soft);
  font: 600 13px/1.3 var(--font-sans);
  color: var(--fg-default);
}
.ds-panel__body  { padding: 14px; }
.ds-panel__footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: 10px 14px;
  background: var(--surface-2);
  border-top: 1px solid var(--border-soft);
}

/* ── Form field components — replaces Bootstrap .form-control / .form-select
   Mirrors the token-based styles in frontend/src/ui/forms.module.css so
   server-rendered forms match island-rendered forms. */
.field-label {
  display: block;
  font-weight: 500;
  font-size: var(--t-body);
  color: var(--fg-default);
  margin-bottom: 5px;
}
.field-label .req { color: var(--crimson-600); }
.field-label .opt { color: var(--fg-subtle); font-weight: 400; }

.field-input,
.field-textarea,
.field-select {
  display: block;
  width: 100%;
  box-sizing: border-box;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  color: var(--fg-default);
  font-family: var(--font-sans);
  font-size: 16px; /* prevent iOS auto-zoom on focus */
  padding: 7px 10px;
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
@media (min-width: 768px) {
  .field-input, .field-textarea, .field-select { font-size: var(--t-body); }
}
.field-input::placeholder,
.field-textarea::placeholder { color: var(--fg-subtle); }
.field-input:focus,
.field-textarea:focus,
.field-select:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 1px var(--border-focus);
}
.field-input:disabled,
.field-textarea:disabled,
.field-select:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  background: var(--surface-2);
}
.field-input.invalid,
.field-textarea.invalid,
.field-select.invalid { border-color: var(--status-error-border); }
.field-textarea { resize: vertical; min-height: 80px; }
.field-select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%235C6678'%3e%3cpath d='M4 6l4 4 4-4'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 14px;
  padding-right: 30px;
}

/* ── Custom branded select (cs-wrap / cs-trigger / cs-menu / cs-option) ─────
   Used by both Jinja templates (via cs-select.js auto-init) and the React
   <Select> component. The native <select class="cs-native"> stays in the DOM
   hidden so form submission, required validation, and onChange still work. */
.cs-wrap { position: relative; }
.cs-native {
  position: absolute;
  inset: 0;
  opacity: 0 !important;
  pointer-events: none !important;
  width: 100%;
  height: 100%;
  appearance: none;
  -webkit-appearance: none;
}
.cs-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  background: var(--surface-1);
  color: var(--fg-default);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  font: 500 var(--t-body) / 1.4 var(--font-sans);
  width: 100%;
  box-sizing: border-box;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  outline: none;
  transition: border-color 120ms ease, box-shadow 120ms ease;
  min-height: 35px;
}
.cs-trigger:focus,
.cs-trigger[aria-expanded="true"] {
  border-color: var(--border-focus, var(--blue-600));
  box-shadow: 0 0 0 2px var(--blue-100);
}
.cs-trigger[aria-expanded="true"] { border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
.cs-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.4;
  color: var(--fg-default);
}
.cs-value.placeholder { color: var(--fg-subtle); }
.cs-chevron {
  color: var(--ink-500, var(--fg-muted));
  flex-shrink: 0;
  display: inline-flex;
  transition: transform 120ms ease;
}
.cs-trigger[aria-expanded="true"] .cs-chevron { transform: rotate(180deg); }
.cs-menu {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 1050;
  background: var(--surface-1);
  border: 1px solid var(--border-focus, var(--blue-600));
  border-top: none;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  box-shadow: 0 4px 12px rgba(14, 26, 43, 0.14);
  max-height: 260px;
  overflow-y: auto;
  padding: 4px 0;
  display: none;
}
.cs-menu.open { display: block; }
.cs-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 12px;
  font: 400 var(--t-body) / 1.3 var(--font-sans);
  color: var(--fg-default);
  cursor: pointer;
  transition: background 80ms;
}
.cs-option:hover { background: var(--ink-04, #f4f5f8); }
.cs-option.selected { background: var(--blue-050, #eef4fb); }
.cs-option-label { flex: 1; min-width: 0; }
.cs-tick { color: var(--blue-600); display: inline-flex; flex-shrink: 0; }
.cs-group-label {
  padding: 6px 12px 3px;
  font: 600 var(--t-eyebrow) / 1 var(--font-sans);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-muted);
  pointer-events: none;
}
.cs-wrap--sm .cs-trigger { padding: 3px 8px; font-size: var(--t-small, 13px); min-height: 28px; }
.cs-wrap--sm .cs-option { padding: 5px 10px; font-size: var(--t-small, 13px); }

/* ── Insights section tabs (Your patch / Elections / Atlas / How the models
   work) ─────────────────────────────────────────────────────────────────
   Server-rendered counterpart of SectionShell.module.css's .tabs/.tab/
   .tabIsActive, for the Jinja-rendered Insights pages (Elections, Atlas,
   methodology) that sit outside the React patch island. Kept token-for-token
   with the React version so the spine looks identical crossing between the
   two, and both mirror the app-wide .nav-tabs / .dg-tab-nav underline tab
   pattern (civic-blue active state, hover reveals the underline) rather than
   inventing their own tab styling. */
.ins-tabs {
  display: flex;
  gap: 2px;
  align-items: stretch;
  border-bottom: 1px solid var(--border-default);
  margin: 0 0 var(--space-5);
  flex-wrap: wrap;
}
.ins-tab {
  font: inherit;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--fg-muted);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 8px 14px;
  text-decoration: none;
  margin-bottom: -1px;
}
.ins-tab:hover {
  color: var(--fg-default);
  border-bottom-color: var(--border-default);
}
.ins-tab-active {
  color: var(--action-default);
  font-weight: 600;
  border-bottom-color: var(--action-default);
}
.ins-tab-end { margin-left: auto; }

/* Checkbox and radio rows */
.field-check {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: 4px;
}
.field-check--inline { display: inline-flex; margin-right: var(--space-4); }
.field-check-input {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-1) center / 80% no-repeat;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.field-check-input[type="radio"] { border-radius: 50%; }
.field-check-input:checked {
  background-color: var(--action-default);
  border-color: var(--action-default);
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' d='M5 10l3.5 3.5L15 7'/%3e%3c/svg%3e");
}
.field-check-input[type="radio"]:checked {
  background-image: none;
  box-shadow: inset 0 0 0 4px var(--action-default);
  background-color: var(--surface-1);
  border-color: var(--action-default);
}
.field-check-input:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}
.field-check-label {
  font-size: var(--t-body);
  color: var(--fg-default);
  cursor: pointer;
}

/* ── Layout BEM helpers — replaces Bootstrap utility soup for common patterns.
   Use these for structural layout; keep Bootstrap utilities for one-offs.   */

/* Page-level title + actions row */
.page-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.page-header__title { flex: 1 1 0; min-width: 0; }
.page-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
  margin-inline-start: auto;
}

/* Sub-tab nav + optional right-side action button */
.tab-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  flex-wrap: wrap;
}
.tab-toolbar__tabs {
  display: flex;
  gap: var(--space-1);
  flex: 1 1 0;
  min-width: 0;
  flex-wrap: wrap;
}
.tab-toolbar__action { flex-shrink: 0; }

/* Section-level title + right side */
.section-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.section-header > :first-child { flex: 1 1 0; min-width: 0; }
.section-header > :last-child:not(:first-child) { flex-shrink: 0; margin-inline-start: auto; }
.section-header--center { align-items: center; }

/* Bootstrap button and form-control remapping → base.html inline <style> */

/* ── Bootstrap component remapping — badges, alerts, progress, nav tabs ─────
 * Migrated from base.html inline <style> block. Overrides Bootstrap defaults
 * with design-system tokens; works because cadrify.css loads after bootstrap. */
/* Badges & pills — rectangular, 2px radius. No fully-rounded chips.
   Typography per the Figma Status pill: Inter Semi Bold caps — mono is
   reserved for machine strings (IDs, codes, timestamps). */
.badge {
  border-radius: var(--radius-sm) !important;
  font-family: var(--font-sans);
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  padding: 2px 7px;
  letter-spacing: 0.05em;
  border: 1px solid transparent;
}
.badge.rounded-pill { border-radius: var(--radius-sm) !important; }

.bg-success, .badge.bg-success {
  background: var(--status-active-bg) !important;
  color: var(--status-active-fg) !important;
  border-color: var(--status-active-border);
}
.bg-warning, .badge.bg-warning {
  background: var(--status-draft-bg) !important;
  color: var(--status-draft-fg) !important;
  border-color: var(--status-draft-border);
}
.bg-danger, .badge.bg-danger {
  background: var(--status-error-bg) !important;
  color: var(--status-error-fg) !important;
  border-color: var(--status-error-border);
}
.bg-info, .badge.bg-info {
  background: var(--status-info-bg) !important;
  color: var(--status-info-fg) !important;
  border-color: var(--status-info-border);
}
.bg-primary, .badge.bg-primary {
  background: var(--action-default) !important;
  color: var(--white) !important;
  border-color: var(--action-hover);
}
.bg-secondary, .badge.bg-secondary {
  background: var(--status-neutral-bg) !important;
  color: var(--status-neutral-fg) !important;
  border-color: var(--status-neutral-border);
}
.bg-light, .badge.bg-light {
  background: var(--surface-2) !important;
  color: var(--fg-default) !important;
}
.bg-dark, .badge.bg-dark {
  background: var(--ink-900) !important;
  color: var(--paper-soft) !important;
}

.text-success { color: var(--forest-700) !important; }
html[data-theme="dark"] .text-success { color: var(--status-active-fg) !important; }
.text-warning { color: var(--signal-700) !important; }
.text-danger  { color: var(--crimson-600) !important; }
.text-info    { color: var(--action-hover) !important; }
.text-primary { color: var(--action-default) !important; }
.text-muted, .text-secondary { color: var(--fg-muted) !important; }

/* Alerts */
.alert {
  border-radius: var(--radius-md);
  border: 1px solid;
  padding: 12px 16px;
  background: var(--surface-1);
}
.alert-success {
  background: var(--status-active-bg);
  color: var(--status-active-fg);
  border-color: var(--status-active-border);
}
.alert-warning {
  background: var(--status-draft-bg);
  color: var(--status-draft-fg);
  border-color: var(--status-draft-border);
}
.alert-danger {
  background: var(--status-error-bg);
  color: var(--status-error-fg);
  border-color: var(--status-error-border);
}
.alert-info, .alert-primary {
  background: var(--status-info-bg);
  color: var(--status-info-fg);
  border-color: var(--status-info-border);
}

/* Dropdowns */
.dropdown-menu {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-2);
  background: var(--surface-1);
  color: var(--fg-default);
  padding: 4px 0;
}
.dropdown-item { color: var(--fg-default); padding: 8px 16px; }
.dropdown-item:hover, .dropdown-item:focus { background: var(--ink-04); color: var(--fg-default); }
.dropdown-item.active, .dropdown-item:active {
  background: var(--blue-050);
  color: var(--action-hover);
}
.dropdown-divider {
  border-top: 1px solid var(--border-soft);
  margin: 4px 0;
}

/* Breadcrumb */
.breadcrumb {
  font-size: var(--t-small); color: var(--fg-muted);
  /* Stay on a single line; long crumbs truncate with an ellipsis rather
     than wrapping onto a second row (common on narrow screens with long
     org / group names). */
  flex-wrap: nowrap;
  min-width: 0;
  overflow: hidden;
}
/* Middle crumbs (usually short, e.g. "Campaigns") stay whole; the
   endpoints — the org name and the current page, typically the longest —
   absorb the truncation. */
.breadcrumb-item { white-space: nowrap; flex-shrink: 0; }
.breadcrumb-item:first-child,
.breadcrumb-item:last-child {
  min-width: 2.5rem;
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}
.breadcrumb-item + .breadcrumb-item::before {
  color: var(--fg-subtle);
  content: "/";
  padding: 0 6px;
}
.breadcrumb-item a { color: var(--action-default); }
.breadcrumb-item.active { color: var(--fg-muted); }

/* Modals */
.modal-content {
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-3);
}
.modal-header { border-bottom: 1px solid var(--border-soft); background: var(--surface-2); }
.modal-footer { border-top: 1px solid var(--border-soft); background: var(--surface-2); }
.modal-backdrop.show { opacity: 1; background: var(--ink-scrim); }

/* Pagination */
.pagination .page-link {
  border-color: var(--border-default);
  color: var(--action-default);
  background: var(--surface-1);
  border-radius: var(--radius-sm);
  margin: 0 2px;
}
.pagination .page-item.active .page-link {
  background: var(--action-default);
  border-color: var(--action-default);
  color: var(--white);
}
.pagination .page-item.disabled .page-link { color: var(--fg-subtle); }

/* List groups */
.list-group { border-radius: var(--radius-md); }
.list-group-item {
  border-color: var(--border-soft);
  background: var(--surface-1);
  color: var(--fg-default);
}
.list-group-item.active {
  background: var(--blue-050);
  border-color: var(--border-default);
  color: var(--action-hover);
}

/* Progress — thin civic-blue bar (per system loading-state rules) */
.progress {
  background: var(--bg-well);
  border-radius: var(--radius-sm);
  height: 4px;
}
.progress-bar { background: var(--action-default); }

/* Nav tabs */
.nav-tabs { border-bottom: 1px solid var(--border-default); }
.nav-tabs .nav-link {
  color: var(--fg-muted);
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 8px 14px;
  font-weight: 500;
}
.nav-tabs .nav-link:hover {
  color: var(--fg-default);
  background: transparent;
  border-bottom-color: var(--border-default);
}
.nav-tabs .nav-link.active {
  color: var(--action-hover);
  background: transparent;
  border-bottom-color: var(--action-default);
}

/* Mono affordances for IDs/timestamps/codes */
.mono, .text-mono { font-family: var(--font-mono); font-size: 0.92em; letter-spacing: 0.01em; color: var(--fg-code); }
.badge-ward { font-size: var(--t-mono); font-family: var(--font-mono); }
.nav-link-active { background: var(--blue-050); border-radius: var(--radius-sm); }

/* ── .notice — inline page-level alert with left accent stripe ───────────────
 * Use instead of Bootstrap .alert for in-page contextual messages.
 * Matches the React Banner component (ui/Banner.tsx) for visual consistency.
 * Jinja: {% include 'macros/_notice.html' %} or use the class directly.     */
.notice {
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--border-default);
  border-left-width: 3px;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: var(--t-body);
}
.notice__icon  { flex-shrink: 0; margin-top: 1px; }
.notice__body  { flex: 1; min-width: 0; }
.notice__title { font-weight: 600; margin-bottom: 3px; }

.notice--info    { background: var(--status-info-bg);   border-left-color: var(--accent-action); }
.notice--info    .notice__icon { color: var(--accent-action); }
.notice--success { background: var(--status-active-bg); border-left-color: var(--accent-active); }
.notice--success .notice__icon { color: var(--accent-active); }
.notice--warning { background: var(--status-draft-bg);  border-left-color: var(--accent-warn); }
.notice--warning .notice__icon { color: var(--accent-warn); }
.notice--error   { background: var(--status-error-bg);  border-left-color: var(--accent-danger); }
.notice--error   .notice__icon { color: var(--accent-danger); }

/* ── Settings save state ───────────────────────────────────────────────────
 * Keeps the primary save action visible before long settings forms and gives
 * unsaved work the same signal treatment as other draft-state notices. */
.settings-save-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.settings-save-header__copy { min-width: 0; }
.settings-save-header__title {
  margin: 0 0 var(--space-1);
  font: 600 18px/1.3 var(--font-sans);
  color: var(--fg-default);
}
.settings-save-header__description {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--t-small);
}
.settings-unsaved-bar {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  padding: 10px 14px;
  background: var(--status-draft-bg);
  border: 1px solid var(--status-draft-border);
  border-left: 3px solid var(--accent-warn);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  color: var(--status-draft-fg);
  font-size: var(--t-small);
}
.settings-unsaved-bar[hidden] { display: none; }
.settings-unsaved-bar__title { font-weight: 600; }

@media (max-width: 575px) {
  .settings-save-header { align-items: center; }
  .settings-save-header__description { display: none; }
  .settings-unsaved-bar { align-items: flex-start; flex-direction: column; gap: 2px; }
}

/* ── .ds-btn — canonical button, per the Figma Button component ──────────────
 * Variants: --primary (civic blue), --secondary (surface + hairline),
 * --ghost (transparent), --danger (destructive only, never decorative).
 * Geometry: 13px Inter Medium, 7x14 padding, 2px radius, 6px gap.
 * Prefer this over Bootstrap .btn in new and converted templates.          */
.ds-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1-5);
  font: 500 var(--t-small)/1.25 var(--font-sans);
  padding: 7px 14px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.ds-btn:disabled, .ds-btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.ds-btn:focus-visible {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 2px var(--blue-100);
}
.ds-btn--sm { font-size: var(--t-mono); padding: 4px 10px; }

.ds-btn--primary { background: var(--action-default); border-color: var(--action-default); color: var(--action-on); }
.ds-btn--primary:hover:not(:disabled), .ds-btn--primary:active:not(:disabled) {
  background: var(--action-hover); border-color: var(--action-hover); color: var(--action-on);
}
.ds-btn--secondary { background: var(--surface-1); border-color: var(--border-default); color: var(--fg-default); }
.ds-btn--secondary:hover:not(:disabled), .ds-btn--secondary:active:not(:disabled) {
  background: var(--bg-well); border-color: var(--border-default); color: var(--fg-default);
}
.ds-btn--ghost { background: transparent; border-color: transparent; color: var(--fg-default); }
.ds-btn--ghost:hover:not(:disabled), .ds-btn--ghost:active:not(:disabled) { background: var(--bg-well); }
.ds-btn--danger { background: var(--crimson-600); border-color: var(--crimson-600); color: var(--white); }
.ds-btn--danger:hover:not(:disabled), .ds-btn--danger:active:not(:disabled) {
  background: var(--crimson-700); border-color: var(--crimson-700); color: var(--white);
}
.ds-btn--danger:focus-visible { box-shadow: 0 0 0 2px var(--crimson-100); border-color: var(--crimson-600); }

/* ── .ds-chip — applied-filter chip, per the Figma Filter chip component ─────
 * Selected uses the info tone. Remove control is a nested button.          */
.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1-5);
  font: 400 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default);
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 4px 10px;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.ds-chip:hover { background: var(--bg-well); border-color: var(--border-strong); }
.ds-chip:focus-visible {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 2px var(--blue-100);
}
.ds-chip--selected {
  background: var(--status-info-bg);
  border-color: var(--action-default);
  color: var(--status-info-fg);
}
.ds-chip--disabled, .ds-chip:disabled { opacity: .45; cursor: not-allowed; }
.ds-chip__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px; height: 12px;
  padding: 0; border: 0; background: none;
  color: inherit; cursor: pointer; opacity: .7;
}
.ds-chip__remove:hover { opacity: 1; }

/* ── .status-chip — token-backed status pill for Jinja templates ─────────────
 * Global equivalent of the React StatusPill component. Per the Figma Status
 * pill: Inter Semi Bold caps, rectangular, never fully rounded — mono is
 * reserved for machine strings. Optional leading dot: .status-chip__dot.
 * Use: <span class="status-chip chip-active">Active</span>               */
.status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1-5);
  font: 600 10.5px/1.5 var(--font-sans);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  white-space: nowrap;
}
.status-chip__dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.status-chip.chip-active  { background: var(--status-active-bg);  color: var(--status-active-fg);  border-color: var(--status-active-border);  }
.status-chip.chip-draft   { background: var(--status-draft-bg);   color: var(--status-draft-fg);   border-color: var(--status-draft-border);   }
.status-chip.chip-info    { background: var(--status-info-bg);    color: var(--status-info-fg);    border-color: var(--status-info-border);    }
.status-chip.chip-error   { background: var(--status-error-bg);   color: var(--status-error-fg);   border-color: var(--status-error-border);   }
.status-chip.chip-danger  { background: var(--status-error-bg);   color: var(--status-error-fg);   border-color: var(--status-error-border);   }
.status-chip.chip-success { background: var(--status-active-bg);  color: var(--status-active-fg);  border-color: var(--status-active-border);  }
.status-chip.chip-muted   { background: var(--status-neutral-bg); color: var(--status-neutral-fg); border-color: var(--status-neutral-border); }
.status-chip.chip-warn    { background: var(--status-draft-bg);   color: var(--status-draft-fg);   border-color: var(--status-draft-border);   }

/* ── .ds-page-header — breadcrumb, title and primary action, per Figma ───────
 * Breadcrumb row (12px, `/` separators, current page 13px Medium), then the
 * title row: 24px SemiBold h1 left, primary action right. Replaces the
 * hand-rolled page-header wrappers and fixes the h1/h2/h4 drift.
 * --bare drops the background and side padding for use inside containers
 * that already pad the content column.                                     */
.ds-page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  padding: var(--space-5) var(--space-6) var(--space-4);
}
.ds-page-header--bare {
  background: transparent;
  border-bottom: 0;
  padding: 0 0 var(--space-4);
}
.ds-page-header__crumbs {
  display: flex;
  align-items: center;
  gap: var(--space-1-5);
  font: 400 var(--t-mono)/1.3 var(--font-sans);
  color: var(--fg-muted);
}
.ds-page-header__crumbs a {
  color: var(--fg-muted);
  text-decoration: none;
}
.ds-page-header__crumbs a:hover { text-decoration: underline; }
.ds-page-header__crumb-sep { color: var(--fg-subtle); }
.ds-page-header__crumb--current {
  font: 500 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default);
}
.ds-page-header__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.ds-page-header__title {
  margin: 0;
  font: 600 24px/1.15 var(--font-sans);
  letter-spacing: -0.02em;
  color: var(--fg-default);
}
.ds-page-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-inline-start: auto;
}

/* ── .ds-stat — KPI stat tile, per the Figma Stat tile component ─────────────
 * Label (eyebrow), value (32px SemiBold), optional mono delta pill, caption.
 * Delta tone follows whether the direction is good, not whether the number
 * rose. The single KPI figure in the system — do not rebuild it locally.   */
.ds-stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.ds-stat__label {
  font: 600 var(--t-eyebrow)/1 var(--font-sans);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.ds-stat__value {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font: 600 32px/1.15 var(--font-sans);
  letter-spacing: -0.02em;
  color: var(--fg-default);
}
.ds-stat__delta {
  font: 500 var(--t-mono)/1.3 var(--font-mono);
  letter-spacing: 0.02em;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  background: var(--status-neutral-bg);
  color: var(--status-neutral-fg);
}
.ds-stat__delta--good { background: var(--status-active-bg); color: var(--status-active-fg); }
.ds-stat__delta--bad  { background: var(--status-error-bg);  color: var(--status-error-fg);  }
.ds-stat__caption {
  font: 400 var(--t-mono)/1.4 var(--font-sans);
  color: var(--fg-subtle);
}
/* Responsive tile row. auto-fit, not auto-fill: with fewer tiles than fit
   at the 200px floor, auto-fill still reserves the extra tracks (empty but
   present), which pins real tiles to one side with dead space alongside.
   auto-fit collapses empty tracks so the 1fr tiles grow to fill the row.
   Both behave identically once there are enough tiles to wrap. */
.ds-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-4);
}

/* ── Vote-share bar (verification dashboard, "Results so far") ───────────
   Ported from the orphaned verification.html <style> block (see the .vfy-*
   comment further down) — the live embedded pane never loaded it, so the
   track collapsed to 0 height (a bare block div with no content) and every
   candidate row rendered name-only with no bar beneath it. Most visible with
   0 votes counted, where every row hits this at once. */
.vfy-bar-bg {
  height: 8px;
  border-radius: var(--radius-sm);
  background: var(--ink-04);
  overflow: hidden;
}
.vfy-bar-fill {
  height: 100%;
  border-radius: var(--radius-sm);
  transition: width .4s;
}

/* ── .ds-table — data table, per the Figma Table component ───────────────────
 * Zebra-free rows separated by hairlines, sortable header on surface-2,
 * right-aligned mono numerics, optional toolbar and pagination footer.
 * Markup: .ds-table-wrap > (toolbar?) > table.ds-table > (foot?).
 * Cell modifiers: .ds-cell-id (mono, muted), .ds-cell-num (mono, right).
 * Row states: tr:hover well, tr.is-selected info tones. Row actions
 * (.ds-row-action) reveal on hover, selection or keyboard focus.           */
.ds-table-wrap {
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.ds-table-wrap .ds-table-scroll { overflow-x: auto; }
.ds-table__toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-1-5);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-default);
  padding: var(--space-2) 10px;
}
.ds-table__toolbar-label {
  font: 500 10.5px/1 var(--font-sans);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.ds-table__view {
  font: 500 11px/1.3 var(--font-sans);
  color: var(--fg-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 3px 8px;
  cursor: pointer;
  text-decoration: none;
}
.ds-table__view:hover { background: var(--bg-well); color: var(--fg-default); }
.ds-table__view--active {
  background: var(--status-info-bg);
  border-color: var(--blue-100);
  color: var(--action-default);
}
.ds-table {
  width: 100%;
  border-collapse: collapse;
  font: 400 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default);
}
.ds-table thead th {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-default);
  font: 600 11.5px/1.3 var(--font-sans);
  letter-spacing: 0.03em;
  color: var(--fg-muted);
  text-align: left;
  white-space: nowrap;
  padding: 9px var(--space-3);
}
.ds-table thead th.is-sorted { color: var(--fg-default); }
.ds-table thead th.ds-cell-num { text-align: right; }
.ds-table tbody td {
  border-bottom: 1px solid var(--border-soft);
  padding: 10px var(--space-3);
  vertical-align: middle;
}
.ds-table tbody tr:last-child td { border-bottom: 0; }
.ds-table tbody tr:hover td { background: var(--bg-well); }
.ds-table tbody tr.is-selected td {
  background: var(--status-info-bg);
  border-bottom-color: var(--status-info-border);
}
.ds-table .ds-cell-id {
  font: 400 var(--t-mono)/1.3 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--fg-muted);
  white-space: nowrap;
}
.ds-table .ds-cell-num {
  font: 400 var(--t-small)/1.3 var(--font-mono);
  text-align: right;
  white-space: nowrap;
}
.ds-row-action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font: 500 11px/1.3 var(--font-sans);
  color: var(--fg-muted);
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 5px var(--space-2);
  cursor: pointer;
  text-decoration: none;
  visibility: hidden;
}
.ds-table tr:hover .ds-row-action,
.ds-table tr.is-selected .ds-row-action,
.ds-row-action:focus-visible { visibility: visible; }
.ds-row-action:hover { color: var(--fg-default); border-color: var(--border-strong); }
.ds-table__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  border-top: 1px solid var(--border-default);
  padding: 9px var(--space-3);
}
.ds-table__count {
  font: 400 var(--t-mono)/1.3 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--fg-muted);
}
.ds-table__pager {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font: 500 var(--t-mono)/1.3 var(--font-mono);
  letter-spacing: 0.02em;
  color: var(--fg-default);
}
.ds-table__page-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  color: var(--fg-default);
  cursor: pointer;
  text-decoration: none;
}
.ds-table__page-btn:hover:not(:disabled) { background: var(--bg-well); }
.ds-table__page-btn:disabled, .ds-table__page-btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ── .ds-dist — part-to-whole distribution bar, per Figma ────────────────────
 * Any five-class breakdown: score (--score-1..5), party, or --chart-1..5
 * series. 10px track on the well, 2px radius, segments butt with no gap.
 * Segment widths are set inline (style="width: 38%"), colour via
 * background-color inline or a token class. Do not rebuild it per feature. */
.ds-dist {
  display: flex;
  height: 10px;
  background: var(--bg-well);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.ds-dist__seg { height: 100%; flex-shrink: 0; }
.ds-dist__seg--score1 { background: var(--score-1); }
.ds-dist__seg--score2 { background: var(--score-2); }
.ds-dist__seg--score3 { background: var(--score-3); }
.ds-dist__seg--score4 { background: var(--score-4); }
.ds-dist__seg--score5 { background: var(--score-5); }
.ds-dist__seg--none { background: var(--score-none); }
.ds-dist__seg--out { background: var(--score-out); }

/* Core (party) bar "no data" segment. Per-party segment colours come from
 * the ward's own resolved Core list (app/domain/core_parties), set inline
 * per segment in the template, not fixed classes here - a ward can offer
 * any registered party, so there is no fixed set of codes to enumerate. */
.ds-dist__seg--core-none { background: var(--score-none); }

/* Knocked-status bar segments. --signal-200 is pinned to its light-mode
 * hex rather than the token, deliberately not following it into dark
 * mode: the token's dark override (#2E2810) is a near-black tint meant
 * for backgrounds, not a fill - it would make this segment invisible
 * against the dark card. */
.ds-dist__seg--knocked-data { background: var(--forest-500); }
.ds-dist__seg--knocked-none { background: #E5D696; }
.ds-dist__seg--not-knocked  { background: var(--ink-300); }

/* ── .ds-wizard — multi-step flow (stepper + step panel + footer), per Figma ─
 * Stepper: done marker = teal + tick, current = civic blue + number, upcoming
 * = well + hairline. Connectors fill teal behind completed steps. The panel is
 * a surface-1 card with a header and padded body; the footer carries "Step N
 * of M" and the Back / Continue actions. Drive the state from the template
 * (each step page passes its own index), never from bespoke per-page CSS.    */
.ds-wizard { display: flex; flex-direction: column; gap: var(--space-5); }
.ds-wizard__stepper {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.ds-wizard__step { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.ds-wizard__marker {
  flex-shrink: 0;
  width: 24px; height: 24px;
  border-radius: var(--radius-full);
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 10.5px/1.5 var(--font-sans);
  letter-spacing: 0.05em;
  border: 1px solid transparent;
}
.ds-wizard__marker svg { display: block; }
.ds-wizard__step--done    .ds-wizard__marker { background: var(--forest-fill); color: var(--white); }
.ds-wizard__step--current .ds-wizard__marker { background: var(--action-default); color: var(--white); }
.ds-wizard__step--upcoming .ds-wizard__marker {
  background: var(--bg-well); border-color: var(--border-soft); color: var(--fg-subtle);
}
.ds-wizard__step-label { font: 400 var(--t-small)/1.3 var(--font-sans); color: var(--fg-default); white-space: nowrap; }
.ds-wizard__step--upcoming .ds-wizard__step-label { color: var(--fg-subtle); }
.ds-wizard__connector { flex: 1 1 0; min-width: 12px; height: 1px; background: var(--border-default); }
.ds-wizard__connector--done { background: var(--forest-fill); }

.ds-wizard__panel {
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.ds-wizard__panel-head {
  border-bottom: 1px solid var(--border-soft);
  padding: var(--space-4) var(--space-5);
}
.ds-wizard__panel-title { margin: 0; font: 600 16px/1.3 var(--font-sans); color: var(--fg-default); }
.ds-wizard__panel-body {
  padding: var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-4);
}
.ds-wizard__panel-lead { margin: 0; font: 400 var(--t-body)/1.5 var(--font-sans); color: var(--fg-muted); }

.ds-wizard__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  border-top: 1px solid var(--border-default);
  padding: var(--space-3) 0 0;
}
.ds-wizard__step-count {
  font: 400 var(--t-mono)/1.3 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.ds-wizard__footer-actions { display: flex; align-items: center; gap: var(--space-2); margin-inline-start: auto; }

/* Mobile: the stepper scrolls sideways and drops to markers-only if tight. */
@media (max-width: 640px) {
  .ds-wizard__stepper { overflow-x: auto; gap: var(--space-2); padding-bottom: var(--space-1); }
  .ds-wizard__connector { min-width: 8px; }
  .ds-wizard__step--upcoming .ds-wizard__step-label,
  .ds-wizard__step--done .ds-wizard__step-label { display: none; }
}

/* ── Layout: fix Bootstrap justify-content-between + flex-wrap wrapping ──────
 * When justify-content:space-between is combined with flex-wrap, a single item
 * that wraps to a new row left-aligns (space-between with one item = flex-start).
 * This makes action buttons appear below and to the left of page headers.
 *
 * Fix: margin-inline-start:auto on the last flex child absorbs all free space
 * to its left, which works on the same row (no-op when sibling fills space) AND
 * when wrapping (pushes the lone item to the right of its row).
 *
 * Covers all Bootstrap responsive justify-content-*-between breakpoint variants,
 * and the custom .tab-toolbar component which has the same wrapping problem.   */
.d-flex.justify-content-between.flex-wrap > :last-child,
.d-flex.justify-content-sm-between.flex-wrap > :last-child,
.d-flex.justify-content-md-between.flex-wrap > :last-child,
.d-flex.justify-content-lg-between.flex-wrap > :last-child,
.flex-wrap.justify-content-between > :last-child,
.flex-wrap.justify-content-sm-between > :last-child,
.flex-wrap.justify-content-md-between > :last-child,
.flex-wrap.justify-content-lg-between > :last-child {
  margin-inline-start: auto;
}

/* tab-toolbar: push any non-tabs child (the action button) to the right
   even when the tab buttons wrap onto a second line.                      */
.tab-toolbar > :not(.tab-toolbar__tabs) {
  margin-inline-start: auto;
}

/* ── dg-empty-state ──────────────────────────────────────────────────────── */
.dg-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-7) var(--space-5);
  text-align: center;
}
.dg-empty-state__title {
  margin: 0;
  font: 500 var(--t-body)/var(--lh-snug) var(--font-sans);
  color: var(--fg-default);
}
.dg-empty-state__body {
  margin: 0;
  font: 400 var(--t-small)/var(--lh-body) var(--font-sans);
  color: var(--fg-muted);
  max-width: 44ch;
}
.dg-empty-state__action { display: flex; gap: var(--space-2); }

/* ── Party mark (swatch or logo, verification tally/entry/dashboard) ─────
   Ported from the standalone verification.html <style> block, which only
   that orphaned page's own <head> ever loaded — the live embedded pane
   (_verification_pane.html, mounted inside detail.html) never included it,
   so every .vfy-* / .party-swatch class here rendered completely unstyled:
   no swatch, no vote-share bar, no entry-form card/row/input chrome. Moved
   into the shared stylesheet so both surfaces (and any future one) get it
   for free instead of relying on a per-page <style> block staying in sync. */
.party-swatch {
  display: inline-block;
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-sm);
  vertical-align: middle;
}
.party-mark {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  vertical-align: middle;
}
.party-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ── Data entry form (verification, "Data entry" sub-tab) ────────────────── */
.vfy-entry-card {
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: 1.25rem;
}
.vfy-cand-row {
  display: grid;
  grid-template-columns: 14px 1fr 100px;
  align-items: center;
  gap: .6rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--ink-04);
}
.vfy-cand-row:last-child { border-bottom: none; }
.vfy-cand-name { font-weight: 600; font-size: .85rem; }
.vfy-cand-party { font-size: .75rem; color: var(--fg-muted); }
.vfy-votes-input {
  text-align: right;
  font-size: 1rem;
  font-weight: 600;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: .3rem .6rem;
  width: 100%;
}
.vfy-votes-input:focus {
  border-color: var(--dg-brand);
  outline: none;
  box-shadow: 0 0 0 2px rgba(0, 169, 90, .15);
}
.vfy-votes-input.is-invalid {
  border-color: var(--crimson-600, #9B2C2C);
  box-shadow: 0 0 0 2px rgba(155, 44, 44, .15);
}

/* ── Prediction banner (verification dashboard) ───────────────────────────── */
.vfy-pred-banner {
  background: var(--status-draft-bg);
  border: 1px solid var(--status-draft-border);
  border-radius: var(--radius-md);
  padding: 1rem 1.25rem;
  margin-bottom: 1.25rem;
}

/* ── Tally sheet preview (verification, tally tab) ───────────────────────
   Ad-hoc Bootstrap `.border`/`.rounded` utilities read as raw browser-grey
   outlines even with no data entered yet. Use the Ink/12% hairline + the
   radii scale instead, same as every other bordered surface. */
.tally-sheet-preview {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
}
.tally-dashed-box {
  border: 1px dashed var(--border-default);
  border-radius: var(--radius-sm);
}
.tally-score-box {
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
}

/* ── Dashboard bar lists (score + party affiliation) ─────────────────
   Fixed 5px gap, does not scale with grid-cell height. The compact
   WidgetCard is auto-height (no h-100) so any extra cell space appears
   below the card border, never between rows. */
.dash-bar-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.dash-bar-list .dash-score-row {
  margin-bottom: 0;
  font-size: 12px;
  line-height: 1.3;
}
.dash-bar-list .dash-score-row .gs-track { height: 8px; }
.dash-bar-list .dash-score-row .gs-track.dbv-track { height: 10px; }
/* Party affiliation wraps the list plus an optional legend footer. */
.dash-bar-list-wrap {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* ── Persuasion heatmap (average score by door-knocker) ──────────────
   Global so it styles the grid widget and the dashboards management page. */
.ci-heatmap {
  display: flex;
  flex-direction: column;
}
.ci-table-scroll {
  overflow: auto;
}
.ci-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-small);
}
.ci-table th,
.ci-table td {
  border: 1px solid var(--border-default, rgba(14, 26, 43, 0.1));
  padding: 6px 8px;
  text-align: center;
  vertical-align: middle;
  white-space: nowrap;
}
.ci-table thead th {
  background: var(--bg-well);
  color: var(--fg-muted);
  font-weight: 600;
  position: sticky;
  top: 0;
  z-index: 1;
}
.ci-table .ci-name-h,
.ci-table .ci-name {
  text-align: left;
}
.ci-table .ci-name {
  color: var(--fg-default);
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Door-knocker names read as body text, not as blue links. */
.ci-table .ci-name-link {
  color: inherit;
  text-decoration: none;
}
.ci-table .ci-name-link:hover {
  text-decoration: underline;
  text-decoration-color: var(--border-default);
}
/* Numeric cells use the mono face. */
.ci-table .ci-cell {
  font: 500 var(--t-mono)/1.3 var(--font-mono);
  text-align: right;
  letter-spacing: 0.01em;
}
.ci-table .ci-total {
  font-weight: 600;
  background: var(--bg-well);
}
.ci-table thead .ci-total {
  text-align: center;
}
/* The small voter-count figure tucked under each average. */
.ci-table .ci-n {
  display: block;
  font-size: 10px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--fg-muted);
  opacity: 0.85;
}
.ci-table .ci-cell:not(.ci-low) .ci-n { color: inherit; }
/* Small samples are de-emphasised rather than colour-scaled. */
.ci-table .ci-low {
  background: var(--bg-well);
  color: var(--fg-muted);
}
/* No data in this cell. */
.ci-table .ci-empty {
  color: var(--fg-muted);
  text-align: center;
  opacity: 0.55;
}

/* ── .ds-dropzone — file upload drop target, per the Figma Dropzone component ─
 * A clickable, drag-and-drop file field. Wraps a visually-hidden native
 * <input type=file> so it posts inside any normal <form> with no JS required
 * for submission; js/dropzone.js progressively enhances the drag, preview and
 * clear behaviour. Three looks: idle (dashed), drag-over (civic blue) and
 * error (crimson), matching the component's variants. The default is the tall
 * vertical form; add .ds-dropzone--compact for a single-row inline field. */
.ds-dropzone {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-3); box-sizing: border-box; width: 100%;
  padding: var(--space-6) var(--space-4);
  border: 1.5px dashed var(--border-default); border-radius: var(--radius-lg);
  background: var(--surface-1); color: var(--fg-muted);
  text-align: center; cursor: pointer;
  transition: border-color .12s ease, background .12s ease, color .12s ease;
}
.ds-dropzone:hover { border-color: var(--blue-600); background: var(--ink-04); }
.ds-dropzone:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; }
.ds-dropzone.is-dragover {
  border-style: solid; border-color: var(--blue-600);
  background: var(--blue-050); color: var(--action-default, var(--blue-600));
}
.ds-dropzone.has-file { border-style: solid; border-color: var(--border-default); color: var(--fg-default); }
.ds-dropzone.has-error {
  border-style: dashed; border-color: var(--status-error-border);
  background: var(--status-error-bg); color: var(--status-error-fg);
}

/* Native input, kept in the DOM (so the form still posts) but visually hidden. */
.dz-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); border: 0;
}

/* Idle content: icon chip, prompt, hint, browse affordance. */
.dz-idle { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.ds-dropzone.has-file .dz-idle { display: none; }
.dz-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--radius-full);
  background: var(--ink-04); color: var(--fg-subtle);
}
.ds-dropzone.is-dragover .dz-chip { background: var(--blue-050); color: var(--blue-600); }
.ds-dropzone.has-error .dz-chip { background: transparent; color: var(--status-error-fg); }
.dz-title { font-size: var(--t-body); font-weight: 500; color: inherit; }
.dz-sub   { font-size: var(--t-small); color: var(--fg-muted); }
.ds-dropzone.has-error .dz-sub { color: var(--status-error-fg); }
.dz-hint  { font: 400 var(--t-mono)/1 var(--font-mono); color: var(--fg-subtle);
            text-transform: uppercase; letter-spacing: var(--tr-wide); }
.dz-browse {
  margin-top: var(--space-1); display: inline-flex; align-items: center;
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  border: 1px solid var(--border-default); background: var(--surface-1);
  font-size: var(--t-small); font-weight: 500; color: var(--fg-default);
}
.ds-dropzone:hover .dz-browse { border-color: var(--blue-600); color: var(--blue-600); }

/* Chosen-file row (single-file dropzone). For multi-file lists, use .ds-file-item. */
.dz-chosen { display: none; align-items: center; gap: var(--space-3); width: 100%; min-width: 0; text-align: left; }
.ds-dropzone.has-file .dz-chosen { display: flex; }
.dz-file { display: flex; flex-direction: column; min-width: 0; }
.dz-name { font-size: var(--t-small); font-weight: 500; color: var(--fg-default);
           white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dz-size { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-muted); }
.dz-remove {
  margin-left: auto; flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; cursor: pointer;
  border: 1px solid var(--border-default); border-radius: var(--radius-sm);
  background: var(--surface-1); color: var(--fg-muted);
}
.dz-remove:hover { border-color: var(--crimson-600); color: var(--crimson-600); }
.dz-error { font-size: var(--t-small); color: var(--crimson-600); margin: var(--space-1) 0 0; }
.dz-error:empty { display: none; }

/* Compact single-row variant, for inline fields and dense lists. */
.ds-dropzone--compact {
  flex-direction: row; align-items: center; justify-content: flex-start;
  gap: var(--space-3); min-height: 52px; padding: var(--space-2) var(--space-3); text-align: left;
}
.ds-dropzone--compact .dz-idle { flex-direction: row; align-items: center; gap: var(--space-3); }
.ds-dropzone--compact .dz-chip { width: 28px; height: 28px; }
.ds-dropzone--compact .dz-browse { margin-top: 0; padding: 0; border: 0; background: none;
  color: var(--blue-600); text-decoration: underline; text-underline-offset: 2px; }

/* ── .ds-file-item — a single uploaded file, per the Figma File item component ─
 * A row for an in-progress or finished upload: name, progress or meta, and an
 * action. Used by JS-driven multi-file uploaders (the React Dropzone renders
 * the same shape). Tone comes from a modifier: default (uploading), .is-done,
 * .is-error. */
.ds-file-item {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-3); box-sizing: border-box;
  border: 1px solid var(--border-soft, var(--ink-08)); border-radius: var(--radius-md);
  background: var(--surface-1);
}
.ds-file-item + .ds-file-item { margin-top: var(--space-2); }
.ds-file-item .fi-chip {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--radius-md);
  background: var(--ink-04); color: var(--fg-subtle);
}
.ds-file-item .fi-mid { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; flex: 1 1 auto; }
.ds-file-item .fi-name { font-size: var(--t-small); font-weight: 500; color: var(--fg-default);
                         white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-file-item .fi-meta { font-size: var(--t-small); color: var(--fg-muted); }
.ds-file-item.is-error .fi-meta { color: var(--crimson-600); }
.ds-file-item .fi-actions { flex: none; display: inline-flex; align-items: center; gap: var(--space-2); }

/* ── Leaderboards ──────────────────────────────────────────────────────────
   Medal tints for the top three places and the row avatars. Tokens sit here
   rather than the palette block because they are rank markers specific to
   this surface, not part of the semantic palette. */
:root {
  --medal-gold:   #96751A;
  --medal-silver: #77808F;
  --medal-bronze: #9C6644;
}
html[data-theme="dark"] {
  --medal-gold:   #C9A83A;
  --medal-silver: #A9B2C0;
  --medal-bronze: #C08552;
}
.lb-table .lb-rank { width: 2.5rem; }
.lb-medal { display: inline-flex; vertical-align: middle; }
.lb-medal--gold   { color: var(--medal-gold); }
.lb-medal--silver { color: var(--medal-silver); }
.lb-medal--bronze { color: var(--medal-bronze); }
.lb-avatar {
  width: 24px; height: 24px; border-radius: 50%;
  object-fit: cover; flex: none;
}
.lb-avatar--initials {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--forest-600); color: #FFFFFF;
  font-size: 10px; font-weight: 600; letter-spacing: 0.02em;
}
.lb-avatar--lg { width: 64px; height: 64px; }
.lb-avatar--lg.lb-avatar--initials { font-size: 20px; }

/* ── Profile activity overview (leaderboards volunteer page) ─────────────
   14-day mini bar chart inside a .ds-stat tile. Today's bar is full
   strength, earlier days recede. The baseline is the tile's only axis. */
.ps-bars { display: block; margin-top: var(--space-2); }
.ps-bar { fill: var(--action-default); opacity: 0.3; }
.ps-bar--today { opacity: 1; }
.ps-baseline { stroke: var(--border-strong); stroke-width: 1; }

/* ── Scope bar ──────────────────────────────────────────────────────────────
   The universe and time frame every number on the branch dashboard and the
   wards page is denominated by. Shared, so the two pages cannot drift.
   Sits inline in the page header row next to the title, not as its own
   full-width bar, so it carries no box chrome of its own - the select and
   the range trigger already look like controls on their own, wrapping the
   whole cluster in another border/background just doubled up on that and
   read as an oversized, empty-looking panel next to plain title text. */
.sb-bar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--space-3);
}
.sb-universe { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin: 0; }
.sb-label {
  font: 600 var(--t-eyebrow)/1.3 var(--font-sans);
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-subtle); white-space: nowrap;
}
.sb-select { min-width: 170px; }
.sb-size { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-muted); }
.sb-spacer { flex: 1 1 auto; }

.sb-range { position: relative; }
.sb-range__trigger {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 7px 10px 7px 12px;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  font: 400 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default);
  cursor: pointer; list-style: none; white-space: nowrap;
}
.sb-range__trigger::-webkit-details-marker { display: none; }
.sb-range__trigger:hover { border-color: var(--border-strong); }
.sb-range[open] .sb-range__trigger { border-color: var(--action-default); }
.sb-range__panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
  display: flex; align-items: stretch;
  background: var(--surface-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-3);
}
.sb-presets {
  list-style: none; margin: 0; padding: var(--space-2);
  border-right: 1px solid var(--border-soft);
  background: var(--surface-2);
  border-radius: var(--radius-md) 0 0 var(--radius-md);
  min-width: 150px;
}
.sb-preset {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: 7px 10px; border-radius: var(--radius-sm);
  font: 400 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default); text-decoration: none; white-space: nowrap;
}
.sb-preset:hover { background: var(--surface-3); text-decoration: none; }
.sb-preset.is-current { color: var(--action-default); font-weight: 500; }
.sb-custom { padding: var(--space-3); display: grid; gap: var(--space-2); min-width: 210px; }
.sb-custom__title {
  font: 600 var(--t-eyebrow)/1.3 var(--font-sans);
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-subtle);
}
.sb-field { display: grid; gap: 3px; }
.sb-field > span { font: 400 var(--t-small)/1.2 var(--font-sans); color: var(--fg-muted); }
.sb-field input {
  padding: 6px 8px; font: 400 var(--t-small)/1.3 var(--font-sans);
  color: var(--fg-default); background: var(--surface-1);
  border: 1px solid var(--border-default); border-radius: var(--radius-sm);
}
.sb-custom__actions { display: flex; justify-content: flex-end; margin-top: 2px; }

/* ── Coverage meter ─────────────────────────────────────────────────────────
   A percentage of the selected universe, with the counts that make it up. */
.cv-grid {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.cv-meter {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
}
.cv-meter__label {
  font: 600 var(--t-eyebrow)/1.3 var(--font-sans);
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-subtle);
}
.cv-meter__value {
  font: 600 32px/1.15 var(--font-sans); letter-spacing: -.02em;
  color: var(--fg-default);
}
.cv-track {
  height: 8px; border-radius: var(--radius-sm);
  background: var(--bg-well); overflow: hidden;
}
.cv-track__fill { height: 100%; border-radius: var(--radius-sm); background: var(--action-default); }
.cv-track__fill--forest { background: var(--forest-fill); }
.cv-track__fill--signal { background: var(--signal-600); }
/* Chart-palette (500-rung) fills for large solid progress areas, e.g. project
   cards, where the accent 600s read too dark per the Figma Progress bar spec. */
.cv-track__fill--chart1 { background: var(--chart-1); }
.cv-track__fill--chart2 { background: var(--chart-2); }
.cv-meter__caption { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-subtle); }

/* ── Pace-by-ward chart (branch dashboard) ──────────────────────────────── */
.pace-chart { width: 100%; height: auto; }
.pace-grid { stroke: var(--border-soft); stroke-width: 1; }
.pace-axis-label { font: 400 10px var(--font-mono); fill: var(--fg-subtle); }
.pace-line { fill: none; stroke-width: 2; }
.pace-picker__row {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 2px 0; white-space: nowrap;
  font: 400 var(--t-small)/1.4 var(--font-sans); color: var(--fg-default);
}
.pace-swatch { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; }

/* ── Attention ward row (dashboard target list) ─────────────────────────── */
.aw-row {
  display: flex; align-items: center; gap: var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  text-decoration: none; color: inherit;
}
.aw-row + .aw-row { margin-top: var(--space-2); }
.aw-row:hover { border-color: var(--border-strong); text-decoration: none; }
.aw-row__id { flex: 0 0 210px; min-width: 0; }
.aw-row__name { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.aw-row__name strong { font: 600 var(--t-h3)/1.3 var(--font-sans); color: var(--fg-default); }
.aw-row__gss { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-subtle); }
.aw-row__meter { flex: 1 1 0; min-width: 130px; display: grid; gap: 4px; }
.aw-row__meter-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.aw-row__meter-label { font: 500 var(--t-small)/1.2 var(--font-sans); color: var(--fg-muted); }
.aw-row__meter-value { font: 500 var(--t-mono)/1.2 var(--font-mono); color: var(--fg-default); }
.aw-row__meter-caption { font: 400 var(--t-mono)/1.2 var(--font-mono); color: var(--fg-subtle); }
.aw-row__last { flex: 0 0 130px; text-align: right; }
.aw-row__last-label { font: 600 var(--t-eyebrow)/1.3 var(--font-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-subtle); }
.aw-row__last-value { font: 500 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-default); }
.aw-track { height: 6px; border-radius: var(--radius-sm); background: var(--bg-well); overflow: hidden; }

/* ── Ward card (wards page) ─────────────────────────────────────────────── */
.wc-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.wc-card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3);
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  text-decoration: none; color: inherit;
}
.wc-card:hover { border-color: var(--border-strong); text-decoration: none; }
.wc-card__head { display: flex; align-items: center; gap: var(--space-2); }
.wc-card__name { font: 600 var(--t-h3)/1.3 var(--font-sans); color: var(--fg-default); }
.wc-card__gss { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-subtle); }
.wc-card__spacer { flex: 1 1 auto; }
.wc-kpis { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.wc-kpi__label { font: 600 var(--t-eyebrow)/1.3 var(--font-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-subtle); }
.wc-kpi__value { font: 600 var(--t-h2)/1.15 var(--font-sans); letter-spacing: -.02em; color: var(--fg-default); }
.wc-kpi__caption { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-subtle); }
.wc-card__rule { height: 1px; background: var(--border-soft); }

/* Dense stacked distribution: label beside the bar, not above it, so all
 * three (Score, Core, Knocked) fit in roughly the height one used to take. */
.wc-dist { display: flex; flex-direction: column; gap: 3px; }
.wc-dist__row { display: flex; align-items: center; gap: var(--space-2); }
.wc-dist__row-label {
  /* 50px, not 44px: "Knocked" is the longest of the three labels and
     needs the extra room, otherwise it overflows its flex-basis and
     steals width from the bar next to it, making that row's bar render
     visibly shorter than Score's and Core's. */
  flex: 0 0 50px;
  font: 600 9px/1.3 var(--font-sans); letter-spacing: .04em; text-transform: uppercase;
  color: var(--fg-subtle);
}
.wc-dist__row .ds-dist { flex: 1 1 auto; }

/* ── Ward group header ──────────────────────────────────────────────────── */
.wg-header { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; padding: var(--space-3) 0; }
.wg-drag-handle {
  display: inline-flex; align-items: center; flex: 0 0 auto;
  color: var(--fg-subtle); cursor: grab; touch-action: none;
}
.wg-drag-handle:active { cursor: grabbing; }
.wg-section.sortable-ghost { opacity: .4; }
.wg-toggle {
  all: unset; display: flex; align-items: center; gap: var(--space-2);
  cursor: pointer; font: 600 var(--t-h3)/1.3 var(--font-sans); color: var(--fg-default);
}
.wg-toggle:hover { color: var(--action-default); }
.wg-toggle:focus-visible { outline: 2px solid var(--action-default); outline-offset: 2px; border-radius: var(--radius-sm); }
.wg-chevron { flex: 0 0 auto; transition: transform var(--dur-fast) var(--ease-out); }
.wg-toggle[aria-expanded="false"] .wg-chevron { transform: rotate(-90deg); }
.wg-count { font: 400 var(--t-mono)/1.3 var(--font-mono); color: var(--fg-subtle); font-weight: 400; }
.wg-header__spacer { flex: 1 1 auto; }
.wg-meter { flex: 0 0 170px; display: grid; gap: 4px; }
.wg-meter__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.wg-meter__label { font: 500 var(--t-small)/1.2 var(--font-sans); color: var(--fg-muted); }
.wg-meter__value { font: 500 var(--t-mono)/1.2 var(--font-mono); color: var(--fg-default); }
.wg-body.is-collapsed { display: none; }

/* ── Wards comparison table ─────────────────────────────────────────────── */
.wt-metric { display: grid; gap: 2px; text-align: right; }
.wt-metric__value { font: 500 var(--t-mono)/1.2 var(--font-mono); color: var(--fg-default); }
.wt-metric__caption { font: 400 var(--t-mono)/1.2 var(--font-mono); color: var(--fg-subtle); }
.wt-score { min-width: 140px; }
/* ── Ward tags (wards page + branch dashboard) ──────────────────────────── */
.wc-tag {
  display: inline-block; padding: 2px 8px;
  border: 1px solid var(--border-default); border-radius: var(--radius-sm);
  background: var(--surface-1); color: var(--fg-muted);
  font: 500 var(--t-eyebrow)/1.4 var(--font-mono); letter-spacing: .03em;
  white-space: nowrap;
}
.wt-tagedit { position: relative; display: inline-block; }
.wt-tagedit > summary { list-style: none; }
.wt-tagedit > summary::-webkit-details-marker { display: none; }
.wt-tagedit__panel {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 40;
  min-width: 190px;
  background: var(--surface-overlay);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  box-shadow: 0 4px 16px rgba(14, 26, 43, .12);
  text-align: left;
}
.wt-tagedit__panel label {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 2px 0; white-space: nowrap;
  font: 400 var(--t-small)/1.4 var(--font-sans); color: var(--fg-default);
}
